65bf003397e55927776facd7654de161ea8c1e20
lrnassar
Fri Jan 30 15:59:33 2026 -0800
Downloading our major CDNs and changing their references to all be local, there are some more corner cases that were low priority as discussed in the ticket. This work is done to improve performance, expecially for overseas users. Refs #33998
diff --git src/hg/js/facetedComposite.js src/hg/js/facetedComposite.js
index 2a2f31e613b..8c2298f5acc 100644
--- src/hg/js/facetedComposite.js
+++ src/hg/js/facetedComposite.js
@@ -1,442 +1,442 @@
// 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 = "https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js";
- const DATATABLES_SELECT_URL = "https://cdn.datatables.net/select/1.7.0/js/dataTables.select.min.js";
+ const DATATABLES_URL = "../js/dataTables-1.13.6.min.js";
+ const DATATABLES_SELECT_URL = "../js/dataTables.select-1.7.0.min.js";
const CSS_URLS = [
- "https://cdn.datatables.net/1.13.6/css/jquery.dataTables.min.css", // dataTables CSS
- "https://cdn.datatables.net/select/1.7.0/css/select.dataTables.min.css", // dataTables Select CSS
+ "../style/dataTables-1.13.6.min.css", // dataTables CSS
+ "../style/dataTables.select-1.7.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"));
const loadOptional = url => // load if possible otherwise carry on
url ?
fetch(url).then(r => r.ok ? r.json() : null).catch(() => null)
: Promise.resolve(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) : "";
})();
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");
fetch("/cgi-bin/cartDump", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: `hgsid=${hgsid}&db=${db}&${uriForUpdate}`,
}).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: "Data type",
}));
Object.keys(embeddedData.dataTypes).forEach(name => {
const label = document.createElement("label");
label.innerHTML = `
${name}`;
selector.appendChild(label);
});
const selectedDataTypes = new Set( // get dataTypes selected initially
Object.entries(embeddedData.dataTypes).filter(([_, val]) => val === 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); });
}
function initTable(allData) {
const { metadata, rowToIdx, colNames } = allData;
const ordinaryColumns = colNames.map(key => ({ // all but checkboxes
data: key,
title: toTitleCase(key),
}));
const checkboxColumn = {
data: null,
orderable: false,
className: "select-checkbox",
defaultContent: "",
title: `
`,
// no render function needed
};
const columns = [checkboxColumn, ...ordinaryColumns];
const table = $("#theMetaDataTable").DataTable({
data: metadata,
deferRender: true, // seems faster
columns: columns,
responsive: true,
// autoWidth: true, // might help columns shrink to fit content
order: [[1, "asc"]], // sort by the first data column, not checkbox
pageLength: 50, // show 50 rows per page by default
lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]],
select: { style: "multi", selector: "td:first-child" },
initComplete: function() { // Check appropriate boxes
const api = this.api();
embeddedData.dataElements.forEach(rowName => {
const rowIndex = rowToIdx[rowName];
if (rowIndex !== undefined) {
api.row(rowIndex).select();
}
});
},
drawCallback: function() { // Reset header "select all" checkbox
$("#select-all")
.prop("checked", false)
.prop("indeterminate", false);
},
});
// 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.className === "select-checkbox") { // show selected items
const label = document.createElement("label");
label.title = "Show only selected rows";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.dataset.selectFilter = "true";
label.appendChild(checkbox);
cell.appendChild(label);
} 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 () {
table.column($(this).parent().index()).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();
});
$("#theMetaDataTable thead input[data-select-filter]")
.on("change", function () { table.draw(); });
// implement the 'select all' at the top of the checkbox column
$("#theMetaDataTable thead").on("click", "#select-all", function () {
const rowIsChecked = this.checked;
if (rowIsChecked) {
table.rows({ page: "current" }).select();
} else {
table.rows({ page: "current" }).deselect();
}
});
return table;
} // end initTable
function initFilters(table, allData) {
const { metadata, colorMap, colNames } = allData;
// iterate once over entire data not separately per attribute
const possibleValues = {};
for (const entry of metadata) {
for (const [key, val] of Object.entries(entry)) {
if (possibleValues[key] === null || possibleValues[key] === undefined) {
possibleValues[key] = new Map();
}
const map = possibleValues[key];
map.set(val, (map.get(val) ?? 0) + 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) => {
// skip attributes if they should be excluded from checkbox sets
if (excludeCheckboxes.includes(key)) {
return;
}
const sortedPossibleVals = Array.from(possibleValues[key].entries());
sortedPossibleVals.sort((a, b) => // neg: less-than
a[1] !== b[1] ? b[1] - a[1] : a[0].localeCompare(b[0]));
// Use 'maxCheckboxes' most frequent items (if they appear > 1 time)
let topToShow = sortedPossibleVals
.filter(([val, count]) =>
val.trim().toUpperCase() !== "NA") // why only > 1?
//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;
}
// Add headings and filter checkboxes (only top maxCheckboxes)
const cbSetsDiv = document.createElement("div");
cbSetsDiv.appendChild(Object.assign(document.createElement("strong"), {
textContent: toTitleCase(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} (${count})`));
cbSetsDiv.appendChild(label);
});
filtersDiv.appendChild(cbSetsDiv);
}); // done creating checkbox filters for each column
const checkboxAttributeIndexes = // checkbox sets => cols in data table
colNames.reduce((acc, key, colIdx) => {
if (!excludeCheckboxes.includes(key)) { acc.push(colIdx); }
return acc;
}, []);
// Now do logic to implement checkboxes-based rows display
checkboxAttributeIndexes.forEach((colIdx, idx) => {
const attrDiv = filtersDiv.children[idx];
const cboxes = [ // need this to be 'array' to use 'filter'
...attrDiv.querySelectorAll("input[type=checkbox]")
];
cboxes.forEach(cb => { // Add set of checkboxes for this attribute
cb.addEventListener("change", () => {
const chk = cboxes.filter(c => c.checked).map(c => c.value);
const query = chk.length ? "^(" + chk.join("|") + ")$" : "";
table.column(colIdx + 1).search(query, true, false).draw();
});
});
// Make a "clear" button
const clearBtn = document.createElement("button");
clearBtn.textContent = "Clear";
clearBtn.type = "button"; // prevent form submit if inside a form
clearBtn.addEventListener("click", () => {
cboxes.forEach(cb => cb.checked = false); // Uncheck all
// Recalculate the (now cleared) search term and update table
table.column(colIdx + 1).search("", true, false).draw();
});
// Prepend the "clear" button
attrDiv.insertBefore(clearBtn, attrDiv.children[1] || null);
});
return table; // to chain calls
} // end initFilters
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 selectedRows = table.rows({selected: true}).data().toArray();
const uriForUpdate = new URLSearchParams({ "cartDump.metaDataId": mdid, "noDisplay": 1 });
selectedRows.forEach(obj => // 'de' for data element
uriForUpdate.append(`${mdid}.de`, obj[primaryKey]));
if (hasDataTypes) {
// Collect checked data types
const selectedDataTypes = [];
document.querySelectorAll("input.cbgroup").forEach(cb => {
if (cb.checked) {
selectedDataTypes.push(cb.value);
}
});
// Require at least one data type when the selector exists
if (selectedDataTypes.length === 0) {
alert("Please select at least one data type.");
return; // abort submission
}
selectedDataTypes.forEach(dat => // 'dt' for data type
uriForUpdate.append(`${mdid}.dt`, dat));
} else {
// No data types configured for this track: send empty-string sentinel
uriForUpdate.append(`${mdid}.dt`, "");
}
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 } = embeddedData;
const CACHE_KEY = mdid;
const CACHE_TIMESTAMP = `${CACHE_KEY}_time_stamp`;
const CACHE_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours
const now = Date.now();
const cachedTime = parseInt(localStorage.getItem(CACHE_TIMESTAMP), 10);
let cachedData = null;
let useCache = false;
if (cachedTime && (now - cachedTime < CACHE_EXPIRY_MS)) {
const cachedStr = localStorage.getItem(CACHE_KEY);
cachedData = cachedStr ? JSON.parse(cachedStr) : null;
useCache = !!cachedData;
}
if (useCache) {
initAll(cachedData);
return;
}
fetch(metadataUrl)
.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).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;
});
if (!metadata.length || !colNames.length) {
localStorage.removeItem(CACHE_KEY);
localStorage.removeItem(CACHE_TIMESTAMP);
return;
}
const rowToIdx = Object.fromEntries(
metadata.map((row, i) => [row[primaryKey], i])
);
colorMap = isValidColorMap(colorMap) ? colorMap : null;
const freshData = { metadata, rowToIdx, colNames, colorMap };
// cache the data
localStorage.setItem(CACHE_KEY, JSON.stringify(freshData));
localStorage.setItem(CACHE_TIMESTAMP, now.toString());
initAll(freshData);
});
});
} // 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();
});
});
});