function Table(
data,
{
columns = data.columns,
rows = 11.5,
sort,
reverse = false,
format,
align = {},
width = {},
layout = "fixed"
} = {}
) {
data = arrayof(data);
if (columns === undefined) columns = columnsof(data);
format = formatof(format, data, columns);
align = alignof(align, data, columns);
let n = rows * 2;
let currentSortHeader = null, currentReverse = false;
let selected = new Set();
let anchor = null, head = null;
let index = Uint32Array.from(data, (_, i) => i);
const tbody = html`<tbody>`;
const tr = html`<tr><td><input type=checkbox></td>${columns.map(column => html`<td style=${{textAlign: align[column]}}>`)}`;
const theadr = html`<tr><th><input type=checkbox onclick=${reselectAll}></th>${columns.map((column) => html`<th title=${column} style=${{width: width[column], textAlign: align[column]}} onclick=${event => resort(event, column)}><span></span>${column}</th>`)}</tr>`;
const root = html`<div class=${id} style="max-height: ${(rows + 1) * 24 - 1}px;">
<table style=${{tableLayout: layout}}>
<thead>${data.length || columns.length ? theadr : null}</thead>
${tbody}
</table>
</div>`;
function inputof(tr) {
return tr.firstChild.firstChild;
}
function orderof(th) {
return th.firstChild;
}
function render(i, j) {
return Array.from(index.subarray(i, j), i => {
const itr = tr.cloneNode(true);
itr.classList.toggle("selected", selected.has(i));
const input = inputof(itr);
input.onclick = reselect;
input.checked = selected.has(i);
input.name = i;
for (let j = 0; j < columns.length; ++j) {
let column = columns[j];
let value = data[i][column];
if (!defined(value)) continue;
value = format[column](value);
if (!(value instanceof Node)) value = document.createTextNode(value);
itr.childNodes[j + 1].appendChild(value);
}
return itr;
});
}
function unselect(i) {
let j = index.indexOf(i);
if (j < tbody.childNodes.length) {
const tr = tbody.childNodes[j];
tr.classList.toggle("selected", false);
inputof(tr).checked = false;
}
selected.delete(i);
}
function select(i) {
let j = index.indexOf(i);
if (j < tbody.childNodes.length) {
const tr = tbody.childNodes[j];
tr.classList.toggle("selected", true);
inputof(tr).checked = true;
}
selected.add(i);
}
function* range(i, j) {
i = index.indexOf(i), j = index.indexOf(j);
if (i < j) while (i <= j) yield index[i++];
else while (j <= i) yield index[j++];
}
function first(set) {
return set[Symbol.iterator]().next().value;
}
function reselectAll(event) {
if (selected.size) {
for (let i of selected) unselect(i);
anchor = head = null;
if (event.detail) event.currentTarget.blur();
} else {
selected = new Set(index);
for (const tr of tbody.childNodes) {
tr.classList.toggle("selected", true);
inputof(tr).checked = true;
}
}
reinput();
}
function reselect(event) {
let i = +this.name;
if (event.shiftKey) {
if (anchor === null) anchor = selected.size ? first(selected) : index[0];
else for (let i of range(anchor, head)) unselect(i);
head = i;
for (let i of range(anchor, head)) select(i);
} else {
anchor = head = i;
if (selected.has(i)) {
unselect(i);
anchor = head = null;
if (event.detail) event.currentTarget.blur();
} else {
select(i);
}
}
reinput();
}
function resort(event, column) {
const th = event.currentTarget;
let compare;
if (currentSortHeader === th && currentReverse) {
orderof(currentSortHeader).textContent = "";
currentSortHeader.classList.toggle("sort", false);
currentSortHeader = null;
currentReverse = false;
compare = ascending;
} else {
if (currentSortHeader === th) {
currentReverse = true;
} else {
if (currentSortHeader) {
orderof(currentSortHeader).textContent = "";
currentSortHeader.classList.toggle("sort", false);
}
currentSortHeader = th, currentReverse = false;
}
const order = currentReverse ? descending : ascending;
compare = (a, b) => order(data[a][column], data[b][column]);
orderof(th).textContent = currentReverse ? "▾" : "▴";
th.classList.toggle("sort", true);
}
index.sort(compare);
selected = new Set(Array.from(selected).sort(compare));
root.scrollTo(0, 0);
while (tbody.firstChild) tbody.firstChild.remove();
tbody.append(...render(0, n = rows * 2));
anchor = head = null;
reinput();
}
function reinput() {
theadr.classList.toggle("selected", selected.size);
inputof(theadr).checked = selected.size;
revalue();
root.dispatchEvent(new CustomEvent("input"));
}
function revalue() {
root.value = Array.from(selected.size ? selected : index, i => data[i]);
root.value.columns = columns;
}
root.onscroll = () => {
if (root.scrollHeight - root.scrollTop < 400 && n < data.length) {
tbody.append(...render(n, n += rows));
}
};
if (data.length) {
tbody.append(...render(0, n));
} else {
tbody.append(html`<tr>${columns.length ? html`<td>` : null}<td rowspan=${columns.length} style="padding-left: 1em; font-style: oblique;">No results.</td></tr>`);
}
revalue();
if (sort !== undefined) {
let i = columns.indexOf(sort);
if (i >= 0) {
if (reverse) currentSortHeader = theadr.childNodes[i + 1];
resort({currentTarget: theadr.childNodes[i + 1]}, columns[i]);
}
}
if (!Table.style) Table.style = document.body.appendChild(style);
return root;
}