7a2dbf6721582d73ac9665d9231ffd7fdb703d3a
chmalee
Wed Jun 3 15:04:56 2026 -0700
Add a link to create a myVariants item to the menu bar under My Data, refs #33808
diff --git src/hg/js/hgTracks.js src/hg/js/hgTracks.js
index 19d6115ad8c..15039276089 100644
--- src/hg/js/hgTracks.js
+++ src/hg/js/hgTracks.js
@@ -1,7456 +1,7479 @@
// hgTracks.js - Javascript for use in hgTracks CGI
// Copyright (C) 2008 The Regents of the University of California
// "use strict";
// Don't complain about line break before '||' etc:
/* jshint -W014 */
/* jshint esversion: 8 */
var debug = false;
/* Data passed in from CGI via the hgTracks object:
*
* string cgiVersion // CGI_VERSION
* string chromName // current chromosome
* int winStart // genomic start coordinate (0-based, half-open)
* int winEnd // genomic end coordinate
* int newWinWidth // new width (in bps) if user clicks on the top ruler
* boolean revCmplDisp // true if we are in reverse display
* int insideX // width of side-bar (in pixels)
* int rulerClickHeight // height of ruler (in pixels) - zero if ruler is hidden
* boolean inPlaceUpdate // true if in-place-update is turned on
* int imgBox* // various drag-scroll values
* boolean measureTiming // true if measureTiming is on
* Object trackDb // hash of trackDb entries for tracks which are visible on current page
* string highlight // highlight string, in format chrom#start#end#color|chrom2#start2#end2#color2|...
* string prevHlColor // the last highlight color that the user picked
*/
/* IE11 compatibility - IE doesn't have string startsWith and never will */
if (!String.prototype.startsWith) {
String.prototype.startsWith = function(searchString, position) {
position = position || 0;
return this.indexOf(searchString, position) === position;
};
}
function initVars()
{ // There are various entry points, so we call initVars in several places to make sure all is well
if (typeof(hgTracks) !== "undefined" && !genomePos.original) {
// remember initial position and size so we can restore it if user cancels
genomePos.original = genomePos.getOriginalPos();
genomePos.originalSize = $('#size').text().replace(/,/g, ""); // strip out any commas
dragSelect.originalCursor = jQuery('body').css('cursor');
if (typeof (igv) !== "undefined") {
igv.initIgvUcsc();
}
imageV2.imgTbl = $('#imgTbl');
// imageV2.enabled === true unless: advancedJavascript===false, or trackSearch, or config pg
imageV2.enabled = (imageV2.imgTbl && imageV2.imgTbl.length > 0);
// jQuery load function with stuff to support drag selection in track img
if (theClient.isSafari()) {
// Safari has the following bug: if we update the hgTracks map dynamically,
// the browser ignores the changes (even though if you look in the DOM the changes
// are there). So we have to do a full form submission when the user changes
// visibility settings or track configuration.
// As of 5.0.4 (7533.20.27) this is problem still exists in safari.
// As of 5.1 (7534.50) this problem appears to have been fixed - unfortunately,
// logs for 7/2011 show vast majority of safari users are pre-5.1 (5.0.5 is by far
// the most common).
//
// Early versions of Chrome had this problem too, but this problem went away
// as of Chrome 5.0.335.1 (or possibly earlier).
//
// KRR/JAT 2/2016:
// This Safari issue is likely resolved in all current versions. However the test
// for version had been failing, likely for some time now.
// (As of 9.0.9, possibly earlier, the 3rd part of the version is included in the
// user agent string, so must be accounted for in string match)
// Consequences were that page refresh was used instead of img update (e.g.
// for drag-zoom). And UI dialog was unable to update (e.g. via Apply button).
imageV2.mapIsUpdateable = false;
var reg = new RegExp("Version\/([0-9]+.[0-9]+)(.[0-9]+)? Safari");
var a = reg.exec(navigator.userAgent);
if (a && a[1]) {
var version = Number(a[1]);
if (version >= 5.1) {
imageV2.mapIsUpdateable = true;
}
}
}
imageV2.inPlaceUpdate = hgTracks.inPlaceUpdate && imageV2.mapIsUpdateable;
}
}
/////////////////////////////////////
////////// Genomic position /////////
/////////////////////////////////////
var genomePos = {
original: null,
originalSize: 0,
linkFixup: function (pos, id, reg, endParamName)
{ // fixup external links (e.g. ensembl)
var ele = $(document.getElementById(id));
if (ele.length) {
var link = ele.attr('href');
var a = reg.exec(link);
if (a && a[1]) {
ele.attr('href', a[1] + pos.start + "&" + endParamName + "=" + pos.end);
}
}
},
setByCoordinates: function (chrom, start, end)
{
var newPosition = chrom + ":" + start + "-" + end;
genomePos.set(newPosition, end - start + 1);
return newPosition;
},
getElement: function ()
{
// Return position box object
var tags = document.getElementsByName("position");
// There are multiple tags with name === "position" (the visible position text input
// and a hidden with id='positionHidden'); we return value of visible element.
for (var i = 0; i < tags.length; i++) {
var ele = tags[i];
if (ele.id !== "positionHidden") {
return ele;
}
}
return null;
},
get: function ()
{
// Return current value of position box
var ele = genomePos.getElement();
if (ele) {
return ele.value;
}
return null;
},
getOriginalPos: function ()
{
return genomePos.original || genomePos.get();
},
revertToOriginalPos: function ()
{
// undo changes to position (i.e. after user aborts a drag-and-select).
this.set(this.original, this.originalSize);
},
undisguisePosition: function(position) // UN-DISGUISE VMODE
{ // find the virt position
// position should be real chrom span
var pos = parsePosition(position);
if (!pos)
return position; // some parsing error, return original
var start = pos.start - 1;
var end = pos.end;
var chromName = hgTracks.windows[0].chromName;
if (pos.chrom !== chromName)
return position; // return original
var newStart = -1;
var newEnd = -1;
var lastW = null;
var windows = null;
for (j=0; j < 3; ++j) {
if (j === 0) windows = hgTracks.windowsBefore;
if (j === 1) windows = hgTracks.windows;
if (j === 2) windows = hgTracks.windowsAfter;
for (i=0,len=windows.length; i < len; ++i) {
var w = windows[i];
// double check chrom is same thoughout all windows, otherwise warning, return original value
if (w.chromName != chromName) {
return position; // return original
}
// check that the regions are ascending and non-overlapping
if (lastW && w.winStart < lastW.winEnd) {
return position; // return original
}
// overlap with position?
// if intersection,
if (w.winEnd > start && end > w.winStart) {
var s = Math.max(start, w.winStart);
var e = Math.min(end, w.winEnd);
var cs = s - w.winStart + w.virtStart;
var ce = e - w.winStart + w.virtStart;
if (newStart === -1)
newStart = cs;
newEnd = ce;
}
lastW = w;
}
}
// return new virt undisguised position as a string
var newPos = "multi:" + (newStart+1) + "-" + newEnd;
return newPos;
},
disguiseSize: function(position) // DISGUISE VMODE
{ // find the real size of the windows spanned
// position should be a real chrom span
var pos = parsePosition(position);
if (!pos)
return 0;
var start = pos.start - 1;
var end = pos.end;
var newSize = 0;
var windows = null;
for (j=0; j < 3; ++j) {
if (j === 0) windows = hgTracks.windowsBefore;
if (j === 1) windows = hgTracks.windows;
if (j === 2) windows = hgTracks.windowsAfter;
for (i=0,len=windows.length; i < len; ++i) {
var w = windows[i];
// overlap with position?
// if intersection,
if (w.winEnd > start && end > w.winStart) {
var s = Math.max(start, w.winStart);
var e = Math.min(end, w.winEnd);
newSize += (e - s);
}
}
}
// return real size of the disguised position
return newSize;
},
disguisePosition: function(position) // DISGUISE VMODE
{ // find the single-chrom range spanned
// position should be virt
var pos = parsePosition(position);
if (!pos)
return position; // some parsing error, return original
var start = pos.start - 1;
var end = pos.end;
var chromName = hgTracks.windows[0].chromName;
var newStart = -1;
var newEnd = -1;
var lastW = null;
var windows = null;
for (j=0; j < 3; ++j) {
if (j === 0) windows = hgTracks.windowsBefore;
if (j === 1) windows = hgTracks.windows;
if (j === 2) windows = hgTracks.windowsAfter;
for (i=0,len=windows.length; i < len; ++i) {
var w = windows[i];
// double check chrom is same thoughout all windows, otherwise warning, return original value
if (w.chromName != chromName) {
return position; // return undisguised original
}
// check that the regions are ascending and non-overlapping
if (lastW && w.winStart < lastW.winEnd) {
return position; // return undisguised original
}
// overlap with position?
// if intersection,
if (w.virtEnd > start && end > w.virtStart) {
var s = Math.max(start, w.virtStart);
var e = Math.min(end, w.virtEnd);
var cs = s - w.virtStart + w.winStart;
var ce = e - w.virtStart + w.winStart;
if (newStart === -1)
newStart = cs;
newEnd = ce;
}
lastW = w;
}
}
// return new non-virt disguised position as a string
var newPos = chromName + ":" + (newStart+1) + "-" + newEnd;
return newPos;
},
set: function (position, size)
{ // Set value of position and size (in hiddens and input elements).
// We assume size has already been commified.
// Either position or size may be null.
// stack dump // DEBUG
//console.trace();
// NOT work on safari
//var obj = {};
//Error.captureStackTrace(obj);
//warn("genomePos.set() called "+obj.stack);
position = position.replace(/,/g, ""); // strip out any commas
position.replace("virt:", "multi:");
if (position) {
// DISGUISE VMODE
//warn("genomePos.set() called, position = "+position);
if (hgTracks.virtualSingleChrom && (position.search("multi:")===0)) {
var newPosition = genomePos.disguisePosition(position);
//warn("genomePos.set() position = "+position+", newPosition = "+newPosition);
position = newPosition;
}
}
if (position) {
// There are multiple tags with name === "position"
// (one in TrackHeaderForm and another in TrackForm).
var tags = document.getElementsByName("position");
for (var i = 0; i < tags.length; i++) {
var ele = tags[i];
ele.value = position;
}
}
var pos = parsePosition(position);
if ($('#positionDisplay').length) {
// add commas to positionDisplay
var commaPosition = position;
if (pos)
commaPosition = pos.chrom+":"+commify(pos.start)+"-"+commify(pos.end);
$('#positionDisplay').text(commaPosition);
}
if (size) {
if (hgTracks.virtualSingleChrom && (position.search("multi:")!==0)) {
var newSize = genomePos.disguiseSize(position);
//warn("genomePos.set() position = "+position+", newSize = "+newSize);
if (newSize > 0)
size = newSize;
}
$('#size').text(commify(size)); // add commas
}
if (pos) {
// fixup external static links on page'
// Example ensembl link:
// http://www.ensembl.org/Homo_sapiens/contigview?chr=21&start=33031934&end=33041241
genomePos.linkFixup(pos, "ensemblLink", new RegExp("(.+start=)[0-9]+"), "end");
// Example NCBI Map Viewer link (obsolete):
// https://www.ncbi.nlm.nih.gov/mapview/maps.cgi?taxid=9606&CHR=21&BEG=33031934&END=33041241
genomePos.linkFixup(pos, "ncbiLink", new RegExp("(.+BEG=)[0-9]+"), "END");
// Example NCBI Genome Data Viewer link
// https://www.ncbi.nlm.nih.gov/genome/gdv/browser/?id=GCF_000001405.37&chr=4&from=45985744&to=45991655&context=genome
genomePos.linkFixup(pos, "ncbiLink", new RegExp("(.+from=)[0-9]+"), "to");
// Example medaka link:
// http://utgenome.org/medakabrowser_ens_jump.php?revision=version1.0&chr=chromosome18&start=14435198&end=14444829
genomePos.linkFixup(pos, "medakaLink", new RegExp("(.+start=)[0-9]+"), "end");
var link;
var reg;
var a;
if ($('#wormbaseLink').length) {
// e.g. http://www.wormbase.org/db/gb2/gbrowse/c_elegans?name=II:14646301-14667800
link = $('#wormbaseLink').attr('href');
reg = new RegExp("(.+:)[0-9]+");
a = reg.exec(link);
if (a && a[1]) {
$('#wormbaseLink').attr('href', a[1] + pos.start + "-" + pos.end);
}
}
// Fixup DNA link; e.g.:
// hgc?hgsid=2999470&o=114385768&g=getDna&i=mixed&c=chr7&l=114385768&r=114651696&db=panTro2&hgsid=2999470
if ($('#dnaLink').length) {
link = $('#dnaLink').attr('href');
reg = new RegExp("(.+&o=)[0-9]+.+&db=[^&]+(.*)");
a = reg.exec(link);
if (a && a[1]) {
var url = a[1] + (pos.start - 1) + "&g=getDna&i=mixed&c=" + pos.chrom;
url += "&l=" + (pos.start - 1) + "&r=" + pos.end + "&db=" + getDb() + a[2];
$('#dnaLink').attr('href', url);
}
}
}
if (!imageV2.backSupport)
imageV2.markAsDirtyPage();
},
getXLimits : function(img, slop) {
// calculate the min/max x position for drag-select, such that user cannot drag into the label area
var imgWidth = jQuery(img).width();
var imgOfs = jQuery(img).offset();
var leftX = hgTracks.revCmplDisp ? imgOfs.left - slop :
imgOfs.left + hgTracks.insideX - slop;
var rightX = hgTracks.revCmplDisp ? imgOfs.left + imgWidth - hgTracks.insideX + slop :
imgOfs.left + imgWidth + slop;
return [leftX, rightX];
},
check: function (img, selection)
{ // return true if user's selection is still w/n the img (including some slop).
var imgWidth = jQuery(img).width();
var imgHeight = jQuery(img).height();
var imgOfs = jQuery(img).offset();
var slop = 10;
// No need to check the x limits anymore, as imgAreaSelect is doing that now.
return ( (selection.event.pageY >= (imgOfs.top - slop))
&& (selection.event.pageY < (imgOfs.top + imgHeight + slop)));
},
pixelsToBases: function (img, selStart, selEnd, winStart, winEnd, addHalfBp)
{ // Convert image coordinates to chromosome coordinates
var imgWidth = jQuery(img).width() - hgTracks.insideX;
var width = hgTracks.winEnd - hgTracks.winStart;
var mult = width / imgWidth; // mult is bp/pixel multiplier
// where does a bp position start on the screen?
// For things like drag-select, if the user ends just before the nucleotide itself, do not count
// the nucleotide itself as selected. But for things like clicks onto
// a selection, if the user right-clicks just before the middle of the
// nucleotide, we certainly want to use this position.
var halfBpWidth = 0;
if (addHalfBp)
halfBpWidth = (imgWidth / width) / 2; // how many pixels does one bp take up;
var startDelta; // startDelta is how many bp's to the right/left
var x1;
// The magic number three appear at another place in the code
// as LEFTADD. It was originally annotated as "borders or cgi item calc
// ?" by Larry. It has to be used when going any time when converting
// between pixels and coordinates.
selStart -= 3;
selEnd -= 3;
if (hgTracks.revCmplDisp) {
x1 = Math.min(imgWidth, selStart);
startDelta = Math.floor(mult * (imgWidth - x1 - halfBpWidth));
} else {
x1 = Math.max(hgTracks.insideX, selStart);
startDelta = Math.floor(mult * (x1 - hgTracks.insideX + halfBpWidth));
}
var endDelta;
var x2;
if (hgTracks.revCmplDisp) {
endDelta = startDelta;
x2 = Math.min(imgWidth, selEnd);
startDelta = Math.floor(mult * (imgWidth - x2 + halfBpWidth));
} else {
x2 = Math.max(hgTracks.insideX, selEnd);
endDelta = Math.floor(mult * (x2 - hgTracks.insideX - halfBpWidth));
}
var newStart = hgTracks.winStart + startDelta;
var newEnd = hgTracks.winStart + 1 + endDelta;
// if user selects space between two bases, start>end can happen
if (newStart >= newEnd)
newStart = newEnd-1;
if (newEnd > winEnd) {
newEnd = winEnd;
}
return {chromStart : newStart, chromEnd : newEnd};
},
chromToVirtChrom: function (chrom, chromStart, chromEnd)
{ // Convert regular chromosome position to virtual chrom coordinates using hgTracks.windows list
// Consider the first contiguous set of overlapping regions to define the match (for now).
// only works for regions covered by the current hgTracks.windows
var virtStart = -1, virtEnd = -1;
var s,e;
var i, len;
for (i = 0, len = hgTracks.windows.length; i < len; ++i) {
var w = hgTracks.windows[i];
var overlap = (chrom == w.chromName && chromEnd > w.winStart && w.winEnd > chromStart);
if (virtStart == -1) {
if (overlap) {
// when they overlap the first time
s = Math.max(chromStart, w.winStart);
e = Math.min(chromEnd, w.winEnd);
virtStart = w.virtStart + (s - w.winStart);
virtEnd = w.virtStart + (e - w.winStart);
} else {
// until they overlap
// do nothing
}
} else {
if (overlap) {
// while they continue to overlap, extend
e = Math.min(chromEnd, w.winEnd);
virtEnd = w.virtStart + (e - w.winStart);
} else {
// when they do not overlap anymore, stop
break;
}
}
}
return {chromStart : virtStart, chromEnd : virtEnd};
},
selectionPixelsToBases: function (img, selection)
{ // Convert selection x1/x2 coordinates to chromStart/chromEnd.
return genomePos.pixelsToBases(img, selection.x1, selection.x2,
hgTracks.winStart, hgTracks.winEnd, true);
},
update: function (img, selection, singleClick)
{
var pos = genomePos.pixelsToBases(img, selection.x1, selection.x2,
hgTracks.winStart, hgTracks.winEnd, true);
// singleClick is true when the mouse hasn't moved (or has only moved a small amount).
if (singleClick) {
var center = (pos.chromStart + pos.chromEnd)/2;
pos.chromStart = Math.floor(center - hgTracks.newWinWidth/2);
pos.chromEnd = pos.chromStart + hgTracks.newWinWidth;
// clip
if (pos.chromStart < hgTracks.chromStart)
pos.chromStart = hgTracks.chromStart; // usually 1
if (pos.chromEnd > hgTracks.chromEnd)
pos.chromEnd = hgTracks.chromEnd; // usually virt chrom size
// save current position so that that it may be restored after highlight or cancel.
genomePos.original = genomePos.getOriginalPos();
genomePos.originalSize = $('#size').text().replace(/,/g, ""); // strip out any commas
}
var newPosition = genomePos.setByCoordinates(hgTracks.chromName,
pos.chromStart+1, pos.chromEnd);
return newPosition;
},
handleChange: function (response, status)
{
var json = JSON.parse(response);
genomePos.set(json.pos);
},
changeAssemblies: function (ele) // UNUSED? Larry's experimental code
{ // code to update page when user changes assembly select list.
$.ajax({
type: "GET",
url: "../cgi-bin/hgApi",
data: cart.varsToUrlData({ 'hgsid': getHgsid(), 'cmd': 'defaultPos', 'db': getDb() }),
dataType: "html",
trueSuccess: genomePos.handleChange,
success: catchErrorOrDispatch,
error: errorHandler,
cache: true
});
return false;
},
convertedVirtCoords : {chromStart : -1, chromEnd : -1},
handleConvertChromPosToVirtCoords: function (response, status)
{
var virtStart = -1, virtEnd = -1;
var newJson = scrapeVariable(response, "convertChromToVirtChrom");
if (!newJson) {
warn("convertChromToVirtChrom object is missing from the response");
} else {
virtStart = newJson.virtWinStart;
virtEnd = newJson.virtWinEnd;
}
genomePos.convertedVirtCoords = {chromStart : virtStart, chromEnd : virtEnd};
},
convertChromPosToVirtCoords: function (chrom, chromStart, chromEnd)
{ // code to convert chrom position to virt coords
genomePos.convertedVirtCoords = {chromStart : -1, chromEnd : -1}; // reset
var pos = chrom+":"+(chromStart+1)+"-"+chromEnd; // easier to pass 1 parameter than 3
$.ajax({
type: "GET",
async: false, // wait for result
url: "../cgi-bin/hgTracks",
data: cart.varsToUrlData({ 'hgt.convertChromToVirtChrom': pos, 'hgt.trackImgOnly' : 1, 'hgsid': getHgsid(), 'db': getDb() }),
dataType: "html",
trueSuccess: genomePos.handleConvertChromPosToVirtCoords,
success: catchErrorOrDispatch,
error: errorHandler,
cache: false
});
return genomePos.convertedVirtCoords;
},
positionDisplayDialog: function ()
// Show the virtual and real positions of the windows
{
var position = genomePos.get();
position.replace("virt:", "multi:");
var positionDialog = $("#positionDialog")[0];
if (!positionDialog) {
$("body").append("
");
positionDialog = $("#positionDialog")[0];
}
if (hgTracks.windows) {
var i, len, end;
var matches = /^multi:[0-9]+-([0-9]+)/.exec(position);
var modeType = (hgTracks.virtModeType === "customUrl" ? "Custom regions on virtual chromosome" :
(hgTracks.virtModeType === "exonMostly" ? "Exon view of" :
(hgTracks.virtModeType === "geneMostly" ? "Gene view of" :
(hgTracks.virtModeType === "singleAltHaplo" ? "Alternate haplotype as virtual chromosome" :
"Unknown mode"))));
var str = modeType + " " + position;
if (matches) {
end = matches[1];
if (end < hgTracks.chromEnd) {
str += ". Full virtual region is multi:1-" + hgTracks.chromEnd + ". Zoom out to view.";
}
}
if (!(hgTracks.virtualSingleChrom && (hgTracks.windows.length === 1))) {
var w;
if (hgTracks.windows.length <= 10) {
str += "
\n";
for (i=0,len=hgTracks.windows.length; i < len; ++i) {
w = hgTracks.windows[i];
str += "
\n";
}
}
$("#positionDisplayPosition").html(str);
} else {
$("#positionDisplayPosition").html(position);
}
$(positionDialog).dialog({
modal: true,
title: "Multi-Region Position Ranges",
closeOnEscape: true,
resizable: false,
autoOpen: false,
minWidth: 400,
minHeight: 40,
close: function() {
// All exits to dialog should go through this
$(imageV2.imgTbl).imgAreaSelect({hide:true});
$(this).hide();
$('body').css('cursor', ''); // Occasionally wait cursor got left behind
}
});
$(positionDialog).dialog('open');
}
};
/////////////////////////////////////
//// Creating items /////
/////////////////////////////////////
var myVariants = {
createBedForm: function(dialogEle) {
// Shared top fields (visible in all manual modes)
const commonTopFields = [
{ label: "Label", id: "name", type: "text", placeholder: "Optional item label",
info: "A short label for this annotation, displayed in the browser" },
{ label: "Color", id: "color", type: "text" },
];
// SNV-only field (between commonTop and alt/sequence)
const snvOnlyFields = [
{ label: "Ref", id: "ref", type: "text", placeholder: "Optional reference allele sequence",
info: "Reference allele sequence at this position" },
];
// Alt/Sequence field: shown for SNV (label="Alt") and CNV (label="Sequence")
const altField = { label: "Alt", id: "alt", type: "text",
placeholder: "Optional alternate allele sequence",
info: "Alternate (variant) allele sequence" };
// Position fields: shown always for Transcript; collapsible for SNV/CNV
const positionFields = [
{ label: "Chromosome", id: "chrom", type: "text" },
{ label: "Start", id: "start", type: "number",
info: "1-based start position on the chromosome" },
{ label: "End", id: "end", type: "number",
info: "1-based end position on the chromosome (inclusive)" },
{ label: "Score", id: "score", type: "number",
info: "Score from 0-1000. Higher scores display darker" },
{ label: "Strand", id: "strand", type: "text",
info: "Strand: + for forward, - for reverse, . for unknown" },
];
// Transcript-only CDS fields (displayed as CDS Start/End; stored as thickStart/thickEnd)
const cdsFields = [
{ label: "CDS Start", id: "thickStart", type: "number",
info: "Start of the coding region" },
{ label: "CDS End", id: "thickEnd", type: "number",
info: "End of the coding region" },
];
// Shared bottom fields
const commonBottomFields = [
{ label: "Mouseover", id: "mouseover", type: "text", placeholder: "Short text shown on hover",
info: "Short text shown when hovering over this item. If empty, the label and alleles are displayed" },
{ label: "Description", id: "description", type: "textarea", placeholder: "Longer description/notes",
info: "Longer notes or comments about this annotation. Displayed on the details page" },
{ label: "Project", id: "project", type: "text", placeholder: "Optional project name",
info: "Group annotations by project. Projects with no annotations are automatically removed from the list" },
];
// CNV vocabulary, mirrored on the server in myVariantsTrack.c:validCnvTypes
const cnvTypeOptions = ["deletion", "duplication", "insertion", "inversion",
"translocation", "complex", "breakend"];
const form = document.createElement("form");
form.className = "myVariants-form";
form.action = "hgTracks";
form.method = "post";
form.id = "myVariants-form";
// First: HGVS/position input box (primary interaction method)
let quickInpDiv = document.createElement("div");
quickInpDiv.id = "hgvsInputDiv";
form.appendChild(quickInpDiv);
let quickInp = document.createElement("input");
let quickInpLabel = document.createElement("label");
quickInp.type = "text";
quickInp.id = "hgvsInput";
quickInp.name = "myVariantsHgvsInput";
quickInp.style.width = "300px";
quickInpLabel.textContent = "Enter HGVS, position, or gene symbol";
quickInpLabel.style.display = "inline-block";
quickInpLabel.style.minWidth = "200px";
quickInpLabel.style.marginRight = "8px";
quickInpLabel.for = "hgvsInput";
let resizeDialog = function() {
var dialog = $("#myVariantsDialog");
var hgvsVisible = document.getElementById("hgvsInputDiv").style.display !== "none";
if (hgvsVisible) {
dialog.dialog("option", "width", 580);
dialog.dialog("option", "height", "auto");
dialog.dialog("option", "position", { my: "center", at: "center", of: window });
} else {
dialog.dialog("option", "width", Math.min(1000, window.innerWidth * 0.9));
dialog.dialog("option", "height", Math.min(800, window.innerHeight * 0.9));
dialog.dialog("option", "position", { my: "top+30", at: "top", of: window });
}
};
let updatePositionSummary = function() {
let summaryText = document.getElementById("positionSummaryText");
if (!summaryText) return;
let chrom = document.getElementById("chrom");
let start = document.getElementById("start");
let end = document.getElementById("end");
if (chrom && start && end) {
let startVal = parseInt(start.value);
let endVal = parseInt(end.value);
let startFmt = isNaN(startVal) ? start.value : startVal.toLocaleString();
let endFmt = isNaN(endVal) ? end.value : endVal.toLocaleString();
summaryText.textContent = chrom.value + ":" + startFmt + "-" + endFmt;
}
};
// Switch the form into one of four modes and remember the choice so the
// next dialog open restarts in the same place.
let applyMode = function(mode) {
const isHgvs = mode === "hgvs";
const isTranscript = mode === "transcript";
const isSnv = mode === "snv";
const isCnv = mode === "cnv";
document.getElementById("hgvsInputDiv").style.display = isHgvs ? "" : "none";
document.getElementById("manualInputDiv").style.display = isHgvs ? "none" : "";
document.getElementById("hgvsManualToggle").textContent =
isHgvs ? "Or edit item fields manually" : "Back to quick input mode";
if (!isHgvs) {
document.getElementById("typeTranscript").checked = isTranscript;
document.getElementById("typeSnv").checked = isSnv;
document.getElementById("typeCnv").checked = isCnv;
}
document.getElementById("refWrapper").style.display = isSnv ? "" : "none";
document.getElementById("altWrapper").style.display = (isSnv || isCnv) ? "" : "none";
document.getElementById("altLabel").textContent = isCnv ? "Sequence" : "Alt";
document.getElementById("cnvTypeWrapper").style.display = isCnv ? "" : "none";
if (isTranscript) {
advancedDiv.style.display = "none";
cdsBlocksDiv.style.display = "none";
posWraps.forEach(function(w) { w.style.display = ""; });
cdsWraps.forEach(function(w) { w.style.display = ""; });
blocksSection.style.display = "";
const order = [
typeRadioBar, posSummary,
posWraps[0], posWraps[1], posWraps[2],
nameWrap,
posWraps[3], posWraps[4],
cdsWraps[0], cdsWraps[1],
colorWrap,
mouseoverWrap, descriptionWrap, projectWrap,
blocksSection,
advancedDiv, cdsBlocksDiv,
refWrapper, cnvTypeWrapper, altWrapper,
customFieldsSection
];
order.forEach(function(el) { manualInpDiv.appendChild(el); });
} else {
// Pack wrappers back into their collapsible containers.
posWraps.forEach(function(w) { advancedDiv.appendChild(w); });
cdsWraps.forEach(function(w) { cdsBlocksDiv.appendChild(w); });
cdsBlocksDiv.appendChild(blocksSection);
advancedDiv.style.display = "none";
cdsBlocksDiv.style.display = "none";
const order = [
typeRadioBar, posSummary,
advancedDiv,
nameWrap, colorWrap,
refWrapper, cnvTypeWrapper, altWrapper,
cdsBlocksDiv,
mouseoverWrap, descriptionWrap, projectWrap,
customFieldsSection
];
order.forEach(function(el) { manualInpDiv.appendChild(el); });
}
document.getElementById("positionSummaryDiv").style.display = isTranscript ? "none" : "";
if (posEditLink) posEditLink.textContent = "[edit]";
try { localStorage.setItem("myVariantsLastMode", mode); } catch (e) { /* private mode */ }
resizeDialog();
};
let currentType = function() {
if (document.getElementById("typeTranscript").checked) return "transcript";
if (document.getElementById("typeCnv").checked) return "cnv";
return "snv";
};
let toggleForm = function(event) {
event.preventDefault();
const inHgvs = document.getElementById("hgvsInputDiv").style.display !== "none";
applyMode(inHgvs ? currentType() : "hgvs");
};
let toggleContainer = document.createElement("p");
let toggle = document.createElement("a");
toggle.href = "#";
toggle.id = "hgvsManualToggle";
toggle.addEventListener("click", toggleForm);
toggle.textContent = "Or edit item fields manually";
toggleContainer.appendChild(toggle);
quickInpDiv.appendChild(quickInpLabel);
quickInpDiv.appendChild(quickInp);
form.appendChild(toggleContainer);
// Manual input div (contains simple + advanced fields)
let manualInpDiv = document.createElement("div");
manualInpDiv.id = "manualInputDiv";
manualInpDiv.style.display = "none";
let posSummary = document.createElement("div");
posSummary.id = "positionSummaryDiv";
posSummary.style.cssText = "margin-bottom:12px; padding:6px 10px; background:#f0f4f8; border:1px solid #d0d7de; border-radius:4px; font-size:13px;";
let posSummaryLabel = document.createElement("b");
posSummaryLabel.textContent = "Position: ";
posSummary.appendChild(posSummaryLabel);
let posSummaryText = document.createElement("span");
posSummaryText.id = "positionSummaryText";
posSummary.appendChild(posSummaryText);
let posSummarySuffix = document.createElement("span");
posSummarySuffix.style.color = "#888";
posSummarySuffix.textContent = " (from current view)";
posSummary.appendChild(posSummarySuffix);
let posEditLink = document.createElement("a");
posEditLink.href = "#";
posEditLink.textContent = "[edit]";
posEditLink.style.cssText = "margin-left:8px; font-size:12px;";
posEditLink.addEventListener("click", function(event) {
event.preventDefault();
let advDiv = document.getElementById("advancedFieldsDiv");
if (!advDiv) return;
if (advDiv.style.display === "none") {
advDiv.style.display = "";
posEditLink.textContent = "[hide]";
let chromField = document.getElementById("chrom");
if (chromField) {
chromField.scrollIntoView({behavior: "smooth", block: "nearest"});
chromField.focus();
}
} else {
advDiv.style.display = "none";
posEditLink.textContent = "[edit]";
}
});
posSummary.appendChild(posEditLink);
manualInpDiv.appendChild(posSummary);
// Helper function to create form field (uses createInfoIcon from utils.js)
let createField = function(field, container) {
const wrapper = document.createElement("div");
wrapper.style.marginBottom = "8px";
const label = document.createElement("label");
label.htmlFor = field.id;
label.textContent = field.label;
label.style.display = "inline-block";
label.style.minWidth = "140px";
let input;
if (field.type === "textarea") {
input = document.createElement("textarea");
input.rows = "4";
input.cols = "60";
} else {
input = document.createElement("input");
input.type = field.type;
}
input.id = field.id;
if (field.id === "chrom") {
input.value = hgTracks.chromName;
}
if (field.type === "number") {
input.min = 0;
if (field.id === "start" || field.id === "thickStart") {
input.value = hgTracks.winStart;
}
if (field.id === "end" || field.id === "thickEnd") {
input.value = hgTracks.winEnd;
}
if (field.id === "score") {
input.value = 0;
input.max = 1000;
}
}
if (field.id === "strand") {
input.value = ".";
}
if (field.id === "color") {
input.value = "#000000";
input.style.width = "70px";
}
if (field.placeholder) {
input.placeholder = field.placeholder;
}
wrapper.appendChild(label);
wrapper.appendChild(input);
if (field.info) {
wrapper.appendChild(createInfoIcon(field.info));
}
container.appendChild(wrapper);
return input;
};
// Helper function to create project field with dropdown if projects exist
let createProjectField = function(container) {
const wrapper = document.createElement("div");
wrapper.style.marginBottom = "8px";
const label = document.createElement("label");
label.htmlFor = "project";
label.textContent = "Project";
label.style.display = "inline-block";
label.style.minWidth = "140px";
wrapper.appendChild(label);
// Check if we have existing projects from the server
let existingProjects = (typeof hgTracks !== 'undefined' && hgTracks.myVariantsProjects)
? hgTracks.myVariantsProjects : [];
if (existingProjects.length > 0) {
// Create dropdown with existing projects
let select = document.createElement("select");
select.id = "projectSelect";
select.style.marginRight = "8px";
// Add empty option
let emptyOpt = document.createElement("option");
emptyOpt.value = "";
emptyOpt.textContent = "(none)";
select.appendChild(emptyOpt);
// Add existing projects
existingProjects.forEach(proj => {
let opt = document.createElement("option");
opt.value = proj;
opt.textContent = proj;
select.appendChild(opt);
});
// Add "Add new..." option
let newOpt = document.createElement("option");
newOpt.value = "__new__";
newOpt.textContent = "Add new...";
select.appendChild(newOpt);
wrapper.appendChild(select);
// Hidden text input for new project (shown when "Add new..." selected)
let newProjectInput = document.createElement("input");
newProjectInput.type = "text";
newProjectInput.id = "project";
newProjectInput.placeholder = "Enter new project name";
newProjectInput.style.display = "none";
wrapper.appendChild(newProjectInput);
// Toggle between dropdown and text input
select.addEventListener("change", function() {
if (select.value === "__new__") {
newProjectInput.style.display = "";
newProjectInput.focus();
} else {
newProjectInput.style.display = "none";
newProjectInput.value = select.value;
}
});
// Keep text input synced with dropdown selection
newProjectInput.addEventListener("blur", function() {
if (newProjectInput.value === "" && select.value === "__new__") {
select.value = "";
newProjectInput.style.display = "none";
}
});
} else {
// No existing projects - just show text input
let input = document.createElement("input");
input.type = "text";
input.id = "project";
input.placeholder = "Optional project name";
wrapper.appendChild(input);
}
// Add info icon
wrapper.appendChild(createInfoIcon("Group annotations by project. Projects with no annotations are automatically removed from the list"));
container.appendChild(wrapper);
};
// Type radio bar: Transcript | Variant -> Short | CNV. Inserted at the very
// top of manualInpDiv (before the existing position summary).
let typeRadioBar = document.createElement("div");
typeRadioBar.id = "typeRadioBar";
typeRadioBar.style.cssText = "margin-bottom:12px; padding:8px 10px; background:#f7f7f7; border:1px solid #ddd; border-radius:4px;";
typeRadioBar.innerHTML =
"
Annotation type
" +
"
" +
"" +
"" +
"" +
"
";
manualInpDiv.insertBefore(typeRadioBar, manualInpDiv.firstChild);
let onTypeRadioChange = function() {
applyMode(currentType());
};
// The form is not yet in the document, so query the local subtree.
typeRadioBar.querySelectorAll("input[type=radio]").forEach(function(r) {
r.addEventListener("change", onTypeRadioChange);
});
let colorInput = null;
let nameWrap, colorWrap;
commonTopFields.forEach(function(field) {
let input = createField(field, manualInpDiv);
if (field.id === "color") { colorInput = input; colorWrap = input.parentNode; }
else if (field.id === "name") { nameWrap = input.parentNode; }
});
// SNV-only ref input
let refWrapper = document.createElement("div");
refWrapper.id = "refWrapper";
createField(snvOnlyFields[0], refWrapper);
manualInpDiv.appendChild(refWrapper);
// CNV-only type select
let cnvTypeWrapper = document.createElement("div");
cnvTypeWrapper.id = "cnvTypeWrapper";
cnvTypeWrapper.style.marginBottom = "8px";
let cnvTypeLabel = document.createElement("label");
cnvTypeLabel.htmlFor = "cnvType";
cnvTypeLabel.textContent = "CNV type";
cnvTypeLabel.style.cssText = "display:inline-block; min-width:140px;";
let cnvTypeSelect = document.createElement("select");
cnvTypeSelect.id = "cnvType";
cnvTypeOptions.forEach(function(t) {
let opt = document.createElement("option");
opt.value = t;
opt.textContent = t;
cnvTypeSelect.appendChild(opt);
});
cnvTypeWrapper.appendChild(cnvTypeLabel);
cnvTypeWrapper.appendChild(cnvTypeSelect);
cnvTypeWrapper.appendChild(createInfoIcon("CNV vocabulary follows gnomAD"));
manualInpDiv.appendChild(cnvTypeWrapper);
// Alt / Sequence input (shared between SNV and CNV; label switches in applyMode)
let altWrapper = document.createElement("div");
altWrapper.id = "altWrapper";
altWrapper.style.marginBottom = "8px";
let altLabel = document.createElement("label");
altLabel.htmlFor = "alt";
altLabel.id = "altLabel";
altLabel.textContent = "Alt";
altLabel.style.cssText = "display:inline-block; min-width:140px;";
let altInput = document.createElement("input");
altInput.type = "text";
altInput.id = "alt";
altInput.placeholder = altField.placeholder;
altWrapper.appendChild(altLabel);
altWrapper.appendChild(altInput);
altWrapper.appendChild(createInfoIcon(altField.info));
manualInpDiv.appendChild(altWrapper);
let advancedDiv = document.createElement("div");
advancedDiv.id = "advancedFieldsDiv";
advancedDiv.style.display = "none";
let posWraps = positionFields.map(function(field) {
return createField(field, advancedDiv).parentNode;
});
manualInpDiv.appendChild(advancedDiv);
let cdsBlocksDiv = document.createElement("div");
cdsBlocksDiv.id = "cdsBlocksDiv";
cdsBlocksDiv.style.display = "none";
let cdsWraps = cdsFields.map(function(field) {
return createField(field, cdsBlocksDiv).parentNode;
});
let blocksSection = document.createElement("div");
blocksSection.id = "blocksSection";
blocksSection.style.cssText = "margin-top:12px; padding-top:10px; border-top:1px solid #ddd;";
let blocksLabel = document.createElement("div");
blocksLabel.style.cssText = "font-weight:bold; margin-bottom:8px; font-size:13px;";
blocksLabel.textContent = "Blocks (optional)";
blocksSection.appendChild(blocksLabel);
let blocksHint = document.createElement("div");
blocksHint.style.cssText = "font-size:12px; color:#666; margin-bottom:6px;";
blocksHint.textContent = "Offsets are relative to Start. First offset must be 0; " +
"last block must reach End. Leave empty for a single full-span block.";
blocksSection.appendChild(blocksHint);
let blocksContainer = document.createElement("div");
blocksContainer.id = "blocksContainer";
blocksSection.appendChild(blocksContainer);
let hCount = document.createElement("input"); hCount.type = "hidden"; hCount.id = "blockCount";
let hSizes = document.createElement("input"); hSizes.type = "hidden"; hSizes.id = "blockSizes";
let hStarts = document.createElement("input"); hStarts.type = "hidden"; hStarts.id = "chromStarts";
blocksSection.appendChild(hCount);
blocksSection.appendChild(hSizes);
blocksSection.appendChild(hStarts);
cdsBlocksDiv.appendChild(blocksSection);
manualInpDiv.appendChild(cdsBlocksDiv);
let mouseoverWrap, descriptionWrap, projectWrap;
commonBottomFields.forEach(function(field) {
if (field.id === "project") {
createProjectField(manualInpDiv);
projectWrap = manualInpDiv.lastElementChild;
} else {
let input = createField(field, manualInpDiv);
if (field.id === "mouseover") mouseoverWrap = input.parentNode;
else if (field.id === "description") descriptionWrap = input.parentNode;
}
});
let customFieldsSection = document.createElement("div");
customFieldsSection.id = "customFieldsSection";
customFieldsSection.style.cssText = "margin-top:12px; padding-top:10px; border-top:1px solid #ddd;";
let customFieldsLabel = document.createElement("div");
customFieldsLabel.style.cssText = "font-weight:bold; margin-bottom:8px; font-size:13px;";
customFieldsLabel.textContent = "Custom Fields";
customFieldsSection.appendChild(customFieldsLabel);
let customFieldsList = document.createElement("div");
customFieldsList.id = "customFieldsList";
customFieldsSection.appendChild(customFieldsList);
let reservedNames = ["bin", "chrom", "chromStart", "chromEnd", "name", "score",
"strand", "thickStart", "thickEnd", "itemRgb", "blockCount", "blockSizes",
"chromStarts", "description", "db", "ref", "alt", "project", "mouseover",
"itemType", "cnvType", "id"];
let validateFieldName = function(nameInput) {
let name = nameInput.value.trim();
if (!name) return; // empty is ok, will be skipped on submit
let valid = /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name);
if (!valid) {
alert("Invalid field name: '" + name + "'. Must start with a letter or underscore, " +
"and contain only letters, numbers, and underscores.");
nameInput.focus();
return false;
}
if (name.startsWith("_hidden_")) {
alert("Field names cannot start with '_hidden_'.");
nameInput.focus();
return false;
}
if (reservedNames.indexOf(name) >= 0) {
alert("'" + name + "' is a reserved field name.");
nameInput.focus();
return false;
}
// Check for duplicates among all custom field name inputs
let allNames = document.querySelectorAll("#customFieldsList .customFieldName");
let count = 0;
allNames.forEach(function(inp) {
if (inp.value.trim() === name) count++;
});
if (count > 1) {
alert("Duplicate field name: '" + name + "'.");
nameInput.focus();
return false;
}
return true;
};
let addCustomFieldRow = function(existingName) {
// existingName: if provided, this is a pre-populated existing field (read-only name)
let row = document.createElement("div");
row.className = "customFieldRow";
row.style.cssText = "display:flex; align-items:center; gap:6px; margin-bottom:6px;";
if (existingName)
row.dataset.existing = "true";
let nameInput = document.createElement("input");
nameInput.type = "text";
nameInput.className = "customFieldName";
nameInput.placeholder = "Field name";
nameInput.style.cssText = "width:120px;";
if (existingName) {
nameInput.value = existingName;
nameInput.readOnly = true;
nameInput.style.cssText = "width:120px; background:#e8e8e8; color:#555;";
} else {
// Add blur validation for new field names
nameInput.addEventListener("blur", function() {
if (nameInput.value.trim())
validateFieldName(nameInput);
});
}
let valueInput = document.createElement("input");
valueInput.type = "text";
valueInput.className = "customFieldValue";
valueInput.placeholder = "Value (optional)";
valueInput.style.cssText = "width:180px;";
row.appendChild(nameInput);
row.appendChild(valueInput);
if (existingName) {
// Hide button for existing fields (soft-delete via _hidden_ prefix)
let hideBtn = document.createElement("button");
hideBtn.type = "button";
hideBtn.textContent = "Hide";
hideBtn.title = "Hide this custom field (data preserved, can be restored later)";
hideBtn.style.cssText = "border:1px solid #ccc; background:#f5f5f5; color:#888; font-size:11px; cursor:pointer; padding:1px 6px; border-radius:3px;";
hideBtn.addEventListener("click", function() {
if (!confirm("Hide the '" + existingName + "' field? Data will be preserved but the field will no longer appear."))
return;
// Send ALTER TABLE CHANGE COLUMN request to rename with _hidden_ prefix
let hideUrl = "../cgi-bin/hgTracks?hgt_doJsCommand=" +
encodeURIComponent("myVariants myVariants " + JSON.stringify({hideField: existingName})) +
"&hgt.trackImgOnly=1&hgt.ideogramToo=1&hgsid=" + getHgsid() + "&db=" + getDb();
fetch(hideUrl, { method: "POST", credentials: "same-origin" })
.then(function() {
row.remove();
})
.catch(function(err) {
alert("Error hiding field: " + err.message);
});
});
row.appendChild(hideBtn);
} else {
// Remove button for newly added rows
let removeBtn = document.createElement("button");
removeBtn.type = "button";
removeBtn.textContent = "\u00D7";
removeBtn.title = "Remove this field";
removeBtn.style.cssText = "border:none; background:none; color:#c00; font-size:18px; cursor:pointer; padding:0 4px; line-height:1;";
removeBtn.addEventListener("click", function() {
row.remove();
});
row.appendChild(removeBtn);
}
customFieldsList.appendChild(row);
if (!existingName)
nameInput.focus();
};
// Pre-populate existing custom fields from server
let existingCustomFields = (typeof hgTracks !== 'undefined' && hgTracks.myVariantsCustomFields)
? hgTracks.myVariantsCustomFields : [];
existingCustomFields.forEach(function(fieldName) {
addCustomFieldRow(fieldName);
});
let addBtn = document.createElement("button");
addBtn.type = "button";
addBtn.id = "addCustomFieldBtn";
addBtn.textContent = "+ Add Custom Field";
addBtn.style.cssText = "margin-top:4px; padding:3px 10px; font-size:12px; cursor:pointer;";
addBtn.addEventListener("click", function(event) {
event.preventDefault();
addCustomFieldRow();
});
customFieldsSection.appendChild(addBtn);
// Hidden fields restore UI
let hiddenFields = (typeof hgTracks !== 'undefined' && hgTracks.myVariantsHiddenFields)
? hgTracks.myVariantsHiddenFields : [];
if (hiddenFields.length > 0) {
let hiddenSection = document.createElement("div");
hiddenSection.id = "hiddenFieldsSection";
hiddenSection.style.cssText = "margin-top:8px; padding:4px 8px; background:#faf8f0; border:1px solid #e0dcc8; border-radius:3px; font-size:12px;";
let hiddenLabel = document.createElement("span");
hiddenLabel.style.cssText = "color:#888; margin-right:8px;";
hiddenLabel.textContent = "Hidden fields:";
hiddenSection.appendChild(hiddenLabel);
hiddenFields.forEach(function(fieldName) {
let chip = document.createElement("span");
chip.style.cssText = "display:inline-block; margin:2px 4px; padding:2px 6px; background:#e8e8e8; border-radius:3px;";
chip.textContent = fieldName + " ";
let restoreBtn = document.createElement("a");
restoreBtn.href = "#";
restoreBtn.textContent = "restore";
restoreBtn.style.cssText = "color:#36c; font-size:11px;";
restoreBtn.addEventListener("click", function(event) {
event.preventDefault();
// Add it back as a pre-populated existing field row
addCustomFieldRow(fieldName);
// Remove the chip
chip.remove();
// Hide the section if no more hidden fields
if (hiddenSection.querySelectorAll("span[style]").length <= 1)
hiddenSection.style.display = "none";
});
chip.appendChild(restoreBtn);
hiddenSection.appendChild(chip);
});
customFieldsSection.appendChild(hiddenSection);
}
manualInpDiv.appendChild(customFieldsSection);
// Keep the position summary in sync when position fields change
["chrom", "start", "end"].forEach(function(id) {
let el = advancedDiv.querySelector("#" + id);
if (el) el.addEventListener("input", updatePositionSummary);
});
let chromEl = advancedDiv.querySelector("#chrom");
let startEl = advancedDiv.querySelector("#start");
let endEl = advancedDiv.querySelector("#end");
if (chromEl && startEl && endEl) {
let startVal = parseInt(startEl.value);
let endVal = parseInt(endEl.value);
let startFmt = isNaN(startVal) ? startEl.value : startVal.toLocaleString();
let endFmt = isNaN(endVal) ? endEl.value : endVal.toLocaleString();
posSummaryText.textContent = chromEl.value + ":" + startFmt + "-" + endFmt;
}
form.appendChild(manualInpDiv);
// Add hidden field to encode form values as JSON
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = 'hgt_doJsCommand';
form.appendChild(hiddenInput);
const trackNameInput = document.createElement('input');
trackNameInput.type = 'hidden';
trackNameInput.id = 'trackName';
trackNameInput.name = 'trackName';
trackNameInput.value = "myVariants";
form.appendChild(trackNameInput);
dialogEle.appendChild(form);
// Initialize Spectrum color picker after form is added to DOM
if (colorInput) {
$(colorInput).spectrum({
hideAfterPaletteSelect: true,
color: colorInput.value,
showPalette: true,
showInput: true,
showSelectionPalette: true,
showInitial: true,
preferredFormat: "hex",
localStorageKey: "myVariantsColors"
});
}
// Mount the block editor; reads Start/End from the chrom range fields.
// Pass the container element directly: at this point the form is in
// the dialog div but the dialog hasn't been appended to body yet, so
// getElementById would still miss "blocksContainer".
// Stash the widget handle on the form so createItem can call validate().
if (typeof myVariantsBlocks !== "undefined") {
form.blocksWidget = myVariantsBlocks.mount(blocksContainer, {
getStart: function () {
return parseInt(document.getElementById("start").value, 10);
},
getEnd: function () {
return parseInt(document.getElementById("end").value, 10);
},
hiddenCountInput: hCount,
hiddenSizesInput: hSizes,
hiddenStartsInput: hStarts
});
}
// Stash applyMode on the form so init() can restore last-used mode.
form.applyMode = applyMode;
return form;
},
init: function () {
// show a jquery-ui dialog when a user clicks on the 'Add Annotation' button
let dialog = document.getElementById('myVariantsDialog');
if (!dialog) {
dialog = document.createElement("div");
dialog.id = "myVariantsDialog";
dialog.style = "display: none";
dialogButtons = {};
// Call the function to build the form, but only if logged in already
if (!userIsLoggedIn) {
let msg = document.createElement("div");
msg.id = "logInMessage";
let href = (typeof myVariantsLoginUrl !== "undefined" && myVariantsLoginUrl)
? myVariantsLoginUrl : "./hgSession";
let link = document.createElement("a");
link.href = href;
link.textContent = "log in";
msg.appendChild(document.createTextNode("Please "));
msg.appendChild(link);
msg.appendChild(document.createTextNode(" to use this feature."));
dialog.appendChild(msg);
} else {
let form = this.createBedForm(dialog);
document.body.append(dialog);
dialogButtons.Submit = function() {
// extract the form elements and check
myVariants.createItem(form);
};
}
dialogButtons.Cancel = function(){
$(this).dialog("close");
};
$(dialog).dialog({
title: "My Annotations",
resizable: false,
height: "auto",
width: 580,
modal: true,
closeOnEscape: true,
autoOpen: false,
buttons: dialogButtons,
open: function() {
// Restore the last input mode the user picked (or default to HGVS).
let form = document.getElementById("myVariants-form");
if (form && form.applyMode) {
let saved;
try { saved = localStorage.getItem("myVariantsLastMode"); } catch (e) { saved = null; }
const validModes = ["hgvs", "transcript", "snv", "cnv"];
form.applyMode(validModes.indexOf(saved) >= 0 ? saved : "hgvs");
}
},
close: function() {
// Reset block rows so the next open starts clean.
let form = document.getElementById("myVariants-form");
if (form && form.blocksWidget) {
form.blocksWidget.clear();
}
}
});
} else {
// Reopening after an async image update; refresh the position fields.
// The dialog's open callback re-applies the last-used mode.
let form = document.getElementById("myVariants-form");
let start = form.elements.start;
let end = form.elements.end;
let thickStart = form.elements.thickStart;
let thickEnd = form.elements.thickEnd;
if (start) start.value = hgTracks.winStart;
if (end) end.value = hgTracks.winEnd;
if (thickStart) thickStart.value = hgTracks.winStart;
if (thickEnd) thickEnd.value = hgTracks.winEnd;
let summaryText = document.getElementById("positionSummaryText");
if (summaryText) {
let chromEl = document.getElementById("chrom");
let startFmt = parseInt(hgTracks.winStart).toLocaleString();
let endFmt = parseInt(hgTracks.winEnd).toLocaleString();
summaryText.textContent =
(chromEl ? chromEl.value : hgTracks.chromName) + ":" + startFmt + "-" + endFmt;
}
}
// if we clicked outside of the pop up, close the popup:
document.addEventListener('click', (e) => {
let dialogEl = document.getElementById("myVariantsDialog");
if (!dialogEl) return;
// If the click handler that ran first removed its own target from
// the document (eg a row remove button, or a Grammarly/extension
// DOM swap on the description field), e.target is now detached
// and `.contains(e.target)` would falsely report "outside". Skip.
if (!document.contains(e.target)) return;
let dialogContainer = dialogEl.parentElement;
// Check if click target is inside the dialog (handles native dropdowns that render outside bounds)
if (dialogContainer && !dialogContainer.contains(e.target)) {
$("#myVariantsDialog").dialog("close");
}
});
},
createItem: function(form) {
// sends a post to hgTracks that adds a new item to the users custom track
// and updates the image to include this track if it wasn't already there
// Block validation only matters for the transcript path; SNV/CNV don't carry blocks.
let blockResult = {ok: true, noBlocks: true};
const isTranscript = !!(document.getElementById("typeTranscript") &&
document.getElementById("typeTranscript").checked);
if (isTranscript && form.blocksWidget && form.blocksWidget.getRowCount() > 0) {
blockResult = form.blocksWidget.validate();
if (!blockResult.ok) {
alert("Block error: " + blockResult.msg);
return;
}
}
const data = {};
if (form.elements.hgvsInput.value) {
data.hgvsInput = form.elements.hgvsInput.value;
} else {
const itemType = isTranscript ? "transcript"
: (document.getElementById("typeCnv").checked ? "cnv" : "snv");
Array.from(form.elements).forEach( (ele) => {
if (ele.name === "myVariantsHgvsInput" || ele.name === "hgt_doJsCommand" ||
ele.name === "myVariantsType" ||
(ele.tagName !== "INPUT" && ele.tagName !== "TEXTAREA")) {return;}
const key = ele.id;
let value = ele.value;
// Handle Spectrum color picker - get the value from spectrum if available
if (ele.id === "color" && $(ele).spectrum) {
let spectrumColor = $(ele).spectrum("get");
if (spectrumColor) {
value = spectrumColor.toHexString();
}
}
data[key] = value;
});
data.itemType = itemType;
data.cnvType = (itemType === "cnv") ? document.getElementById("cnvType").value : "";
if (itemType === "cnv") data.ref = "";
else if (itemType === "transcript") { data.ref = ""; data.alt = ""; }
// Collect custom fields from the dynamic rows
let customRows = document.querySelectorAll("#customFieldsList .customFieldRow");
if (customRows.length > 0) {
let customFields = [];
customRows.forEach(function(row) {
let name = row.querySelector(".customFieldName").value.trim();
if (name) {
let value = row.querySelector(".customFieldValue").value;
customFields.push({name: name, value: value});
}
});
if (customFields.length > 0) {
data.extraFields = customFields;
}
}
// Convert hidden block fields from CSV strings to arrays of ints.
// Drop blocks entirely for SNV/CNV; server synthesizes a single full-span
// block. For transcript with noBlocks (rows added but every size empty),
// also drop and let the server synthesize.
let bc = parseInt(data.blockCount, 10);
if (!isTranscript || !bc || blockResult.noBlocks) {
delete data.blockCount;
delete data.blockSizes;
delete data.chromStarts;
} else {
data.blockCount = bc;
data.blockSizes = data.blockSizes.split(",").map(Number);
data.chromStarts = data.chromStarts.split(",").map(Number);
}
}
// Show loading indicator
const loadingId = showLoadingImage("imgTbl");
document.body.style.cursor = "wait";
// Build request - use fetch() instead of form.submit()
const trackName = form.elements.namedItem("trackName").value;
const req = encodeURIComponent(`myVariants ${trackName} ${JSON.stringify(data)}`);
const url = cart.addUpdatesToUrl(`../cgi-bin/hgTracks?hgt_doJsCommand=${req}&trackName=${trackName}&hgt.trackImgOnly=1&hgt.ideogramToo=1&hgsid=${getHgsid()}&db=${getDb()}`);
fetch(url, {
method: "POST",
credentials: "same-origin"
})
.then(response => {
if (!response.ok) {
throw new Error("Network response was not ok: " + response.status);
}
return response.text();
})
.then(html => {
hideLoadingImage(loadingId);
document.body.style.cursor = "";
myVariants.handleCreateSuccess(html, data);
})
.catch(error => {
hideLoadingImage(loadingId);
document.body.style.cursor = "";
warn("Error creating annotation: " + error.message);
console.error("Fetch error:", error);
});
},
handleCreateSuccess: function(response, data) {
// Close the create dialog
$("#myVariantsDialog").dialog("close");
// Update the image using the existing pattern
imageV2.updateImgAndMap.call({cmd: 'wholeImage'}, response, 'success');
// Extract new item coordinates from server response
const newItemPos = scrapeVariable(response, "newItemPos");
let variantChrom, variantStart, variantEnd;
if (newItemPos) {
// Server returned the coordinates
variantChrom = newItemPos.chrom;
variantStart = newItemPos.start;
variantEnd = newItemPos.end;
} else {
// Fallback: try to get from form data (manual entry)
variantChrom = data.chrom || hgTracks.chromName;
variantStart = parseInt(data.start, 10) || hgTracks.winStart;
variantEnd = parseInt(data.end, 10) || hgTracks.winEnd;
}
// Check if variant is in current window
const inCurrentWindow = (variantChrom === hgTracks.chromName &&
variantStart < hgTracks.winEnd &&
variantEnd > hgTracks.winStart);
if (inCurrentWindow) {
// Already visible - just show brief success message
return;
}
// Check stored preference
const navPref = localStorage.getItem("myVariants_navPref");
if (navPref === "jump") {
// Auto-navigate to variant
myVariants.navigateToVariant(variantChrom, variantStart, variantEnd);
return;
} else if (navPref === "stay") {
// Stay here, just show message
warn("Annotation created at " + variantChrom + ":" +
(variantStart+1).toLocaleString() + "-" + variantEnd.toLocaleString());
return;
}
// No preference saved - show dialog
myVariants.showNavigationDialog(variantChrom, variantStart, variantEnd);
},
showNavigationDialog: function(chrom, start, end) {
const posStr = chrom + ":" + (start+1).toLocaleString() + "-" + end.toLocaleString();
// Create dialog content
const content = document.createElement("div");
// Build message with position
const msg = document.createElement("p");
msg.style.marginBottom = "0.5em";
msg.innerHTML = "Annotation created at " + posStr + "";
content.appendChild(msg);
// Show dialog with buttons
$(content).dialog({
title: "Annotation Created",
modal: true,
width: 600,
buttons: {
"Go to Annotation": function() {
if (document.getElementById("myVariantsRememberNav").checked) {
localStorage.setItem("myVariants_navPref", "jump");
}
$(this).dialog("close");
myVariants.navigateToVariant(chrom, start, end);
},
"Stay Here": function() {
if (document.getElementById("myVariantsRememberNav").checked) {
localStorage.setItem("myVariants_navPref", "stay");
}
$(this).dialog("close");
}
},
open: function() {
// Add checkbox to button pane, inline with buttons
const dialog = $(this);
const buttonPane = dialog.closest(".ui-dialog").find(".ui-dialog-buttonpane");
const checkboxSpan = document.createElement("span");
checkboxSpan.style.cssText = "display: inline-block; vertical-align: middle; margin-right: 1em;";
checkboxSpan.innerHTML = "";
checkboxSpan.appendChild(createInfoIcon(
"Save your preference. Future annotations outside the current view will automatically " +
"use this choice. Reset via Configure page or cartReset."
));
// Insert before the button set
buttonPane.find(".ui-dialog-buttonset").before(checkboxSpan);
// Force dialog to recalculate its size
dialog.dialog("option", "height", "auto");
}
});
},
navigateToVariant: function(chrom, start, end) {
// Add small padding around the variant (max 100bp)
const padding = 100;
const paddedStart = Math.max(0, start - padding);
const paddedEnd = end + padding;
// Navigate using existing mechanism
const pos = chrom + ":" + paddedStart + "-" + paddedEnd;
imageV2.navigateInPlace("position=" + pos, null, true);
},
showDialog: function() {
let dialog = document.getElementById('myVariantsDialog');
// Clear dynamically-added custom field rows; for existing fields, just clear the value
let customFieldsList = document.getElementById("customFieldsList");
if (customFieldsList) {
let rows = customFieldsList.querySelectorAll(".customFieldRow");
rows.forEach(function(row) {
if (row.dataset.existing === "true") {
// Existing field - just clear the value input
let valInput = row.querySelector(".customFieldValue");
if (valInput) valInput.value = "";
} else {
// Dynamically added row - remove it
row.remove();
}
});
}
$(dialog).dialog("open");
},
};
/////////////////
//// posting ////
/////////////////
var posting = {
blockUseMap: false,
blockMapClicks: function ()
// Blocks clicking on map items when in effect. Drag opperations frequently call this.
{
posting.blockUseMap=true;
},
allowMapClicks:function ()
// reallows map clicks. Called after operations that compete with clicks (e.g. dragging)
{
$('body').css('cursor', ''); // Explicitly remove wait cursor.
posting.blockUseMap=false;
},
mapClicksAllowed: function ()
// Verify that click-competing operation (e.g. dragging) isn't currently occurring.
{
return (posting.blockUseMap === false);
},
blockTheMapOnMouseMove: function (ev)
{
if (!posting.blockUseMap && mouse.hasMoved(ev)) {
posting.blockUseMap=true;
}
},
mapClk: function ()
{
var done = false;
// if we clicked on a merged item then show all the items, similar to clicking a
// dense track to turn it to pack
if (!(this && this.href)) {
writeToApacheLog(`no href for mapClk, this = `);
}
let parsedUrl = parseUrl(this.href);
let cgi = parsedUrl.cgi;
// for some reason, '-' characters are encoded here? unencode them so lookups into
// hgTracks.trackDb will work
let id = "";
if (typeof parsedUrl.queryArgs.g !== "undefined") {
id = parsedUrl.queryArgs.g.replace("%2D", "-");
}
if (parsedUrl.queryArgs.i === "mergedItem") {
updateObj={};
updateObj[id+".doMergeItems"] = 0;
hgTracks.trackDb[id][id+".doMergeItems"] = 0;
cart.setVarsObj(updateObj,null,false);
imageV2.requestImgUpdate(id, id + ".doMergeItems=0");
return false;
}
if (done)
return false;
else {
// first check if we are allowed to click, we could be in the middle
// of a drag select
if (posting.blockUseMap === true) {
return false;
} else if (cgi === "hgGene") {
window.hgcOrHgGeneArgs = parsedUrl.queryArgs;
id = parsedUrl.queryArgs.hgg_type;
popUpHgcOrHgGene.hgc(id, this.href);
return false;
} else if (cgi === "hgTrackUi") {
rec = hgTracks.trackDb[id];
if (rec && tdbIsLeaf(rec)) {
popUp.hgTrackUi(id, false);
} else {
location.assign(href);
}
} else if (cgi === "hgc") {
if (id.startsWith("multiz")) {
// multiz tracks have a form that lets you change highlighted bases
// that does not play well in a pop up
// toga tracks require bootstrap which does not work with something
location.assign(href);
return false;
}
popUpHgcOrHgGene.hgc(id, this.href);
return false;
} else {
// must be changing the density of a track, save the settings and post the form:
return posting.saveSettings(this);
}
}
},
saveSettings: function (obj)
{
if (posting.blockUseMap === true) {
return false;
}
if (!obj || !obj.href) // called directly with obj
obj = this; // and from callback without obj
if ($(obj).hasClass('noLink')) // TITLE_BUT_NO_LINK
return false;
if (obj.href.match('#') || obj.target.length > 0) {
//alert("Matched # ["+obj.href+"] or has target:"+obj.target);
return true;
}
var thisForm = normed($(obj).parents('form').first());
if (!thisForm)
thisForm = normed($("FORM").first());
if (thisForm) {
//alert("posting form:"+$(thisForm).attr('name'));
return postTheForm($(thisForm).attr('name'),cart.addUpdatesToUrl(obj.href));
}
return true;
}
};
/////////////////////////
//// cart updating /////
///////////////////////
var cart = {
// Controls queuing and ultimately updating cart variables vis ajax or submit. Queued vars
// are held in an object with unique keys preventing duplicate updates and ensuring last update
// takes precedence. WARNING: be careful creating an object with variables on the fly:
// cart.setVarsObj({track: vis}) is invalid but cart.setVarsObj({'knownGene': vis}) is ok!
updateQueue: {},
updatesWaiting: function ()
{ // returns TRUE if updates are waiting.
return objNotEmpty(cart.updateQueue);
},
addUpdatesToUrl: function (url)
{ // adds any outstanding cart updates to the url, then clears the queue
if (cart.updatesWaiting()) {
//console.log('cart.addUpdatesToUrl: '+objKeyCount(cart.updateQueue)+' vars');
var updates = cart.varsToUrlData(); // clears the queue
if (!url || url.length === 0)
return updates;
if (updates.length > 0) {
var dataOnly = (url.indexOf("cgi-bin") === -1); // all urls should be to our cgis
if (!dataOnly && url.lastIndexOf("?") === -1)
url += "?" + updates;
else
url += '&' + updates;
}
}
return url;
},
varsToUrlData: function (varsObj)
{ // creates a url data (var1=val1&var2=val2...) string from vars object and queue
// The queue will be emptied by this call.
cart.queueVarsObj(varsObj); // lay ontop of queue, to give new values precedence
// Now convert to url data and clear queue
var urlData = '';
if (cart.updatesWaiting()) {
//console.log('cart.varsToUrlData: '+objKeyCount(cart.updateQueue)+' vars');
urlData = varHashToQueryString(cart.updateQueue);
cart.updateQueue = {};
}
return urlData;
},
setVarsObj: function (varsObj, errFunc, async)
{ // Set all vars in a var hash, appending any queued updates
// The default behavior is async = true
//console.log('cart.setVarsObj: were:'+objKeyCount(cart.updateQueue) +
// ' new:'+objKeyCount(varsObj);
cart.queueVarsObj(varsObj); // lay ontop of queue, to give new values precedence
// Now ajax update all in queue and clear queue
if (cart.updatesWaiting()) {
setVarsFromHash(cart.updateQueue, errFunc, async);
cart.updateQueue = {};
}
},
setVars: function (names, values, errFunc, async)
{ // ajax updates the cart, and includes any queued updates.
cart.updateSessionPanel(); // handles hide from left minibutton
cart.setVarsObj(arysToObj(names, values), errFunc, async);
},
queueVarsObj: function (varsObj)
{ // Add object worth of cart updates to the 'to be updated' queue, so they can be sent to
// the server later. Note: hash allows overwriting previous updates to the same variable.
if (typeof varsObj !== 'undefined' && objNotEmpty(varsObj)) {
//console.log('cart.queueVarsObj: were:'+objKeyCount(cart.updateQueue) +
// ' new:'+objKeyCount(varsObj));
for (var name in varsObj) {
cart.updateQueue[name] = varsObj[name];
}
}
},
addVarsToQueue: function (names,values)
{ // creates a string of updates to save for ajax batch or a submit
cart.queueVarsObj(arysToObj(names,values));
},
updateSessionPanel: function()
/* This function tries to find out if the current cart changed enough. It is not used anymore, as the
* current active rec. track set name is not shown anymore */
{
// this is never set anymore
if (typeof recTrackSetsDetectChanges === 'undefined' || recTrackSetsDetectChanges === null)
return;
// change color of text
$('span.gbSessionChangeIndicator').addClass('gbSessionChanged');
// change mouseover on the panel. A bit fragile here inserting text in the mouseover specified in
// hgTracks.js, so depends on match with text there, and should present same message as C code
// (Perhaps this could be added as a script tag, so not duplicated)
var txt = $('span.gbSessionLabelPanel').attr('title');
if (txt && !txt.match(/with changes/)) {
$('span.gbSessionLabelPanel').attr('title', txt.replace(
"track set",
"track set, with changes (added or removed tracks) you have requested"));
}
}
};
///////////////////////////////////////////////
//// visibility (mixed with group toggle) /////
///////////////////////////////////////////////
var vis = {
// map cgi enum visibility codes to strings
enumOrder: new Array("hide", "dense", "full", "pack", "squish"),
update: function (track, visibility)
{ // Updates visibility state in hgTracks.trackDb and any visible elements on the page.
// returns true if we modify at least one select in the group list
var rec = hgTracks.trackDb[track];
var selectUpdated = false;
$("select[name=" + escapeJQuerySelectorChars(track) + "]").each(function(t) {
$(this).attr('class', visibility === 'hide' ? 'hiddenText' : 'normalText');
$(this).val(visibility);
selectUpdated = true;
});
if (rec) {
rec.localVisibility = visibility;
}
return selectUpdated;
},
get: function (track)
{ // return current visibility for given track
var rec = hgTracks.trackDb[track];
if (rec) {
if (rec.localVisibility) {
return rec.localVisibility;
} else {
return vis.enumOrder[rec.visibility];
}
} else {
return null;
}
},
makeTrackVisible: function (track)
{ // Sets the vis box to visible, and ques a cart update, but does not update the image
// Trusts that the cart update will be submitted later.
if (track && vis.get(track) !== "full") {
vis.update(track, 'pack');
cart.addVarsToQueue([track], ['pack']);
}
},
toggleForGroup: function (button, prefix)
{ // toggle visibility of a track group; prefix is the prefix of all the id's of tr's in the
// relevant group. This code also modifies the corresponding hidden fields and the gif
// of the +/- img tag.
imageV2.markAsDirtyPage();
if (arguments.length > 2)
return setTableRowVisibility(button, prefix, "hgtgroup", "group",false,arguments[2]);
else
return setTableRowVisibility(button, prefix, "hgtgroup", "group", true);
},
expandAllGroups: function (newState)
{ // Set visibility of all track groups to newState (true means expanded).
// This code also modifies the corresponding hidden fields and the gif's of the +/- img tag.
imageV2.markAsDirtyPage();
$(".toggleButton[id$='_button']").each( function (i) {
// works for old img type AND new BUTTONS_BY_CSS // - 7: clip '_button' suffix
vis.toggleForGroup(this,this.id.substring(0,this.id.length - 7),newState);
});
return false;
},
initForAjax: function()
{ // To better support the back-button, it is good to eliminate extraneous form puts
// Towards that end, we support visBoxes making ajax calls to update cart.
var sels = $('select.normalText,select.hiddenText');
$(sels).on("change", function() {
var track = $(this).attr('name');
let newVis = $(this).val();
if (newVis === 'hide') {
var rec = hgTracks.trackDb[track];
if (rec)
rec.visibility = 0;
// else Would be nice to hide subtracks as well but that may be overkill
$(document.getElementById('tr_' + track)).remove();
cart.updateSessionPanel();
imageV2.drawHighlights();
$(this).attr('class', 'hiddenText');
} else
$(this).attr('class', 'normalText');
document.querySelectorAll('[name="'+track+'"]').forEach( (sel) => {
sel.value = newVis;
});
cart.setVars([track], [$(this).val()]);
imageV2.markAsDirtyPage();
return false;
});
// Now we can rid the submt of the burden of all those vis boxes
var form = $('form#TrackForm');
$(form).on("submit", function () {
$('select.normalText,select.hiddenText').prop('disabled',true);
});
$(form).attr('method','get');
},
restoreFromBackButton: function()
// Re-enabling vis dropdowns is necessary because initForAjax() disables them on submit.
{
$('select.normalText,select.hiddenText').prop('disabled',false);
}
};
////////////////////////////////////////////////////////////
// dragSelect is also known as dragZoom or shift-dragZoom //
////////////////////////////////////////////////////////////
var dragSelect = {
hlColor : '#aac6ff', // current highlight color
hlColorDefault: '#aac6ff', // default highlight color, if nothing specified
areaSelector: null, // formerly "imgAreaSelect". jQuery element used for imgAreaSelect
originalCursor: null,
startTime: null,
escPressed : false, // flag is set when user presses Escape
selectStart: function (img, selection)
{
initVars();
dragSelect.escPressed = false;
if (rightClick.menu) {
rightClick.menu.hide();
}
var now = new Date();
dragSelect.startTime = now.getTime();
posting.blockMapClicks();
},
selectChange: function (img, selection)
{
if (selection.x1 !== selection.x2) {
if (genomePos.check(img, selection)) {
genomePos.update(img, selection, false);
jQuery('body').css('cursor', dragSelect.originalCursor);
} else {
jQuery('body').css('cursor', 'not-allowed');
}
}
return true;
},
findHighlightIdxForPos : function(findPos) {
// return the index of the highlight string e.g. hg19.chrom1:123-345#AABBDCC that includes a chrom range findPos
// mostly copied from drawHighlights()
var currDb = getDb();
if (hgTracks.highlight) {
var hlArray = hgTracks.highlight.split("|"); // support multiple highlight items
for (var i = 0; i < hlArray.length; i++) {
hlString = hlArray[i];
pos = parsePositionWithDb(hlString);
imageV2.undisguiseHighlight(pos);
if (!pos)
continue; // ignore invalid position strings
pos.start--;
if (pos.chrom === hgTracks.chromName && pos.db === currDb
&& pos.start <= findPos.chromStart && pos.end >= findPos.chromEnd) {
return i;
}
}
}
return null;
},
saveHlColor : function (hlColor)
// save the current hlColor to the object and also the cart variable hlColor and return it.
// hlColor is a 6-character hex string prefixed by #
{
dragSelect.hlColor = hlColor;
cart.setVars(["prevHlColor"], [dragSelect.hlColor], null, false);
hgTracks.prevHlColor = hlColor; // cart.setVars does not update the hgTracks-variables. The cart-variable system is a problem.
return hlColor;
},
loadHlColor : function ()
// load hlColor from prevHlColor in the cart, or use default color, set and return it
// color is a 6-char hex string prefixed by #
{
if (hgTracks.prevHlColor)
dragSelect.hlColor = hgTracks.prevHlColor;
else
dragSelect.hlColor = dragSelect.hlColorDefault;
return dragSelect.hlColor;
},
highlightThisRegion: function(newPosition, doAdd, hlColor)
// set highlighting newPosition in server-side cart and apply the highlighting in local UI.
// hlColor can be undefined, in which case it defaults to the last used color or the default light blue
// if hlColor is set, it is also saved into the cart.
// if doAdd is true, the highlight is added to the current list. If it is false, all old highlights are deleted.
{
newPosition.replace("virt:", "multi:");
var hlColorName = null;
if (hlColor==="" || hlColor===null || hlColor===undefined)
hlColorName = dragSelect.loadHlColor();
else
hlColorName = dragSelect.saveHlColor( hlColor );
var pos = parsePosition(newPosition);
var start = pos.start;
var end = pos.end;
var newHighlight = makeHighlightString(getDb(), pos.chrom, start, end, hlColorName);
newHighlight = imageV2.disguiseHighlight(newHighlight);
var oldHighlight = hgTracks.highlight;
if (oldHighlight===undefined || doAdd===undefined || doAdd===false || oldHighlight==="") {
// just set/overwrite the old highlight position, this used to be the default
hgTracks.highlight = newHighlight;
}
else {
// add to the end of a |-separated list
hgTracks.highlight = oldHighlight+"|"+newHighlight;
}
// we include enableHighlightingDialog because it may have been changed by the dialog
var cartSettings = { 'highlight': hgTracks.highlight,
'enableHighlightingDialog': hgTracks.enableHighlightingDialog ? 1 : 0 };
if (hgTracks.windows && !hgTracks.virtualSingleChrom) {
var nonVirtChrom = "";
var nonVirtStart = -1;
var nonVirtEnd = -1;
for (i=0,len=hgTracks.windows.length; i < len; ++i) {
var w = hgTracks.windows[i];
// overlap with new position?
if (w.virtEnd > start && end > w.virtStart) {
var s = Math.max(start, w.virtStart);
var e = Math.min(end, w.virtEnd);
var cs = s - w.virtStart + w.winStart;
var ce = e - w.virtStart + w.winStart;
if (nonVirtChrom === "") {
nonVirtChrom = w.chromName;
nonVirtStart = cs;
nonVirtEnd = ce;
} else {
if (w.chromName === nonVirtChrom) {
nonVirtEnd = Math.max(ce, nonVirtEnd);
} else {
break;
}
}
}
}
if (nonVirtChrom !== "")
cartSettings.nonVirtHighlight = makeHighlightString(getDb(), nonVirtChrom, nonVirtStart, (nonVirtEnd+1), hlColorName);
} else if (hgTracks.windows && hgTracks.virtualSingleChrom) {
cartSettings.nonVirtHighlight = hgTracks.highlight;
}
// TODO if not virt, do we need to erase cart nonVirtHighlight ?
cart.setVarsObj(cartSettings);
imageV2.drawHighlights();
},
selectionEndDialog: function (newPosition)
// Let user choose between zoom-in and highlighting.
{
newPosition.replace("virt:", "multi:");
// if the user hit Escape just before, do not show this dialo
if (dragSelect.startTime===null)
return;
var dragSelectDialog = $("#dragSelectDialog")[0];
if (!dragSelectDialog) {
$("body").append("
" +
"
"+
"
Hold Shift+drag to show this dialog" +
"
Hold Alt+drag (Windows) or Option+drag (Mac) to add a highlight" +
"
Hold Ctrl+drag (Windows) or Cmd+drag (Mac) to zoom" +
"
To cancel, press Esc anytime during the drag" +
"
Using the keyboard, highlight the current position with h then m" +
"
Clear all highlights with View - Clear Highlights or h then c" +
"
Clear specific highlights with right click > Remove highlight" +
"
To merely save the color for the next keyboard or right-click > Highlight operations, click 'Save Color' below" +
"
" +
"Don't show this again and always zoom with shift. " +
"Re-enable via 'View - Configure Browser' (c then f)
"+
"Selected chromosome position: ");
dragSelectDialog = $("#dragSelectDialog")[0];
// reset value
// allow to click checkbox by clicking on the label
$('#hlNotShowAgainMsg').on("click", function() { $('#disableDragHighlight').trigger("click");});
// click "add highlight" when enter is pressed in color input box
$("#hlColorInput").on("keyup", function(event){
if(event.keyCode == 13){
$(".ui-dialog-buttonset button:nth-child(3)").trigger("click");
}
});
}
if (hgTracks.windows) {
var i,len;
var newerPosition = newPosition;
if (hgTracks.virtualSingleChrom && (newPosition.search("multi:")===0)) {
newerPosition = genomePos.disguisePosition(newPosition);
}
var str = newerPosition + " \n";
var str2 = " \n";
str2 += "
\n";
var pos = parsePosition(newPosition);
var start = pos.start - 1;
var end = pos.end;
var selectedRegions = 0;
for (i=0,len=hgTracks.windows.length; i < len; ++i) {
var w = hgTracks.windows[i];
// overlap with new position?
if (w.virtEnd > start && end > w.virtStart) {
var s = Math.max(start, w.virtStart);
var e = Math.min(end, w.virtEnd);
var cs = s - w.virtStart + w.winStart;
var ce = e - w.virtStart + w.winStart;
str2 += "
" + w.chromName + ":" + (cs+1) + "-" + ce + "
\n";
selectedRegions += 1;
}
}
str2 += "
\n";
if (!(hgTracks.virtualSingleChrom && (selectedRegions === 1))) {
str += str2;
}
$("#dragSelectPosition").html(str);
} else {
$("#dragSelectPosition").html(newPosition);
}
// Build the button set in two passes so that "Create Item" only
// appears when myVariants is enabled in hg.conf. doMyVariants is
// emitted by the server only inside the gated hg.conf block, so on
// a server with the feature off the variable is undefined and the
// button is omitted entirely.
let dragSelectButtons = {
"Zoom In": function() {
// Zoom to selection
$(this).dialog("option", "revertToOriginalPos", false);
if ($("#disableDragHighlight").prop('checked'))
hgTracks.enableHighlightingDialog = false;
if (imageV2.inPlaceUpdate) {
if (hgTracks.virtualSingleChrom && (newPosition.search("multi:")===0)) {
newPosition = genomePos.disguisePosition(newPosition); // DISGUISE
}
var params = "db=" + getDb() + "&position=" + newPosition;
if (!hgTracks.enableHighlightingDialog)
params += "&enableHighlightingDialog=0";
imageV2.navigateInPlace(params, null, true);
} else {
$('body').css('cursor', 'wait');
if (!hgTracks.enableHighlightingDialog)
cart.setVarsObj({'enableHighlightingDialog': 0 },null,false); // async=false
document.TrackHeaderForm.submit();
}
$(this).dialog("close");
},
"Single Highlight": function() {
// Clear old highlight and Highlight selection
$(imageV2.imgTbl).imgAreaSelect({hide:true});
if ($("#disableDragHighlight").prop('checked'))
hgTracks.enableHighlightingDialog = false;
var hlColor = $("#hlColorInput").val();
dragSelect.highlightThisRegion(newPosition, false, hlColor);
$(this).dialog("close");
},
"Add Highlight": function() {
// Highlight selection
if ($("#disableDragHighlight").prop('checked'))
hgTracks.enableHighlightingDialog = false;
var hlColor = $("#hlColorInput").val();
dragSelect.highlightThisRegion(newPosition, true, hlColor);
$(this).dialog("close");
},
"Save Color": function() {
var hlColor = $("#hlColorInput").val();
dragSelect.saveHlColor( hlColor );
$(this).dialog("close");
}
};
if (typeof doMyVariants !== 'undefined' && doMyVariants) {
dragSelectButtons["Add Annotation"] = function() {
let data = {};
let pos = parsePosition(newPosition);
data.chrom = pos.chrom;
data.start = data.thickStart = pos.start.toString();
data.end = data.thickEnd = pos.end.toString();
data.score = "0";
data.strand = ".";
data.color = $("#hlColorInput").val();
data.name = "";
data.description = "";
data.ref = "";
data.alt = "";
data.trackName = "myVariants";
$(this).dialog("close");
let req = encodeURIComponent(`myVariants myVariants ${JSON.stringify(data)}`);
jQuery('body').css('cursor', 'wait');
$.ajax({
type: "POST",
url: "../cgi-bin/hgTracks",
data: cart.addUpdatesToUrl(`hgt_doJsCommand=${req}&trackName=myVariants&hgsid=${getHgsid()}`),
dataType: "html",
trueSuccess: imageV2.updateImgAndMap,
success: catchErrorOrDispatch,
error: errorHandler,
cmd: 'wholeImage',
loadingId: showLoadingImage("imgTbl"),
cache: false
});
};
}
dragSelectButtons.Cancel = function() {
$(this).dialog("close");
};
$(dragSelectDialog).dialog({
modal: true,
title: "Drag-and-select",
closeOnEscape: true,
resizable: false,
autoOpen: false,
revertToOriginalPos: true,
minWidth: 750,
buttons: dragSelectButtons,
open: function () { // Make zoom the focus/default action
$(this).parents('.ui-dialog-buttonpane button:eq(0)').trigger("focus");
},
close: function() {
// All exits to dialog should go through this
$(imageV2.imgTbl).imgAreaSelect({hide:true});
if ($(this).dialog("option", "revertToOriginalPos"))
genomePos.revertToOriginalPos();
if ($("#disableDragHighlight").prop('checked'))
$(this).remove();
else
$(this).hide();
$('body').css('cursor', ''); // Occasionally wait cursor got left behind
$("#hlColorPicker").spectrum("hide");
}
});
$(dragSelectDialog).dialog('open');
},
selectEnd: function (img, selection, event)
{
var now = new Date();
var doIt = false;
var rulerClicked = selection.y1 <= hgTracks.rulerClickHeight; // = drag on base position track (no shift)
if (dragSelect.originalCursor)
jQuery('body').css('cursor', dragSelect.originalCursor);
if (dragSelect.escPressed)
return false;
// ignore releases outside of the image rectangle (allowing a 10 pixel slop)
if (genomePos.check(img, selection)) {
// ignore single clicks that aren't in the top of the image
// (this happens b/c the clickClipHeight test in dragSelect.selectStart
// doesn't occur when the user single clicks).
doIt = (dragSelect.startTime !== null || rulerClicked);
}
if (doIt) {
// dragSelect.startTime is null if mouse has never been moved
var singleClick = ( (selection.x2 === selection.x1)
|| dragSelect.startTime === null
|| (now.getTime() - dragSelect.startTime) < 100);
var newPosition = genomePos.update(img, selection, singleClick);
newPosition.replace("virt:", "multi:");
if (newPosition) {
if (event.altKey) {
// with the alt-key, only highlight the region, do not zoom
dragSelect.highlightThisRegion(newPosition, true);
$(imageV2.imgTbl).imgAreaSelect({hide:true});
} else {
if (hgTracks.enableHighlightingDialog && !(event.metaKey || event.ctrlKey))
// don't show the dialog if: clicked on ruler, if dialog deactivated or meta/ctrl was pressed
dragSelect.selectionEndDialog(newPosition);
else {
// in every other case, show the dialog
$(imageV2.imgTbl).imgAreaSelect({hide:true});
if (imageV2.inPlaceUpdate) {
if (hgTracks.virtualSingleChrom && (newPosition.search("multi:")===0)) {
newPosition = genomePos.disguisePosition(newPosition); // DISGUISE
}
imageV2.navigateInPlace("db=" + getDb() + "&position=" + newPosition, null, true);
} else {
jQuery('body').css('cursor', 'wait');
document.TrackHeaderForm.submit();
}
}
}
} else {
$(imageV2.imgTbl).imgAreaSelect({hide:true});
genomePos.revertToOriginalPos();
}
dragSelect.startTime = null;
// blockMapClicks/allowMapClicks() is necessary if selectEnd was over a map item.
setTimeout(posting.allowMapClicks,50);
return true;
}
},
load: function (firstTime)
{
var imgHeight = 0;
if (imageV2.enabled)
imgHeight = imageV2.imgTbl.innerHeight() - 1; // last little bit makes border look ok
// No longer disable without ruler, because shift-drag still works
if (typeof(hgTracks) !== "undefined") {
if (hgTracks.rulerClickHeight === undefined || hgTracks.rulerClickHeight === null)
hgTracks.rulerClickHeight = 0; // will be zero if no ruler track
var heights = hgTracks.rulerClickHeight;
var xLimits = genomePos.getXLimits($(imageV2.imgTbl), 0);
dragSelect.areaSelector = jQuery((imageV2.imgTbl).imgAreaSelect({
minX : xLimits[0],
maxX : xLimits[1],
selectionColor: 'blue',
outerColor: '',
minHeight: imgHeight,
maxHeight: imgHeight,
onSelectStart: dragSelect.selectStart,
onSelectChange: dragSelect.selectChange,
onSelectEnd: dragSelect.selectEnd,
autoHide: false, // gets hidden after possible dialog
movable: false,
clickClipHeight: heights
}));
// remove any ongoing drag-selects when the esc key is pressed anywhere for this document
// This allows to abort zooming / highlighting
$(document).on("keyup", function(e){
if(e.keyCode === 27) {
$(imageV2.imgTbl).imgAreaSelect({hide:true});
dragSelect.escPressed = true;
}
});
// hide and redraw all current highlights when the browser window is resized
$(window).on("resize", function() {
$(imageV2.imgTbl).imgAreaSelect({hide:true});
imageV2.drawHighlights();
});
}
}
};
/////////////////////////////////////
//// Chrom Drag/Zoom/Expand code ////
/////////////////////////////////////
jQuery.fn.chromDrag = function(){
this.each(function(){
// Plan:
// mouseDown: determine where in map: convert to img location: pxDown
// mouseMove: flag drag
// mouseUp: if no drag, then create href centered on bpDown loc with current span
// if drag, then create href from bpDown to bpUp
// if ctrlKey then expand selection to containing cytoBand(s)
// Image dimensions all in pix
var img = { top: -1, scrolledTop: -1, height: -1, left: -1, scrolledLeft: -1, width: -1 };
// chrom Dimensions beg,end,size in bases, rest in pix
var chr = { name: "", reverse: false, beg: -1, end: -1, size: -1,
top: -1, bottom: -1, left: -1, right: -1, width: -1 };
var pxDown = 0; // pix X location of mouseDown
var chrImg = $(this);
var mouseIsDown = false;
var mouseHasMoved = false;
var hilite = null;
initialize();
function initialize(){
findDimensions();
if (chr.top === -1)
warn("chromIdeo(): failed to register "+this.id);
else {
hiliteSetup();
$('area.cytoBand').off('mousedown'); // Make sure this is only bound once
$('area.cytoBand').on("mousedown", function(e)
{ // mousedown on chrom portion of image only (map items)
updateImgOffsets();
pxDown = e.clientX - img.scrolledLeft;
var pxY = e.clientY - img.scrolledTop;
if (mouseIsDown === false
&& isWithin(chr.left,pxDown,chr.right) && isWithin(chr.top,pxY,chr.bottom)) {
mouseIsDown = true;
mouseHasMoved = false;
$(document).on('mousemove',chromMove);
$(document).on( 'mouseup', chromUp);
hiliteShow(pxDown,pxDown);
return false;
}
});
}
}
function chromMove(e)
{ // If mouse was down, determine if dragged, then show hilite
if ( mouseIsDown ) {
var pxX = e.clientX - img.scrolledLeft;
var relativeX = (pxX - pxDown);
if (mouseHasMoved || (mouseHasMoved === false && Math.abs(relativeX) > 2)) {
mouseHasMoved = true;
if (isWithin(chr.left,pxX,chr.right))
hiliteShow(pxDown,pxX);
else if (pxX < chr.left)
hiliteShow(pxDown,chr.left);
else
hiliteShow(pxDown,chr.right);
}
}
}
function chromUp(e)
{ // If mouse was down, handle final selection
$(document).off('mousemove',chromMove);
$(document).off('mouseup',chromUp);
chromMove(e); // Just in case
if (mouseIsDown) {
updateImgOffsets();
var bands;
var pxUp = e.clientX - img.scrolledLeft;
var pxY = e.clientY - img.scrolledTop;
if (isWithin(0,pxY,img.height)) { // within vertical range or else cancel
var selRange = { beg: -1, end: -1, width: -1 };
var dontAsk = true;
if (e.ctrlKey) {
bands = findCytoBand(pxDown,pxUp);
if (bands.end > -1) {
pxDown = bands.left;
pxUp = bands.right;
mouseHasMoved = true;
dontAsk = false;
selRange.beg = bands.beg;
selRange.end = bands.end;
hiliteShow(pxDown,pxUp);
}
}
else if (mouseHasMoved) {
// bounded by chrom dimensions: but must remain within image!
if (isWithin(-20,pxUp,chr.left))
pxUp = chr.left;
if (isWithin(chr.right,pxUp,img.width + 20))
pxUp = chr.right;
if ( isWithin(chr.left,pxUp,chr.right+1) ) {
selRange.beg = convertToBases(pxDown);
selRange.end = convertToBases(pxUp);
if (Math.abs(selRange.end - selRange.beg) < 20)
mouseHasMoved = false; // Drag so small: treat as simple click
else
dontAsk = false;
}
}
if (mouseHasMoved === false) { // Not else because small drag turns this off
hiliteShow(pxUp,pxUp);
var curWidth = hgTracks.winEnd - hgTracks.winStart;
// Notice that beg is based upon up position
selRange.beg = convertToBases(pxUp) - Math.round(curWidth/2);
selRange.end = selRange.beg + curWidth;
}
if (selRange.end > -1) {
// prompt, then submit for new position
selRange = rangeNormalizeToChrom(selRange,chr);
if (mouseHasMoved === false) { // Update highlight by converting bp back to pix
pxDown = convertFromBases(selRange.beg);
pxUp = convertFromBases(selRange.end);
hiliteShow(pxDown,pxUp);
}
//if ((selRange.end - selRange.beg) < 50000)
// dontAsk = true;
if (dontAsk
|| confirm("Jump to new position:\n\n"+chr.name+":"+commify(selRange.beg)+
"-"+commify(selRange.end)+" size:"+commify(selRange.width)) ) {
genomePos.setByCoordinates(chr.name, selRange.beg, selRange.end);
// Stop the presses :0)
$('area.cytoBand').on("mousedown", function(e) { return false; });
if (imageV2.backSupport) {
imageV2.navigateInPlace("db=" + getDb() + "&position=" +
encodeURIComponent(genomePos.get().replace(/,/g,'')) +
"&findNearest=1",null,true);
hiliteCancel();
} else
document.TrackHeaderForm.submit();
return true; // Make sure the setTimeout below is not called.
}
}
}
hiliteCancel();
setTimeout(posting.allowMapClicks,50);
}
mouseIsDown = false;
mouseHasMoved = false;
}
function isWithin(beg,here,end)
{ // Simple utility
return ( beg <= here && here < end );
}
function convertToBases(pxX)
{ // Simple utility to convert pix to bases
var offset = (pxX - chr.left)/chr.width;
if (chr.reverse)
offset = 1 - offset;
return Math.round(offset * chr.size);
}
function convertFromBases(bases)
{ // Simple utility to convert bases to pix
var offset = bases/chr.size;
if (chr.reverse)
offset = 1 - offset;
return Math.round(offset * chr.width) + chr.left;
}
function findDimensions()
{ // Called at init: determine the dimensions of chrom from 'cytoband' map items
var lastX = -1;
$('area.cytoBand').each(function(ix) {
var loc = this.coords.split(",");
if (loc.length === 4) {
var myLeft = parseInt(loc[0]);
var myRight = parseInt(loc[2]);
if (chr.top === -1) {
chr.left = myLeft;
chr.right = myRight;
chr.top = parseInt(loc[1]);
chr.bottom = parseInt(loc[3]);
} else {
if (chr.left > myLeft)
chr.left = myLeft;
if (chr.right < parseInt(loc[2]))
chr.right = parseInt(loc[2]);
}
let title = this.getAttribute("data-tooltip") || this.getAttribute("title");
var range = title.substr(title.lastIndexOf(':')+1);
var pos = range.split('-');
if (pos.length === 2) {
if (chr.name.length === 0) {
chr.beg = parseInt(pos[0]);
//chr.end = parseInt(pos[1]);
chr.name = title.substring(title.lastIndexOf(' ')+1,
title.lastIndexOf(':'));
} else {
if (chr.beg > parseInt(pos[0]))
chr.beg = parseInt(pos[0]);
}
if (chr.end < parseInt(pos[1])) {
chr.end = parseInt(pos[1]);
if (lastX === -1)
lastX = myRight;
else if (lastX > myRight)
chr.reverse = true; // end is advancing, but X is not, so reverse
} else if (lastX !== -1 && lastX < myRight)
chr.reverse = true; // end is not advancing, but X is, so reverse
}
$(this).css( 'cursor', 'text');
$(this).attr("href","");
}
});
chr.size = (chr.end - chr.beg );
chr.width = (chr.right - chr.left);
}
function findCytoBand(pxDown,pxUp)
{ // Called when mouseup and ctrl: Find the bounding cytoband dimensions (in pix and bases)
var cyto = { left: -1, right: -1, beg: -1, end: -1 };
$('area.cytoBand').each(function(ix) {
var loc = this.coords.split(",");
if (loc.length === 4) {
var myLeft = parseInt(loc[0]);
var myRight = parseInt(loc[2]);
var range;
var pos;
if (cyto.left === -1 || cyto.left > myLeft) {
if ( isWithin(myLeft,pxDown,myRight) || isWithin(myLeft,pxUp,myRight) ) {
cyto.left = myLeft;
range = this.title.substr(this.title.lastIndexOf(':')+1);
pos = range.split('-');
if (pos.length === 2) {
cyto.beg = (chr.reverse ? parseInt(pos[1]) : parseInt(pos[0]));
}
}
}
if (cyto.right === -1 || cyto.right < myRight) {
if ( isWithin(myLeft,pxDown,myRight) || isWithin(myLeft,pxUp,myRight) ) {
cyto.right = myRight;
range = this.title.substr(this.title.lastIndexOf(':')+1);
pos = range.split('-');
if (pos.length === 2) {
cyto.end = (chr.reverse ? parseInt(pos[0]) : parseInt(pos[1]));
}
}
}
}
});
return cyto;
}
function rangeNormalizeToChrom(selection,chrom)
{ // Called before presenting or using base range: make sure chrom selection
// is within chrom range
if (selection.end < selection.beg) {
var tmp = selection.end;
selection.end = selection.beg;
selection.beg = tmp;
}
selection.width = (selection.end - selection.beg);
selection.beg += 1;
if (selection.beg < chrom.beg) {
selection.beg = chrom.beg;
selection.end = chrom.beg + selection.width;
}
if (selection.end > chrom.end) {
selection.end = chrom.end;
selection.beg = chrom.end - selection.width;
if (selection.beg < chrom.beg) { // spans whole chrom
selection.width = (selection.end - chrom.beg);
selection.beg = chrom.beg + 1;
}
}
return selection;
}
function hiliteShow(down,cur)
{ // Called on mousemove, mouseup: set drag hilite dimensions
var topY = img.top;
var high = img.height;
var begX = -1;
var wide = -1;
if (cur < down) {
begX = cur + img.left;
wide = (down - cur);
} else {
begX = down + img.left;
wide = (cur - down);
}
$(hilite).css({ left: begX + 'px', width: wide + 'px', top: topY + 'px',
height: high + 'px', display:'' });
$(hilite).show();
}
function hiliteCancel(left,width,top,height)
{ // Called on mouseup: Make green drag hilite disappear when no longer wanted
$(hilite).hide();
$(hilite).css({ left: '0px', width: '0px', top: '0px', height: '0px' });
}
function hiliteSetup()
{ // Called on init: setup of drag region hilite (but don't show yet)
if (hilite === null) { // setup only once
hilite = jQuery("");
$(hilite).css({ backgroundColor: 'green', opacity: 0.4, borderStyle: 'solid',
borderWidth: '1px', bordercolor: '#0000FF' });
$(hilite).css({ display: 'none', position: 'absolute', overflow: 'hidden', zIndex: 1 });
jQuery($(chrImg).parents('body')).append($(hilite));
}
return hilite;
}
function updateImgOffsets()
{ // Called on mousedown: Gets the current offsets
var offs = $(chrImg).offset();
img.top = Math.round(offs.top );
img.left = Math.round(offs.left);
img.scrolledTop = img.top - $("body").scrollTop();
img.scrolledLeft = img.left - $("body").scrollLeft();
if (theClient.isIePre11()) {
img.height = $(chrImg).outerHeight();
img.width = $(chrImg).outerWidth();
} else {
img.height = $(chrImg).height();
img.width = $(chrImg).width();
}
return img;
}
});
};
//////////////////////////
//// Drag Scroll code ////
//////////////////////////
jQuery.fn.panImages = function(){
// globals across all panImages
genomePos.original = genomePos.getOriginalPos(); // redundant but makes certain original is set.
var leftLimit = hgTracks.imgBoxLeftLabel * -1;
var rightLimit = (hgTracks.imgBoxPortalWidth - hgTracks.imgBoxWidth + leftLimit);
var only1xScrolling = ((hgTracks.imgBoxWidth - hgTracks.imgBoxPortalWidth) === 0);
var prevX = (hgTracks.imgBoxPortalOffsetX + hgTracks.imgBoxLeftLabel) * -1;
var portalWidth = 0;
var portalAbsoluteX = 0;
var savedPosition;
var highlightAreas = null; // Used to ensure dragSelect highlight will scroll.
this.each(function(){
var pic;
var pan;
if ( $(this).is("img") ) {
pan = $(this).parent("div");
pic = $(this);
}
else if ( $(this).is("div.scroller") ) {
pan = $(this);
pic = $(this).children("img#panImg"); // Get the real pic
}
if (!pan || !pic) {
throw "Not a div with child image! 'panImages' can only be used with divs contain images.";
}
// globals across all panImages
portalWidth = $(pan).width();
portalAbsoluteX = $(pan).offset().left;
// globals to one panImage
var newX = 0;
var mouseDownX = 0;
var mouseIsDown = false;
var beyondImage = false;
var atEdge = false;
initialize();
function initialize(){
$(pan).parents('td.tdData').on("mousemove", function(e) {
if (e.shiftKey)
$(this).css('cursor',"crosshair"); // shift-dragZoom
else if ( theClient.isIePre11() ) // IE will override map item cursors if this gets set
$(this).css('cursor',""); // normal pointer when not over clickable item
});
panAdjustHeight(prevX);
pan.on("mousedown", function(e){
if (e.which > 1 || e.button > 1 || e.shiftKey || e.metaKey || e.altKey || e.ctrlKey)
return true;
if (mouseIsDown === false) {
if (rightClick.menu) {
rightClick.menu.hide();
}
mouseIsDown = true;
mouseDownX = e.clientX;
highlightAreas = $('.highlightItem');
atEdge = (!beyondImage && (prevX >= leftLimit || prevX <= rightLimit));
$(document).on('mousemove',panner);
$(document).on('mouseup', panMouseUp); // Will exec only once
return false;
}
});
}
function panner(e) {
//if (!e) e = window.event;
if ( mouseIsDown ) {
var relativeX = (e.clientX - mouseDownX);
if (relativeX !== 0) {
if (posting.mapClicksAllowed()) {
// need to throw up a z-index div. Wait mask?
savedPosition = genomePos.get();
dragMaskShow();
posting.blockMapClicks();
}
var decelerator = 1;
//var wingSize = 1000; // 0 stops the scroll at the edges.
// Remeber that offsetX (prevX) is negative
newX = prevX + relativeX;
if ( newX >= leftLimit ) { // scrolled all the way to the left
if (atEdge) { // Do not drag straight off edge. Force second drag
beyondImage = true;
newX = leftLimit + (newX - leftLimit)/decelerator;// slower
//if (newX >= leftLimit + wingSize) // Don't go too far over the edge!
// newX = leftLimit + wingSize;
} else
newX = leftLimit;
} else if ( newX < rightLimit ) { // scrolled all the way to the right
if (atEdge) { // Do not drag straight off edge. Force second drag
beyondImage = true;
newX = rightLimit - (rightLimit - newX)/decelerator;// slower
//if (newX < rightLimit - wingSize) // Don't go too far over the edge!
// newX = rightLimit - wingSize;
} else
newX = rightLimit;
} else if (newX >= rightLimit && newX < leftLimit)
beyondImage = false; // could have scrolled back without mouse up
posStatus = panUpdatePosition(newX,true);
newX = posStatus.newX;
// do not update highlights if we are at the end of a chromsome
if (!posStatus.isOutsideChrom)
scrollHighlight(relativeX);
var nowPos = newX.toString() + "px";
$(".panImg").css( {'left': nowPos });
$('.tdData').css( {'backgroundPosition': nowPos } );
if (!only1xScrolling)
panAdjustHeight(newX); // Will dynamically resize image while scrolling.
}
}
}
function panMouseUp(e) { // Must be a separate function instead of pan.mouseup event.
//if (!e) e = window.event;
if (mouseIsDown) {
dragMaskClear();
$(document).off('mousemove',panner);
$(document).off('mouseup',panMouseUp);
mouseIsDown = false;
// timeout incase the dragSelect.selectEnd was over a map item. select takes precedence.
setTimeout(posting.allowMapClicks,50);
// Outside image? Then abandon.
var curY = e.pageY;
var imgTbl = $('#imgTbl');
var north = $(imgTbl).offset().top;
var south = north + $(imgTbl).height();
if (curY < north || curY > south) {
atEdge = false;
beyondImage = false;
if (savedPosition)
genomePos.set(savedPosition);
var oldPos = prevX.toString() + "px";
$(".panImg").css( {'left': oldPos });
$('.tdData').css( {'backgroundPosition': oldPos } );
if (highlightAreas)
imageV2.drawHighlights();
return true;
}
// Do we need to fetch anything?
if (beyondImage) {
if (imageV2.inPlaceUpdate) {
var pos = parsePosition(genomePos.get());
imageV2.navigateInPlace("db=" + getDb() + "&position=" +
encodeURIComponent(pos.chrom + ":" + pos.start + "-" + pos.end),
null, true);
} else {
document.TrackHeaderForm.submit();
}
return true; // Make sure the setTimeout below is not called.
}
// Just a normal scroll within a >1X image
if (prevX !== newX) {
prevX = newX;
if (!only1xScrolling) {
//panAdjustHeight(newX); // Will resize image AFTER scrolling.
// Important, since AJAX could lead to reinit after this within bounds scroll
hgTracks.imgBoxPortalOffsetX = (prevX * -1) - hgTracks.imgBoxLeftLabel;
hgTracks.imgBoxPortalLeft = newX.toString() + "px";
}
}
}
}
}); // end of this.each(function(){
function panUpdatePosition(newOffsetX,bounded)
{
// Updates the 'position/search" display with change due to panning
var closedPortalStart = hgTracks.imgBoxPortalStart + 1; // Correction for half open
var portalWidthBases = hgTracks.imgBoxPortalEnd - closedPortalStart;
var portalScrolledX = hgTracks.imgBoxPortalOffsetX+hgTracks.imgBoxLeftLabel + newOffsetX;
var recalculate = false;
var newPortalStart = 0;
if (hgTracks.revCmplDisp)
newPortalStart = closedPortalStart + // As offset goes down, so do bases seen.
Math.round(portalScrolledX*hgTracks.imgBoxBasesPerPixel);
else
newPortalStart = closedPortalStart - // As offset goes down, bases seen goes up!
Math.round(portalScrolledX*hgTracks.imgBoxBasesPerPixel);
if (newPortalStart < hgTracks.chromStart && bounded) { // Stay within bounds
newPortalStart = hgTracks.chromStart;
recalculate = true;
}
var newPortalEnd = newPortalStart + portalWidthBases;
if (newPortalEnd > hgTracks.chromEnd && bounded) {
newPortalEnd = hgTracks.chromEnd;
newPortalStart = newPortalEnd - portalWidthBases;
recalculate = true;
}
if (newPortalStart > 0) {
var newPos = hgTracks.chromName + ":" + newPortalStart + "-" + newPortalEnd;
genomePos.set(newPos); // no need to change the size
}
if (recalculate && hgTracks.imgBoxBasesPerPixel > 0) {
// Need to recalculate X for bounding drag
portalScrolledX = (closedPortalStart - newPortalStart) / hgTracks.imgBoxBasesPerPixel;
newOffsetX = portalScrolledX - (hgTracks.imgBoxPortalOffsetX+hgTracks.imgBoxLeftLabel);
}
if (typeof (igv) !== "undefined") {
igv.updateIgvStartPosition(newPortalStart);
}
ret = {};
ret.newX = newOffsetX;
ret.isOutsideChrom = recalculate;
return ret;
}
function mapTopAndBottom(mapName,east,west)
{
// Find the top and bottom px given left and right boundaries
var mapPortal = { top: -10, bottom: -10 };
var items = $("map[name='"+mapName+"']").children();
if ($(items).length>0) {
$(items).each(function(t) {
var loc = this.coords.split(",");
var aleft = parseInt(loc[0]);
var aright = parseInt(loc[2]);
if (aleft < west && aright >= east) {
var atop = parseInt(loc[1]);
var abottom = parseInt(loc[3]);
if (mapPortal.top < 0 ) {
mapPortal.top = atop;
mapPortal.bottom = abottom;
} else if (mapPortal.top > atop) {
mapPortal.top = atop;
} else if (mapPortal.bottom < abottom) {
mapPortal.bottom = abottom;
}
}
});
}
return mapPortal;
}
function panAdjustHeight(newOffsetX) {
// Adjust the height of the track data images so that bed items scrolled off screen
// do not waste vertical real estate
// Is the > 1x?
if (only1xScrolling)
return;
var east = newOffsetX * -1;
var west = east + portalWidth;
$(".panImg").each(function(t) {
var mapid = this.id.replace('img_','map_');
var hDiv = $(this).parent();
var north = parseInt($(this).css("top")) * -1;
var south = north + $(hDiv).height();
var mapPortal = mapTopAndBottom(mapid,east,west);
if (mapPortal.top > 0) {
var topdif = Math.abs(mapPortal.top - north);
var botdif = Math.abs(mapPortal.bottom - south);
if (topdif > 2 || botdif > 2) {
$(hDiv).height( mapPortal.bottom - mapPortal.top );
north = mapPortal.top * -1;
$(this).css( {'top': north.toString() + "px" });
// Need to adjust side label height as well!
var imgId = this.id.split("_");
var titlePx = 0;
var center = $("#img_center_"+imgId[2]);
if (center.length > 0) {
titlePx = $(center).parent().height();
north += titlePx;
}
var side = $("#img_side_"+imgId[2]);
if (side.length > 0) {
$(side).parent().height( mapPortal.bottom - mapPortal.top + titlePx);
$(side).css( {'top': north.toString() + "px" });
}
var btn = $("#p_btn_"+imgId[2]);
if (btn.length > 0) {
$(btn).height( mapPortal.bottom - mapPortal.top + titlePx);
} else {
btn = $("#img_btn_"+imgId[2]);
if (btn.length > 0) {
$(btn).parent().height( mapPortal.bottom - mapPortal.top + titlePx);
$(btn).css( {'top': top.toString() + "px" });
}
}
}
}
});
dragMaskResize(); // Resizes the dragMask to match current image size
}
function dragMaskShow()
{ // Sets up the dragMask to show grabbing cursor within image
// and not allowed north and south of image
var imgTbl = $('#imgTbl');
// Find or create the waitMask (which masks the whole page)
var dragMask = normed($('div#dragMask'));
if (!dragMask) {
$("body").prepend("");
dragMask = $('div#dragMask');
}
$('body').css('cursor','not-allowed');
$(dragMask).css('cursor',"url(../images/grabbing.cur),w-resize");
$(dragMask).css({opacity:0.0,display:'block',
top: $(imgTbl).position().top.toString() + 'px',
height: $(imgTbl).height().toString() + 'px' });
}
function dragMaskResize()
{ // Resizes dragMask (called when image is dynamically resized in >1x scrolling)
var imgTbl = $('#imgTbl');
// Find or create the waitMask (which masks the whole page)
var dragMask = normed($('div#dragMask'));
if (dragMask) {
$(dragMask).height( $(imgTbl).height() );
}
}
function dragMaskClear() { // Clears the dragMask
$('body').css('cursor','auto');
var dragMask = normed($('#dragMask'));
if (dragMask)
$(dragMask).hide();
}
function scrollHighlight(relativeX)
// Scrolls the highlight region if one exists
{
if (highlightAreas) {
for (i=0; i 0 && hgTracks.trackDb) {
var title;
var rec = hgTracks.trackDb[id];
if (rec) {
title = rec.shortLabel;
} else {
title = id;
}
return {id: id, title: "configure " + title};
} else {
return null;
}
},
findMapItem: function (e)
{ // Find mapItem for given event; returns item object or null if none found.
if (rightClick.currentMapItem) {
return rightClick.currentMapItem;
}
// rightClick for non-map items that can be resolved to their parent tr and
// then trackName (e.g. items in gray bar)
var tr = $( e.target ).parents('tr.imgOrd');
if ($(tr).length === 1) {
var a = /tr_(.*)/.exec($(tr).attr('id')); // voodoo
if (a && a[1]) {
var id = a[1];
return rightClick.makeMapItem(id);
}
}
return null;
},
windowOpenFailedMsg: function ()
{
warn("Your web browser prevented us from opening a new window.\n\n" +
"Please change your browser settings to allow pop-up windows from " +
document.domain + ".");
},
handleZoomCodon: function (response, status)
{
var json = JSON.parse(response);
if (json.pos) {
imageV2.navigateInPlace("db=" + getDb() + "&position="+json.pos);
} else {
alert(json.error);
}
},
handleViewImg: function (response, status)
{ // handles view image response, which must get new image without imageV2 gimmickery
jQuery('body').css('cursor', '');
var str = "]*SRC='([^']+)'";
var reg = new RegExp(str);
var a = reg.exec(response);
if (a && a[1]) {
if ( ! window.open(a[1]) ) {
rightClick.windowOpenFailedMsg();
}
return;
}
warn("Couldn't parse out img src");
},
myPrompt: function (msg, callback)
{ // replacement for prompt; avoids misleading/confusing security warnings which are caused
// by prompt in IE 7+. Callback is called if user presses "OK".
$("body").append("
");
$('#myPromptText').on('keypress', function(e) {
if (e.which === 13) { // listens for return key
e.preventDefault(); // prevents return from also submitting whole form
$("#myPrompt").dialog("close");
callback($("#myPromptText").val());
}
});
$("#myPrompt").dialog({
modal: true,
closeOnEscape: true,
buttons: { "OK": function() {
var myPromptText = $("#myPromptText").val();
$(this).dialog("close");
callback(myPromptText);
}
}
});
},
hit: function (menuItemClicked, menuObject, cmd, args)
{
setTimeout( function() {
rightClick.hitFinish(menuItemClicked, menuObject, cmd, args);
}, 1);
},
hitFinish: function (menuItemClicked, menuObject, cmd, args)
{ // dispatcher for context menu hits
var id = rightClick.selectedMenuItem.id;
var url = null; // TODO: Break this giant routine with shared vars into some sub-functions
var href = null;
var rec = null;
var row = null;
var rows = null;
var selectUpdated = null;
function mySuccess() {}
if (menuObject.shown) {
// warn("Spinning: menu is still shown");
setTimeout(function() { rightClick.hitFinish(menuItemClicked, menuObject, cmd); }, 10);
return;
}
if (cmd === 'selectWholeGene' || cmd === 'getDna' || cmd === 'highlightItem' || cmd === 'highlightThisRegion') {
// bring whole gene into view or redirect to DNA screen.
let href = rightClick.selectedMenuItem.href;
let url = URL.parse(rightClick.selectedMenuItem.href);
let chrom = url.searchParams.get('hgg_chrom');
if (!chrom)
chrom = url.searchParams.get('c');
let chromStart = url.searchParams.get('hgg_start');
if (!chromStart)
chromStart = url.searchParams.get('o');
let chromEnd = url.searchParams.get('hgg_end');
if (!chromEnd)
chromEnd = url.searchParams.get('t');
chromStart = parseInt(chromStart) + 1;
chromEnd = parseInt(chromEnd);
// Many links leave out the chrom (b/c it's in the server side cart as "c")
// var chrom = hgTracks.chromName; // This is no longer acceptable
// with multi-window capability drawing multiple positions on multiple chroms.
if (!chrom || chrom.length === 0 || !chromStart || !chromEnd) {// 1-based chromStart
warn("couldn't parse out genomic coordinates");
} else {
if (cmd === 'getDna') {
// NOTE: this should be shared with URL generation for getDna blue bar menu
url = "../cgi-bin/hgc?g=getDna&i=mixed&c=" + chrom;
url += "&l=" + (chromStart - 1) + "&r=" + chromEnd;
url += "&db=" + getDb() + "&hgsid=" + getHgsid();
if ( ! window.open(url) ) {
rightClick.windowOpenFailedMsg();
}
} else if (cmd === 'highlightItem') {
if (hgTracks.windows && !hgTracks.virtualSingleChrom) {
// orig way only worked if the entire item was visible in the windows.
//var result = genomePos.chromToVirtChrom(chrom, parseInt(chromStart-1), parseInt(chromEnd));
var result = genomePos.convertChromPosToVirtCoords(chrom, parseInt(chromStart-1), parseInt(chromEnd));
if (result.chromStart != -1)
{
var newPos2 = hgTracks.chromName+":"+(result.chromStart+1)+"-"+result.chromEnd;
dragSelect.highlightThisRegion(newPos2, true);
}
} else {
var newChrom = hgTracks.chromName;
if (hgTracks.windows && hgTracks.virtualSingleChrom) {
newChrom = hgTracks.windows[0].chromName;
}
var newPos3 = newChrom+":"+(parseInt(chromStart))+"-"+parseInt(chromEnd);
dragSelect.highlightThisRegion(newPos3, true);
}
} else if (cmd === 'highlightThisItem') {
} else {
var newPosition = genomePos.setByCoordinates(chrom, chromStart, chromEnd);
var reg = new RegExp("hgg_gene=([^&]+)");
var b = reg.exec(href);
var name;
// pull item name out of the url so we can set hgFind.matches (redmine 3062)
if (b && b[1]) {
name = b[1];
} else {
reg = new RegExp("[&?]i=([^&]+)");
b = reg.exec(href);
if (b && b[1]) {
name = b[1];
}
}
if (imageV2.inPlaceUpdate) {
// XXXX This attempt to "update whole track image in place" didn't work
// for a variety of reasons (e.g. safari doesn't parse map when we
// update on the client side), so this is currently dead code.
// However, this now works in all other browsers, so we may turn this
// on for non-safari browsers (see redmine #4667).
jQuery('body').css('cursor', '');
var data = "hgt.trackImgOnly=1&hgt.ideogramToo=1&position=" +
newPosition + "&hgsid=" + getHgsid() + "&db=" + getDb();
if (name)
data += "&hgFind.matches=" + name;
$.ajax({
type: "GET",
url: "../cgi-bin/hgTracks",
data: cart.addUpdatesToUrl(data),
dataType: "html",
trueSuccess: imageV2.updateImgAndMap,
success: catchErrorOrDispatch,
error: errorHandler,
cmd: cmd,
loadingId: showLoadingImage("imgTbl"),
cache: false
});
} else {
// do a full page refresh to update hgTracks image
jQuery('body').css('cursor', 'wait');
var ele;
if (document.TrackForm)
ele = document.TrackForm;
else
ele = document.TrackHeaderForm;
if (name)
// Add or update form input with gene to highlight
suggestBox.updateFindMatches(name);
ele.submit();
}
}
}
} else if (cmd === 'zoomCodon' || cmd === 'zoomExon') {
var num, ajaxCmd, msg;
if (cmd === 'zoomCodon') {
msg = "Please enter the codon number to zoom to:";
ajaxCmd = 'codonToPos';
} else {
msg = "Please enter the exon number to zoom to:";
ajaxCmd = 'exonToPos';
}
rightClick.myPrompt(msg, function(results) {
$.ajax({
type: "GET",
url: "../cgi-bin/hgApi",
data: cart.varsToUrlData({ 'hgsid': getHgsid(), 'db': getDb(), 'cmd': ajaxCmd, 'num': results,
'table': args.table, 'name': args.name, 'chrom': hgTracks.chromName}),
trueSuccess: rightClick.handleZoomCodon,
success: catchErrorOrDispatch,
error: errorHandler,
cache: true
});
});
} else if (cmd === 'hgTrackUi_popup') {
// Launches the popup but shields the ajax with a waitOnFunction
popUp.hgTrackUi( rightClick.selectedMenuItem.id, false );
} else if (cmd === 'hgTrackUi_popup_description') {
// Launches the popup but shields the ajax with a waitOnFunction
popUp.hgTrackUi( rightClick.selectedMenuItem.id, true );
} else if (cmd === 'changeTrackColor') {
rightClick.showColorPicker(id);
} else if (cmd === 'hgTrackUi_follow') {
url = "hgTrackUi?hgsid=" + getHgsid() + "&g=";
rec = hgTracks.trackDb[id];
if (tdbHasParent(rec) && tdbIsLeaf(rec))
url += rec.parentTrack;
else {
// The button already has the ref
var link = normed($( 'td#td_btn_'+ rightClick.selectedMenuItem.id ).children('a'));
if (link)
url = $(link).attr('href');
else
url += rightClick.selectedMenuItem.id;
}
location.assign(url);
} else if (cmd === 'newCollection') {
$.ajax({
type: "PUT",
async: false,
url: "../cgi-bin/hgCollection",
data: "cmd=newCollection&track=" + id + "&hgsid=" + getHgsid(),
trueSuccess: mySuccess,
success: catchErrorOrDispatch,
error: errorHandler,
});
imageV2.fullReload();
} else if (cmd === 'addCollection') {
var shortLabel = $(menuItemClicked).text().substring(9).slice(0,-1);
var ii;
var collectionName;
for(ii=0; ii < hgTracks.collections.length; ii++) {
if ( hgTracks.collections[ii].shortLabel === shortLabel) {
collectionName = hgTracks.collections[ii].track;
break;
}
}
$.ajax({
type: "PUT",
async: false,
url: "../cgi-bin/hgCollection",
data: "cmd=addTrack&track=" + id + "&collection=" + collectionName + "&hgsid=" + getHgsid(),
trueSuccess: mySuccess,
success: catchErrorOrDispatch,
error: errorHandler,
});
imageV2.fullReload();
} else if (cmd === "hideOthers") {
rightClick.hideOthers(id);
} else if (cmd === "moveTop") {
rightClick.moveTo(id, "top");
} else if (cmd === "moveBottom") {
rightClick.moveTo(id, "bottom");
} else if ((cmd === 'sortExp') || (cmd === 'sortSim')) {
url = "hgTracks?hgsid=" + getHgsid() + "&" + cmd + "=";
rec = hgTracks.trackDb[id];
if (tdbHasParent(rec) && tdbIsLeaf(rec))
url += rec.parentTrack;
else {
// The button already has the ref
var link2 = normed($( 'td#td_btn_'+ rightClick.selectedMenuItem.id ).children('a'));
if (link2)
url = $(link2).attr('href');
else
url += rightClick.selectedMenuItem.id;
}
location.assign(url);
} else if (cmd === 'viewImg') {
// Fetch a new copy of track img and show it to the user in another window. This code
// assume we have updated remote cart with all relevant chages (e.g. drag-reorder).
jQuery('body').css('cursor', 'wait');
$.ajax({
type: "GET",
url: "../cgi-bin/hgTracks",
data: cart.varsToUrlData({ 'hgt.imageV1': '1','hgt.trackImgOnly': '1',
'hgsid': getHgsid(), 'db': getDb() }),
dataType: "html",
trueSuccess: rightClick.handleViewImg,
success: catchErrorOrDispatch,
error: errorHandler,
cmd: cmd,
cache: false
});
} else if (cmd === 'openLink' || cmd === 'followLink') {
href = rightClick.selectedMenuItem.href;
var vars = new Array("c", "l", "r", "db");
var valNames = new Array("chromName", "winStart", "winEnd");
for (var i in vars) {
// make sure the link contains chrom and window width info
// (necessary b/c we are stripping hgsid and/or the cart may be empty);
// but don't add chrom to wikiTrack links (see redmine #2476).
var v = vars[i];
var val;
if (v === "db") {
val = getDb();
} else {
val = hgTracks[valNames[i]];
}
if (val
&& id !== "wikiTrack"
&& (href.indexOf("?" + v + "=") === -1)
&& (href.indexOf("&" + v + "=") === -1)) {
href = href + "&" + v + "=" + val;
}
}
if (cmd === 'followLink') {
popUpHgcOrHgGene.hgc(rightClick.selectedMenuItem.id, href);
} else {
// XXXX This is blocked by Safari's popup blocker (without any warning message).
href = removeHgsid(href);
if ( ! window.open(href) ) {
rightClick.windowOpenFailedMsg();
}
}
} else if (cmd === 'float') {
if (rightClick.floatingMenuItem && rightClick.floatingMenuItem === id) {
$.floatMgr.FOArray = [];
rightClick.floatingMenuItem = null;
} else {
if (rightClick.floatingMenuItem) {
// This doesn't work.
$('#img_data_' + rightClick.floatingMenuItem).parent().restartFloat();
// This does work
$.floatMgr.FOArray = [];
}
rightClick.floatingMenuItem = id;
rightClick.reloadFloatingItem();
imageV2.requestImgUpdate(id, "hgt.transparentImage=0", "");
}
} else if (cmd === 'hideSet') {
row = $( 'tr#tr_' + id );
rows = dragReorder.getContiguousRowSet(row);
if (rows && rows.length > 0) {
var varsToUpdate = {};
// from bottom up, just in case remove screws with us
for (var ix=rows.length - 1; ix >= 0; ix--) {
var rowId = $(rows[ix]).attr('id').substring('tr_'.length);
// Remove subtrack level vis and explicitly uncheck.
varsToUpdate[rowId] = '[]';
varsToUpdate[rowId+'_sel'] = 0;
$(rows[ix]).remove();
}
if (objNotEmpty(varsToUpdate)) {
cart.setVarsObj(varsToUpdate);
}
imageV2.afterImgChange(true);
}
} else if (cmd === 'hideComposite') {
rec = hgTracks.trackDb[id];
if (tdbIsSubtrack(rec)) {
let trid = id.replaceAll(".","\\.");
row = $( 'tr#tr_' + trid );
rows = dragReorder.getCompositeSet(row);
// from bottom up, just in case remove screws with us
if (rows && rows.length > 0) {
for (var rIx=rows.length - 1; rIx >= 0; rIx--) {
$(rows[rIx]).remove();
}
selectUpdated = vis.update(rec.parentTrack, 'hide');
cart.setVars( [rec.parentTrack], ['hide']);
imageV2.afterImgChange(true);
}
}
} else if (cmd === 'jumpToHighlight') { // If highlight exists for this assembly, jump to it
if (hgTracks.highlight && rightClick.clickedHighlightIdx!==null) {
var newPos = getHighlight(hgTracks.highlight, rightClick.clickedHighlightIdx);
if (newPos && newPos.db === getDb()) {
if ( $('#highlightItem').length === 0) { // not visible? jump to it
var curPos = parsePosition(genomePos.get());
var diff = ((curPos.end - curPos.start) - (newPos.end - newPos.start));
if (diff > 0) { // new position is smaller then current, then center it
newPos.start = Math.max( Math.floor(newPos.start - (diff/2) ), 0 );
newPos.end = newPos.start + (curPos.end - curPos.start);
}
}
if (imageV2.inPlaceUpdate) {
var params = "db=" + getDb() + "&position=" + newPos.chrom+':'+newPos.start+'-'+newPos.end;
imageV2.navigateInPlace(params, null, true);
} else {
genomePos.setByCoordinates(newPos.chrom, newPos.start, newPos.end);
jQuery('body').css('cursor', 'wait');
document.TrackHeaderForm.submit();
}
}
}
} else if (cmd === 'removeHighlight') {
var highlights = hgTracks.highlight.split("|");
highlights.splice(rightClick.clickedHighlightIdx, 1); // splice = remove element from array
hgTracks.highlight = highlights.join("|");
cart.setVarsObj({'highlight' : hgTracks.highlight});
imageV2.drawHighlights();
} else if (cmd === 'toggleMerge') {
// toggle both the cart (if the user goes to trackUi)
// and toggle args[key], if the user doesn't leave hgTracks
var key = id + ".doMergeItems";
var updateObj = {};
if (args[key] === 1) {
args[key] = 0;
updateObj[key] = 0;
cart.setVarsObj(updateObj,null,false);
imageV2.requestImgUpdate(id, id + ".doMergeItems=0");
} else {
args[key] = 1;
updateObj[key] = 1;
cart.setVarsObj(updateObj,null,false);
imageV2.requestImgUpdate(id, id + ".doMergeItems=1");
}
} else { // if ( cmd in 'hide','dense','squish','pack','full','show' )
// Change visibility settings:
//
// First change the select on our form:
rec = hgTracks.trackDb[id];
selectUpdated = vis.update(id, cmd);
// Now change the track image
if (imageV2.enabled && cmd === 'hide') {
// Hide local display of this track and update server side cart.
// Subtracks controlled by 2 settings so del vis and set sel=0.
rightClick.hideTracks([id]);
} else if (!imageV2.mapIsUpdateable) {
jQuery('body').css('cursor', 'wait');
if (selectUpdated) {
// assert(document.TrackForm);
document.TrackForm.submit();
} else {
// Add vis update to queue then submit
cart.setVars([id], [cmd], null, false); // synchronous
document.TrackHeaderForm.submit();
}
} else {
imageV2.requestImgUpdate(id, id + "=" + cmd, "", cmd);
}
}
},
makeHitCallback: function (title)
{ // stub to avoid problem with a function closure w/n a loop
return function(menuItemClicked, menuObject) {
rightClick.hit(menuItemClicked, menuObject, title); return true;
};
},
reloadFloatingItem: function ()
{ // currently dead (experimental code)
if (rightClick.floatingMenuItem) {
$('#img_data_' + rightClick.floatingMenuItem).parent().makeFloat(
{x:"current",y:"current", speed: 'fast', alwaysVisible: true, alwaysTop: true});
}
},
makeImgTag: function (img)
{ // Return img tag with explicit dimensions for img (dimensions are currently hardwired).
// This fixes the "weird shadow problem when first loading the right-click menu"
// seen in FireFox 3.X, which occurred b/c FF doesn't actually fetch the image until
// the menu is being shown.
return "";
},
showColorPicker: function (trackName)
{ // Show a small dialog with a spectrum color picker for changing track color
var rec = hgTracks.trackDb[trackName];
if (!rec || !rec.defaultColor)
return;
var currentColor = (rec.colorOverrideOn && rec.colorOverride) ?
rec.colorOverride : rec.defaultColor;
var dialogId = "trackColorDialog";
$("#" + dialogId).remove();
// Build with static template + data injected via .text()/.val()/.prop() so that
// hub-provided shortLabel cannot inject HTML.
var $dlg = $("
").attr("id", dialogId).html(
"
Pick a new color for :
" +
"" +
" " +
"
");
$dlg.find("p b").text(rec.shortLabel);
$dlg.find("#trackColorText").val(currentColor);
$dlg.find("#trackColorOn").prop("checked", !!rec.colorOverrideOn);
$("body").append($dlg);
var hexColorRe = /^#[0-9a-fA-F]{6}$/;
$("#trackColorPicker").spectrum({
color: currentColor,
showPalette: true,
showSelectionPalette: true,
showInitial: true,
showInput: true,
preferredFormat: "hex",
localStorageKey: "genomebrowser",
hideAfterPaletteSelect: true,
change: function(color) {
$("#trackColorText").val(color.toHexString());
$("#trackColorOn").prop("checked", true);
}
});
$("#trackColorText").on("change", function() {
var val = $(this).val();
if (!hexColorRe.test(val))
return;
$("#trackColorPicker").spectrum("set", val);
$("#trackColorOn").prop("checked", true);
});
var applyColor = function() {
var color = $("#trackColorText").val();
if (!hexColorRe.test(color)) {
warn("Invalid color '" + color + "'. Expected hex format like #1a2b3c.");
return false;
}
var isOn = $("#trackColorOn").is(":checked") ? "1" : "0";
rec.colorOverride = color;
rec.colorOverrideOn = (isOn === "1");
cart.setVars(
[trackName + ".colorOverride", trackName + ".colorOverrideOn"],
[color, isOn], null, false);
imageV2.requestImgUpdate(trackName,
trackName + ".colorOverride=" + encodeURIComponent(color) +
"&" + trackName + ".colorOverrideOn=" + isOn);
return true;
};
$("#" + dialogId).dialog({
modal: true,
title: "Change Track Color",
closeOnEscape: true,
resizable: false,
minWidth: 400,
buttons: {
"Apply": function() { applyColor(); },
"Ok": function() {
if (applyColor())
$(this).dialog("close");
}
},
close: function() {
$("#trackColorPicker").spectrum("destroy");
$(this).remove();
}
});
},
// CGIs now use HTML tags, e.g. "Transcript: ENST00000297261.7 Strand:"
mouseOverToLabel: function(title)
{
if (title.search(/Transcript: ?<[/]b>/) !== -1) {
title = title.split(" ")[0].split("")[1];
}
// for older UCSC genes tracks, the protein name is forced onto the item name
if (title.search(/&hgg_prot=/) !== -1) {
title = title.split("&hgg_prot=")[0];
}
return title;
},
// when "exonNumbers on", the mouse over text is not a good item description for the right-click menu
// "exonNumbers on" is the default for genePred/bigGenePred tracks but can also be actived for bigBed and others
// We don't have the value of the tdb variable "exonNumbers" here, so just use a heuristic to see if it's on
mouseOverToExon: function(title)
{
var exonNum = 0;
var exonRe = /(Exon) (\d+) /;
var matches = exonRe.exec(title);
if (matches !== null && matches[2].length > 0)
exonNum = matches[2];
return exonNum;
},
load: function (img)
{
rightClick.menu = img.contextMenu(function() {
popUp.cleanup(); // Popup box is not getting closed properly so must do it here
if ( ! rightClick.selectedMenuItem ) // This is literally an edge case so ignore
return;
var o; // TODO: Break this giant routine with shared vars into some sub-functions
var str;
var rec = null;
var menu = [];
var selectedImg = rightClick.makeImgTag("greenChecksm.png");
var blankImg = rightClick.makeImgTag("invisible16.png");
var done = false;
if (rightClick.selectedMenuItem && rightClick.selectedMenuItem.id) {
var href = rightClick.selectedMenuItem.href;
var isHgc, isGene;
if (href) {
isGene = href.match("hgGene");
isHgc = href.match("hgc");
}
var id = rightClick.selectedMenuItem.id;
rec = hgTracks.trackDb[id];
var offerHideSubset = false;
var offerHideComposite = false;
var offerSingles = true;
let trid = id.replaceAll(".","\\.");
var row = $( 'tr#tr_' + trid );
if (row) {
var btn = $(row).find('p.btnBlue'); // btnBlue means cursor over left button
if (btn.length === 1) {
var compositeSet = dragReorder.getCompositeSet(row);
if (compositeSet && compositeSet.length > 0) { // There is composite set
offerHideComposite = true;
$( compositeSet ).find('p.btn').addClass('blueButtons');// blue persists
var subSet = dragReorder.getContiguousRowSet(row);
if (subSet && subSet.length > 1) {
offerSingles = false;
if (subSet.length < compositeSet.length) {
offerHideSubset = true;
$( subSet ).addClass("greenRows"); // green persists
}
}
}
}
}
// First option is hide sets
if (offerHideComposite) {
if (offerHideSubset) {
o = {};
o[blankImg + " hide track subset (green)"] = {
onclick: rightClick.makeHitCallback('hideSet')};
menu.push(o);
}
o = {};
str = blankImg + " hide track set";
if (offerHideSubset)
str += " (blue)";
o[str] = {onclick: rightClick.makeHitCallback('hideComposite')};
menu.push(o);
}
// Second set of options: visibility for single track
if (offerSingles) {
if (offerHideComposite)
menu.push($.contextMenu.separator);
// XXXX what if select is not available (b/c trackControlsOnMain is off)?
// Move functionality to a hidden variable?
var select = $("select[name=" + escapeJQuerySelectorChars(id) + "]");
if (select.length > 1)
// Not really needed if $('#hgTrackUiDialog').html(""); has worked
select = [ $(select)[0] ];
var cur = $(select).val();
if (cur) {
$(select).children().each(function(index, o) {
var title = $(this).val();
str = blankImg + " " + title;
if (title === cur)
str = selectedImg + " " + title;
o = {};
o[str] = {onclick: function (menuItemClicked, menuObject) {
rightClick.hit(menuItemClicked, menuObject, title);
return true;}};
menu.push(o);
});
done = true;
} else {
if (rec) {
// XXXX check current state from a hidden variable.
var visStrings = new Array("hide","dense","squish","pack","full");
for (var i in visStrings) {
// use maxVisibility and change hgTracks so it can hide subtracks
o = {};
str = blankImg + " " + visStrings[i];
if (rec.onlyVisibility) {
if (visStrings[i] == "hide" || visStrings[i] === rec.onlyVisibility) {
if (rec.localVisibility) {
if (visStrings[i] === rec.localVisibility) {
str = selectedImg + " " + visStrings[i];
}
} else if (visStrings[i] === vis.enumOrder[rec.visibility]) {
str = selectedImg + " " + visStrings[i];
}
o[str] = { onclick:
rightClick.makeHitCallback(visStrings[i])
};
menu.push(o);
}
}
else {
if (rec.canPack
|| (visStrings[i] !== "pack" && visStrings[i] !== "squish")) {
if (rec.localVisibility) {
if (visStrings[i] === rec.localVisibility) {
str = selectedImg + " " + visStrings[i];
}
} else if (visStrings[i] === vis.enumOrder[rec.visibility]) {
str = selectedImg + " " + visStrings[i];
}
o[str] = { onclick:
rightClick.makeHitCallback(visStrings[i])
};
menu.push(o);
}
}
}
done = true;
}
}
}
if (done) {
o = {};
var any = false;
var title = rightClick.selectedMenuItem.title || "feature";
var maxLength = 60;
if ((isGene || isHgc || id === "wikiTrack") && href.indexOf("i=mergedItem") === -1) {
// Add "Open details..." item
var displayItemFunctions = false;
if (rec) {
if (rec.type.indexOf("wig") === 0
|| rec.type.indexOf("bigWig") === 0
|| id === "wikiTrack") {
displayItemFunctions = false;
} else if (rec.type.indexOf("expRatio") === 0) {
displayItemFunctions = title !== "zoomInMore";
} else {
displayItemFunctions = true;
}
// For barChart mouseovers, replace title (which may be a category
// name+value) with item name
if (rec.type.indexOf("barChart") === 0
|| rec.type.indexOf("bigBarChart") === 0) {
let a = /i=([^&]+)/.exec(href);
if (a && a[1]) {
title = a[1];
}
}
}
// pick out the exon number from the mouseover text
// Probably should be a data-exonNum tag on the DOM element
var exonNum = rightClick.mouseOverToExon(title);
// remove special genePred exon mouseover html text
// CGIs now use HTML tags, e.g. "Transcript: ENST00000297261.7 Strand:"
title = rightClick.mouseOverToLabel(decodeURIComponent(title));
if (title.length > maxLength) {
title = title.substring(0, maxLength) + "...";
}
if (isHgc) {
// For GTEx gene and UniProt mouseovers, replace title (which may be a tissue name) with
// item (gene) name. Also need to unescape the urlencoded characters and the + sign.
let a = /i=([^&]+)/.exec(href);
if (a && a[1]) {
title = decodeURIComponent(a[1].replace(/\+/g, " "));
}
}
if (displayItemFunctions) {
o[rightClick.makeImgTag("magnify.png") + " Zoom to " + title] = {
onclick: function(menuItemClicked, menuObject) {
rightClick.hit(menuItemClicked, menuObject,
"selectWholeGene"); return true;
}
};
o[rightClick.makeImgTag("highlight.png") + " Highlight " + title] =
{ onclick: function(menuItemClicked, menuObject) {
rightClick.hit(menuItemClicked, menuObject,
"highlightItem");
return true;
}
};
//o[rightClick.makeImgTag("highlight.png") + " Highlight THIS item"] =
// { onclick: function(menuItemClicked, menuObject) {
// rightClick.hit(menuItemClicked, menuObject,
// "highlightThisItem");
// return true;
// }
// };
if (rightClick.supportZoomCodon &&
(rec.type.indexOf("genePred") !== -1 || rec.type.indexOf("bigGenePred") !== -1)) {
// http://hgwdev-larrym.gi.ucsc.edu/cgi-bin/hgGene?hgg_gene=uc003tqk.2&hgg_prot=P00533&hgg_chrom=chr7&hgg_start=55086724&hgg_end=55275030&hgg_type=knownGene&db=hg19&c=chr7
var name, table;
var reg = new RegExp("hgg_gene=([^&]+)");
let a = reg.exec(href);
if (a && a[1]) {
name = a[1];
reg = new RegExp("hgg_type=([^&]+)");
a = reg.exec(href);
if (a && a[1]) {
table = a[1];
}
} else {
// http://hgwdev-larrym.gi.ucsc.edu/cgi-bin/hgc?o=55086724&t=55275031&g=refGene&i=NM_005228&c=chr7
// http://hgwdev-larrym.gi.ucsc.edu/cgi-bin/hgc?o=55086713&t=55270769&g=wgEncodeGencodeManualV4&i=ENST00000455089&c=chr7
reg = new RegExp("i=([^&]+)");
a = reg.exec(href);
if (a && a[1]) {
name = a[1];
reg = new RegExp("g=([^&]+)");
a = reg.exec(href);
if (a && a[1]) {
table = a[1];
}
}
}
if (name && table) {
if (exonNum > 0) {
o[rightClick.makeImgTag("magnify.png")+" Zoom to this exon"] = {
onclick: function(menuItemClicked, menuObject) {
$.ajax({
type: "GET",
url: "../cgi-bin/hgApi",
data: cart.varsToUrlData({ 'hgsid': getHgsid(), 'db': getDb(),
'cmd': "exonToPos", 'num': exonNum,
'table': table, 'name': name, 'chrom': hgTracks.chromName}),
trueSuccess: rightClick.handleZoomCodon,
success: catchErrorOrDispatch,
error: errorHandler,
cache: true
});
return true; }
};
o[rightClick.makeImgTag("magnify.png")+" Enter codon to zoom to..."] =
{ onclick: function(menuItemClicked, menuObject) {
rightClick.hit(menuItemClicked, menuObject,
"zoomCodon",
{name: name, table: table, 'chrom': hgTracks.chromName});
return true;}
};
o[rightClick.makeImgTag("magnify.png")+" Enter exon to zoom to..."] =
{ onclick: function(menuItemClicked, menuObject) {
rightClick.hit(menuItemClicked, menuObject,
"zoomExon",
{name: name, table: table, 'chrom': hgTracks.chromName});
return true;}
};
}
}
}
o[rightClick.makeImgTag("dnaIcon.png")+" Get DNA for "+title] = {
onclick: function(menuItemClicked, menuObject) {
rightClick.hit(menuItemClicked, menuObject, "getDna");
return true; }
};
}
o[rightClick.makeImgTag("bookOut.png")+
" Open details page in new window..."] = {
onclick: function(menuItemClicked, menuObject) {
rightClick.hit(menuItemClicked, menuObject, "openLink");
return true; }
};
any = true;
}
if (href && href.length > 0 && href.indexOf("i=mergedItem") === -1) {
// Add "Show details..." item
if (title.indexOf("Click to alter ") === 0) {
// suppress the "Click to alter..." items
} else if (rightClick.selectedMenuItem.href.indexOf("cgi-bin/hgTracks")
!== -1) {
// suppress menu items for hgTracks links (e.g. Next/Prev map items).
} else {
var item;
if (title === "zoomInMore")
// avoid showing menu item that says
// "Show details for zoomInMore..." (redmine 2447)
item = rightClick.makeImgTag("book.png") + " Show details...";
else
item = rightClick.makeImgTag("book.png")+" Show details for "+
title + "...";
o[item] = {onclick: function(menuItemClicked, menuObject) {
rightClick.hit(menuItemClicked,menuObject,"followLink");
return true; }
};
any = true;
}
}
if (any) {
menu.push($.contextMenu.separator);
menu.push(o);
}
}
}
menu.push($.contextMenu.separator);
o = {};
o[rightClick.makeImgTag("hiddenIcon.png") + " Hide all other tracks "] = {
onclick: function(menuItemClicked, menuObject) {
rightClick.hit(menuItemClicked, menuObject, "hideOthers");
return true; }
};
menu.push(o);
//o = {};
//o[" Float "] = {
//onclick: function(menuItemClicked, menuObject) {
//rightClick.hit(menuItemClicked, menuObject, "float");
//return true; }
//};
//menu.push(o);
o = {};
o[rightClick.makeImgTag("ab_up.gif") + " Move to top "] = {
onclick: function(menuItemClicked, menuObject) {
rightClick.hit(menuItemClicked, menuObject, "moveTop");
return true; }
};
menu.push(o);
o = {};
o[rightClick.makeImgTag("ab_down.gif") + " Move to bottom "] = {
onclick: function(menuItemClicked, menuObject) {
rightClick.hit(menuItemClicked, menuObject, "moveBottom");
return true; }
};
menu.push(o);
if (rightClick.selectedMenuItem && rec) {
// Add cfg options at just shy of end...
o = {};
if (tdbIsLeaf(rec)) {
if (rec.configureBy !== 'none'
&& (!tdbIsCompositeSubtrack(rec) || rec.configureBy !== 'clickThrough')) {
// Note that subtracks never do clickThrough because
// parentTrack cfg is the desired clickThrough
o[rightClick.makeImgTag("wrench.png")+" Configure "+rec.shortLabel] = {
onclick: function(menuItemClicked, menuObject) {
rightClick.hit(menuItemClicked, menuObject, "hgTrackUi_popup");
return true; }
};
}
if (rec.parentTrack) {
o[rightClick.makeImgTag("folderWrench.png")+" Configure "+
rec.parentLabel + " track set..."] = {
onclick: function(menuItemClicked, menuObject) {
rightClick.hit(menuItemClicked,menuObject,"hgTrackUi_follow");
return true; }
};
}
} else {
o[rightClick.makeImgTag("folderWrench.png")+" Configure "+rec.shortLabel +
" track set..."] = {
onclick: function(menuItemClicked, menuObject) {
rightClick.hit(menuItemClicked, menuObject, "hgTrackUi_follow");
return true; }
};
}
if (jQuery.floatMgr) {
o[(rightClick.selectedMenuItem.id === rightClick.floatingMenuItem ?
selectedImg : blankImg) + " float"] = {
onclick: function(menuItemClicked, menuObject) {
rightClick.hit(menuItemClicked, menuObject, "float");
return true; }
};
}
// add a toggle to hide/show the merged item(s)
mergeTrack = rightClick.selectedMenuItem.id + ".doMergeItems";
if (rec.hasOwnProperty(mergeTrack)) {
var hasMergedItems = rec[mergeTrack] === 1;
titleStr = rightClick.makeImgTag("wrench.png") + " ";
if (hasMergedItems) {
titleStr += "Show merged items";
} else {
titleStr += "Merge items that span the current region";
}
o[titleStr] = {onclick: function(menuItemClick, menuObject) {
rightClick.hit(menuItemClick, menuObject, "toggleMerge", rec);
return true; }
};
}
o[rightClick.makeImgTag("book.png")+" Track Description "+rec.shortLabel] = {
onclick: function(menuItemClicked, menuObject) {
rightClick.hit(menuItemClicked, menuObject, "hgTrackUi_popup_description");
return true; }
};
if (rec.defaultColor) {
o[rightClick.makeImgTag("palette.png")+" Change Track Color"] = {
onclick: function(menuItemClicked, menuObject) {
rightClick.hit(menuItemClicked, menuObject, "changeTrackColor");
return true; }
};
}
menu.push($.contextMenu.separator);
menu.push(o);
}
menu.push($.contextMenu.separator);
if (hgTracks.highlight && rightClick.clickedHighlightIdx!==null) {
var currentlySeen = ($('#highlightItem').length > 0);
o = {};
// Jumps to highlight when not currently seen in image
var text = (currentlySeen ? " Zoom" : " Jump") + " to highlight";
o[rightClick.makeImgTag("highlightZoom.png") + text] = {
onclick: rightClick.makeHitCallback('jumpToHighlight')
};
if ( currentlySeen ) { // Remove only when seen
o[rightClick.makeImgTag("highlightRemove.png") +
" Remove highlight"] = {
onclick: rightClick.makeHitCallback('removeHighlight')
};
}
menu.push(o);
}
if (rec.isCustomComposite)
{
// add delete from composite
}
else if ((!rec.type.startsWith("wigMaf")) &&
(rec.type.startsWith("bigWig") || rec.type.startsWith("multiWig") || rec.type.startsWith("wig") || rec.type.startsWith("bedGraph"))) {
o = {};
o[" Make a New Collection with \"" + rec.shortLabel + "\""] = {
onclick: rightClick.makeHitCallback("newCollection")
};
menu.push(o);
if (hgTracks.collections) {
var ii;
for(ii=0; ii < hgTracks.collections.length; ii++) {
o = {};
o[" Add to \"" + hgTracks.collections[ii].shortLabel + "\""] = {
onclick: rightClick.makeHitCallback("addCollection")
};
menu.push(o);
}
}
menu.push($.contextMenu.separator);
}
// add sort options if this is a custom composite
if (rec.isCustomComposite && tdbHasParent(rec) && tdbIsLeaf(rec)) {
o = {};
o[" Sort by Magnitude "] = {
onclick: function(menuItemClicked, menuObject) {
rightClick.hit(menuItemClicked, menuObject, "sortExp");
return true; }
};
menu.push(o);
o = {};
o[" Sort by Similarity "] = {
onclick: function(menuItemClicked, menuObject) {
rightClick.hit(menuItemClicked, menuObject, "sortSim");
return true; }
};
menu.push(o);
menu.push($.contextMenu.separator);
}
// Add view image at end
o = {};
o[rightClick.makeImgTag("eye.png") + " View image"] = {
onclick: function(menuItemClicked, menuObject) {
rightClick.hit(menuItemClicked, menuObject, "viewImg");
return true; }
};
menu.push(o);
return menu;
},
{
beforeShow: function(e) {
// console.log(mapItems[rightClick.selectedMenuItem]);
rightClick.selectedMenuItem = rightClick.findMapItem(e);
// find the highlight that was clicked
var imageX = (imageV2.imgTbl[0].getBoundingClientRect().left) + imageV2.LEFTADD;
var xDiff = (e.clientX) - imageX;
var clickPos = genomePos.pixelsToBases(img, xDiff, xDiff+1, hgTracks.winStart, hgTracks.winEnd, false);
rightClick.clickedHighlightIdx = dragSelect.findHighlightIdxForPos(clickPos);
// XXXX? posting.blockUseMap = true;
return true;
},
hideTransition:'hide', // hideCallback fails if these are not defined.
hideSpeed:10,
hideCallback: function() {
$('p.btn.blueButtons').removeClass('blueButtons');
$('tr.trDraggable.greenRows').removeClass('greenRows');
}
});
return;
}
};
//////////////////////////////////
//// external tools ////
//////////////////////////////////
function showExtToolDialog() {
/* show the 'send to external tool' dialog */
// information about external tools is stored in the extTools global list
// defined by a