← back to Mfr Review Viewer Corruption

public/kv-list.js

88 lines

/* kv-list.js — compact key/value record list. Zero dependencies.
 *
 * Renders each record as ONE scannable row: a fixed identity block on the left
 * (title + subline + optional badge) and every visible field as inline
 * uppercase-micro-label + value chips flowing to the right, wrapping responsively.
 *
 * Standalone extraction of three-view.js's `listCompact()` (CRCP TK-10526),
 * renamed .kv-* so it drops into any page without dragging the rest of three-view.
 *
 * KVList.render({
 *   mount:  '#kv',                       // selector | element (required)
 *   rows:   [ {...}, ... ],              // record objects, in display order (required)
 *   fields: [ {k,l,calc,money,num,pct,date,html,suf}, ... ],  // field metadata (required)
 *   title:    row => ({name, sub?, href?}),   // left identity block
 *   badgeHtml:row => '<span class="kv-badge">…</span>' | '',  // optional badge
 *   fvis:     k => bool,                 // optional per-field visibility filter
 *   rowClick: (row, el) => {},           // optional row click handler
 *   empty:    'No matches.',             // optional empty message
 * })
 */
(function (global) {
  function esc(s) {
    return String(s == null ? '' : s)
      .replace(/&/g, '&amp;').replace(/</g, '&lt;')
      .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  }

  function fmt(v, f) {
    if (f.html) return v == null ? '—' : String(v);        // page-formatted HTML passes through
    if (v == null || v === '') return '—';
    if (f.money) return '$' + (+v).toLocaleString() + (f.suf || '');
    if (f.pct)   return (+v).toFixed(2) + '%';
    if (f.num)   return (+v).toLocaleString() + (f.suf || '');
    if (f.date)  { try { return new Date(v).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); } catch (e) { return '—'; } }
    return esc(v) + (f.suf || '');
  }

  var fval = function (r, f) { return f.calc ? f.calc(r) : r[f.k]; };

  function resolve(m) {
    if (!m) return null;
    return typeof m === 'string' ? document.querySelector(m) : m;
  }

  function render(O) {
    var mount = resolve(O.mount);
    if (!mount) { console.warn('[kv-list] mount not found:', O.mount); return; }

    var rows   = O.rows || [];
    var fields = (O.fields || []).filter(function (f) {
      if (f.k === 'title' || f.badge) return false;
      return O.fvis ? O.fvis(f.k) : true;
    });

    if (!rows.length) {
      mount.innerHTML = '<div class="kv-empty">' + esc(O.empty || 'No matches.') + '</div>';
      return;
    }

    mount.innerHTML = '<div class="kv-rows">' + rows.map(function (r, i) {
      var t  = O.title ? O.title(r) : { name: r.name || r.title || '', sub: '' };
      var nm = t.href
        ? '<a href="' + esc(t.href) + '" target="_blank" rel="noopener noreferrer">' + esc(t.name) + '</a>'
        : esc(t.name);
      var badge = O.badgeHtml ? (O.badgeHtml(r) || '') : '';
      return '<div class="kv-lrow' + (O.rowClick ? ' kv-click' : '') + '" data-kvri="' + i + '">'
        + '<div class="kv-lmain"><span class="kv-la">' + nm + '</span>'
        + (t.sub ? '<span class="kv-lsub">' + esc(t.sub) + '</span>' : '') + badge + '</div>'
        + '<div class="kv-lmeta">'
        + fields.map(function (f) {
            return '<span class="kv-lm"><span class="kv-lmk">' + esc(f.l) + '</span>' + fmt(fval(r, f), f) + '</span>';
          }).join('')
        + '</div></div>';
    }).join('') + '</div>';

    if (O.rowClick) {
      mount.querySelectorAll('.kv-lrow').forEach(function (el) {
        el.addEventListener('click', function (e) {
          if (e.target.closest('a')) return;
          O.rowClick(rows[+el.dataset.kvri], el);
        });
      });
    }
  }

  global.KVList = { render: render, fmt: fmt };
})(typeof window !== 'undefined' ? window : this);