Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions profiles/w3c.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const modules = [
import("../src/core/data-type.js"),
import("../src/core/anchor-expander.js"),
import("../src/core/dfn-panel.js"),
import("../src/core/sortable-table.js"),
import("../src/core/custom-elements/index.js"),
import("../src/core/web-monetization.js"),
import("../src/core/dfn-contract.js"),
Expand Down
28 changes: 28 additions & 0 deletions src/core/sortable-table.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// @ts-check
import css from "../styles/sortable-table.css.js";
import { fetchBase } from "./text-loader.js";

export const name = "core/sortable-table";

export async function run() {
if (!document.querySelector("table.sortable")) {
return;
}

const style = document.createElement("style");
style.textContent = css;
document.head.insertBefore(style, document.querySelector("link"));

const script = document.createElement("script");
script.id = "respec-sortable-table";
script.textContent = await loadScript();
document.body.append(script);
}

async function loadScript() {
try {
return (await import("text!./sortable-table.runtime.js")).default;
} catch {
return fetchBase("./src/core/sortable-table.runtime.js");
}
}
157 changes: 157 additions & 0 deletions src/core/sortable-table.runtime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// @ts-check
if (document.respec) {
document.respec.ready.then(setupSortableTable);
} else {
setupSortableTable();
}

function setupSortableTable() {
const ASC = "ascending";
const DESC = "descending";

/** @type {Record<string, "ascending" | "descending" | null>} */
const NEXT_DIR = { [ASC]: DESC, [DESC]: null };

/** @type {WeakMap<HTMLTableElement, WeakMap<HTMLTableCellElement, "ascending" | "descending">>} */
const STATE = new WeakMap();

/**
* @param {HTMLTableCellElement} th
* @param {"ascending" | "descending" | null} dir
*/
const updateButton = (th, dir) => {
// Cache original label before button text is appended.
if (!th.dataset.label) {
th.dataset.label = th.textContent.trim();
}

let button = th.querySelector("button");
if (!button) {
button = document.createElement("button");
button.type = "button";
Comment thread
marcoscaceres marked this conversation as resolved.
th.append(button);
}

const icon = dir === ASC ? "▲" : dir === DESC ? "▼" : "⇕";
const label = th.dataset.label;

if (dir) {
const opposite = dir === ASC ? DESC : ASC;
button.setAttribute(
"aria-label",
`Sorted ${dir} by ${label}. Click to sort ${opposite}.`
);
th.setAttribute("aria-sort", dir);
} else {
button.setAttribute("aria-label", `Sort by ${label}`);
th.removeAttribute("aria-sort");
}
button.textContent = icon;
};

/** @type {NodeListOf<HTMLTableElement>} */
const tables = document.querySelectorAll("table.sortable");
tables.forEach(table => {
if (!table.tHead) return;
table.tHead.querySelectorAll("th").forEach(th => updateButton(th, null));
});

/**
* @param {MouseEvent} ev
* @returns {HTMLTableCellElement | null}
*/
const getTrigger = ev => {
if (!(ev.target instanceof HTMLElement)) return null;
const th = ev.target.closest("th");
if (th instanceof HTMLTableCellElement && th.closest("table.sortable")) {
return th;
}
return null;
Comment thread
marcoscaceres marked this conversation as resolved.
};

/**
* Stamps original row indices so the table can be reset to its initial order.
* @param {HTMLTableRowElement[]} rows
*/
const stampOriginalOrder = rows => {
rows.forEach((row, i) => {
if (!row.dataset.sortIndex) {
row.dataset.sortIndex = String(i);
}
});
};

document.addEventListener("click", ev => {
const th = getTrigger(ev);
if (!th) return;

/** @type {HTMLTableElement | null} */
const table = th.closest("table.sortable");
if (!table || !table.tBodies.length) return;

let tableState = STATE.get(table);
if (!tableState) {
tableState = new WeakMap();
STATE.set(table, tableState);
}

const current = tableState.get(th) ?? null;
/** @type {"ascending" | "descending" | null} */
const next = current === null ? ASC : NEXT_DIR[current];

if (table.tHead) {
table.tHead.querySelectorAll("th").forEach(otherTh => {
if (otherTh !== th) {
tableState.delete(otherTh);
updateButton(otherTh, null);
}
});
}

if (next === null) {
tableState.delete(th);
} else {
tableState.set(th, next);
}
updateButton(th, next);

const tbody = table.tBodies[0];
/** @type {HTMLTableRowElement[]} */
const rows = Array.from(tbody.rows);
stampOriginalOrder(rows);

if (next === null) {
rows.sort(
(a, b) =>
parseInt(/** @type {string} */ (a.dataset.sortIndex), 10) -
parseInt(/** @type {string} */ (b.dataset.sortIndex), 10)
);
} else {
/** @type {HTMLTableRowElement | null} */
const headerRow = /** @type {HTMLTableRowElement | null} */ (
th.closest("tr")
);
if (!headerRow) return;
const colIndex = Array.from(headerRow.cells).indexOf(th);
if (colIndex === -1) return;

const isNumeric = rows.every(row => {
const text = row.cells[colIndex]?.textContent?.trim() ?? "";
return text !== "" && !isNaN(Number(text));
});
Comment thread
marcoscaceres marked this conversation as resolved.

const dir = next === ASC ? 1 : -1;
rows.sort((a, b) => {
const x = a.cells[colIndex]?.textContent?.trim() ?? "";
const y = b.cells[colIndex]?.textContent?.trim() ?? "";
const cmp = isNumeric ? Number(x) - Number(y) : x.localeCompare(y);
return dir * cmp;
});
}

/** @type {typeof tbody} */
const newTbody = tbody.cloneNode(false);
for (const row of rows) newTbody.append(row);
tbody.replaceWith(newTbody);
Comment thread
marcoscaceres marked this conversation as resolved.
});
}
31 changes: 31 additions & 0 deletions src/styles/sortable-table.css.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const css = String.raw;

// prettier-ignore
export default css`
.sortable th {
vertical-align: middle;
white-space: nowrap;
}

.sortable th button {
background: transparent;
color: inherit;
border: none;
font-size: 0.7em;
padding: 0 0.3em;
cursor: pointer;
opacity: 0.5;
vertical-align: middle;
}

.sortable th button:hover,
.sortable th button:focus {
opacity: 1;
outline: 1px solid;
}

.sortable th[aria-sort="ascending"] button,
.sortable th[aria-sort="descending"] button {
opacity: 1;
}
`;
Loading