146 lines
6.6 KiB
JavaScript
146 lines
6.6 KiB
JavaScript
/*!
|
|
* Tectum
|
|
* Copyright © 2026 Machiel Heinen.
|
|
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
|
* Zie LICENTIE.md.
|
|
*/
|
|
// ============================================================
|
|
// core/ribbon.js — Ribbon (window.Ribbon): de bedieningsbalk bovenin. Eén
|
|
// plek voor alle commando's en instellingen; de weergaven eronder blijven
|
|
// vrij.
|
|
//
|
|
// Tabbladen komen uit bronnen: 'app' (Bestand, Beeld, Extra) en 'editor'
|
|
// (de actieve editor, bijv. Start, Gevel, Dak, ... en een contextueel
|
|
// tabblad voor de selectie). Tabbladen met dezelfde sleutel uit meerdere
|
|
// bronnen worden samengevoegd (bijv. Beeld: weergave-opties van de woning
|
|
// + vensterknoppen van de app).
|
|
//
|
|
// Ribbon.setSource('editor', [{
|
|
// key, label, order (sortering), contextual (gekleurd, achteraan),
|
|
// groups: () => [{ label, html }], // inhoud, opnieuw bij elke weergave
|
|
// wire: (body) => {}, // events koppelen na het tekenen
|
|
// onShow: () => {} // tabblad wordt actief
|
|
// }]);
|
|
// Ribbon.activate(key) · Ribbon.refresh() · Ribbon.active() · Ribbon.onChange(fn)
|
|
//
|
|
// Groepen: een raster van 3 rijen hoog; velden (UI-kit) staan per kolom
|
|
// onder elkaar. Grote knop: Ribbon.big(...), kleine: Ribbon.small(...).
|
|
// Plain script (geen ES-module).
|
|
// ============================================================
|
|
|
|
window.Ribbon = (() => {
|
|
const sources = {}; // naam -> [tab]
|
|
const SOURCE_ORDER = ['app', 'editor'];
|
|
let root = null, tabsEl = null, bodyEl = null;
|
|
let activeKey = null, lastPlainKey = null;
|
|
let auto = true; // tabblad nog niet door de gebruiker gekozen: voorkeur 'start'
|
|
let collapsed = false, notified = null;
|
|
const listeners = [];
|
|
const PREF_KEY = 'tectum.ribbon.collapsed';
|
|
|
|
const icon = (n) => `<i class="bi bi-${n}" aria-hidden="true"></i>`;
|
|
// Grote knop (3 rijen hoog): icoon boven het label.
|
|
const big = (label, iconName, attrs, cls) =>
|
|
`<button type="button" class="rb-btn rb-big${cls ? ' ' + cls : ''}" ${attrs || ''}>${icon(iconName)}<span>${label}</span></button>`;
|
|
// Kleine knop (1 rij).
|
|
const small = (label, iconName, attrs, cls) =>
|
|
`<button type="button" class="rb-btn rb-small${cls ? ' ' + cls : ''}" ${attrs || ''}>${icon(iconName)}<span>${label}</span></button>`;
|
|
// Waarde-regel (alleen lezen) binnen een groep.
|
|
const kv = (label, value, unit) =>
|
|
`<div class="rb-kv"><span>${label}</span><b>${value}${unit ? `<small>${unit}</small>` : ''}</b></div>`;
|
|
|
|
function allTabs() {
|
|
const byKey = new Map();
|
|
SOURCE_ORDER.concat(Object.keys(sources).filter(n => !SOURCE_ORDER.includes(n))).forEach(name => {
|
|
(sources[name] || []).forEach(t => {
|
|
if (!byKey.has(t.key)) byKey.set(t.key, { key: t.key, label: t.label, order: t.order || 0, contextual: !!t.contextual, icon: t.icon, parts: [] });
|
|
const m = byKey.get(t.key);
|
|
if (t.order !== undefined && name !== 'app') m.order = t.order;
|
|
if (t.label && name !== 'app') m.label = t.label;
|
|
if (t.contextual) m.contextual = true;
|
|
m.parts.push(t);
|
|
});
|
|
});
|
|
return Array.from(byKey.values()).sort((a, b) => (a.contextual - b.contextual) || (a.order - b.order));
|
|
}
|
|
|
|
function renderTabs(list) {
|
|
tabsEl.innerHTML = list.map(t =>
|
|
`<button type="button" class="rb-tab${t.key === activeKey ? ' active' : ''}${t.contextual ? ' rb-ctx' : ''}" data-rb-tab="${t.key}">${t.contextual && t.icon ? icon(t.icon) : ''}<span>${t.label}</span></button>`).join('');
|
|
}
|
|
|
|
function renderBody(list) {
|
|
const tab = list.find(t => t.key === activeKey);
|
|
if (!tab) { bodyEl.innerHTML = ''; return; }
|
|
const groups = [];
|
|
tab.parts.forEach(p => (p.groups ? p.groups() : []).forEach(g => g && groups.push(g)));
|
|
bodyEl.innerHTML = groups.map(g =>
|
|
`<section class="rb-group${g.cls ? ' ' + g.cls : ''}"><div class="rb-group-body">${g.html}</div><div class="rb-group-label">${g.label}</div></section>`).join('') +
|
|
`<button type="button" class="rb-collapse" title="Ribbon inklappen (dubbelklik op een tabblad)">${icon('chevron-up')}</button>`;
|
|
tab.parts.forEach(p => p.wire && p.wire(bodyEl));
|
|
}
|
|
|
|
function render() {
|
|
if (!root) return;
|
|
const list = allTabs();
|
|
if (!list.some(t => t.key === activeKey) || (auto && !list.some(t => t.key === activeKey && t.contextual))) {
|
|
const pick = [lastPlainKey, 'start'].find(k => list.some(t => t.key === k));
|
|
activeKey = pick || (list.find(t => !t.contextual) || {}).key || null;
|
|
}
|
|
renderTabs(list);
|
|
renderBody(list);
|
|
root.classList.toggle('rb-collapsed', collapsed);
|
|
if (notified !== activeKey) { notified = activeKey; listeners.forEach(fn => fn(activeKey)); }
|
|
}
|
|
|
|
function activate(key) {
|
|
const list = allTabs();
|
|
const tab = list.find(t => t.key === key);
|
|
if (!tab) return;
|
|
activeKey = key;
|
|
auto = false;
|
|
if (!tab.contextual) lastPlainKey = key;
|
|
if (collapsed) { collapsed = false; savePref(); }
|
|
render();
|
|
tab.parts.forEach(p => p.onShow && p.onShow());
|
|
}
|
|
|
|
function savePref() { try { localStorage.setItem(PREF_KEY, collapsed ? '1' : '0'); } catch (e) { /* alleen deze sessie */ } }
|
|
|
|
function init(el) {
|
|
root = el;
|
|
try { collapsed = localStorage.getItem(PREF_KEY) === '1'; } catch (e) { collapsed = false; }
|
|
tabsEl = el.querySelector('.rb-tabs');
|
|
bodyEl = el.querySelector('.rb-body');
|
|
tabsEl.addEventListener('click', (e) => {
|
|
const b = e.target.closest('[data-rb-tab]');
|
|
if (!b) return;
|
|
if (b.dataset.rbTab === activeKey && collapsed) { collapsed = false; savePref(); render(); return; }
|
|
activate(b.dataset.rbTab);
|
|
});
|
|
tabsEl.addEventListener('dblclick', (e) => {
|
|
if (!e.target.closest('[data-rb-tab]')) return;
|
|
collapsed = !collapsed; savePref(); render();
|
|
});
|
|
bodyEl.addEventListener('click', (e) => {
|
|
if (e.target.closest('.rb-collapse')) { collapsed = true; savePref(); render(); }
|
|
});
|
|
render();
|
|
}
|
|
|
|
// Bron vervangen. Een nieuw contextueel tabblad (andere sleutel) wordt
|
|
// actief als opts.focus; verdwijnt het actieve, dan terug naar het vorige.
|
|
function setSource(name, tabs, opts) {
|
|
sources[name] = tabs || [];
|
|
const ctx = (tabs || []).find(t => t.contextual);
|
|
if (opts && opts.focus && ctx) { activeKey = ctx.key; if (collapsed) { collapsed = false; savePref(); } }
|
|
render();
|
|
if (opts && opts.focus && ctx && ctx.onShow) ctx.onShow();
|
|
}
|
|
|
|
// fn(key) bij elke wissel van het actieve tabblad.
|
|
function onChange(fn) { listeners.push(fn); return () => { const i = listeners.indexOf(fn); if (i >= 0) listeners.splice(i, 1); }; }
|
|
|
|
return { init, setSource, activate, refresh: render, active: () => activeKey, onChange, big, small, kv };
|
|
})();
|