Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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(a.dataset.sortIndex) - parseInt(b.dataset.sortIndex)
);
} 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(parseFloat(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
? parseFloat(x) - parseFloat(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
Loading