import { state } from '../state.js'; import { logger } from '../utils/logger.js'; import { getNodeTemplate, getEdgeTemplate, getMultiSelectTemplate } from './templates.js'; import { getRichCardTemplate } from './templates/rich_card.js'; import { handleNodeUpdate } from './handlers/node.js'; import { handleEdgeUpdate } from './handlers/edge.js'; import { handleRichCardUpdate } from './handlers/rich_card.js'; // --- Utility: Simple Debounce to prevent excessive updates --- function debounce(func, wait) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func(...args), wait); }; } let sidebarElement = null; export function initPropertiesSidebar() { sidebarElement = document.getElementById('properties-sidebar'); if (!sidebarElement) return; const closeBtn = sidebarElement.querySelector('.close-sidebar'); const footerBtn = sidebarElement.querySelector('#sidebar-apply'); const content = sidebarElement.querySelector('.sidebar-content'); if (closeBtn) { closeBtn.addEventListener('click', () => { unhighlight(); toggleSidebar(false); }); } if (footerBtn) { footerBtn.addEventListener('click', () => { unhighlight(); applyChangesGlobal(); toggleSidebar(false); }); } // Event Delegation: Register once at initialization to prevent memory leaks and redundant listeners if (content) { content.addEventListener('input', (e) => { const input = e.target; if (!input.classList.contains('prop-input')) return; }); content.addEventListener('change', (e) => { const input = e.target; if (!input.classList.contains('prop-input')) return; const isText = (input.type === 'text' || input.tagName === 'TEXTAREA' || input.type === 'number'); if (!isText) { applyChangesGlobal(); } }); content.addEventListener('keydown', (e) => { if (e.key === 'Enter' && e.target.tagName !== 'TEXTAREA') { applyChangesGlobal(); toggleSidebar(false); } }); } if (state.graph) { state.graph.on('selection:changed', ({ selected }) => { unhighlight(); renderProperties(); if (selected && selected.length > 0) toggleSidebar(true); }); const safeRender = () => { const active = document.activeElement; const isEditing = active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA'); if (sidebarElement && sidebarElement.contains(active) && isEditing) { return; } // Use setTimeout to ensure X6 data sync is complete before re-rendering setTimeout(() => renderProperties(), 0); }; state.graph.on('cell:change:data', ({ options }) => { if (options && options.silent) return; safeRender(); }); state.graph.on('cell:change:attrs', ({ options }) => { if (options && options.silent) return; safeRender(); }); } } const applyChangesGlobal = async () => { if (!state.graph) return; const selected = state.graph.getSelectedCells(); if (selected.length !== 1) return; const cell = selected[0]; const data = cell.getData() || {}; if (cell.shape === 'drawnet-rich-card') { await handleRichCardUpdate(cell, data); } else if (cell.isNode()) { await handleNodeUpdate(cell, data); } else { await handleEdgeUpdate(cell, data); } import('/static/js/modules/persistence.js').then(m => m.markDirty()); }; const debouncedApplyGlobal = debounce(applyChangesGlobal, 300); export function toggleSidebar(open) { if (!sidebarElement) return; if (open === undefined) { sidebarElement.classList.toggle('open'); } else if (open) { sidebarElement.classList.add('open'); } else { sidebarElement.classList.remove('open'); } } export function renderProperties() { if (!state.graph || !sidebarElement) return; const selected = state.graph.getSelectedCells(); const content = sidebarElement.querySelector('.sidebar-content'); if (!content) return; logger.info(`[Sidebar] renderProperties start`, { selectedCount: selected.length, firstId: selected[0]?.id }); if (!selected || selected.length === 0) { content.innerHTML = `