341 lines
14 KiB
JavaScript
341 lines
14 KiB
JavaScript
// web-cad v6 – Interactions (theme, tabs, drawers, mobile right-tab-bar)
|
||
(function() {
|
||
'use strict';
|
||
|
||
// ===================== THEME TOGGLE =====================
|
||
const themeBtn = document.getElementById('theme-toggle');
|
||
const html = document.documentElement;
|
||
if (themeBtn) {
|
||
themeBtn.addEventListener('click', () => {
|
||
const cur = html.getAttribute('data-theme');
|
||
html.setAttribute('data-theme', cur === 'dark' ? 'light' : 'dark');
|
||
});
|
||
}
|
||
|
||
// ===================== RIGHTBAR (desktop) TAB SWITCHING =====================
|
||
document.querySelectorAll('.rightbar-tab').forEach(tab => {
|
||
tab.addEventListener('click', () => {
|
||
const panel = tab.dataset.panel;
|
||
document.querySelectorAll('.rightbar-tab').forEach(t => { t.classList.remove('active'); t.setAttribute('aria-selected', 'false'); });
|
||
document.querySelectorAll('.rightbar-panel').forEach(p => p.classList.remove('active'));
|
||
tab.classList.add('active');
|
||
tab.setAttribute('aria-selected', 'true');
|
||
const target = document.querySelector(`[data-panel-content="${panel}"]`);
|
||
if (target) target.classList.add('active');
|
||
// sync mobile drawer if open
|
||
const dTab = document.querySelector(`[data-drawer-tab="${panel}"]`);
|
||
if (dTab) dTab.click();
|
||
});
|
||
});
|
||
|
||
// ===================== DRAWER TAB SWITCHING (mobile) =====================
|
||
document.querySelectorAll('.drawer-tab').forEach(tab => {
|
||
tab.addEventListener('click', () => {
|
||
const panel = tab.dataset.drawerTab;
|
||
document.querySelectorAll('.drawer-tab').forEach(t => t.classList.remove('active'));
|
||
tab.classList.add('active');
|
||
// Re-render drawer body for this panel
|
||
renderDrawerRight(panel);
|
||
// sync desktop rightbar
|
||
const rTab = document.querySelector(`.rightbar-tab[data-panel="${panel}"]`);
|
||
if (rTab) {
|
||
document.querySelectorAll('.rightbar-tab').forEach(t => t.classList.remove('active'));
|
||
rTab.classList.add('active');
|
||
document.querySelectorAll('.rightbar-panel').forEach(p => p.classList.remove('active'));
|
||
const target = document.querySelector(`[data-panel-content="${panel}"]`);
|
||
if (target) target.classList.add('active');
|
||
}
|
||
});
|
||
});
|
||
|
||
// ===================== RIGHT-TAB-BAR (mobile vertical) =====================
|
||
document.querySelectorAll('.right-tab-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const which = btn.dataset.drawer;
|
||
document.querySelectorAll('.right-tab-btn').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
// activate drawer tab + open drawer
|
||
const dTab = document.querySelector(`[data-drawer-tab="${which}"]`);
|
||
if (dTab) dTab.click();
|
||
openDrawer('right');
|
||
});
|
||
});
|
||
|
||
// ===================== DRAWER OPEN/CLOSE =====================
|
||
const scrim = document.getElementById('scrim');
|
||
const drawerLeft = document.getElementById('drawer-left');
|
||
const drawerRight = document.getElementById('drawer-right');
|
||
|
||
function openDrawer(which) {
|
||
const drawerLeftBody = document.getElementById('drawer-left-body');
|
||
const drawerRightBody = document.getElementById('drawer-right-body');
|
||
if (which === 'left' && drawerLeft) {
|
||
drawerLeft.classList.add('open');
|
||
drawerLeft.setAttribute('aria-hidden', 'false');
|
||
// Use children.length instead of innerHTML.trim() because placeholder HTML comments would prevent re-render
|
||
if (drawerLeftBody && drawerLeftBody.children.length === 0) {
|
||
renderDrawerLeft();
|
||
}
|
||
}
|
||
if (which === 'right' && drawerRight) {
|
||
drawerRight.classList.add('open');
|
||
drawerRight.setAttribute('aria-hidden', 'false');
|
||
if (drawerRightBody && drawerRightBody.children.length === 0) {
|
||
const active = document.querySelector('.drawer-tab.active');
|
||
renderDrawerRight(active ? active.dataset.drawerTab : 'tool');
|
||
}
|
||
}
|
||
if (scrim) scrim.classList.add('show');
|
||
document.body.classList.add('drawer-open');
|
||
}
|
||
|
||
function closeDrawers() {
|
||
if (drawerLeft) { drawerLeft.classList.remove('open'); drawerLeft.setAttribute('aria-hidden', 'true'); }
|
||
if (drawerRight) { drawerRight.classList.remove('open'); drawerRight.setAttribute('aria-hidden', 'true'); }
|
||
if (scrim) scrim.classList.remove('show');
|
||
document.body.classList.remove('drawer-open');
|
||
}
|
||
|
||
// Hamburger opens left drawer
|
||
const hamburger = document.getElementById('hamburger-btn');
|
||
if (hamburger) {
|
||
hamburger.addEventListener('click', () => openDrawer('left'));
|
||
}
|
||
|
||
// Close buttons
|
||
document.querySelectorAll('[data-close-drawer]').forEach(b => {
|
||
b.addEventListener('click', closeDrawers);
|
||
});
|
||
|
||
// Scrim click closes
|
||
if (scrim) scrim.addEventListener('click', closeDrawers);
|
||
|
||
// ESC key closes
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') closeDrawers();
|
||
});
|
||
|
||
// ===================== RENDER DRAWER LEFT (clone leftbar) =====================
|
||
function renderDrawerLeft() {
|
||
const src = document.querySelector('.leftbar');
|
||
const dst = document.getElementById('drawer-left-body');
|
||
if (!src || !dst) return;
|
||
// Clone leftbar body (skip header which has toggle)
|
||
const clone = src.cloneNode(true);
|
||
// Remove the inner toggle button (drawer has its own close)
|
||
const innerToggle = clone.querySelector('.leftbar-toggle');
|
||
if (innerToggle) innerToggle.remove();
|
||
// Remove header (drawer has its own header)
|
||
const header = clone.querySelector('.leftbar-header');
|
||
if (header) header.remove();
|
||
// Remove footer (not needed in drawer)
|
||
const footer = clone.querySelector('.leftbar-footer');
|
||
if (footer) footer.remove();
|
||
// Mark as drawer version + force display (the original .leftbar is hidden on mobile)
|
||
clone.classList.add('drawer-content-left');
|
||
clone.removeAttribute('id');
|
||
clone.style.cssText = 'display: flex; flex-direction: column; flex: 1; min-height: 0; border: none; background: transparent;';
|
||
dst.innerHTML = '';
|
||
dst.appendChild(clone);
|
||
// Wire up tool buttons inside drawer
|
||
dst.querySelectorAll('.tool-btn').forEach(b => {
|
||
b.addEventListener('click', () => {
|
||
document.querySelectorAll('.tool-btn').forEach(x => x.classList.remove('active'));
|
||
b.classList.add('active');
|
||
// Close drawer on tool selection (mobile UX)
|
||
setTimeout(() => closeDrawers(), 200);
|
||
});
|
||
});
|
||
}
|
||
|
||
// ===================== RENDER DRAWER RIGHT (clone rightbar panel) =====================
|
||
function renderDrawerRight(panel) {
|
||
const src = document.querySelector(`[data-panel-content="${panel}"]`);
|
||
const dst = document.getElementById('drawer-right-body');
|
||
const titleEl = document.getElementById('drawer-right-title');
|
||
if (!src || !dst) return;
|
||
const labels = { tool: 'Werkzeug', layer: 'Layer', library: 'Bibliothek', ki: 'KI Copilot' };
|
||
if (titleEl) titleEl.textContent = labels[panel] || 'Panel';
|
||
dst.innerHTML = '';
|
||
const clone = src.cloneNode(true);
|
||
clone.classList.add('active');
|
||
clone.style.cssText = 'display: block; width: 100%;';
|
||
dst.appendChild(clone);
|
||
// Re-wire KI chips and library categories inside drawer
|
||
dst.querySelectorAll('.ki-chip').forEach(chip => {
|
||
chip.addEventListener('click', () => {
|
||
const input = dst.querySelector('.ki-input');
|
||
if (input) { input.value = chip.textContent.trim(); input.focus(); }
|
||
});
|
||
});
|
||
dst.querySelectorAll('.lib-cat').forEach(cat => {
|
||
cat.addEventListener('click', () => {
|
||
dst.querySelectorAll('.lib-cat').forEach(c => c.classList.remove('active'));
|
||
cat.classList.add('active');
|
||
});
|
||
});
|
||
}
|
||
|
||
// ===================== RIBBON TAB SWITCHING =====================
|
||
document.querySelectorAll('.ribbon-tab').forEach(tab => {
|
||
tab.addEventListener('click', () => {
|
||
document.querySelectorAll('.ribbon-tab').forEach(t => { t.classList.remove('active'); t.setAttribute('aria-selected', 'false'); });
|
||
tab.classList.add('active');
|
||
tab.setAttribute('aria-selected', 'true');
|
||
});
|
||
});
|
||
|
||
// ===================== TOOL SELECTION (desktop) =====================
|
||
document.querySelectorAll('.leftbar .tool-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.leftbar .tool-btn').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
});
|
||
});
|
||
|
||
// ===================== LEFTBAR TOGGLE (desktop) =====================
|
||
const leftToggle = document.getElementById('leftbar-toggle');
|
||
if (leftToggle) {
|
||
leftToggle.addEventListener('click', () => {
|
||
const main = document.querySelector('.main');
|
||
if (main) main.classList.toggle('left-collapsed');
|
||
});
|
||
}
|
||
|
||
// ===================== CANVAS VIEW TABS =====================
|
||
document.querySelectorAll('.canvas-view-tab').forEach(tab => {
|
||
tab.addEventListener('click', () => {
|
||
document.querySelectorAll('.canvas-view-tab').forEach(t => t.classList.remove('active'));
|
||
tab.classList.add('active');
|
||
});
|
||
});
|
||
|
||
// ===================== STATUS BAR TOGGLES =====================
|
||
document.querySelectorAll('.status-item').forEach(item => {
|
||
item.addEventListener('click', () => item.classList.toggle('active'));
|
||
});
|
||
|
||
// ===================== CANVAS TOOLBAR (grid, ortho, snap) =====================
|
||
['btn-grid', 'btn-ortho', 'btn-snap'].forEach(id => {
|
||
const b = document.getElementById(id);
|
||
if (b) {
|
||
b.addEventListener('click', () => {
|
||
b.classList.toggle('active');
|
||
const pressed = b.classList.contains('active');
|
||
b.setAttribute('aria-pressed', pressed ? 'true' : 'false');
|
||
// Also toggle matching status bar item
|
||
const map = { 'btn-grid': 'GRID', 'btn-ortho': 'ORTHO', 'btn-snap': 'SNAP' };
|
||
const label = map[id];
|
||
if (label) {
|
||
document.querySelectorAll('.status-item').forEach(s => {
|
||
if (s.textContent.trim().startsWith(label)) s.classList.toggle('active', pressed);
|
||
});
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// ===================== ZOOM =====================
|
||
let zoom = 100;
|
||
const zoomDisplay = document.getElementById('zoom-display');
|
||
document.getElementById('btn-zoom-in')?.addEventListener('click', () => {
|
||
zoom = Math.min(400, zoom + 25);
|
||
if (zoomDisplay) zoomDisplay.textContent = zoom + '%';
|
||
});
|
||
document.getElementById('btn-zoom-out')?.addEventListener('click', () => {
|
||
zoom = Math.max(10, zoom - 25);
|
||
if (zoomDisplay) zoomDisplay.textContent = zoom + '%';
|
||
});
|
||
document.getElementById('btn-zoom-fit')?.addEventListener('click', () => {
|
||
zoom = 100;
|
||
if (zoomDisplay) zoomDisplay.textContent = zoom + '%';
|
||
});
|
||
|
||
// ===================== LIBRARY CATEGORIES (desktop) =====================
|
||
document.querySelectorAll('.rightbar .lib-cat').forEach(cat => {
|
||
cat.addEventListener('click', () => {
|
||
document.querySelectorAll('.rightbar .lib-cat').forEach(c => c.classList.remove('active'));
|
||
cat.classList.add('active');
|
||
});
|
||
});
|
||
|
||
// ===================== KI CHIPS (desktop) =====================
|
||
document.querySelectorAll('.rightbar .ki-chip').forEach(chip => {
|
||
chip.addEventListener('click', () => {
|
||
const input = document.querySelector('.rightbar .ki-input');
|
||
if (input) { input.value = chip.textContent.trim(); input.focus(); }
|
||
});
|
||
});
|
||
|
||
// ===================== KEYBOARD SHORTCUTS =====================
|
||
const toolMap = { v: 'select', l: 'line', c: 'circle', m: 'move', p: 'pan', z: 'zoom-win', e: 'erase' };
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.target.matches('input, textarea, select')) return;
|
||
if (e.key === 'Escape') return; // handled by drawer
|
||
const k = e.key.toLowerCase();
|
||
if (toolMap[k]) {
|
||
const btn = document.querySelector(`.tool-btn[data-tool="${toolMap[k]}"]`);
|
||
if (btn) btn.click();
|
||
}
|
||
});
|
||
})();
|
||
|
||
// ===================== TREE COMPONENT TOGGLE =====================
|
||
(function initTrees() {
|
||
// Delegate click on [data-tree-toggle] rows (folder rows)
|
||
document.addEventListener('click', (e) => {
|
||
const toggleBtn = e.target.closest('.tree-toggle');
|
||
if (toggleBtn) {
|
||
const row = toggleBtn.closest('[data-tree-toggle]');
|
||
if (!row) return;
|
||
e.stopPropagation();
|
||
const node = row.parentElement;
|
||
toggleTreeNode(node);
|
||
return;
|
||
}
|
||
// Click anywhere on folder row also toggles (except the count which is passive)
|
||
const folderRow = e.target.closest('[data-tree-toggle]');
|
||
if (folderRow && !e.target.closest('.tree-count')) {
|
||
const node = folderRow.parentElement;
|
||
toggleTreeNode(node);
|
||
}
|
||
});
|
||
// Leaf selection (single-select within siblings)
|
||
document.addEventListener('click', (e) => {
|
||
const leaf = e.target.closest('[data-tree-select]');
|
||
if (!leaf) return;
|
||
e.stopPropagation();
|
||
// Remove .active from siblings within same parent tree
|
||
const tree = leaf.closest('.tree');
|
||
if (tree) {
|
||
tree.querySelectorAll('[data-tree-select].active').forEach(el => el.classList.remove('active'));
|
||
}
|
||
leaf.classList.add('active');
|
||
});
|
||
|
||
function toggleTreeNode(node) {
|
||
if (!node || !node.classList.contains('tree-node')) return;
|
||
const expanded = node.getAttribute('aria-expanded') === 'true';
|
||
node.setAttribute('aria-expanded', expanded ? 'false' : 'true');
|
||
const children = node.querySelector(':scope > .tree-children');
|
||
if (children) {
|
||
if (expanded) { children.setAttribute('hidden', ''); }
|
||
else { children.removeAttribute('hidden'); }
|
||
}
|
||
}
|
||
|
||
// Drag-and-drop for library leafs (visual only - mockup)
|
||
document.addEventListener('dragstart', (e) => {
|
||
const draggable = e.target.closest('.lib-draggable');
|
||
if (!draggable) return;
|
||
draggable.classList.add('dragging');
|
||
const name = draggable.querySelector('.tree-name')?.textContent || 'Block';
|
||
e.dataTransfer.setData('text/plain', name);
|
||
e.dataTransfer.effectAllowed = 'copy';
|
||
});
|
||
document.addEventListener('dragend', (e) => {
|
||
const draggable = e.target.closest('.lib-draggable');
|
||
if (draggable) draggable.classList.remove('dragging');
|
||
});
|
||
})();
|