/** * Properties Panel - Visual editor for nodes, focus options, and graph settings * Similar to Photoshop's properties panel */ export class PropertiesPanel { /** @type {HTMLElement} */ #panel; /** @type {(() => void) | null} */ #onChange = null; /** @type {any} */ #currentNode = null; /** @type {any} */ #graphData = null; /** * @param {HTMLElement} panel * @param {(() => void) | null} [onChange] */ constructor(panel, onChange = null) { this.#panel = panel; this.#onChange = onChange; this.#bindEvents(); } #bindEvents() { // Toggle button (close) const toggleBtn = this.#panel.querySelector('.properties-toggle-btn'); toggleBtn?.addEventListener('click', () => { this.#panel.classList.toggle('collapsed'); }); // Tab button (open when collapsed) const tabBtn = this.#panel.querySelector('.properties-panel-tab'); tabBtn?.addEventListener('click', () => { this.#panel.classList.remove('collapsed'); }); // Collapsible sections this.#panel.querySelectorAll('.section-header.collapsible').forEach(header => { header.addEventListener('click', () => { header.classList.toggle('collapsed'); }); }); // Node property inputs document.getElementById('propNodeTitle')?.addEventListener('input', () => this.#updateNodeProperty('title')); document.getElementById('propNodeX')?.addEventListener('input', () => this.#updateNodeProperty('x')); document.getElementById('propNodeY')?.addEventListener('input', () => this.#updateNodeProperty('y')); document.getElementById('propNodeWidth')?.addEventListener('input', () => this.#updateNodeProperty('width')); // Focus options inputs document.getElementById('propFocusDuration')?.addEventListener('input', () => this.#updateFocusOptions()); document.getElementById('propFocusAnchorX')?.addEventListener('input', () => this.#updateFocusOptions()); document.getElementById('propFocusAnchorY')?.addEventListener('input', () => this.#updateFocusOptions()); document.getElementById('propWorldBoxWidth')?.addEventListener('input', () => this.#updateFocusOptions()); document.getElementById('propWorldBoxHeight')?.addEventListener('input', () => this.#updateFocusOptions()); // Add breakpoint button document.getElementById('addBreakpointBtn')?.addEventListener('click', () => this.#addBreakpoint()); // Apply settings button document.getElementById('applySettingsBtn')?.addEventListener('click', () => this.#applyGraphSettings()); // Graph settings document.getElementById('propGraphDraggable')?.addEventListener('change', () => { if (this.#graphData?.settings) { this.#graphData.settings.draggable = document.getElementById('propGraphDraggable').checked; this.#onChange?.(); } }); } /** * Load a node into the properties panel * * @param {any} node */ loadNode(node) { this.#currentNode = node; if (!node) { document.getElementById('nodePropertiesSection').style.display = 'none'; document.getElementById('focusOptionsSection').style.display = 'none'; document.getElementById('noSelectionMessage').style.display = 'block'; return; } document.getElementById('nodePropertiesSection').style.display = 'block'; document.getElementById('focusOptionsSection').style.display = 'block'; document.getElementById('noSelectionMessage').style.display = 'none'; // Populate node properties const idInput = document.getElementById('propNodeId'); const typeInput = document.getElementById('propNodeType'); const titleInput = document.getElementById('propNodeTitle'); const xInput = document.getElementById('propNodeX'); const yInput = document.getElementById('propNodeY'); const widthInput = document.getElementById('propNodeWidth'); if (idInput) idInput.value = node.id || ''; if (typeInput) typeInput.value = node.type || ''; if (titleInput) titleInput.value = node.title || ''; if (xInput) xInput.value = node.position?.x ?? 0; if (yInput) yInput.value = node.position?.y ?? 0; if (widthInput) widthInput.value = node.width || ''; // Populate focus options const focusOpts = node.focusOptions || {}; const durationInput = document.getElementById('propFocusDuration'); const anchorXInput = document.getElementById('propFocusAnchorX'); const anchorYInput = document.getElementById('propFocusAnchorY'); const boxWidthInput = document.getElementById('propWorldBoxWidth'); const boxHeightInput = document.getElementById('propWorldBoxHeight'); if (durationInput) durationInput.value = focusOpts.durationMs || ''; if (anchorXInput) anchorXInput.value = focusOpts.anchorX ?? ''; if (anchorYInput) anchorYInput.value = focusOpts.anchorY ?? ''; if (boxWidthInput) boxWidthInput.value = focusOpts.minWorldBox?.width || ''; if (boxHeightInput) boxHeightInput.value = focusOpts.minWorldBox?.height || ''; // Render breakpoints this.#renderBreakpoints(); } /** * Load graph settings * * @param {any} graphData */ loadGraphSettings(graphData) { this.#graphData = graphData; const draggableCheck = document.getElementById('propGraphDraggable'); const introDurationInput = document.getElementById('propIntroDuration'); const viewXInput = document.getElementById('propInitViewX'); const viewYInput = document.getElementById('propInitViewY'); const viewWidthInput = document.getElementById('propInitViewWidth'); const viewHeightInput = document.getElementById('propInitViewHeight'); const settings = graphData?.settings || {}; const viewbox = settings.initialViewbox || {}; if (draggableCheck) draggableCheck.checked = settings.draggable ?? true; if (introDurationInput) introDurationInput.value = settings.introDurationMs || ''; if (viewXInput) viewXInput.value = viewbox.x || ''; if (viewYInput) viewYInput.value = viewbox.y || ''; if (viewWidthInput) viewWidthInput.value = viewbox.width || ''; if (viewHeightInput) viewHeightInput.value = viewbox.height || ''; } #updateNodeProperty(prop) { if (!this.#currentNode) return; const titleInput = document.getElementById('propNodeTitle'); const xInput = document.getElementById('propNodeX'); const yInput = document.getElementById('propNodeY'); const widthInput = document.getElementById('propNodeWidth'); if (prop === 'title' && titleInput) { this.#currentNode.title = titleInput.value; } else if (prop === 'x' && xInput) { this.#currentNode.position.x = parseFloat(xInput.value) || 0; } else if (prop === 'y' && yInput) { this.#currentNode.position.y = parseFloat(yInput.value) || 0; } else if (prop === 'width' && widthInput) { this.#currentNode.width = widthInput.value ? parseFloat(widthInput.value) : undefined; } this.#onChange?.(); } #updateFocusOptions() { if (!this.#currentNode) return; if (!this.#currentNode.focusOptions) { this.#currentNode.focusOptions = {}; } const durationInput = document.getElementById('propFocusDuration'); const anchorXInput = document.getElementById('propFocusAnchorX'); const anchorYInput = document.getElementById('propFocusAnchorY'); const boxWidthInput = document.getElementById('propWorldBoxWidth'); const boxHeightInput = document.getElementById('propWorldBoxHeight'); if (durationInput?.value) { this.#currentNode.focusOptions.durationMs = parseFloat(durationInput.value); } if (anchorXInput?.value) { this.#currentNode.focusOptions.anchorX = parseFloat(anchorXInput.value); } if (anchorYInput?.value) { this.#currentNode.focusOptions.anchorY = parseFloat(anchorYInput.value); } if (boxWidthInput?.value || boxHeightInput?.value) { if (!this.#currentNode.focusOptions.minWorldBox) { this.#currentNode.focusOptions.minWorldBox = {}; } if (boxWidthInput?.value) { this.#currentNode.focusOptions.minWorldBox.width = parseFloat(boxWidthInput.value); } if (boxHeightInput?.value) { this.#currentNode.focusOptions.minWorldBox.height = parseFloat(boxHeightInput.value); } } this.#onChange?.(); } #renderBreakpoints() { const container = document.getElementById('breakpointsList'); if (!container) return; container.innerHTML = ''; const responsiveBoxes = this.#currentNode?.focusOptions?.responsiveWorldBox; if (!responsiveBoxes) return; const breakpoints = Array.isArray(responsiveBoxes) ? responsiveBoxes : [responsiveBoxes]; breakpoints.forEach((bp, index) => { const item = document.createElement('div'); item.className = 'breakpoint-item'; item.innerHTML = `