← back to Rentv 2026

public/2026/elements.js

165 lines

/* RENTV 2026 — Element Numbering overlay (shared drop-in).
 *
 * Purpose: give every significant element on every template a STABLE number so
 * Steve can reference "Template C, element 14" in future feedback.
 *
 * Include on any page:  <script src="/2026/elements.js" defer></script>
 * Toggle:  the "①② Elements" button (bottom-left) · the "e" key · or ?num=1 in the URL.
 *
 * Numbers are assigned in DOM order over a curated selector set, so they are
 * stable across reloads (as long as the page structure doesn't change). Each
 * numbered element gets a badge; a searchable index panel lists them all and
 * clicking an index row scrolls to + flashes that element. State persists in
 * localStorage per page path. Zero dependencies; fully removable (toggle off
 * restores the DOM).
 */
(function () {
  var SEL = [
    'header', 'nav', 'section', 'main', 'footer',
    '[data-ad-slot]',
    '.card', '.slot', '.vslot', '.vidcard', '.rescard', '.tile', '.panel',
    'h1', 'h2', 'h3', 'h4',
    'nav a', 'footer a',
    'button', 'a.btn', '.btn', '.chip', '.cta',
    'input', 'select', 'textarea',
    '.hero', '.vhero', '.controls', '.filters', '.rate', '.invgrid', '.river', '.vidrow'
  ].join(',');

  var KEY = 'rentv_num_' + location.pathname;
  var on = false, badges = [], touched = [];

  function label(el) {
    var t = (el.getAttribute('aria-label') || el.getAttribute('placeholder') || '').trim();
    if (!t) {
      var clone = el.cloneNode(true);
      // strip our own badge text if present
      t = (clone.textContent || '').replace(/\s+/g, ' ').trim();
    }
    if (!t && el.getAttribute('data-ad-slot')) t = 'ad-slot:' + el.getAttribute('data-ad-slot');
    if (!t && el.tagName === 'IMG') t = el.getAttribute('alt') || 'image';
    var cls = (el.className && typeof el.className === 'string') ? '.' + el.className.split(/\s+/).slice(0, 2).join('.') : '';
    return { tag: el.tagName.toLowerCase() + cls, text: t.slice(0, 60) };
  }

  function css() {
    if (document.getElementById('rentv-num-css')) return;
    var s = document.createElement('style'); s.id = 'rentv-num-css';
    s.textContent = [
      '.rentv-el-badge{position:absolute;z-index:2147483000;top:0;left:0;transform:translate(-4px,-9px);',
      '  background:#c8102e;color:#fff;font:800 10px/1 ui-monospace,Menlo,monospace;padding:2px 4px;border-radius:4px;',
      '  box-shadow:0 1px 4px rgba(0,0,0,.4);pointer-events:none;letter-spacing:.02em}',
      '.rentv-num-mark{outline:1px dashed rgba(200,16,46,.5);outline-offset:1px}',
      '.rentv-num-flash{outline:3px solid #f0b429!important;transition:outline .2s}',
      '#rentv-num-toggle{position:fixed;left:14px;bottom:14px;z-index:2147483600;background:#0b0e12;color:#fff;',
      '  border:1px solid #c8102e;border-radius:9px;padding:9px 13px;font:800 12px Inter,system-ui,sans-serif;',
      '  cursor:pointer;box-shadow:0 8px 26px rgba(0,0,0,.4)}',
      '#rentv-num-toggle.on{background:#c8102e}',
      '#rentv-num-index{position:fixed;right:14px;top:70px;bottom:70px;width:290px;z-index:2147483500;background:#0b0e12;',
      '  color:#e7ecf1;border:1px solid #1c232b;border-radius:12px;display:none;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,.5)}',
      '#rentv-num-index.on{display:flex}',
      '#rentv-num-index .h{padding:11px 13px;border-bottom:1px solid #1c232b;display:flex;gap:8px;align-items:center}',
      '#rentv-num-index .h b{font:800 12px Inter;letter-spacing:.04em}',
      '#rentv-num-index .h .ct{margin-left:auto;color:#7b848d;font:700 11px Inter}',
      '#rentv-num-index input{width:100%;margin:8px 10px 6px;width:calc(100% - 20px);padding:7px 9px;border:1px solid #1c232b;',
      '  background:#11161c;color:#e7ecf1;border-radius:7px;font:13px Inter}',
      '#rentv-num-index .list{overflow:auto;flex:1;padding:2px 6px 10px}',
      '#rentv-num-index .row{display:flex;gap:8px;padding:6px 8px;border-radius:6px;cursor:pointer;font:12px/1.3 Inter}',
      '#rentv-num-index .row:hover{background:#161d25}',
      '#rentv-num-index .row .n{color:#fff;font:800 11px ui-monospace,monospace;background:#c8102e;border-radius:4px;padding:1px 5px;height:fit-content}',
      '#rentv-num-index .row .m .tg{color:#8b949e;font-weight:700}',
      '#rentv-num-index .row .m .tx{color:#c3cbd3}',
      '#rentv-num-index .cp{padding:8px 12px;border-top:1px solid #1c232b;color:#7b848d;font:11px Inter}'
    ].join('');
    document.head.appendChild(s);
  }

  function build() {
    var seen = new Set();
    var els = Array.prototype.slice.call(document.querySelectorAll(SEL)).filter(function (el) {
      if (seen.has(el)) return false; seen.add(el);
      if (el.closest('#rentv-num-index') || el.id === 'rentv-num-toggle') return false;
      var r = el.getBoundingClientRect();
      return r.width > 8 && r.height > 8; // visible-ish
    });
    var rows = '';
    els.forEach(function (el, i) {
      var n = i + 1;
      el.setAttribute('data-el', n);
      el.classList.add('rentv-num-mark');
      touched.push(el);
      var cs = getComputedStyle(el);
      if (cs.position === 'static') { el.style.position = 'relative'; el.setAttribute('data-num-posfix', '1'); }
      var b = document.createElement('span'); b.className = 'rentv-el-badge'; b.textContent = n;
      el.appendChild(b); badges.push(b);
      var L = label(el);
      rows += '<div class="row" data-goto="' + n + '"><span class="n">' + n + '</span><span class="m"><span class="tg">' +
        L.tag + '</span> <span class="tx">' + (L.text ? L.text.replace(/[<>&]/g, '') : '') + '</span></span></div>';
    });
    idx.querySelector('.list').innerHTML = rows;
    idx.querySelector('.ct').textContent = els.length + ' elements';
  }

  function clear() {
    badges.forEach(function (b) { b.remove(); }); badges = [];
    touched.forEach(function (el) {
      el.classList.remove('rentv-num-mark'); el.removeAttribute('data-el');
      if (el.getAttribute('data-num-posfix')) { el.style.position = ''; el.removeAttribute('data-num-posfix'); }
    });
    touched = [];
    idx.querySelector('.list').innerHTML = '';
  }

  var idx, toggleBtn;
  function ui() {
    css();
    toggleBtn = document.createElement('button'); toggleBtn.id = 'rentv-num-toggle';
    toggleBtn.innerHTML = '①② Elements'; document.body.appendChild(toggleBtn);
    idx = document.createElement('div'); idx.id = 'rentv-num-index';
    idx.innerHTML = '<div class="h"><b>Element index</b><span class="ct"></span></div>' +
      '<input placeholder="filter by number or text…" aria-label="Filter elements">' +
      '<div class="list"></div>' +
      '<div class="cp">Click a row to jump. Ref future edits as “' + pageTag() + ' · el N”.</div>';
    document.body.appendChild(idx);

    toggleBtn.addEventListener('click', function () { set(!on); });
    document.addEventListener('keydown', function (e) {
      if (e.key === 'e' && !/input|textarea|select/i.test((e.target.tagName || ''))) set(!on);
    });
    idx.querySelector('input').addEventListener('input', function (e) {
      var q = e.target.value.toLowerCase();
      idx.querySelectorAll('.row').forEach(function (r) {
        r.style.display = (!q || r.textContent.toLowerCase().indexOf(q) >= 0) ? '' : 'none';
      });
    });
    idx.querySelector('.list').addEventListener('click', function (e) {
      var r = e.target.closest('[data-goto]'); if (!r) return;
      var el = document.querySelector('[data-el="' + r.dataset.goto + '"]'); if (!el) return;
      el.scrollIntoView({ behavior: 'smooth', block: 'center' });
      el.classList.add('rentv-num-flash'); setTimeout(function () { el.classList.remove('rentv-num-flash'); }, 1400);
    });
  }

  function pageTag() {
    var m = location.pathname.match(/2026\/([a-e])\.html/); if (m) return 'Template ' + m[1].toUpperCase();
    if (/2026\/?$/.test(location.pathname)) return 'Selector';
    if (/advertise/.test(location.pathname)) return 'Advertise';
    return location.pathname;
  }

  function set(v) {
    on = v; toggleBtn.classList.toggle('on', on); idx.classList.toggle('on', on);
    toggleBtn.innerHTML = on ? '✕ Hide numbers' : '①② Elements';
    if (on) build(); else clear();
    try { localStorage.setItem(KEY, on ? '1' : '0'); } catch (e) { }
  }

  function boot() {
    ui();
    var auto = /[?&]num=1/.test(location.search);
    try { if (!auto && localStorage.getItem(KEY) === '1') auto = true; } catch (e) { }
    if (auto) set(true);
  }
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); else boot();
  window.RENTVNumber = { on: function () { set(true); }, off: function () { set(false); } };
})();