diff --git a/docs/plans/sortable-table-finish-deferred.plan.md b/docs/plans/sortable-table-finish-deferred.plan.md index b0817a4cc..87a3a20ce 100644 --- a/docs/plans/sortable-table-finish-deferred.plan.md +++ b/docs/plans/sortable-table-finish-deferred.plan.md @@ -1,24 +1,22 @@ --- name: Sortable table finish (deferred) -overview: "After Phase 0 merge: steps 5–7 — JS-built filters, cleanup, CKEditor allowlist PR to main." -status: deferred +overview: "Steps 5–6 done on branch; step 7 — CKEditor allowlist PR to main." +status: in-progress --- # Sortable table: Phase 0, then steps 5–7 -**Deferred until Phase 0 PRs are merged.** Pick up from here for dynamic filters + hack retirement. - ## Phase 0: Ship current code Merge as-is: Core-CMS sortableTable, Core-Styles promote util, tup-ui snippet pins. ## Step 5: Dynamic filter markup in JS -Table-only in CMS; JS builds fieldset/controls/output; ARIA set in code. +**Done (branch):** Table-only in CMS; `data-sortable-filters` JSON on the table; JS builds fieldset/controls/output with ARIA in code. Legacy CMS-authored filter groups still work. ## Step 6: Clean up -Remove snippet promote; dedupe JS; bump SHAs. +**Done (branch):** Snippet promote removed; CDN pins use `Core-Styles@v2.57.0` and `Core-CMS@v4.40.0-rc6`. ## Step 7: CKEditor ARIA — separate branch → `main` diff --git a/taccsite_cms/static/site_cms/js/modules/sortableTable.html b/taccsite_cms/static/site_cms/js/modules/sortableTable.html index d4008866a..57f64622d 100644 --- a/taccsite_cms/static/site_cms/js/modules/sortableTable.html +++ b/taccsite_cms/static/site_cms/js/modules/sortableTable.html @@ -1,6 +1,8 @@ + + - -
- Results in the table update as you type or select filters - - - - -
- - - + +
diff --git a/taccsite_cms/static/site_cms/js/modules/sortableTable.js b/taccsite_cms/static/site_cms/js/modules/sortableTable.js index 28f551605..484d5da32 100644 --- a/taccsite_cms/static/site_cms/js/modules/sortableTable.js +++ b/taccsite_cms/static/site_cms/js/modules/sortableTable.js @@ -13,39 +13,297 @@ const SORT_TABLE_CLASS = 'js-sortable'; const FILTER_CLASS = 'js-sortable-filter'; const FILTER_LIST_CLASS = 'js-sortable-filter-list'; const OUTPUT_CLASS = 'js-sortable-total'; -const LIST_CLASS = 'list'; -const SORT_BUTTON_CLASS = 'sort'; +const THEIR_LIST_CLASS = 'list'; +const THEIR_BUTTON_CLASS = 'sort'; const DEFAULT_TABLE_SELECTOR = 'table.' + SORT_TABLE_CLASS; const NOT_SORTABLE_SELECTOR = 'th.not-sortable'; +const FILTERS_DATA_ATTR = 'data-sortable-filters'; + +const FILTER_TEMPLATE_ID = 'sortable-table-filters'; +const FILTER_SEARCH_LABEL_SELECTOR = 'label:has(input[type="search"])'; +const FILTER_SELECT_LABEL_SELECTOR = 'label:has(select)'; let listJsMissingLogged = false; /** + * @typedef {FilterSpecForSearch | FilterSpecForSelect} FilterSpec + * @typedef {{ type: 'search', placeholder?: string }} FilterSpecForSearch + * @typedef {{ type: 'select', column: number, label?: string }} FilterSpecForSelect + */ + +/** + * @param {string} tableId + * @param {ParentNode} scopeElement + * @returns {NodeListOf} + */ +function findFilterControls(tableId, scopeElement) { + return scopeElement.querySelectorAll( + '.' + FILTER_CLASS + '[aria-controls="' + CSS.escape(tableId) + '"]' + ); +} + +/** + * @param {HTMLInputElement | HTMLSelectElement} control + * @param {string} tableId + */ +function registerFilterControl(control, tableId) { + control.classList.add(FILTER_CLASS); + control.setAttribute('aria-controls', tableId); +} + +/** + * @param {HTMLTableElement} table + * @returns {FilterSpec[] | null} + */ +function parseFilterSpecs(table) { + const json = table.getAttribute(FILTERS_DATA_ATTR); + if (!json) { + return null; + } + try { + const specs = JSON.parse(json); + if (!Array.isArray(specs) || !specs.length) { + return null; + } + return specs; + } catch { + console.warn( + '[sortableTable] Invalid JSON in data-sortable-filters.', + table + ); + return null; + } +} + +/** + * @param {HTMLElement} caption + * @param {HTMLTableElement} table + * @param {FilterSpecForSelect} spec + */ +function setSelectFilterCaption(caption, table, spec) { + const columnIndex = spec.column; + const textFallback = spec.label ?? `Column ${columnIndex + 1}`; + const cell = table.tHead?.rows[0]?.cells[columnIndex]; + + if (cell instanceof HTMLTableCellElement && cell.textContent?.trim()) { + caption.replaceChildren( + ...Array.from(cell.childNodes, (node) => node.cloneNode(true)) + ); + return; + } + caption.textContent = textFallback; +} + +/** + * Puts `