← Commit history

Add <adom-table>: a generic filterable/sortable table widget

Ray ·cec8cdb8e2 ·1mo ago ·parent 657d72f
Every table on the wiki so far has been a bespoke section frame -- the stm32f103c8t6 pinout, the repo-ownership list, the footprint variant tables. Each copy re-implements filtering, sorting, height reporting and theming, and each one drifts. This is that frame once, on the adom-widget runtime. Also ships adom-table-demo.json so the registry entry is verifiable.
2 files changed +205
widgets/adom-table-demo.jsonadded+58
@@ -0,0 +1,58 @@+[+ {+  "mpn": "STM32F103C8T6",+  "package": "LQFP-48",+  "pins": 48,+  "vendor": "ST",+  "status": "active"+ },+ {+  "mpn": "ATTINY85-20SU",+  "package": "SOIC-8",+  "pins": 8,+  "vendor": "Microchip",+  "status": "active"+ },+ {+  "mpn": "SN74GTL2003PWR",+  "package": "TSSOP-20",+  "pins": 20,+  "vendor": "TI",+  "status": "active"+ },+ {+  "mpn": "RT6252BHGH6F",+  "package": "WDFN-6",+  "pins": 6,+  "vendor": "Richtek",+  "status": "active"+ },+ {+  "mpn": "LD1117D33CTR",+  "package": "DPAK",+  "pins": 3,+  "vendor": "ST",+  "status": "active"+ },+ {+  "mpn": "AMS1117-3.3",+  "package": "SOT-223",+  "pins": 4,+  "vendor": "AMS",+  "status": "nrnd"+ },+ {+  "mpn": "74HC08D",+  "package": "SOIC-14",+  "pins": 14,+  "vendor": "Nexperia",+  "status": "active"+ },+ {+  "mpn": "NE555D",+  "package": "SOIC-8",+  "pins": 8,+  "vendor": "TI",+  "status": "nrnd"+ }+]\ No newline at end of file
widgets/adom-table.jsadded+147
@@ -0,0 +1,147 @@+/*!+ * <adom-table> — render a JSON array as a filterable, sortable table.+ *+ * Written because the same table has now been hand-built three times as a bespoke section+ * frame (adom/stm32f103c8t6's pinout, adom/repo-ownership's ownership list, and the+ * footprint variant tables). Each copy re-implements filtering, sorting, height reporting+ * and theming, and each one drifts. This is that frame, once, on the runtime.+ *+ * Data (resolved by the runtime, in its own order of preference):+ *   1. an inline <script type="application/json"> child   <- preferred on a wiki README,+ *      because the readme frame is an opaque origin and a fetch from there is blocked+ *   2. src="/api/pages/<owner>/<slug>/files/<file>.json"  <- works in a SECTION frame+ *   3. el.data = [...]                                     <- dashboards / adom-lbr+ *+ * Accepted shapes:+ *   [ {col: value, ...}, ... ]                     columns inferred from the first row+ *   { columns: [...], rows: [ {...}, ... ] }       explicit column order+ *   { columns: [{key,label,align}], rows: [...] }  explicit labels+ *+ * Attributes:+ *   columns="a,b,c"   restrict and order columns (overrides the data's own order)+ *   heading="..."     caption above the table+ *   filter            show the filter box (default on; filter="off" hides it)+ *   facet="owner"     add a dropdown that filters on one column's distinct values+ *   sort="col"        initial sort column; prefix "-" for descending+ *   dim="key:value"   render rows matching key=value at reduced opacity (e.g. archived)+ */+(function () {+  'use strict';+  if (!window.AdomWidget) return;+  var esc = window.AdomWidget.esc;++  function normalise(data, attrColumns) {+    var rows = [], cols = null;+    if (Array.isArray(data)) rows = data;+    else if (data && Array.isArray(data.rows)) { rows = data.rows; cols = data.columns || null; }+    else if (data && typeof data === 'object') {+      // a bare object of arrays, e.g. {live:[...], dormant:[...]} - concatenate and tag+      var keys = Object.keys(data).filter(function (k) { return Array.isArray(data[k]); });+      if (keys.length) {+        keys.forEach(function (k) {+          data[k].forEach(function (r) { rows.push(Object.assign({ _group: k }, r)); });+        });+      }+    }+    if (attrColumns) cols = attrColumns.split(',').map(function (s) { return s.trim(); });+    if (!cols) cols = rows.length ? Object.keys(rows[0]).filter(function (k) { return k[0] !== '_'; }) : [];+    cols = cols.map(function (c) {+      return typeof c === 'string' ? { key: c, label: c.replace(/[_-]/g, ' ').toUpperCase() } : c;+    });+    return { cols: cols, rows: rows };+  }++  window.AdomWidget.define('adom-table', {+    attributes: ['columns', 'heading', 'filter', 'facet', 'sort', 'dim'],+    loadingText: 'Loading table',+    emptyText: 'No data supplied to <adom-table>',+    css: [+      '.bar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin:0 0 9px}',+      '.bar input,.bar select{background:var(--bg,#0d1117);color:inherit;border:1px solid var(--line,#30363d);',+      'border-radius:6px;padding:6px 9px;font:inherit}',+      '.bar input{min-width:200px}',+      '.muted{opacity:.62;font-size:12px}',+      'table{border-collapse:collapse;width:100%;font-size:13px}',+      'th{text-align:left;font-size:10.5px;letter-spacing:.7px;opacity:.6;padding:6px 8px;',+      'border-bottom:1px solid var(--line,#30363d);cursor:pointer;user-select:none;white-space:nowrap}',+      'th[aria-sort]:after{content:"";opacity:.8}',+      'th[aria-sort="ascending"]:after{content:" \\2191"}',+      'th[aria-sort="descending"]:after{content:" \\2193"}',+      'td{padding:5px 8px;border-bottom:1px solid var(--line2,#161b22);vertical-align:top}',+      'tr.dim td{opacity:.45}',+      'caption{text-align:left;font-weight:600;padding:0 0 8px}'+    ].join(''),+    render: function (data, ctx) {+      var n = normalise(data, this.getAttribute('columns'));+      this._n = n;+      var heading = this.getAttribute('heading');+      var showFilter = this.getAttribute('filter') !== 'off';+      var facet = this.getAttribute('facet');+      var bar = '';+      if (showFilter || facet) {+        bar = '<div class=bar>' ++          (showFilter ? '<input part=filter type=search placeholder="filter…">' : '') ++          (facet ? '<select part=facet><option value="">all ' + esc(facet) + '</option></select>' : '') ++          '<span class=muted data-count></span></div>';+      }+      return bar + '<table>' ++        (heading ? '<caption>' + esc(heading) + '</caption>' : '') ++        '<thead><tr>' + n.cols.map(function (c) {+          return '<th data-k="' + esc(c.key) + '">' + esc(c.label) + '</th>';+        }).join('') + '</tr></thead><tbody></tbody></table>';+    },+    mounted: function (root) {+      var el = this, n = el._n, tb = root.querySelector('tbody');+      var q = root.querySelector('input'), sel = root.querySelector('select');+      var count = root.querySelector('[data-count]');+      var dim = (el.getAttribute('dim') || '').split(':');+      var s = (el.getAttribute('sort') || '').replace(/^-/, '');+      var dir = (el.getAttribute('sort') || '')[0] === '-' ? -1 : 1;++      if (sel) {+        var f = el.getAttribute('facet');+        var vals = {};+        n.rows.forEach(function (r) { if (r[f] != null) vals[r[f]] = 1; });+        Object.keys(vals).sort().forEach(function (v) {+          var o = document.createElement('option'); o.textContent = v; sel.appendChild(o);+        });+      }+      function draw() {+        var term = (q && q.value || '').toLowerCase();+        var fv = sel && sel.value;+        var f = el.getAttribute('facet');+        var out = n.rows.filter(function (r) {+          if (fv && String(r[f]) !== fv) return false;+          if (!term) return true;+          return n.cols.some(function (c) {+            return String(r[c.key] == null ? '' : r[c.key]).toLowerCase().indexOf(term) >= 0;+          });+        });+        if (s) out = out.slice().sort(function (a, b) {+          var A = a[s], B = b[s];+          if (typeof A === 'number' && typeof B === 'number') return (A - B) * dir;+          return String(A).localeCompare(String(B)) * dir;+        });+        tb.innerHTML = out.map(function (r) {+          var isDim = dim.length === 2 && String(r[dim[0]]) === dim[1];+          return '<tr' + (isDim ? ' class=dim' : '') + '>' + n.cols.map(function (c) {+            return '<td>' + esc(r[c.key] == null ? '' : r[c.key]) + '</td>';+          }).join('') + '</tr>';+        }).join('');+        if (count) count.textContent = out.length + ' of ' + n.rows.length;+        root.querySelectorAll('th').forEach(function (th) {+          if (th.dataset.k === s) th.setAttribute('aria-sort', dir > 0 ? 'ascending' : 'descending');+          else th.removeAttribute('aria-sort');+        });+      }+      root.querySelectorAll('th').forEach(function (th) {+        th.addEventListener('click', function () {+          dir = (s === th.dataset.k) ? -dir : 1; s = th.dataset.k; draw();+        });+      });+      if (q) q.addEventListener('input', draw);+      if (sel) sel.addEventListener('change', draw);+      draw();+    }+  });+}());