64 lines
3.7 KiB
JavaScript
64 lines
3.7 KiB
JavaScript
/*!
|
||
* Tectum
|
||
* Copyright © 2026 Machiel Heinen.
|
||
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
||
* Zie LICENTIE.md.
|
||
*/
|
||
// Eigenschappenpaneel rechts: eigenschappen van de selectie, of van de woning als er niets geselecteerd is.
|
||
// Een sectie = { key, label, icon, groups() → [{ label, html }], wire(el) }; elke sectie krijgt een eigen element,
|
||
// zodat wire() alleen de eigen knoppen koppelt.
|
||
|
||
window.Inspector = (() => {
|
||
const icon = (n) => `<i class="bi bi-${n}" aria-hidden="true"></i>`;
|
||
const big = (label, iconName, attrs, cls) =>
|
||
`<button type="button" class="ins-btn ins-big${cls ? ' ' + cls : ''}" ${attrs || ''}>${icon(iconName)}<span>${label}</span></button>`;
|
||
const small = (label, iconName, attrs, cls) =>
|
||
`<button type="button" class="ins-btn${cls ? ' ' + cls : ''}" ${attrs || ''}>${icon(iconName)}<span>${label}</span></button>`;
|
||
const kv = (label, value, unit) =>
|
||
`<div class="ins-kv"><span>${label}</span><b>${value}${unit ? `<small>${unit}</small>` : ''}</b></div>`;
|
||
|
||
const PREF_KEY = 'tectum.inspector.open';
|
||
let open;
|
||
try { open = new Set(JSON.parse(localStorage.getItem(PREF_KEY) || 'null') || ['gevel']); } catch (e) { open = new Set(['gevel']); }
|
||
const saveOpen = () => { try { localStorage.setItem(PREF_KEY, JSON.stringify([...open])); } catch (e) { } };
|
||
|
||
// Een groepskop gelijk aan de sectienaam (bijv. Elektra › Elektra) vervalt.
|
||
const groupsHtml = (groups, secLabel) => groups.filter(Boolean).map(g =>
|
||
`<div class="ins-group${g.cls ? ' ' + g.cls : ''}">${g.label && g.label.toLowerCase() !== String(secLabel || '').toLowerCase() ? `<h3>${g.label}</h3>` : ''}<div class="ins-group-body">${g.html}</div></div>`).join('');
|
||
|
||
// view = { crumbs: [{ label, attrs }], title, icon, sub, top: html, sections: [...], collapsible, onBack }
|
||
function render(el, view) {
|
||
const scrollKey = view.title + '|' + (view.sub || '');
|
||
const keepScroll = el._insKey === scrollKey ? el.scrollTop : 0;
|
||
el._insKey = scrollKey;
|
||
el.innerHTML = `<div class="ins-head">
|
||
${view.crumbs && view.crumbs.length ? `<div class="ins-crumbs">${view.crumbs.map(c => `<button type="button" class="ins-crumb" ${c.attrs || ''}>${c.label}</button>`).join(icon('chevron-right'))}${icon('chevron-right')}</div>` : ''}
|
||
<h2>${view.icon ? icon(view.icon) : ''}<span>${view.title}</span></h2>
|
||
${view.sub ? `<div class="ins-sub">${view.sub}</div>` : ''}
|
||
</div>
|
||
${view.top ? `<div class="ins-top">${view.top}</div>` : ''}
|
||
<div class="ins-sections"></div>`;
|
||
const host = el.querySelector('.ins-sections');
|
||
view.sections.filter(Boolean).forEach(sec => {
|
||
const node = document.createElement('section');
|
||
const collapsible = view.collapsible;
|
||
const isOpen = !collapsible || open.has(sec.key);
|
||
node.className = 'ins-section' + (collapsible ? ' ins-collapsible' : '') + (isOpen ? ' open' : '');
|
||
node.innerHTML = (collapsible ? `<button type="button" class="ins-section-head" aria-expanded="${isOpen}">${sec.icon ? icon(sec.icon) : ''}<span>${sec.label}</span>${icon('chevron-down')}</button>` : '') +
|
||
`<div class="ins-section-body">${groupsHtml(sec.groups ? sec.groups() : [], sec.label)}</div>`;
|
||
host.appendChild(node);
|
||
if (collapsible) node.querySelector('.ins-section-head').addEventListener('click', () => {
|
||
const now = !node.classList.contains('open');
|
||
node.classList.toggle('open', now);
|
||
node.querySelector('.ins-section-head').setAttribute('aria-expanded', now);
|
||
if (now) open.add(sec.key); else open.delete(sec.key);
|
||
saveOpen();
|
||
});
|
||
if (sec.wire) sec.wire(node);
|
||
});
|
||
el.scrollTop = keepScroll;
|
||
}
|
||
|
||
return { big, small, kv, render };
|
||
})();
|