eb076410a62ea6a94b71358424080af261ba2c25 jcasper Thu Mar 26 03:20:14 2026 -0700 Moved hgFetch into hgTrackUi; added etags, Cache-Control directives, and GET use instead of POST when udcTimeout isn't set, all to support browser caching of remote files; bumped the DataTables version for facetedComposite to facilitate making all the checkboxes have a consistent look; refs #36320 diff --git src/hg/js/facetedComposite.js src/hg/js/facetedComposite.js index 4074198a754..932a5c954a0 100644 --- src/hg/js/facetedComposite.js +++ src/hg/js/facetedComposite.js @@ -1,46 +1,54 @@ // 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-1.13.6.min.js"; - const DATATABLES_SELECT_URL = "../js/dataTables.select-1.7.0.min.js"; + 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-1.13.6.min.css", // dataTables CSS - "../style/dataTables.select-1.7.0.min.css", // dataTables Select CSS + "../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")); - const loadOptional = (url, hgsid, track) => // load if possible otherwise carry on - url ? - fetch("/cgi-bin/hgFetch", { + // fetch file dynamically + const loadOptional = (url, hgsid, track) => { // load if possible otherwise carry on + if (!url) return Promise.resolve(null); + const fetchUrl = `/cgi-bin/hgTrackUi?hgsid=${hgsid}&fileUrl=${url}&track=${track}`; + const req = (fetchUrl.length > 2048 || embeddedData.udcTimeout) ? + fetch("/cgi-bin/hgTrackUi", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: `hgsid=${hgsid}&fileUrl=${url}&track=${track}`, - }).then(r => r.ok ? r.json() : null).catch(() => null) - : Promise.resolve(null); + }) + : 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 = (() => { @@ -120,102 +128,106 @@ // 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, - className: "select-checkbox", defaultContent: "", title: ` `, // no render function needed }; const columns = [checkboxColumn, ...ordinaryColumns]; const hasDataTypes = embeddedData.dataTypes && Object.keys(embeddedData.dataTypes).length > 0; const itemLabel = hasDataTypes ? "samples" : "tracks"; const table = $("#theMetaDataTable").DataTable({ data: metadata, deferRender: true, // seems faster columns: columns, + columnDefs: [ { targets:0, render: DataTable.render.select() } ], responsive: true, - dom: "lrtip", // omit 'f' (global search); per-column inputs suffice - // autoWidth: true, // might help columns shrink to fit content + layout: { + topStart: 'pageLength', + topEnd: null, // omit global search + bottomStart: 'info', + bottomEnd: 'paging' + }, 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"]], language: { lengthMenu: `Show _MENU_ ${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() { // Reset header "select all" checkbox $("#select-all") .prop("checked", false) .prop("indeterminate", false); }, }); // Create "show only selected" toggle in the toolbar const lengthDiv = document.querySelector( - "#theMetaDataTable_wrapper .dataTables_length"); + "#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); toggleWrapper.appendChild(Object.assign( document.createElement("span"), {textContent: "Show only selected rows"})); lengthDiv.appendChild(toggleWrapper); // Create active-filters chip bar (hidden when empty) const activeFiltersDiv = document.createElement("div"); activeFiltersDiv.id = "active-filters"; activeFiltersDiv.style.display = "none"; const wrapper = document.getElementById("theMetaDataTable_wrapper"); wrapper.insertBefore(activeFiltersDiv, lengthDiv.nextSibling); // 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") { + if (col.data === null) { // left empty; toggle is now in the toolbar } else if (col.data && col.data.startsWith("_")) { // no search box for hidden-facet 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 () { @@ -518,36 +530,43 @@ 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("/cgi-bin/hgFetch", { + // fetch file dynamically + let 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, }) - .then(response => { + : 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; });