ef4a5ad1ea45eda4fde693f56f2106c553e87d93
chmalee
Wed May 20 15:24:30 2026 -0700
Add cnv type to myVariants, refs #33808
diff --git src/hg/js/hgTracks.js src/hg/js/hgTracks.js
index ba1cda170cb..97b72d6c83e 100644
--- src/hg/js/hgTracks.js
+++ src/hg/js/hgTracks.js
@@ -634,180 +634,259 @@
$(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) {
- // Simple fields shown by default (matches hgc edit form style)
- const simpleFields = [
+ // 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." },
+ 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." },
- { label: "Alt", id: "alt", type: "text", placeholder: "Optional alternate allele sequence",
- info: "Alternate (variant) allele sequence." },
- { 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." },
+ info: "Reference allele sequence at this position" },
];
-
- // Advanced fields hidden by default
- const advancedFields = [
+ // 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." },
+ info: "1-based start position on the chromosome" },
{ label: "End", id: "end", type: "number",
- info: "1-based end position on the chromosome (inclusive)." },
+ info: "1-based end position on the chromosome (inclusive)" },
{ label: "Score", id: "score", type: "number",
- info: "Score from 0-1000. Higher scores display darker." },
+ info: "Score from 0-1000. Higher scores display darker" },
{ label: "Strand", id: "strand", type: "text",
- info: "Strand: + for forward, - for reverse, . for unknown." },
- { label: "Thick Start", id: "thickStart", type: "number",
- info: "Start of thickly drawn region (for display purposes)." },
- { label: "Thick End", id: "thickEnd", type: "number",
- info: "End of thickly drawn region (for display purposes)." },
+ 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";
- // Helper to resize dialog based on current mode
let resizeDialog = function() {
var dialog = $("#myVariantsDialog");
var hgvsVisible = document.getElementById("hgvsInputDiv").style.display !== "none";
- var advancedVisible = document.getElementById("advancedFieldsDiv") &&
- document.getElementById("advancedFieldsDiv").style.display !== "none";
-
if (hgvsVisible) {
- // Quick input mode - compact
dialog.dialog("option", "width", 580);
dialog.dialog("option", "height", "auto");
- } else if (advancedVisible) {
- // Manual mode with advanced fields - larger
- dialog.dialog("option", "width", Math.min(700, window.innerWidth * 0.8));
- dialog.dialog("option", "height", Math.min(600, window.innerHeight * 0.8));
+ dialog.dialog("option", "position", { my: "center", at: "center", of: window });
} else {
- // Manual mode without advanced fields - medium
- dialog.dialog("option", "width", Math.min(650, window.innerWidth * 0.8));
- dialog.dialog("option", "height", "auto");
+ 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 });
}
- // Re-center the dialog after resize
- dialog.dialog("option", "position", { my: "center", at: "center", of: window });
};
- // Helper to format and update the position summary shown in manual mode
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.innerHTML = "Position: " + chrom.value +
- ":" + startFmt + "-" + endFmt + " (from current view)";
+ 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();
- let hgvsInputStyle = document.getElementById("hgvsInputDiv").style.display;
- let manualInputStyle = document.getElementById("manualInputDiv").style.display;
- document.getElementById("hgvsInputDiv").style.display = hgvsInputStyle === "none" ? "" : "none";
- document.getElementById("manualInputDiv").style.display = manualInputStyle === "none" ? "" : "none";
- document.getElementById("hgvsManualToggle").textContent = hgvsInputStyle === "none" ? "Or edit item fields manually" : "Back to quick input mode";
- resizeDialog();
+ 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";
- // Position summary line at top of manual form
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");
- let advToggle = document.getElementById("advancedFieldsToggle");
- if (advDiv && advDiv.style.display === "none") {
+ if (!advDiv) return;
+ if (advDiv.style.display === "none") {
advDiv.style.display = "";
- if (advToggle) advToggle.textContent = "Hide Advanced Fields \u25B2";
- resizeDialog();
- }
- // Scroll to and focus the chrom field
+ 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";
@@ -927,118 +1006,177 @@
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."));
+ wrapper.appendChild(createInfoIcon("Group annotations by project. Projects with no annotations are automatically removed from the list"));
container.appendChild(wrapper);
};
- // Create simple fields
+ // 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;
- simpleFields.forEach(field => {
- // Handle project field specially
- if (field.id === "project") {
- createProjectField(manualInpDiv);
- } else {
+ let nameWrap, colorWrap;
+ commonTopFields.forEach(function(field) {
let input = createField(field, manualInpDiv);
- if (field.id === "color") {
- colorInput = input;
- }
- }
+ if (field.id === "color") { colorInput = input; colorWrap = input.parentNode; }
+ else if (field.id === "name") { nameWrap = input.parentNode; }
});
- // Toggle for advanced fields
- let advancedToggleContainer = document.createElement("p");
- let advancedToggle = document.createElement("a");
- advancedToggle.href = "#";
- advancedToggle.id = "advancedFieldsToggle";
- advancedToggle.textContent = "Show Advanced Fields ▼";
- advancedToggleContainer.appendChild(advancedToggle);
- manualInpDiv.appendChild(advancedToggleContainer);
+ // 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);
- // Advanced fields div (hidden by default)
let advancedDiv = document.createElement("div");
advancedDiv.id = "advancedFieldsDiv";
advancedDiv.style.display = "none";
- advancedDiv.style.marginLeft = "20px";
- advancedDiv.style.borderLeft = "2px solid #ccc";
- advancedDiv.style.paddingLeft = "10px";
+ let posWraps = positionFields.map(function(field) {
+ return createField(field, advancedDiv).parentNode;
+ });
+ manualInpDiv.appendChild(advancedDiv);
- advancedFields.forEach(field => {
- createField(field, advancedDiv);
+ let cdsBlocksDiv = document.createElement("div");
+ cdsBlocksDiv.id = "cdsBlocksDiv";
+ cdsBlocksDiv.style.display = "none";
+ let cdsWraps = cdsFields.map(function(field) {
+ return createField(field, cdsBlocksDiv).parentNode;
});
- // Blocks (BED12) section. Empty rows means a single full-span block.
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);
- // Hidden inputs that the widget keeps in sync. createItem reads them.
- 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";
+ 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);
- advancedDiv.appendChild(blocksSection);
+ 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;
+ }
+ });
- // Custom fields section inside advanced fields
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);
- // Reserved field names that cannot be used as custom field names
let reservedNames = ["bin", "chrom", "chromStart", "chromEnd", "name", "score",
- "strand", "thickStart", "thickEnd", "itemRgb", "description", "db", "ref",
- "alt", "project", "mouseover", "id"];
+ "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;
@@ -1102,54 +1240,50 @@
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();
- resizeDialog();
})
.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();
- resizeDialog();
});
row.appendChild(removeBtn);
}
customFieldsList.appendChild(row);
- if (document.getElementById("myVariantsDialog"))
- resizeDialog();
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";
@@ -1186,68 +1320,47 @@
// 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);
}
- advancedDiv.appendChild(customFieldsSection);
-
- manualInpDiv.appendChild(advancedDiv);
+ manualInpDiv.appendChild(customFieldsSection);
- // Keep the position summary in sync when advanced fields change
+ // 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);
- }
+ if (el) el.addEventListener("input", updatePositionSummary);
});
- // Initialize the position summary text using direct references
- // (the form is not yet in the document, so getElementById won't work)
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.innerHTML = "Position: " + chromEl.value +
- ":" + startFmt + "-" + endFmt + " (from current view)";
- }
-
- // Toggle handler for advanced fields
- advancedToggle.addEventListener("click", function(event) {
- event.preventDefault();
- let advDiv = document.getElementById("advancedFieldsDiv");
- if (advDiv.style.display === "none") {
- advDiv.style.display = "";
- advancedToggle.textContent = "Hide Advanced Fields ▲";
- } else {
- advDiv.style.display = "none";
- advancedToggle.textContent = "Show Advanced Fields ▼";
+ posSummaryText.textContent = chromEl.value + ":" + startFmt + "-" + endFmt;
}
- resizeDialog();
- });
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);
@@ -1275,30 +1388,32 @@
// 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");
@@ -1314,128 +1429,147 @@
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 {
- // got here after async image update, need to update the bed form coordinates
+ // 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;
- start.value = thickStart.value = hgTracks.winStart;
- end.value = thickEnd.value = hgTracks.winEnd;
- // Update the position summary to reflect new coordinates
+ 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.innerHTML = "Position: " + (chromEl ? chromEl.value : hgTracks.chromName) +
- ":" + startFmt + "-" + endFmt + " (from current view)";
+ 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
- // Strict block check now (server will re-check authoritatively).
- // noBlocks means the user added a row but left every size empty;
- // treat as no blocks so the server synthesizes a single full-span block.
+ // Block validation only matters for the transcript path; SNV/CNV don't carry blocks.
let blockResult = {ok: true, noBlocks: true};
- if (form.blocksWidget && form.blocksWidget.getRowCount() > 0) {
+ 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.
- // Omit when blockCount is 0, or when the widget reported noBlocks
- // (rows added but all sizes left empty); server then synthesizes
- // a single full-span block.
+ // 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 (!bc || blockResult.noBlocks) {
+ 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()