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,65 +1,50 @@ // 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 = { @@ -203,60 +188,109 @@ // 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("__")) { @@ -696,31 +730,23 @@ 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(); - }); - }); + });