144 lines
7.2 KiB
JavaScript
144 lines
7.2 KiB
JavaScript
/*!
|
|
* Tectum
|
|
* Copyright © 2026 Machiel Heinen.
|
|
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
|
* Zie LICENTIE.md.
|
|
*/
|
|
// Labels: zelfstandig naamwoord met hoofdletter, geen dubbele punt, uitleg in een hint, eenheid rechts.
|
|
|
|
window.UI = (() => {
|
|
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
const icon = (name, cls) => `<i class="bi bi-${name}${cls ? ' ' + cls : ''}" aria-hidden="true"></i>`;
|
|
const hint = (text) => text ? `<span class="ui-hint" title="${esc(text)}">${icon('info-circle')}</span>` : '';
|
|
|
|
function field(id, label, control, o) {
|
|
return `<div class="ui-field${o && o.disabled ? ' is-disabled' : ''}"><label for="${id}">${label}${hint(o && o.hint)}</label><div class="ui-control">${control}</div></div>`;
|
|
}
|
|
|
|
function num(id, label, value, o) {
|
|
o = o || {};
|
|
const unit = o.unit === undefined ? 'mm' : o.unit;
|
|
return field(id, label,
|
|
`<input type="number" id="${id}" value="${value}" step="${o.step || 50}"${o.min !== undefined ? ` min="${o.min}"` : ''}${o.disabled ? ' disabled' : ''}>` +
|
|
`<span class="ui-unit">${unit}</span>`, o);
|
|
}
|
|
|
|
// Het getal mag buiten het schuifbereik; het model klemt zelf.
|
|
function range(id, label, value, o) {
|
|
o = o || {};
|
|
const unit = o.unit === undefined ? 'mm' : o.unit;
|
|
const lim = `min="${o.min}" max="${o.max}" step="${o.step || 10}"`;
|
|
return `<div class="ui-field ui-field-range${o.disabled ? ' is-disabled' : ''}"><label for="${id}">${label}${hint(o.hint)}</label>` +
|
|
`<div class="ui-control ui-range"><input type="range" id="${id}-r" ${lim} value="${value}"${o.disabled ? ' disabled' : ''}>` +
|
|
`<input type="number" id="${id}" step="${o.step || 10}" value="${value}"${o.disabled ? ' disabled' : ''}><span class="ui-unit">${unit}</span></div></div>`;
|
|
}
|
|
function onRange(id, fn, after) {
|
|
const n = document.getElementById(id), r = document.getElementById(id + '-r');
|
|
if (!n || !r) return;
|
|
r.addEventListener('input', () => { n.value = r.value; fn(parseFloat(r.value)); after && after(); });
|
|
n.addEventListener('input', () => {
|
|
const v = parseFloat(n.value);
|
|
if (!isFinite(v)) return;
|
|
r.value = v;
|
|
fn(v); after && after();
|
|
});
|
|
}
|
|
function setRange(id, v) {
|
|
const n = document.getElementById(id), r = document.getElementById(id + '-r');
|
|
if (n && document.activeElement !== n) n.value = v;
|
|
if (r) r.value = v;
|
|
}
|
|
|
|
function sel(id, label, options, value, o) {
|
|
const opts = Object.entries(options).map(([k, v]) => `<option value="${k}"${k === value ? ' selected' : ''}>${v}</option>`).join('');
|
|
return field(id, label, `<select id="${id}">${opts}</select><span class="ui-unit"></span>`, o);
|
|
}
|
|
|
|
function color(id, label, value, o) {
|
|
return field(id, label, `<input type="color" id="${id}" value="${value}"><span class="ui-unit"></span>`, o);
|
|
}
|
|
function onColor(id, fn, after) {
|
|
const el = document.getElementById(id);
|
|
if (el) el.addEventListener('change', (e) => { fn(e.target.value); after && after(); });
|
|
}
|
|
|
|
function text(id, label, value, o) {
|
|
return field(id, label, `<input type="text" id="${id}" value="${esc(value || '')}"${o && o.placeholder ? ` placeholder="${esc(o.placeholder)}"` : ''}><span class="ui-unit"></span>`, o);
|
|
}
|
|
function onText(id, fn, after) {
|
|
const el = document.getElementById(id);
|
|
if (el) el.addEventListener('input', (e) => { fn(e.target.value); after && after(); });
|
|
}
|
|
|
|
function toggle(id, label, checked, o) {
|
|
return field(id, label,
|
|
`<label class="ui-switch"><input type="checkbox" id="${id}"${checked ? ' checked' : ''}><span></span></label><span class="ui-unit"></span>`, o);
|
|
}
|
|
|
|
const section = (title, iconName, body) =>
|
|
`<section class="ui-section"><h2>${iconName ? icon(iconName) : ''}${title}</h2>${body}</section>`;
|
|
const info = (text) => `<div class="ui-info">${icon('info-circle')}<span>${text}</span></div>`;
|
|
const warn = (text) => `<div class="ui-warn">${icon('exclamation-triangle')}<span>${text}</span></div>`;
|
|
const grid = (...cols) => `<div class="ui-grid">${cols.map(c => `<div>${c}</div>`).join('')}</div>`;
|
|
const button = (label, iconName, attrs, cls) =>
|
|
`<button type="button" class="ui-btn${cls ? ' ' + cls : ''}" ${attrs || ''}>${iconName ? icon(iconName) : ''}<span>${label}</span></button>`;
|
|
const actions = (...btns) => `<div class="ui-actions">${btns.join('')}</div>`;
|
|
|
|
const activeTabs = {};
|
|
function tabs(group, list) {
|
|
const active = list.some(t => t.key === activeTabs[group]) ? activeTabs[group] : list[0].key;
|
|
return `<div class="ui-tabs" data-tab-group="${group}">
|
|
<div class="ui-tab-bar">${list.map(t => `<button type="button" class="ui-tab${t.key === active ? ' active' : ''}" data-tab="${t.key}">${t.icon ? icon(t.icon) : ''}<span>${t.label}</span></button>`).join('')}</div>
|
|
${list.map(t => `<div class="ui-tab-page${t.key === active ? ' active' : ''}" data-tab-page="${t.key}">${t.html}</div>`).join('')}
|
|
</div>`;
|
|
}
|
|
function wireTabs(container) {
|
|
if (container._uiTabsWired) return;
|
|
container._uiTabsWired = true;
|
|
container.addEventListener('click', (e) => {
|
|
const btn = e.target.closest('.ui-tab');
|
|
if (!btn) return;
|
|
const wrap = btn.closest('.ui-tabs');
|
|
activeTabs[wrap.dataset.tabGroup] = btn.dataset.tab;
|
|
wrap.querySelectorAll('.ui-tab').forEach(b => b.classList.toggle('active', b === btn));
|
|
wrap.querySelectorAll('.ui-tab-page').forEach(p => p.classList.toggle('active', p.dataset.tabPage === btn.dataset.tab));
|
|
});
|
|
}
|
|
|
|
function specCard(title, iconName, rows, total) {
|
|
const row = ([label, value, unit]) =>
|
|
`<div class="ui-spec-row"><span>${label}</span><span class="ui-spec-val">${value}${unit ? `<small>${unit}</small>` : ''}</span></div>`;
|
|
return `<div class="ui-spec-card"><h2>${icon(iconName)}${title}</h2>${rows.map(row).join('')}${total ? `<div class="ui-spec-total">${row(total)}</div>` : ''}</div>`;
|
|
}
|
|
|
|
function onNum(id, fn, after, o) {
|
|
const el = document.getElementById(id);
|
|
if (!el) return;
|
|
el.addEventListener('input', (e) => {
|
|
const v = parseFloat(e.target.value);
|
|
if (isFinite(v) && (!(o && o.positive) || v > 0)) { fn(v); after && after(); }
|
|
});
|
|
}
|
|
function onSel(id, fn, after) {
|
|
const el = document.getElementById(id);
|
|
if (el) el.addEventListener('input', (e) => { fn(e.target.value); after && after(); });
|
|
}
|
|
function onToggle(id, fn, after) {
|
|
const el = document.getElementById(id);
|
|
if (el) el.addEventListener('change', (e) => { fn(e.target.checked); after && after(); });
|
|
}
|
|
|
|
function cycle3D(el) {
|
|
el.classList.remove('fullscreen-2d');
|
|
if (!el.classList.contains('fullscreen-3d')) el.classList.add('fullscreen-3d');
|
|
else if (!el.classList.contains('render-only')) el.classList.add('render-only');
|
|
else el.classList.remove('fullscreen-3d', 'render-only');
|
|
}
|
|
function toggle2D(el) {
|
|
el.classList.remove('fullscreen-3d', 'render-only');
|
|
el.classList.toggle('fullscreen-2d');
|
|
}
|
|
|
|
return { esc, icon, hint, num, range, onRange, setRange, sel, toggle, color, onColor, text, onText, section, info, warn, grid, button, actions, tabs, wireTabs, specCard, onNum, onSel, onToggle, cycle3D, toggle2D };
|
|
})();
|