diff --git a/static/js/data-list.js b/static/js/data-list.js new file mode 100644 index 0000000..1d0745e --- /dev/null +++ b/static/js/data-list.js @@ -0,0 +1,69 @@ +(function () { + "use strict"; + + function compareRows(left, right, key, direction) { + const leftValue = left.dataset[key] || ""; + const rightValue = right.dataset[key] || ""; + const numericLeft = Number(leftValue); + const numericRight = Number(rightValue); + const bothNumeric = leftValue !== "" && rightValue !== "" + && Number.isFinite(numericLeft) && Number.isFinite(numericRight); + const comparison = bothNumeric + ? numericLeft - numericRight + : leftValue.localeCompare(rightValue, undefined, { + numeric: true, + sensitivity: "base", + }); + return comparison * direction; + } + + function refreshList(list) { + const body = list.querySelector("[data-data-list-body]"); + const table = list.querySelector("[data-data-list-table]"); + const empty = list.querySelector("[data-data-list-empty]"); + const hasRows = body && body.children.length > 0; + table.hidden = !hasRows; + empty.hidden = hasRows; + } + + function initializeList(list) { + refreshList(list); + + list.addEventListener("click", function (event) { + const sortButton = event.target.closest("[data-sort-key]"); + if (sortButton) { + const body = list.querySelector("[data-data-list-body]"); + const key = sortButton.dataset.sortKey; + const ascending = sortButton.dataset.sortDirection !== "asc"; + const rows = Array.from(body.children); + rows.sort(function (left, right) { + return compareRows(left, right, key, ascending ? 1 : -1); + }); + rows.forEach(function (row) { + body.appendChild(row); + }); + list.querySelectorAll("[data-sort-key]").forEach(function (button) { + button.removeAttribute("data-sort-direction"); + button.closest("th").setAttribute("aria-sort", "none"); + }); + sortButton.dataset.sortDirection = ascending ? "asc" : "desc"; + sortButton.closest("th").setAttribute( + "aria-sort", + ascending ? "ascending" : "descending" + ); + } + + const confirmButton = event.target.closest("[data-confirm]"); + if (confirmButton && !window.confirm(confirmButton.dataset.confirm)) { + event.preventDefault(); + event.stopImmediatePropagation(); + } + }, true); + + new MutationObserver(function () { + refreshList(list); + }).observe(list.querySelector("[data-data-list-body]"), { childList: true }); + } + + document.querySelectorAll("[data-data-list]").forEach(initializeList); +}()); diff --git a/static/yaris.css b/static/yaris.css index 90b9b33..3be8e7c 100644 --- a/static/yaris.css +++ b/static/yaris.css @@ -1 +1,40 @@ -#map { height: 20rem; } \ No newline at end of file +#map { height: 20rem; } + +.app-sidebar { + color: var(--bs-accordion-btn-focus-border-color); + background: #000; +} + +.data-list { + min-height: 10rem; +} + +.data-list-empty { + padding: 2.5rem 1rem; +} + +.data-list-sort { + display: inline-flex; + gap: .5rem; + align-items: center; + padding: 0; + color: inherit; + font: inherit; + font-weight: 700; + background: none; + border: 0; +} + +.data-list-sort[data-sort-direction="asc"] .fa-sort::before { + content: "\f0de"; +} + +.data-list-sort[data-sort-direction="desc"] .fa-sort::before { + content: "\f0dd"; +} + +.data-list-actions { + display: flex; + gap: .5rem; + justify-content: flex-end; +} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..4363f50 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,79 @@ + + + +
+ + +| + {% if column.sortable %} + + {% else %} + {{ column.label }} + {% endif %} + | + {% endfor %} +
|---|
{{ empty_message }}
+Output