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, _originalList: [], sortConfig: { fieldId: null, direction: null }, filterConfig: {}, activeFieldId: null, _internalUpdate: false, _appliedHash: '', _fieldsDirty: false, _masterCellNodes: null }) const perf = createPerfMonitor(); state.perf = perf.stats; const selection = createSelectionManager(container, state); const scroll = createScrollManager(container, state, (renderedCount, isLayoutChange) => { 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(); 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); }; let _scrollRaf = null; container.onScroll = () => { perf.onScroll(); container.refresh(); if (_scrollRaf) return; _scrollRaf = requestAnimationFrame(() => { _scrollRaf = null; const menu = container.querySelector('.dt-column-menu'); if (menu && menu.style.display !== 'none' && state.activeFieldId) { const headerCell = container.querySelector(`.header-cell[data-id="${state.activeFieldId}"]`); if (headerCell) { const btn = headerCell.querySelector('.header-menu-btn'); const btnRect = btn.getBoundingClientRect(); const rootRect = container.getBoundingClientRect(); menu.style.left = (btnRect.right - rootRect.left - 180) + 'px'; menu.style.top = (btnRect.bottom - rootRect.top + 5) + 'px'; } } }); }; container.applySortFilter = (options = {}) => { if (state._internalUpdate) return; const targetFilters = { ...state.filterConfig, ...(options.filters || {}) }; const targetSort = options.sort !== undefined ? (options.sort ? { fieldId: state.activeFieldId, direction: options.sort } : { fieldId: null, direction: null }) : state.sortConfig; const currentHash = JSON.stringify({ s: targetSort, f: targetFilters }); if (state._appliedHash === currentHash && !options.force) return; state._internalUpdate = true; const startTime = performance.now(); let list = (options.force) ? state.list : [...(state._originalList || [])]; if (!options.force) { Object.keys(targetFilters).forEach(fieldId => { const val = targetFilters[fieldId]; if (val) { const lowerVal = String(val).toLowerCase(); list = list.filter(item => String(item[fieldId] ?? '').toLowerCase().includes(lowerVal)); } }); if (targetSort && targetSort.fieldId && targetSort.direction) { list.sort((a, b) => { let va = a[targetSort.fieldId], vb = b[targetSort.fieldId]; if (va === vb) return 0; if (va === null || va === undefined) return 1; if (vb === null || vb === undefined) return -1; const res = va > vb ? 1 : -1; return targetSort.direction === 'asc' ? res : -res; }); } } window.__perfTrace = { evalCount: 0, evalTotal: 0 }; const frameStart = performance.now(); state._appliedHash = currentHash; if (options.sort !== undefined) state.sortConfig = targetSort; state.list = list; state._internalUpdate = false; const frameEnd = performance.now(); const totalTime = frameEnd - startTime; console.log(`[DataTable Performance Profile] Sync Block: ${totalTime.toFixed(2)}ms (Eval: ${window.__perfTrace.evalCount})`); requestAnimationFrame(() => { setTimeout(() => { console.log(`[DataTable Performance Profile] E2E Paint: ${(performance.now() - startTime).toFixed(2)}ms`); }); }); window.__perfTrace = null; }; container.showColumnMenu = (field, e) => { e.stopPropagation(); const btn = e.currentTarget; const rect = btn.getBoundingClientRect(); const rootRect = container.getBoundingClientRect(); const menu = container.querySelector('.dt-column-menu'); state.activeFieldId = field.id; menu.style.display = 'block'; menu.style.left = (rect.right - rootRect.left - 180) + 'px'; menu.style.top = (rect.bottom - rootRect.top + 5) + 'px'; const onGlobalClick = (ev) => { if (menu.contains(ev.target)) return; if (!btn.contains(ev.target)) { container.hideColumnMenu(); container.applySortFilter(); document.removeEventListener('mousedown', onGlobalClick); } }; document.addEventListener('mousedown', onGlobalClick); RefreshState(menu); setTimeout(() => { const input = menu.querySelector('input'); if (input) input.focus(); }, 50); }; container.hideColumnMenu = () => { const menu = container.querySelector('.dt-column-menu'); if (menu) menu.style.display = 'none'; }; container.setSort = (direction) => { container.applySortFilter({ sort: direction }); }; container.clearColumnSettings = () => { const filters = { ...state.filterConfig }; delete filters[state.activeFieldId]; state.filterConfig = filters; container.applySortFilter({ sort: null }); container.hideColumnMenu(); }; // --- Optimized Context Initialization (Using _refExt) --- container._initRow = (rowNode) => { if (!rowNode || !rowNode.children) return; // This runs during framework's parsing of the cloned row. // We set _refExt on each cell clone BEFORE _scanTree visits them. const cells = rowNode.children; for (let i = 0; i < cells.length; i++) { const cell = cells[i]; const fIdx = parseInt(cell.dataset.fidx); if (!isNaN(fIdx)) { cell._refExt = { f: state.fields[fIdx], fIdx: fIdx }; } } }; 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.dataset.fidx !== undefined ? parseInt(cell.dataset.fidx) : (cell._ref?.fIdx ?? Array.from(row.children).indexOf(cell)); const rIdx = row._ref?.rIdx ?? Array.from(container.querySelectorAll('.dt-body-row')).indexOf(row); container.startSelect(rIdx + state._listStartIndex, 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.dataset.fidx !== undefined ? parseInt(cell.dataset.fidx) : (cell._ref?.fIdx ?? Array.from(row.children).indexOf(cell)); const rIdx = row._ref?.rIdx ?? Array.from(container.querySelectorAll('.dt-body-row')).indexOf(row); container.updateSelect(rIdx + state._listStartIndex, 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 fIdx = cell.dataset.fidx !== undefined ? parseInt(cell.dataset.fidx) : (cell._ref?.fIdx ?? Array.from(row.children).indexOf(cell)); const field = cell._ref?.f ?? state.fields[fIdx]; if (item && field) container.editCell(item, field, cell); }; state.__watch('fields', fields => { if (!fields) return; state._fieldsDirty = true; const gridTemplate = fields.map(f => `var(--w-${f.id}, ${f.width || 150}px)`).join(' '); const totalWidth = fields.reduce((sum, f) => sum + (f.width || 150), 0); container.style.setProperty('--dt-grid-template', gridTemplate); container.style.setProperty('--dt-row-width', totalWidth + 'px'); }) state.__watch('list', list => { if (state._fieldsDirty) { state._fieldsDirty = false; const rowTemplate = container.querySelector('.dt-body template[index="rIdx"]'); if (rowTemplate) { const fieldTemplate = rowTemplate.content.querySelector('template[as="f"]'); if (fieldTemplate) { // 1. Capture the master copy of cell nodes once if (!state._masterCellNodes) { state._masterCellNodes = Array.from(fieldTemplate.content.childNodes).map(n => n.cloneNode(true)); } // 2. Transform the field template into a static flattened block fieldTemplate.removeAttribute('$each'); fieldTemplate.removeAttribute('as'); fieldTemplate.removeAttribute('index'); fieldTemplate.setAttribute('$if', 'true'); // High-speed safe released block fieldTemplate.content.textContent = ''; // Idiomatic way to clear document fragment // 3. Inject flattened columns with position markers state.fields.forEach((f, fIdx) => { state._masterCellNodes.forEach(master => { const clone = master.cloneNode(true); if (clone.nodeType === 1) clone.dataset.fidx = fIdx; fieldTemplate.content.appendChild(clone); }); }); } } } if (!state._internalUpdate) state._originalList = [...(list || [])]; scroll.init(); scroll.reset(list); }) 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; 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'; overlay.style.height = (field.type === 'textarea' || field.type === 'TagsInput' ? 'auto' : rect.height + 'px'); State.editingSchema = [{ ...field, name: field.id, label: '' }]; State.editingData = row; RefreshState(overlay); 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 = State.editingData = currentEditingNode = null; container.focus(); }; container.startSelect = selection.startSelect; container.updateSelect = selection.updateSelect; container.deleteSelected = selection.deleteSelected; 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)