import { Component, NewState, Util, RefreshState } from '@web/state' import { State } from '@web/base' import { createPerfMonitor } from './perf.js' import { createScrollManager } from './scroll.js' import { createSelectionManager } from './selection.js' Component.register('DataTable', container => { if (!container.state) container.state = NewState({}) const state = container.state Object.assign(state, { list: [], fields: [], _renderedList: [], prevHeight: 0, postHeight: 0, _listStartIndex: 0, selectedRowCount: 0 }) const perf = createPerfMonitor(); state.perf = perf.stats; const selection = createSelectionManager(container, state); const scroll = createScrollManager(container, state, (renderedCount) => { // Only hide editor if we actually scroll past its visibility range // For now, simple hide for safety container.hideEditor(); selection.applySelectionUI(); }); let _prevSpacer, _postSpacer, _editorOverlay; container.format = (val, field) => { if (field.formatter) return field.formatter(val, field); if (typeof val === 'string') return val; if (val === null || val === undefined) return ''; return typeof val === 'object' ? JSON.stringify(val) : String(val); }; container.refreshNode = (node) => RefreshState(node); container.refresh = () => { const frameStart = perf.startFrame(); scroll.refresh(); // Manual DOM Sync for Spacers (Authorized optimization to avoid $if/$style overhead) if (!_prevSpacer) _prevSpacer = container.querySelector('.dt-spacer-prev'); if (_prevSpacer) { const h = state.prevHeight || 0; _prevSpacer.style.height = h + 'px'; _prevSpacer.style.display = h > 0 ? 'block' : 'none'; } if (!_postSpacer) _postSpacer = container.querySelector('.dt-spacer-post'); if (_postSpacer) { const h = state.postHeight || 0; _postSpacer.style.height = h + 'px'; _postSpacer.style.display = h > 0 ? 'block' : 'none'; } perf.endFrame(frameStart, state._renderedList.length); }; container.onScroll = () => { perf.onScroll(); container.refresh(); }; // --- Optimized Event Delegation & DOM Interaction --- container.onMainMouseDown = e => { const cell = e.target.closest('.dt-cell'); if (!cell) return; const row = cell.closest('.dt-row'); if (!row || row.classList.contains('dt-header-row')) return; const fIdx = cell._ref?.fIdx ?? Array.from(row.children).indexOf(cell); const rIdx = row._ref?.rIdx ?? Array.from(container.querySelectorAll('.dt-body-row')).indexOf(row); const absoluteRow = rIdx + state._listStartIndex; container.startSelect(absoluteRow, fIdx, e); }; container.onMainMouseOver = e => { if (!state.isSelecting) return; const cell = e.target.closest('.dt-cell'); if (!cell) return; const row = cell.closest('.dt-row'); if (!row || row.classList.contains('dt-header-row')) return; const fIdx = cell._ref?.fIdx ?? Array.from(row.children).indexOf(cell); const rIdx = row._ref?.rIdx ?? Array.from(container.querySelectorAll('.dt-body-row')).indexOf(row); const absoluteRow = rIdx + state._listStartIndex; container.updateSelect(absoluteRow, fIdx); }; container.onMainDblClick = e => { const cell = e.target.closest('.dt-cell'); if (!cell) return; const row = cell.closest('.dt-row'); if (!row || row.classList.contains('dt-header-row')) return; const item = row._ref?.item; const field = cell._ref?.f; if (item && field) container.editCell(item, field, cell); }; state.__watch('list', list => { scroll.init(); scroll.reset(list); }) state.__watch('fields', fields => { if (!fields) return const gridTemplate = fields.map(f => `var(--w-${f.id}, ${f.width || 150}px)`).join(' ') container.style.setProperty('--dt-grid-template', gridTemplate) }) // --- Editing Logic (Manual Control, Permanent DOM) --- let currentEditingNode = null; container.editCell = (row, field, cellNode) => { const overlay = container.querySelector('.dt-editor-overlay'); const editor = overlay.querySelector('AutoForm'); const rect = cellNode.getBoundingClientRect(); const rootRect = container.getBoundingClientRect(); currentEditingNode = cellNode; // 1. Precise Positioning with expansion and Z-index let minW = rect.width; if (field.type === 'textarea' || field.type === 'TagsInput') minW = Math.max(rect.width, 300); else if (field.type === 'radio') minW = Math.max(rect.width, 240); overlay.style.display = 'flex'; overlay.style.left = (rect.left - rootRect.left) + 'px'; overlay.style.top = (rect.top - rootRect.top) + 'px'; overlay.style.width = minW + 'px'; // Reset styles to prevent crosstalk from previous editor types overlay.style.height = ''; overlay.style.minHeight = ''; overlay.style.alignItems = 'center'; if (field.type === 'textarea' || field.type === 'TagsInput') { overlay.style.minHeight = (field.type === 'textarea' ? 150 : rect.height) + 'px'; overlay.style.height = 'auto'; overlay.style.alignItems = 'flex-start'; } else { overlay.style.height = (rect.height) + 'px'; } // 2. Direct Driver: Switch context // Use global State for both schema and data to ensure atomic updates State.editingSchema = [{ ...field, name: field.id, label: '' }]; State.editingData = row; RefreshState(overlay); // 3. Focus setTimeout(() => { const input = editor.querySelector('.form-control, .form-select, .form-check-input, input'); if (input) input.focus(); }, 30); }; container.hideEditor = (save = true) => { if (!_editorOverlay) _editorOverlay = container.querySelector('.dt-editor-overlay'); if (!_editorOverlay || _editorOverlay.style.display === 'none') return; _editorOverlay.style.display = 'none'; if (save && currentEditingNode) { RefreshState(currentEditingNode); } State.editingSchema = null; State.editingData = null; currentEditingNode = null; container.focus(); }; // --- Shared Logic --- container.startSelect = selection.startSelect; container.updateSelect = selection.updateSelect; container.deleteSelected = selection.deleteSelected; container.addEventListener('keydown', e => { if (e.ctrlKey || e.metaKey) { const k = e.key.toLowerCase(); if (k === 'c') { e.preventDefault(); selection.copy(); } if (k === 'v') { e.preventDefault(); selection.paste(); } } if (e.key === 'Escape') container.hideEditor(false); }); const onGlobalMouseDown = e => { const overlay = container.querySelector('.dt-editor-overlay'); if (overlay && overlay.style.display !== 'none' && !overlay.contains(e.target)) { container.hideEditor(true); } if (!container.contains(e.target) && !overlay?.contains(e.target)) selection.clearAllActive(); } window.addEventListener('mouseup', selection.endSelect); document.addEventListener('mousedown', onGlobalMouseDown); container._onUnload = () => { document.removeEventListener('mousedown', onGlobalMouseDown) window.removeEventListener('mouseup', selection.endSelect) } }, Util.makeDom(/*html*/`
`), Util.makeDom(/*html*/` `)) if (typeof document !== 'undefined') RefreshState(document.documentElement)