Spaces:
Running
Running
Download assets/js/util.js from SeerRay-Lab/DAREBench: direct link, hf CLI and curl.
- Browser
- Download file 21.7 kB
-
https://huggingface.co/spaces/SeerRay-Lab/DAREBench/resolve/main/assets/js/util.js
- Command line
-
hf download hf://spaces/SeerRay-Lab/DAREBench/assets/js/util.js
-
curl -L -o util.js https://huggingface.co/spaces/SeerRay-Lab/DAREBench/resolve/main/assets/js/util.js
21.7 kB
| /* DAREBench project page: shared helpers. Classic script (works over file://). | |
| Every number rendered on the page is read from window.DARE (assets/data/data.js). */ | |
| (function () { | |
| 'use strict'; | |
| var D = window.DARE; | |
| var U = (window.U = {}); | |
| U.D = D; | |
| U.IMG = 'assets/'; // data.js image paths (img/...) live under assets/ | |
| var SVGNS = 'http://www.w3.org/2000/svg'; | |
| U.$ = function (s, r) { return (r || document).querySelector(s); }; | |
| U.$$ = function (s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); }; | |
| /* Element builder. attrs: class, text, attrs{}, data{}, on{}, style{}; children: nodes/strings. */ | |
| U.h = function (tag, o) { | |
| var el = document.createElement(tag); | |
| o = o || {}; | |
| if (o.class) el.className = o.class; | |
| if (o.text != null) el.textContent = o.text; | |
| if (o.attrs) for (var k in o.attrs) if (o.attrs[k] != null && o.attrs[k] !== false) el.setAttribute(k, o.attrs[k] === true ? '' : o.attrs[k]); | |
| if (o.data) for (var d in o.data) el.dataset[d] = o.data[d]; | |
| if (o.style) for (var st in o.style) el.style.setProperty(st, o.style[st]); | |
| if (o.on) for (var ev in o.on) el.addEventListener(ev, o.on[ev]); | |
| for (var i = 2; i < arguments.length; i++) U.append(el, arguments[i]); | |
| return el; | |
| }; | |
| U.append = function (el, c) { | |
| if (c == null || c === false) return el; | |
| if (Array.isArray(c)) { c.forEach(function (x) { U.append(el, x); }); return el; } | |
| el.appendChild(typeof c === 'string' || typeof c === 'number' ? document.createTextNode(String(c)) : c); | |
| return el; | |
| }; | |
| U.s = function (tag, attrs, parent) { | |
| var el = document.createElementNS(SVGNS, tag); | |
| if (attrs) for (var k in attrs) if (attrs[k] != null) el.setAttribute(k, attrs[k]); | |
| if (parent) parent.appendChild(el); | |
| return el; | |
| }; | |
| /* <svg><use href="#id"/></svg> icon */ | |
| U.icon = function (id, cls) { | |
| var svg = document.createElementNS(SVGNS, 'svg'); | |
| svg.setAttribute('aria-hidden', 'true'); | |
| svg.setAttribute('focusable', 'false'); | |
| if (cls) svg.setAttribute('class', cls); | |
| var use = document.createElementNS(SVGNS, 'use'); | |
| use.setAttribute('href', '#' + id); | |
| svg.appendChild(use); | |
| return svg; | |
| }; | |
| /* ---- data access ---- */ | |
| U.get = function (path) { | |
| return String(path).split('.').reduce(function (o, k) { return o == null ? o : o[k]; }, D); | |
| }; | |
| var byName = {}; | |
| D.models.forEach(function (m) { byName[m.name] = m; byName[m.shortName] = m; }); | |
| U.model = function (n) { return byName[n]; }; | |
| U.group = function (k) { return D.groups.filter(function (g) { return g.key === k; })[0]; }; | |
| U.source = function (k) { return D.sources.filter(function (s) { return s.key === k; })[0]; }; | |
| U.GROUPS = D.groupOrder.map(U.group); | |
| U.TEXT_GROUPS = U.GROUPS.filter(function (g) { return g.modality === 'text'; }); | |
| U.MM_GROUPS = U.GROUPS.filter(function (g) { return g.modality !== 'text'; }); | |
| U.wlVar = function (k) { return 'var(--wl-' + k + ')'; }; | |
| U.css = function (name) { return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); }; | |
| /* ---- formatting (display only; never recomputes paper values) ---- */ | |
| U.int = function (n) { return Number(n).toLocaleString('en-US'); }; | |
| U.pct = function (n) { return n == null ? '—' : Number(n).toFixed(1); }; | |
| U.tok = function (k) { return k == null ? '—' : k + 'k'; }; | |
| U.usd = function (txt) { return txt == null ? '—' : (txt.charAt(0) === '<' ? '<$' + txt.slice(1) : '$' + txt); }; | |
| U.modalityCount = function () { | |
| var m = {}, f = {}; | |
| D.groups.forEach(function (g) { m[g.modality] = 1; f[g.form] = 1; }); | |
| return [Object.keys(m).length, Object.keys(f).length]; | |
| }; | |
| U.matrixLabel = function () { var mc = U.modalityCount(); return mc[0] + '×' + mc[1]; }; | |
| U.scoringLabel = { auto: 'Auto', judge: 'Judge', hybrid: 'Hybrid' }; | |
| U.SCORING_ORDER = ['auto', 'hybrid', 'judge']; // ordinal: increasing reliance on the LLM judge | |
| U.scoringVar = function (k) { return 'var(--score-' + k + ')'; }; | |
| /* Competition ranking (1,2,2,4) by value descending */ | |
| U.rank = function (items, val) { | |
| var sorted = items.slice().sort(function (a, b) { return val(b) - val(a); }); | |
| var ranks = new Map(); | |
| sorted.forEach(function (it, i) { | |
| if (i > 0 && val(it) === val(sorted[i - 1])) ranks.set(it, ranks.get(sorted[i - 1])); | |
| else ranks.set(it, i + 1); | |
| }); | |
| return ranks; | |
| }; | |
| /* Wrap numbers in a sentence with <b> (text built with DOM nodes, not innerHTML) */ | |
| U.emphNums = function (text) { | |
| /* Bold standalone numbers only (not digits inside names such as GPT-5.4 or Qwen3.6-27B). */ | |
| var frag = document.createDocumentFragment(); | |
| var re = /(^|[\s(~+\u2248\u2192:;,])((?:\$|<\$?|[+\u2212])?\d[\d,]*(?:\.\d+)?(?:k|%|\u00d7)?)(?![\w\-.]*\w)/g, last = 0, m; | |
| while ((m = re.exec(text))) { | |
| var start = m.index + m[1].length; | |
| if (start > last) frag.appendChild(document.createTextNode(text.slice(last, start))); | |
| frag.appendChild(U.h('b', { text: m[2] })); | |
| last = start + m[2].length; | |
| } | |
| if (last < text.length) frag.appendChild(document.createTextNode(text.slice(last))); | |
| return frag; | |
| }; | |
| /* Render `code` spans from README markdown */ | |
| U.mdCode = function (text) { | |
| var frag = document.createDocumentFragment(); | |
| String(text).split(/(`[^`]+`)/g).forEach(function (part) { | |
| if (!part) return; | |
| if (part.charAt(0) === '`' && part.charAt(part.length - 1) === '`') frag.appendChild(U.h('code', { text: part.slice(1, -1) })); | |
| else frag.appendChild(document.createTextNode(part)); | |
| }); | |
| return frag; | |
| }; | |
| /* ---- bindings ---- */ | |
| U.bindAll = function (root) { | |
| U.$$('[data-bind]', root).forEach(function (el) { | |
| var v = U.get(el.getAttribute('data-bind')); | |
| if (v != null) el.textContent = v; | |
| }); | |
| U.$$('[data-href]', root).forEach(function (el) { | |
| var v = U.get(el.getAttribute('data-href')); | |
| if (v) { el.setAttribute('href', v); el.setAttribute('rel', 'noopener'); } | |
| }); | |
| }; | |
| /* ---- tooltip ---- */ | |
| var tip, tipOwner = null; | |
| U.tip = { | |
| el: function () { return tip || (tip = document.getElementById('tip')); }, | |
| /* spec: {title, rows:[{label, value, color}], note} */ | |
| show: function (spec, x, y, owner) { | |
| var t = U.tip.el(); | |
| t.textContent = ''; | |
| if (spec.title) t.appendChild(U.h('div', { class: 'tip__title', text: spec.title })); | |
| (spec.rows || []).forEach(function (r) { | |
| var key = U.h('span', { class: 'tip__key' }); | |
| if (r.color) key.style.borderTopColor = r.color; else key.style.borderTopColor = 'transparent'; | |
| t.appendChild(U.h('div', { class: 'tip__row' }, key, U.h('span', { class: 'tip__lab', text: r.label }), U.h('span', { class: 'tip__val', text: r.value }))); | |
| }); | |
| if (spec.note) t.appendChild(U.h('div', { class: 'tip__note', text: spec.note })); | |
| t.hidden = false; | |
| tipOwner = owner || null; | |
| var w = t.offsetWidth, hgt = t.offsetHeight, vw = window.innerWidth, vh = window.innerHeight; | |
| var left = x + 16, top = y + 16; | |
| if (left + w > vw - 8) left = Math.max(8, x - w - 16); | |
| if (top + hgt > vh - 8) top = Math.max(8, y - hgt - 16); | |
| t.style.left = left + 'px'; | |
| t.style.top = top + 'px'; | |
| }, | |
| showAt: function (spec, el) { | |
| var r = el.getBoundingClientRect(); | |
| U.tip.show(spec, r.left + r.width / 2, r.top + r.height / 2, el); | |
| }, | |
| hide: function (owner) { | |
| if (owner && tipOwner && owner !== tipOwner) return; | |
| var t = U.tip.el(); if (t) t.hidden = true; tipOwner = null; | |
| }, | |
| /* keyboard focus: show next to the element and remember it, so the tooltip can be | |
| re-anchored after a scroll instead of floating detached from its owner */ | |
| focus: function (specFn, el) { | |
| focusOwner = { el: el, spec: specFn }; | |
| U.tip.showAt(specFn(), el); | |
| }, | |
| blur: function (el) { | |
| if (focusOwner && focusOwner.el === el) focusOwner = null; | |
| U.tip.hide(el); | |
| } | |
| }; | |
| var focusOwner = null, scrollTimer = null; | |
| /* Attach hover + focus tooltip to an element; specFn returns the spec */ | |
| U.hoverTip = function (el, specFn) { | |
| el.addEventListener('pointermove', function (e) { U.tip.show(specFn(), e.clientX, e.clientY, el); }); | |
| el.addEventListener('pointerleave', function () { U.tip.hide(el); }); | |
| el.addEventListener('focus', function () { U.tip.focus(specFn, el); }); | |
| el.addEventListener('blur', function () { U.tip.blur(el); }); | |
| }; | |
| window.addEventListener('scroll', function () { | |
| U.tip.hide(); | |
| clearTimeout(scrollTimer); | |
| scrollTimer = setTimeout(function () { | |
| if (!focusOwner || document.activeElement !== focusOwner.el) { focusOwner = null; return; } | |
| var r = focusOwner.el.getBoundingClientRect(); | |
| if (r.bottom > 0 && r.top < window.innerHeight) U.tip.showAt(focusOwner.spec(), focusOwner.el); | |
| }, 160); | |
| }, { passive: true }); | |
| /* Escape dismisses the tooltip for good: forget the focus owner so a later scroll does not re-open it */ | |
| document.addEventListener('keydown', function (e) { if (e.key === 'Escape') { focusOwner = null; U.tip.hide(); } }); | |
| /* ---- chart <-> table toggles ---- */ | |
| U.initViewToggles = function () { | |
| U.$$('.view-toggle').forEach(function (btn) { | |
| var t = btn.getAttribute('data-target'); | |
| var chart = document.getElementById(t + '-chart'), table = document.getElementById(t + '-table'); | |
| if (!chart || !table) return; | |
| /* the visible label says what the button will show next ("View as table" / "View as chart"), | |
| so no aria-pressed: a changing name plus a pressed state would double-signal */ | |
| btn.removeAttribute('aria-pressed'); | |
| btn.setAttribute('aria-controls', t + '-chart ' + t + '-table'); | |
| btn.addEventListener('click', function () { | |
| var showTable = table.hidden; | |
| btn.classList.toggle('is-on', showTable); | |
| chart.hidden = showTable; table.hidden = !showTable; | |
| btn.querySelector('span').textContent = showTable ? 'View as chart' : 'View as table'; | |
| var use = btn.querySelector('use'); if (use) use.setAttribute('href', showTable ? '#i-chart' : '#i-table'); | |
| U.tip.hide(); | |
| document.dispatchEvent(new CustomEvent('viewtoggle', { detail: { target: t, table: showTable } })); | |
| }); | |
| }); | |
| }; | |
| /* Build a simple data table: cols [{label, num?, cls?, get(row)->string|node}] */ | |
| U.table = function (cols, rows, caption) { | |
| var t = U.h('table', { class: 'data-table' }); | |
| if (caption) t.appendChild(U.h('caption', { text: caption })); | |
| var thead = U.h('thead'), tr = U.h('tr'); | |
| cols.forEach(function (c) { tr.appendChild(U.h('th', { class: c.num ? 'num' : (c.cls || ''), text: c.label, attrs: { scope: 'col' } })); }); | |
| thead.appendChild(tr); t.appendChild(thead); | |
| var tb = U.h('tbody'); | |
| rows.forEach(function (r) { | |
| var row = U.h('tr'); | |
| cols.forEach(function (c, i) { | |
| var v = c.get(r); | |
| var cell = U.h(i === 0 && c.rowHeader !== false ? 'th' : 'td', { class: c.num ? 'num' : (c.cls || '') }); | |
| if (i === 0 && c.rowHeader !== false) cell.setAttribute('scope', 'row'); | |
| U.append(cell, v == null ? '—' : v); | |
| row.appendChild(cell); | |
| }); | |
| tb.appendChild(row); | |
| }); | |
| t.appendChild(tb); | |
| return t; | |
| }; | |
| /* Put a U.table into a chart's table view (host = the .view--table element), replacing what was there. | |
| The caption moves above the scroller so it never scrolls away with the columns; the scroller is a | |
| named, focusable region (keyboard users can scroll it with the arrow keys), and while it overflows a | |
| "swipe" hint shows and the right edge fades until the last column is reached. */ | |
| U.tableView = function (host, table) { | |
| host.textContent = ''; | |
| host.classList.add('tview'); | |
| var capEl = table.querySelector('caption'), capId = host.id + '-cap'; | |
| var cap = U.h('p', { class: 'tview__cap', text: capEl ? capEl.textContent : '', attrs: { id: capId } }); | |
| if (capEl) capEl.parentNode.removeChild(capEl); | |
| table.setAttribute('aria-labelledby', capId); | |
| var sc = U.h('div', { class: 'table-scroll', attrs: { tabindex: '0', role: 'region', 'aria-labelledby': capId } }, table); | |
| sc.addEventListener('scroll', function () { tviewState(host, sc); }, { passive: true }); | |
| host.appendChild(cap); | |
| host.appendChild(U.h('p', { class: 'tview__swipe', text: 'swipe the table sideways →', attrs: { 'aria-hidden': 'true' } })); | |
| host.appendChild(sc); | |
| tviewState(host, sc); | |
| return sc; | |
| }; | |
| function tviewState(host, sc) { | |
| var over = sc.scrollWidth > sc.clientWidth + 1; | |
| host.classList.toggle('is-over', over); | |
| sc.classList.toggle('at-end', !over || sc.scrollLeft + sc.clientWidth >= sc.scrollWidth - 2); | |
| } | |
| function tviewAll() { | |
| U.$$('.tview').forEach(function (host) { var sc = U.$('.table-scroll', host); if (sc) tviewState(host, sc); }); | |
| } | |
| var tviewTimer; | |
| window.addEventListener('resize', function () { clearTimeout(tviewTimer); tviewTimer = setTimeout(tviewAll, 80); }); | |
| document.addEventListener('viewtoggle', tviewAll); | |
| if (document.fonts && document.fonts.ready) document.fonts.ready.then(tviewAll); | |
| /* ---- segmented radio control ---- */ | |
| U.seg = function (container, options, value, onChange) { | |
| container.textContent = ''; | |
| var btns = options.map(function (o) { | |
| var b = U.h('button', { attrs: { type: 'button', role: 'radio', 'aria-checked': String(o.value === value), tabindex: o.value === value ? '0' : '-1' } }); | |
| if (o.dot) b.appendChild(U.h('span', { class: 'dot', style: { background: o.dot } })); | |
| b.appendChild(document.createTextNode(o.label)); | |
| if (o.title) b.title = o.title; | |
| b.addEventListener('click', function () { select(o.value, true); }); | |
| container.appendChild(b); | |
| return b; | |
| }); | |
| function select(v, fire) { | |
| options.forEach(function (o, i) { | |
| var on = o.value === v; | |
| btns[i].setAttribute('aria-checked', String(on)); | |
| btns[i].tabIndex = on ? 0 : -1; | |
| }); | |
| if (fire) onChange(v); | |
| } | |
| container.addEventListener('keydown', function (e) { | |
| var idx = btns.indexOf(document.activeElement); | |
| if (idx < 0) return; | |
| var n = null; | |
| if (e.key === 'ArrowRight' || e.key === 'ArrowDown') n = (idx + 1) % btns.length; | |
| if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') n = (idx - 1 + btns.length) % btns.length; | |
| if (n != null) { e.preventDefault(); btns[n].focus(); btns[n].click(); } | |
| }); | |
| return { select: select }; | |
| }; | |
| /* ---- copy to clipboard ---- */ | |
| U.copy = function (text, btn) { | |
| function done(ok) { | |
| var span = btn.querySelector('span'); | |
| var prev = span.textContent; | |
| span.textContent = ok ? 'Copied' : 'Press Ctrl+C'; | |
| btn.classList.toggle('is-done', ok); | |
| var use = btn.querySelector('use'); if (use && ok) use.setAttribute('href', '#i-check'); | |
| var live = document.getElementById('live'); if (live) live.textContent = ok ? 'Copied to clipboard' : 'Copy failed'; | |
| setTimeout(function () { span.textContent = prev === 'Copied' ? 'Copy' : prev; btn.classList.remove('is-done'); if (use) use.setAttribute('href', '#i-copy'); }, 1600); | |
| } | |
| function fallback() { | |
| var ta = U.h('textarea', { attrs: { readonly: true, 'aria-hidden': 'true' }, style: { position: 'fixed', left: '-9999px', top: '0' } }); | |
| ta.value = text; document.body.appendChild(ta); ta.select(); | |
| var ok = false; try { ok = document.execCommand('copy'); } catch (e) { ok = false; } | |
| document.body.removeChild(ta); done(ok); | |
| } | |
| if (navigator.clipboard && window.isSecureContext) navigator.clipboard.writeText(text).then(function () { done(true); }, fallback); | |
| else fallback(); | |
| }; | |
| U.copyButton = function (getText, label) { | |
| var b = U.h('button', { class: 'copy-btn', attrs: { type: 'button', 'aria-label': label || 'Copy to clipboard' } }, U.icon('i-copy'), U.h('span', { text: 'Copy', attrs: { 'aria-hidden': 'true' } })); | |
| b.addEventListener('click', function () { U.copy(getText(), b); }); | |
| return b; | |
| }; | |
| /* ---- lightbox ---- */ | |
| U.lightbox = function (src, alt, caption) { | |
| var dlg = document.getElementById('lightbox'); | |
| var img = document.getElementById('lightbox-img'); | |
| /* on phones, show the figure near its natural size inside a pannable box */ | |
| var pan = window.matchMedia && window.matchMedia('(max-width: 720px)').matches; | |
| dlg.classList.toggle('is-pan', !!pan); | |
| img.style.width = ''; | |
| img.onload = function () { if (pan) img.style.width = Math.min(img.naturalWidth, 1400) + 'px'; }; | |
| img.src = src; img.alt = alt || ''; | |
| if (pan && img.complete && img.naturalWidth) img.style.width = Math.min(img.naturalWidth, 1400) + 'px'; | |
| document.getElementById('lightbox-cap').textContent = caption || ''; | |
| if (typeof dlg.showModal === 'function') dlg.showModal(); else dlg.setAttribute('open', ''); | |
| }; | |
| U.initLightbox = function () { | |
| var dlg = document.getElementById('lightbox'); | |
| document.getElementById('lightbox-close').addEventListener('click', function () { dlg.close ? dlg.close() : dlg.removeAttribute('open'); }); | |
| dlg.addEventListener('click', function (e) { if (e.target === dlg) dlg.close(); }); | |
| }; | |
| U.fig = function (key) { return D.caseStudy.figures.filter(function (f) { return f.key === key; })[0]; }; | |
| /* zoomable figure button */ | |
| U.zoomable = function (src, alt, caption, label, opts) { | |
| opts = opts || {}; | |
| var img = U.h('img', { attrs: { src: src, alt: alt || '', loading: opts.eager ? null : 'lazy', decoding: 'async' } }); | |
| var b = U.h('button', { class: 'zoomable', attrs: { type: 'button', 'aria-label': 'Enlarge ' + (label || 'figure') } }, img, | |
| U.h('span', { class: 'zoomable__hint' }, U.icon('i-zoom'), 'Enlarge')); | |
| b.addEventListener('click', function () { U.lightbox(src, alt, caption); }); | |
| return b; | |
| }; | |
| /* ---- resize ---- */ | |
| U.onResize = function (el, fn) { | |
| var last = 0, t; | |
| var ro = new ResizeObserver(function (entries) { | |
| var w = Math.round(entries[0].contentRect.width); | |
| if (w === last || w === 0) return; | |
| last = w; clearTimeout(t); t = setTimeout(function () { fn(w); }, 60); | |
| }); | |
| ro.observe(el); | |
| }; | |
| U.reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; | |
| /* Nice linear ticks */ | |
| U.ticks = function (lo, hi, n) { | |
| var span = hi - lo, step = Math.pow(10, Math.floor(Math.log10(span / n))), err = span / n / step; | |
| if (err >= 7.5) step *= 10; else if (err >= 3.5) step *= 5; else if (err >= 1.5) step *= 2; | |
| var out = [], v = Math.ceil(lo / step) * step; | |
| for (; v <= hi + 1e-9; v += step) out.push(Math.round(v * 1e6) / 1e6); | |
| return out; | |
| }; | |
| /* Log-axis tick labels (1-2-5 per decade) without collisions: 1s first, then 5s, then 2s; | |
| a label is kept only if it clears every kept label by `gap` px and stays inside [x0, x1]. | |
| ticks: [{v, k, x}] -> returns the subset whose labels should be drawn. */ | |
| U.fitTicks = function (ticks, fmt, x0, x1, font, gap) { | |
| gap = gap == null ? 8 : gap; | |
| var kept = []; | |
| [1, 5, 2].forEach(function (k) { | |
| ticks.filter(function (t) { return t.k === k; }).forEach(function (t) { | |
| var w = U.textWidth(fmt(t.v), font || '400 12.5px Nunito'); | |
| var a = t.x - w / 2, b = t.x + w / 2; | |
| if (a < x0 - 1 || b > x1 + 1) return; | |
| for (var i = 0; i < kept.length; i++) if (a < kept[i].b + gap && b > kept[i].a - gap) return; | |
| kept.push({ a: a, b: b, t: t }); | |
| }); | |
| }); | |
| return kept.map(function (x) { return x.t; }); | |
| }; | |
| U.usdTick = function (v) { var dec = Math.max(0, -Math.floor(Math.log10(v) + 1e-9)); return '$' + v.toFixed(dec); }; | |
| /* ---- roving focus: one Tab stop for a set of marks, arrow keys move between them ---- | |
| items: array of focusable elements (in navigation order). onFocus(i) is called when | |
| an item receives focus. Returns {set(i)} to move the tab stop programmatically. */ | |
| U.roving = function (items, onFocus, opts) { | |
| opts = opts || {}; | |
| var cur = Math.max(0, Math.min(items.length - 1, opts.start || 0)); | |
| items.forEach(function (el, i) { | |
| el.setAttribute('tabindex', i === cur ? '0' : '-1'); | |
| el.addEventListener('focus', function () { cur = i; items.forEach(function (x, j) { x.setAttribute('tabindex', j === i ? '0' : '-1'); }); if (onFocus) onFocus(i); }); | |
| el.addEventListener('keydown', function (e) { | |
| var n = null; | |
| if (e.key === 'ArrowRight' || e.key === 'ArrowDown') n = Math.min(items.length - 1, i + 1); | |
| else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') n = Math.max(0, i - 1); | |
| else if (e.key === 'Home') n = 0; | |
| else if (e.key === 'End') n = items.length - 1; | |
| if (n != null) { e.preventDefault(); items[n].focus(); } | |
| }); | |
| }); | |
| return { set: function (i) { cur = i; items.forEach(function (x, j) { x.setAttribute('tabindex', j === i ? '0' : '-1'); }); } }; | |
| }; | |
| /* ---- comic starburst accent (decorative; the number it sits next to is real text) ---- */ | |
| U.burst = function (text, variant) { | |
| return U.h('span', { class: 'burst' + (variant ? ' burst--' + variant : ''), attrs: { 'aria-hidden': 'true' } }, U.h('span', { class: 'burst__t', text: text })); | |
| }; | |
| /* Text width measurement for SVG label layout */ | |
| var measureCtx; | |
| U.textWidth = function (text, font) { | |
| measureCtx = measureCtx || document.createElement('canvas').getContext('2d'); | |
| measureCtx.font = font || '800 12.5px Nunito'; | |
| return measureCtx.measureText(text).width; | |
| }; | |
| })(); | |