6239b81a7033f90749f836e73703c65eabd0de9c
jcasper
Fri Apr 10 15:56:31 2026 -0700
Moved js and css includes for faceted composites into hgTrackUi for better detection
of file changes. Dynamically change the track visibility based on selected subtracks (hiding
it if everything is deselected, making it visible when something is selected). refs #36320
diff --git src/hg/js/facetedComposite.js src/hg/js/facetedComposite.js
index 4df32b24c4f..b0818970768 100644
--- src/hg/js/facetedComposite.js
+++ src/hg/js/facetedComposite.js
@@ -1,726 +1,752 @@
// SPDX-License-Identifier: MIT; (c) 2025 Andrew D Smith (author)
/* jshint esversion: 11 */
$(function() {
/* ADS: Uncomment below to force confirm on unload/reload */
// window.addEventListener("beforeunload", function (e) {
// e.preventDefault(); e.returnValue = ""; });
const DEFAULT_MAX_CHECKBOXES = 20; // ADS: without default, can get crazy
- // ADS: need "matching" versions for the plugins
- const DATATABLES_URL = "../js/dataTables-2.2.2.min.js";
- const DATATABLES_SELECT_URL = "../js/dataTables.select-3.0.0.min.js";
- const CSS_URLS = [
- "../style/dataTables-2.2.2.min.css", // dataTables CSS
- "../style/dataTables.select-3.0.0.min.css", // dataTables Select CSS
- "../style/facetedComposite.css", // Local metadata table CSS
- ];
-
const isValidColorMap = obj => // check the whole thing and ignore if invalid
typeof obj === "object" && obj !== null && !Array.isArray(obj) &&
Object.values(obj).every(x =>
typeof x === "object" && x !== null && !Array.isArray(x) &&
Object.values(x).every(value => typeof value === "string"));
// fetch file dynamically
const loadOptional = (url, hgsid, track) => { // load if possible otherwise carry on
if (!url) return Promise.resolve(null);
let fetchBody = `fileUrl=${url}&track=${track}`;
if (hgsid !== null) {
fetchBody = fetchBody + `&hgsid=${hgsid}`;
}
const fetchUrl = `/cgi-bin/hgTrackUi?${fetchBody}`;
const req = (fetchUrl.length > 2048 || embeddedData.udcTimeout) ?
fetch("/cgi-bin/hgTrackUi", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: fetchBody,
})
: fetch(fetchUrl, {
method: "GET",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
});
return req.then(r => r.ok ? r.json() : null).catch(() => null);
};
- const loadIfMissing = (condition, url, callback) => // for missing plugins
- condition ?
- document.head.appendChild(Object.assign(
- document.createElement("script"), { src: url, onload: callback }))
- : callback();
-
const toTitleCase = str =>
str.toLowerCase()
.split(/[_\s-]+/) // Split on underscore, space, or hyphen
.map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
const escapeRegex = str => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const embeddedData = (() => {
// get data that was embedded in the HTML here to use them as globals
const dataTag = document.getElementById("app-data");
return dataTag ? JSON.parse(dataTag.innerText) : "";
})();
// Store initial checkbox states for delta computation on server
const initialState = {
dataElements: new Set(),
dataTypes: new Set()
};
function generateHTML() {
const container = document.createElement("div");
container.id = "myTag";
container.innerHTML = `
`;
// Instead of appending to body, append into the placeholder div
document.getElementById("metadata-placeholder").appendChild(container);
}
function updateVisibilities(uriForUpdate, submitBtnEvent) {
// get query params from URL
const paramsFromUrl = new URLSearchParams(window.location.search);
const db = paramsFromUrl.get("db");
const hgsid = paramsFromUrl.get("hgsid");
let body = `${uriForUpdate}`;
if (db !== null) {
body = body + `&db=${db}`;
}
if (hgsid !== null) {
body = body + `&hgsid=${hgsid}`;
}
fetch("/cgi-bin/cartDump", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: body,
}).then(() => {
// 'disable' any CSS named elements here to them keep out of cart
const dtLength = submitBtnEvent.
target.form.querySelector("select[name$='_length']");
if (dtLength) {
dtLength.disabled = true;
}
submitBtnEvent.target.form.submit(); // release submit event
});
}
function initDataTypeSelector() {
// Skip if no dataTypes defined or empty object
if (!embeddedData.dataTypes || Object.keys(embeddedData.dataTypes).length === 0) {
return;
}
const selector = document.getElementById("dataTypeSelector");
selector.appendChild(Object.assign(document.createElement("label"), {
innerHTML: "Subtrack types enabled:",
}));
Object.keys(embeddedData.dataTypes).forEach(name => {
const label = document.createElement("label");
const dataType = embeddedData.dataTypes[name];
label.innerHTML = `
${dataType.title}`;
selector.appendChild(label);
});
const selectedDataTypes = new Set( // get dataTypes selected initially
Object.entries(embeddedData.dataTypes).filter(([_, val]) => val.active === 1)
.map(([key]) => key)
);
// initialize data type checkboxes (using class instead of 'name')
document.querySelectorAll("input.cbgroup")
.forEach(cb => { cb.checked = selectedDataTypes.has(cb.value); });
// Capture initial data type state
initialState.dataTypes = new Set(selectedDataTypes);
}
function initTable(allData) {
const { metadata, rowToIdx, colNames } = allData;
const ordinaryColumns = colNames.map(key => ({ // all but checkboxes
data: key,
title: toTitleCase(key.replace(/^_/, "")),
}));
const checkboxColumn = {
data: null,
orderable: false,
defaultContent: "",
title: `
`,
// no render function needed
};
const hasDataTypes = embeddedData.dataTypes &&
Object.keys(embeddedData.dataTypes).length > 0;
const itemLabel = hasDataTypes ? "samples" : "tracks";
const singularLabel = itemLabel.slice(0, -1);
const columns = [checkboxColumn, ...ordinaryColumns];
const table = $("#theMetaDataTable").DataTable({
data: metadata,
deferRender: true, // seems faster
columns: columns,
columnDefs: [ { targets:0, render: DataTable.render.select() } ],
responsive: true,
layout: {
topStart: 'pageLength',
topEnd: null, // omit global search
bottomStart: 'info',
bottomEnd: 'paging'
},
order: [[1, "asc"]], // sort by the first data column, not checkbox
pageLength: 25, // show 25 rows per page by default
lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]],
language: {
lengthMenu: `Show _MENU_ ${itemLabel}`,
select: {
rows: {
0: "",
1: `1 ${singularLabel} selected`,
_: `%d ${itemLabel} selected`
}
},
info: `Showing _START_ to _END_ of _TOTAL_ ${itemLabel}`,
infoFiltered: `(filtered from _MAX_ total ${itemLabel})`,
},
select: { style: "multi", selector: "td:not(:has(a))" },
initComplete: function() { // Check appropriate boxes
const api = this.api();
embeddedData.dataElements.forEach(rowName => {
const rowIndex = rowToIdx[rowName];
if (rowIndex !== undefined) {
api.row(rowIndex).select();
}
});
// Capture initial data element state
initialState.dataElements = new Set(embeddedData.dataElements);
},
drawCallback: function() {
updateSelectAllCheckbox(this.api());
},
});
function updateSelectAllCheckbox(api) {
const filteredCount = api.rows({ search: "applied" }).count();
const selectedCount = api.rows({ search: "applied", selected: true }).count();
$("#select-all")
.prop("checked", filteredCount > 0 && selectedCount === filteredCount)
.prop("indeterminate", selectedCount > 0 && selectedCount < filteredCount);
}
- table.on("select deselect", () => updateSelectAllCheckbox(table));
+ // Find the Display Mode dropdown rendered by C code
+ const visDropdown = document.querySelector(
+ 'select[name="' + embeddedData.track + '"]');
+
+ // Track preferred non-hide visibility for auto-restore
+ let preferredVis = "full";
+ if (visDropdown && visDropdown.value !== "hide") {
+ preferredVis = visDropdown.value;
+ }
+
+ // Track previous selection count for detecting 0<->nonzero transitions
+ let prevSelCount = table.rows({selected: true}).count();
+
+ // Update preferredVis when user manually changes the dropdown
+ if (visDropdown) {
+ visDropdown.addEventListener("change", function() {
+ if (this.value !== "hide") {
+ preferredVis = this.value;
+ }
+ });
+ }
+
updateSelectAllCheckbox(table); // set initial state after pre-selections
// Create "show only selected" toggle in the toolbar
const lengthDiv = document.querySelector(
"#theMetaDataTable_wrapper .dt-length");
const toggleWrapper = document.createElement("div");
toggleWrapper.id = "selected-filter";
const toggleLabel = document.createElement("label");
toggleLabel.classList.add("toggle-switch");
const toggleCheckbox = document.createElement("input");
toggleCheckbox.type = "checkbox";
toggleCheckbox.dataset.selectFilter = "true";
toggleLabel.appendChild(toggleCheckbox);
toggleLabel.appendChild(Object.assign(
document.createElement("span"), {className: "toggle-slider"}));
toggleWrapper.appendChild(toggleLabel);
const toggleText = Object.assign(
document.createElement("span"), {id: "selected-filter-text"});
toggleWrapper.appendChild(toggleText);
lengthDiv.appendChild(toggleWrapper);
+ // Disable toggle initially if no rows are selected
+ toggleCheckbox.disabled = (prevSelCount === 0);
+
function updateSelectedText() {
const selCount = table.rows({selected: true}).count();
const totalCount = table.rows().count();
toggleText.textContent =
`Show only selected ${itemLabel} (${selCount} of ${totalCount} selected)`;
}
updateSelectedText();
- table.on("select deselect", updateSelectedText);
+
+ // Unified handler for selection changes
+ function onSelectionChanged() {
+ const selCount = table.rows({selected: true}).count();
+
+ // Disable toggle when nothing is selected; auto-uncheck if count hits 0
+ toggleCheckbox.disabled = (selCount === 0);
+ if (selCount === 0 && toggleCheckbox.checked) {
+ toggleCheckbox.checked = false;
+ table.draw();
+ }
+
+ // Auto-switch Display Mode on 0<->nonzero transitions
+ if (visDropdown) {
+ if (selCount === 0 && prevSelCount > 0) {
+ visDropdown.value = "hide";
+ } else if (selCount > 0 && prevSelCount === 0) {
+ visDropdown.value = preferredVis;
+ }
+ }
+
+ updateSelectAllCheckbox(table);
+ updateSelectedText();
+ prevSelCount = selCount;
+ }
+ table.on("select deselect", onSelectionChanged);
// Create active-filters chip bar (hidden when empty)
const activeFiltersDiv = document.createElement("div");
activeFiltersDiv.id = "active-filters";
activeFiltersDiv.style.display = "none";
const tableEl = document.getElementById("theMetaDataTable");
tableEl.parentNode.insertBefore(activeFiltersDiv, tableEl);
// define inputs for search functionality for each column in the table
const row = document.querySelector("#theMetaDataTable thead").insertRow();
columns.forEach((col) => {
const cell = row.insertCell();
if (col.data === null) {
// left empty; toggle is now in the toolbar
} else if (col.data && col.data.startsWith("__")) {
// no search box for double-underscore columns
} else {
const input = document.createElement("input");
input.type = "text";
input.placeholder = "Search...";
input.style.width = "100%";
cell.appendChild(input);
}
});
// behaviors for the column-based search functionality
$("#theMetaDataTable thead input[type='text']")
.on("keyup change", function () {
const dtColIdx = $(this).parent().index();
const colName = colNames[dtColIdx - 1]; // offset for checkbox col
if (this.value) {
textFilters.set(colName, this.value.toLowerCase());
} else {
textFilters.delete(colName);
}
table.column(dtColIdx).search(this.value).draw();
});
$.fn.dataTable.ext.search.push(function (_, data, dataIndex) {
const filterInput =
document.querySelector("input[data-select-filter]");
if (!filterInput?.checked) { // If checkbox not checked, show all rows
return true;
}
// Otherwise, only show selected rows
const row = table.row(dataIndex);
return row.select && row.selected();
});
$("#selected-filter input[data-select-filter]")
.on("change", function () { table.draw(); });
// implement the 'select all' at the top of the checkbox column
$("#select-all").closest("label").attr(
"title", `Select all filtered ${itemLabel}`);
$("#theMetaDataTable thead").on("click", "#select-all", function () {
const rowIsChecked = this.checked;
if (rowIsChecked) {
table.rows({ search: "applied" }).select();
} else {
table.rows({ search: "applied" }).deselect();
}
});
return table;
} // end initTable
// Map of colName -> Map of unescapedValue -> spanElement, for dynamic counts
const countSpans = new Map();
// Filter state for cross-facet count computation
const checkboxFilters = new Map(); // colName -> Set (raw values)
const textFilters = new Map(); // colName -> lowercase string
function updateFacetCounts(metadata) {
// For each facet, count values among rows that pass all OTHER filters
// (excluding this facet's own checkbox filter). This way, unchecked
// values show how many rows would be added if you checked them.
for (const [facetCol, valMap] of countSpans) {
const counts = new Map(); // lowercased value -> count
for (const row of metadata) {
let passes = true;
for (const [col, valueSet] of checkboxFilters) {
if (col === facetCol) continue;
if (!valueSet.has(row[col]?.toLowerCase())) {
passes = false; break;
}
}
if (passes) {
for (const [col, text] of textFilters) {
if (!row[col]?.toLowerCase().includes(text)) {
passes = false; break;
}
}
}
if (passes) {
const val = row[facetCol]?.toLowerCase();
counts.set(val, (counts.get(val) ?? 0) + 1);
}
}
for (const [val, span] of valMap) {
span.textContent = `(${counts.get(val.toLowerCase()) ?? 0})`;
}
}
}
function initFilters(table, allData) {
const { metadata, colorMap, colNames } = allData;
// iterate once over entire data not separately per attribute
// Case-insensitive: merge variants, keep first-seen casing as display form
const possibleValues = {}; // key -> Map
for (const entry of metadata) {
for (const [key, val] of Object.entries(entry)) {
if (!possibleValues[key]) {
possibleValues[key] = new Map();
}
const map = possibleValues[key];
const lower = val.toLowerCase();
const existing = map.get(lower);
if (existing) {
existing[1]++;
} else {
map.set(lower, [val, 1]);
}
}
}
let { maxCheckboxes, primaryKey } = embeddedData;
if (maxCheckboxes === null || maxCheckboxes === undefined) {
maxCheckboxes = DEFAULT_MAX_CHECKBOXES;
}
const excludeCheckboxes = [primaryKey];
const filtersDiv = document.getElementById("filters");
colNames.forEach((key, colIdx) => {
// skip attributes if they should be excluded from checkbox sets
if (excludeCheckboxes.includes(key) || key.startsWith("_")) {
return;
}
// possibleValues[key] is Map; extract [displayVal, count]
const sortedPossibleVals = Array.from(possibleValues[key].values());
sortedPossibleVals.sort((a, b) => b[1] - a[1]); // sort by count descending
// Use 'maxCheckboxes' most frequent items (if they appear > 1 time)
let topToShow = sortedPossibleVals
.filter(([val, count]) =>
val.trim().toUpperCase() !== "NA" && count > 1)
.slice(0, maxCheckboxes);
// Any "other/Other/OTHER" entry will be put at the end
let otherKey = null, otherValue = null;
topToShow = topToShow.filter(([val, value]) => {
if (val.toLowerCase() === "other") {
otherKey = val;
otherValue = value;
return false;
}
return true;
});
if (otherValue !== null) {
topToShow.push([otherKey, otherValue]);
}
if (topToShow.length <= 1) { // no point if there's only one group
excludeCheckboxes.push(key);
return;
}
// --- Build the facet group with collapsible structure ---
const facetDiv = document.createElement("div");
facetDiv.classList.add("facet-group");
// Clickable heading that toggles collapse
const heading = Object.assign(document.createElement("strong"), {
textContent: toTitleCase(key),
className: "facet-heading",
});
facetDiv.appendChild(heading);
// Collapsible body: holds Clear button + all checkboxes
const facetBody = document.createElement("div");
facetBody.classList.add("facet-body");
// Clear button — built here so it lives inside the collapsible body
const clearBtn = document.createElement("button");
clearBtn.textContent = "Clear";
clearBtn.type = "button";
facetBody.appendChild(clearBtn);
// Build checkbox labels
const cboxes = [];
const rawValues = []; // parallel to cboxes: unescaped values
if (!countSpans.has(key)) countSpans.set(key, new Map());
const colSpans = countSpans.get(key);
topToShow.forEach(([val, count]) => {
const label = document.createElement("label");
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.value = escapeRegex(val);
label.appendChild(checkbox);
if (colorMap && key in colorMap) {
const colorBox = document.createElement("span");
colorBox.classList.add("color-box");
if (val in colorMap[key]) {
colorBox.style.backgroundColor = colorMap[key][val];
}
label.appendChild(colorBox);
}
label.appendChild(document.createTextNode(`${val} `));
const countSpan = document.createElement("span");
countSpan.textContent = `(${count})`;
label.appendChild(countSpan);
colSpans.set(val, countSpan);
facetBody.appendChild(label);
cboxes.push(checkbox);
rawValues.push(val);
});
facetDiv.appendChild(facetBody);
filtersDiv.appendChild(facetDiv);
// --- Wire up collapse toggle ---
heading.addEventListener("click", () => {
const isCollapsed = facetBody.classList.toggle("collapsed");
heading.classList.toggle("collapsed", isCollapsed);
});
// --- Wire up checkbox filtering (same logic as before) ---
// colIdx is the 0-based index into colNames; DataTable column is
// colIdx + 1 because column 0 is the select-checkbox column.
const dtColIdx = colIdx + 1;
cboxes.forEach(cb => {
cb.addEventListener("change", () => {
const checked = cboxes.filter(c => c.checked).map(c => c.value);
const query = checked.length ? "^(" + checked.join("|") + ")$" : "";
// Track lowercased values for cross-facet counting
const checkedRaw = new Set();
cboxes.forEach((c, i) => {
if (c.checked) checkedRaw.add(rawValues[i].toLowerCase());
});
if (checkedRaw.size) {
checkboxFilters.set(key, checkedRaw);
} else {
checkboxFilters.delete(key);
}
table.column(dtColIdx).search(query, true, false).draw();
updateActiveFilters();
});
});
// --- Wire up Clear button ---
clearBtn.addEventListener("click", () => {
cboxes.forEach(cb => cb.checked = false);
checkboxFilters.delete(key);
table.column(dtColIdx).search("", true, false).draw();
updateActiveFilters();
});
}); // done creating collapsible checkbox filters for each column
// Update facet counts whenever the table is redrawn (filtering, search, etc.)
table.on("draw", () => updateFacetCounts(metadata));
return table; // to chain calls
} // end initFilters
function updateActiveFilters() {
const container = document.getElementById("active-filters");
if (!container) return;
container.innerHTML = "";
const checked = document.querySelectorAll(
"#filters input[type='checkbox']:checked");
if (checked.length === 0) {
container.style.display = "none";
return;
}
// Group by facet name
const groups = new Map();
checked.forEach(cb => {
const facetGroup = cb.closest(".facet-group");
if (!facetGroup) return;
const heading = facetGroup.querySelector(".facet-heading");
if (!heading) return;
const facetName = heading.textContent.trim();
// Get the display text from the label (strip the count suffix)
const label = cb.parentElement;
const labelText = label.textContent.trim();
if (!groups.has(facetName)) groups.set(facetName, []);
groups.get(facetName).push({ labelText, checkbox: cb });
});
groups.forEach((chips, facetName) => {
const groupLabel = document.createElement("span");
groupLabel.className = "filter-chip-group-label";
groupLabel.textContent = facetName + ":";
container.appendChild(groupLabel);
chips.forEach(({ labelText, checkbox }) => {
const chip = document.createElement("span");
chip.className = "filter-chip";
chip.appendChild(document.createTextNode(labelText + " "));
const removeBtn = document.createElement("button");
removeBtn.className = "remove-chip";
removeBtn.type = "button";
removeBtn.textContent = "\u00d7";
removeBtn.addEventListener("click", () => {
checkbox.checked = false;
checkbox.dispatchEvent(new Event("change"));
});
chip.appendChild(removeBtn);
container.appendChild(chip);
});
});
container.style.display = "flex";
}
function initSubmit(table) { // logic for the submit event
const { mdid, primaryKey } = embeddedData; // mdid: metadata identifier
const hasDataTypes = embeddedData.dataTypes &&
Object.keys(embeddedData.dataTypes).length > 0;
document.getElementById("Submit").addEventListener("click", (submitBtnEvent) => {
submitBtnEvent.preventDefault(); // hold the submit button event
const currentDataTypes = [];
if (hasDataTypes) {
// Get current data type selections
document.querySelectorAll("input.cbgroup").forEach(cb => {
if (cb.checked) {
currentDataTypes.push(cb.value);
}
});
// Require at least one data type when the selector exists
if (currentDataTypes.length === 0) {
alert("Please select at least one data type.");
return; // abort submission
}
}
// Get current data element selections
const currentDataElements = table.rows({selected: true}).data().toArray()
.map(obj => obj[primaryKey]);
// Enforce an upper bound on the number of tracks on at the same time.
// This is imperfect when data types are present - some combinations might
// have been manually hidden by the user. But it should be a good ballpark.
const trackLimit = 1000;
if (hasDataTypes) {
if (currentDataTypes.length * currentDataElements.length > trackLimit) {
alert("You have turned on too many subtracks (over 1000) - please uncheck some.");
return; // abort submission
}
} else {
if (currentDataElements.length > trackLimit) {
alert("You have turned on too many subtracks (over 1000) - please uncheck some.");
return; // abort submission
}
}
// Build the parameters for the cart update
const uriForUpdate = new URLSearchParams({
"cartDump.metaDataId": mdid,
"noDisplay": 1
});
// Data elements: was and now
if (initialState.dataElements.size > 0) {
initialState.dataElements.forEach(de =>
uriForUpdate.append(`${mdid}.de_was`, de));
} else {
uriForUpdate.append(`${mdid}.de_was`, "");
}
if (currentDataElements.length > 0) {
currentDataElements.forEach(de =>
uriForUpdate.append(`${mdid}.de_now`, de));
} else {
uriForUpdate.append(`${mdid}.de_now`, "");
}
if (hasDataTypes) {
// Data types: was and now
if (initialState.dataTypes.size > 0) {
initialState.dataTypes.forEach(dt => {
uriForUpdate.append(`${mdid}.dt_was`, dt);});
} else {
uriForUpdate.append(`${mdid}.dt_was`, "");
}
if (currentDataTypes.length > 0) {
currentDataTypes.forEach(dt => {
uriForUpdate.append(`${mdid}.dt_now`, dt);});
} else {
uriForUpdate.append(`${mdid}.dt_now`, "");
}
}
// No ${mdid}.dt* variables indicates that the composite doesn't use data types
updateVisibilities(uriForUpdate, submitBtnEvent);
});
} // end initSubmit
function initAll(dataForTable) {
initDataTypeSelector();
const table = initTable(dataForTable);
initFilters(table, dataForTable);
initSubmit(table);
}
function loadDataAndInit() { // load data and call init functions
const { mdid, primaryKey, metadataUrl, colorSettingsUrl, track } = embeddedData;
const paramsFromUrl = new URLSearchParams(window.location.search);
const hgsid = paramsFromUrl.get("hgsid");
let fetchBody = `fileUrl=${metadataUrl}&track=${track}`;
if (hgsid !== null) {
fetchBody = fetchBody + `&hgsid=${hgsid}`;
}
// fetch file dynamically
const fetchUrl = "/cgi-bin/hgTrackUi?" + fetchBody;
const req = (fetchUrl.length > 2048 || embeddedData.udcTimeout) ?
fetch("/cgi-bin/hgTrackUi", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: fetchBody,
})
: fetch(fetchUrl, {
method: "GET",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
});
req.then(response => {
if (!response.ok) { // a 404 will look like plain text
throw new Error(`HTTP Status: ${response.status}`);
}
return response.text();
})
.then(tsvText => { // metadata table is a TSV file to parse
loadOptional(colorSettingsUrl, hgsid, track).then(colorMap => {
const rows = tsvText.trim().split("\n");
const colNames = rows[0].split("\t");
const metadata = rows.slice(1).map(row => {
const values = row.split("\t");
const obj = {};
colNames.forEach((attrib, i) => { obj[attrib] = values[i]; });
return obj;
});
const rowToIdx = Object.fromEntries(
metadata.map((row, i) => [row[primaryKey], i])
);
colorMap = isValidColorMap(colorMap) ? colorMap : null;
const freshData = { metadata, rowToIdx, colNames, colorMap };
initAll(freshData);
});
})
.catch(err => {
const table = document.getElementById("theMetaDataTable");
if (table) {
table.innerHTML =
`| ` +
`Error loading metadata: ${err.message} |
`;
}
});
} // end loadDataAndInit
- CSS_URLS.map(href => // load all the CSS
- document.head.appendChild(Object.assign(
- document.createElement("link"), { rel: "stylesheet", href })));
-
document.addEventListener("keydown", e => { // block accidental submit
if (e.key === "Enter") { e.preventDefault(); e.stopPropagation(); }
}, true);
- // ADS: only load plugins if they are not already loaded
- loadIfMissing(!$.fn.DataTable, DATATABLES_URL, () => {
- loadIfMissing(!$.fn.dataTable.select, DATATABLES_SELECT_URL, () => {
generateHTML();
loadDataAndInit();
- });
- });
+
});