Back to OScope
oscope/public/js/util.js
Code
112 lines (99 loc) · 4.68 KB
// Small helpers shared by every view.
export const $ = (sel, root = document) => root.querySelector(sel);
export const $$ = (sel, root = document) => [...root.querySelectorAll(sel)];
// h('div.row.active', { onclick, title }, child, 'text', ...)
export function h(tag, attrs, ...kids) {
const [name, ...cls] = tag.split('.');
const el = document.createElement(name || 'div');
if (cls.length) el.className = cls.join(' ');
if (attrs) {
for (const [k, v] of Object.entries(attrs)) {
if (v == null || v === false) continue;
if (k.startsWith('on')) el.addEventListener(k.slice(2), v);
else if (k === 'style' && typeof v === 'object') Object.assign(el.style, v);
else if (k === 'html') el.innerHTML = v; // only ever used with our own markup (icons)
else if (k in el && k !== 'list' && k !== 'type') el[k] = v;
else el.setAttribute(k, v === true ? '' : v);
}
}
for (const k of kids.flat()) if (k != null && k !== false) el.append(k instanceof Node ? k : document.createTextNode(String(k)));
return el;
}
export const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
export function compact(v, digits = 1) {
if (!Number.isFinite(v)) return '–';
const a = Math.abs(v);
if (a >= 1e12) return (v / 1e12).toFixed(digits) + 'T';
if (a >= 1e9) return (v / 1e9).toFixed(digits) + 'B';
if (a >= 1e6) return (v / 1e6).toFixed(digits) + 'M';
if (a >= 1e3) return (v / 1e3).toFixed(digits) + 'K';
if (a >= 100) return v.toFixed(0);
if (a >= 1) return v.toFixed(a >= 10 ? 1 : 2);
if (a === 0) return '0';
return v.toFixed(a >= 0.01 ? 2 : a >= 0.001 ? 3 : 4);
}
export const count = n => (Number.isFinite(n) ? (Math.abs(n) < 1000 ? String(Math.round(n)) : compact(n, Math.abs(n) < 1e4 ? 1 : 0).replace('.0K', 'K')) : '–');
export const usd = v => (v < 0 ? '-$' : '$') + compact(Math.abs(v));
export function sol(v, d) {
if (!Number.isFinite(v)) return '–';
const a = Math.abs(v);
const digits = d ?? (a >= 1000 ? 0 : a >= 100 ? 1 : a >= 1 ? 2 : a >= 0.01 ? 3 : 4);
return a >= 10000 ? compact(v) : v.toFixed(digits);
}
export const pct = (v, d = 1) => (Number.isFinite(v) ? (+v).toFixed(v !== 0 && Math.abs(v) < 1 && d < 2 ? 2 : d) + '%' : '–');
export const signed = (v, f) => (v > 0 ? '+' : v < 0 ? '−' : '') + f(Math.abs(v));
// price of one token in dollars: many zeros are written as 0.0₅123
export function tinyUsd(v) {
if (!Number.isFinite(v) || v <= 0) return '–';
if (v >= 1) return '$' + v.toFixed(v >= 100 ? 2 : 4);
const s = v.toFixed(20).slice(2);
const zeros = s.match(/^0*/)[0].length;
if (zeros < 4) return '$' + v.toFixed(zeros + 4);
const sub = String(zeros).split('').map(d => '₀₁₂₃₄₅₆₇₈₉'[d]).join('');
return '$0.0' + sub + s.slice(zeros, zeros + 4).replace(/0+$/, '');
}
export function age(ts, nowMs = Date.now()) {
if (!ts) return '–';
const s = Math.max(0, Math.floor((nowMs - ts) / 1000));
if (s < 60) return s + 's';
const m = Math.floor(s / 60);
if (m < 60) return m + 'm';
const hh = Math.floor(m / 60);
if (hh < 24) return hh + 'h';
const d = Math.floor(hh / 24);
return d < 30 ? d + 'd' : Math.floor(d / 30) + 'mo';
}
export const short = (a, n = 4) => (a ? a.slice(0, n) + '…' + a.slice(-n) : '');
export function copy(text, label = 'Copied') {
navigator.clipboard?.writeText(text).then(() => toast(label, 'info', 1400)).catch(() => {});
}
let toastRoot;
export function toast(text, kind = 'info', ms = 3000, node) {
toastRoot ||= document.getElementById('toasts');
const t = h('div.toast.' + kind, null, node || h('span', null, text));
toastRoot.prepend(t);
while (toastRoot.children.length > 5) toastRoot.lastChild.remove();
requestAnimationFrame(() => t.classList.add('in'));
setTimeout(() => { t.classList.remove('in'); setTimeout(() => t.remove(), 250); }, ms);
return t;
}
export function safeHref(u) {
try { const x = new URL(u); return x.protocol === 'https:' || x.protocol === 'http:' ? x.href : null; } catch { return null; }
}
export const store = {
get(k, d) { try { const v = localStorage.getItem('oscope.' + k); return v == null ? d : JSON.parse(v); } catch { return d; } },
set(k, v) { try { localStorage.setItem('oscope.' + k, JSON.stringify(v)); } catch {} },
};
// a deterministic colour for a coin with no picture
export function hue(str) {
let x = 0;
for (let i = 0; i < str.length; i++) x = (x * 31 + str.charCodeAt(i)) >>> 0;
return x % 360;
}
export function emitter() {
const m = new Map();
return {
on(e, f) { if (!m.has(e)) m.set(e, new Set()); m.get(e).add(f); return () => m.get(e).delete(f); },
emit(e, ...a) { for (const f of m.get(e) || []) f(...a); },
};
}