/** * App UI elements module. */ const _create = (name = 'div', modFn) => { const el = document.createElement(name); if (modFn) { modFn(el); } return el; } const _option = (text = '', selected = false, label) => { const el = _create('option'); if (label) { el.textContent = label; el.value = text; } else { el.textContent = text; } if (selected) el.selected = true; return el; } const select = (key = '', callback = () => ({}), values = {values: [], labels: []}, current = '') => { const el = _create(); const select = _create('select'); select.onchange = event => { callback(key, event.target.value); }; el.append(select); select.append(_option(0, current === '', 'none')); values.values.forEach((value, index) => { select.append(_option(value, current == value, values.labels?.[index])); }); return el; } const checkbox = (id, cb = () => ({}), checked = false, label = '', cc = '') => { const el = _create(); cc !== '' && el.classList.add(cc); let parent = el; if (label) { const _label = _create('label', (el) => { el.setAttribute('htmlFor', id); }) _label.innerText = label; el.append(_label) parent = _label; } const input = _create('input', (el) => { el.setAttribute('id', id); el.setAttribute('name', id); el.setAttribute('type', 'checkbox'); el.onclick = ((e) => { checked = e.target.checked cb(id, checked) }) checked && el.setAttribute('checked', ''); }); parent.prepend(input); return el; } const panel = (root, title = '', cc = '', content, buttons = [], onToggle) => { const state = { shown: false, loading: false, title: title, } const tHandlers = []; onToggle && tHandlers.push(onToggle); const _root = root || _create('div'); _root.classList.add('panel'); gui.hide(_root); const header = _create('div', (el) => el.classList.add('panel__header')); const _content = _create('div', (el) => { if (cc) { el.classList.add(cc); } el.classList.add('panel__content') }); const _title = _create('span', (el) => { el.classList.add('panel__header__title'); el.innerText = title; }); header.append(_title); header.append(_create('div', (el) => { el.classList.add('panel__header__controls'); buttons.forEach((b => el.append(_create('span', (el) => { if (Object.keys(b).length === 0) { el.classList.add('panel__button_separator'); return } el.classList.add('panel__button'); if (b.cl) b.cl.forEach(class_ => el.classList.add(class_)); if (b.title) el.title = b.title; el.innerText = b.caption; el.addEventListener('click', b.handler) })))) el.append(_create('span', (el) => { el.classList.add('panel__button'); el.innerText = 'X'; el.title = 'Close'; el.addEventListener('click', () => toggle(false)) })) })) root.append(header, _content); if (content) { _content.append(content); } const setContent = (content) => _content.replaceChildren(content) const setLoad = (load = true) => { state.loading = load; _title.innerText = state.loading ? `${state.title}...` : state.title; } const toggle = (() => { let br = window.getComputedStyle(_root.parentElement).borderRadius; return (force) => { state.shown = force !== undefined ? force : !state.shown; // hack for not transparent jpeg corners :_; _root.parentElement.style.borderRadius = state.shown ? '0px' : br; tHandlers.forEach(h => h?.(state.shown, _root)); state.shown ? gui.show(_root) : gui.hide(_root) } })() return { contentEl: _content, isHidden: () => !state.shown, onToggle: (fn) => tHandlers.push(fn), setContent, setLoad, toggle, } } const _bind = (cb = () => ({}), name = '', oldValue) => { const el = _create('button'); el.onclick = () => cb(name, oldValue); el.textContent = name; return el; } const binding = (key = '', value = '', cb = () => ({})) => { const el = _create(); el.setAttribute('class', 'binding-element'); const k = _bind(cb, key, value); el.append(k); const v = _create(); v.textContent = value; el.append(v); return el; } const show = (...els) => { els.forEach(el => el.classList.remove('hidden')) } const inputN = (key = '', cb = () => ({}), current = 0) => { const el = _create(); const input = _create('input'); input.type = 'number'; input.value = current; input.onchange = event => cb(key, event.target.value); el.append(input); return el; } const hide = (el) => { el.classList.add('hidden'); } const toggle = (el, what) => { if (what === undefined) { el.classList.toggle('hidden') return } what ? show(el) : hide(el) } const multiToggle = (elements = [], options = {list: []}) => { if (!options.list.length || !elements.length) return let i = 0 const setText = () => elements.forEach(el => el.innerText = options.list[i].caption) const handleClick = () => { options.list[i].cb() i = (i + 1) % options.list.length setText() } setText() elements.forEach(el => el.addEventListener('click', handleClick)) } const fadeIn = async (el, speed = .1) => { el.style.opacity = '0'; el.style.display = 'block'; return new Promise((done) => (function fade() { let val = parseFloat(el.style.opacity); const proceed = ((val += speed) <= 1); if (proceed) { el.style.opacity = '' + val; requestAnimationFrame(fade); } else { done(); } })() ); } const fadeOut = async (el, speed = .1) => { el.style.opacity = '1'; return new Promise((done) => (function fade() { if ((el.style.opacity -= speed) < 0) { el.style.display = "none"; done(); } else { requestAnimationFrame(fade); } })() ) } const fragment = () => document.createDocumentFragment(); const sleep = async (ms) => new Promise(resolve => setTimeout(resolve, ms)); const fadeInOut = async (el, wait = 1000, speed = .1) => { await fadeIn(el, speed) await sleep(wait); await fadeOut(el, speed) } export const gui = { anim: { fadeIn, fadeOut, fadeInOut, }, binding, checkbox, create: _create, fragment, hide, inputN, multiToggle, panel, select, show, toggle, }