diff --git a/src/index.js b/src/index.js index c58f890..91d4bc1 100644 --- a/src/index.js +++ b/src/index.js @@ -1,69 +1,43 @@ import { Component, NewState, Util, RefreshState } from '@web/state' -import { VirtualScroll } from '@web/base' +import { createPerfMonitor } from './perf.js' +import { createScrollManager } from './scroll.js' +import { createSelectionManager } from './selection.js' Component.register('DataTable', container => { - const vs = VirtualScroll() if (!container.state) container.state = NewState({}) const state = container.state Object.assign(state, { list: [], fields: [], _renderedList: [], prevHeight: 0, postHeight: 0, _listStartIndex: 0, - perf: { refreshTime: 0, refreshCount: 0, scrollCount: 0, totalNodes: 0 } + selectedRowCount: 0 }) - /* PERFORMANCE_TELEMETRY_START - THIS BLOCK IS FOR TESTING AND SHOULD BE REMOVED IN PRODUCTION */ - if (!window.__statePerformanceTelemetry) { - window.__statePerformanceTelemetry = { scanCount: 0, reuseCount: 0, moveCount: 0 }; - } - /* PERFORMANCE_TELEMETRY_END */ + const perf = createPerfMonitor(); + state.perf = perf.stats; + + const selection = createSelectionManager(container, state); + + const scroll = createScrollManager(container, state, (renderedCount) => { + selection.applySelectionUI(); + }); container.refresh = () => { - const start = performance.now() - // Optimization: Expand the virtual viewport to 1.6x height to create a buffer - // that prevents "white flashes" during extremely fast scrolling. - const virtualContainer = { - clientHeight: container.clientHeight * 1.6, - scrollTop: container.scrollTop - }; - const res = vs.calc(virtualContainer, state.list) - if (res) { - /* PERFORMANCE_TELEMETRY_START */ - const frameStartScan = window.__statePerformanceTelemetry.scanCount; - const frameStartMove = window.__statePerformanceTelemetry.moveCount; - const frameStartReuse = window.__statePerformanceTelemetry.reuseCount; - /* PERFORMANCE_TELEMETRY_END */ - - state.perf.refreshCount++ - Object.assign(state, { - prevHeight: res.prevHeight, postHeight: res.postHeight, - _listStartIndex: res.listStartIndex, _renderedList: res.renderedList - }) - - const elapsed = performance.now() - start; - state.perf.refreshTime += elapsed; - state.perf.totalNodes += res.renderedList.length; + const frameStart = perf.startFrame(); + scroll.refresh(); + perf.endFrame(frameStart, state._renderedList.length); + }; - /* PERFORMANCE_TELEMETRY_START */ - const frameScans = window.__statePerformanceTelemetry.scanCount - frameStartScan; - const frameMoves = window.__statePerformanceTelemetry.moveCount - frameStartMove; - const frameReuses = window.__statePerformanceTelemetry.reuseCount - frameStartReuse; - - if (frameScans > 0 || elapsed > 2) { - console.log(`[DataTable Frame] Time: ${elapsed.toFixed(2)}ms, Scans: ${frameScans}, Moves: ${frameMoves}, Reuses: ${frameReuses}, Rows: ${res.renderedList.length}`); - } - /* PERFORMANCE_TELEMETRY_END */ - } - } + container.onScroll = () => { + perf.onScroll(); + container.refresh(); + }; - container.onItemUpdate = (index, node) => { - // vs.update(index + (state._listStartIndex || 0), node) - } + container.onItemUpdate = (index, node) => scroll.updateRowHeight(index, node); state.__watch('list', list => { - state._listStartIndex = 0 - state._renderedList = vs.reset(list, container) || [] - setTimeout(() => { if (state.list === list) vs.init(list, container.refresh) }) + scroll.init(); + scroll.reset(list); }) state.__watch('fields', fields => { @@ -72,44 +46,107 @@ Component.register('DataTable', container => { container.style.setProperty('--dt-grid-template', gridTemplate) }) - container.onScroll = () => { - state.perf.scrollCount++ - container.refresh() + // Expose selection methods to template + container.startSelect = selection.startSelect; + container.updateSelect = selection.updateSelect; + container.deleteSelected = selection.deleteSelected; + + // Copy & Paste (simplified) + const escapeTSV = val => { + const str = String(val ?? '') + return (str.includes('\t') || str.includes('\n') || str.includes('"')) ? '"' + str.replace(/"/g, '""') + '"' : str + } + + container.copy = async () => { + const bounds = selection.getSelectionBounds(); + if (!bounds) return; + const text = state.list.slice(bounds.minRow, bounds.maxRow + 1).map(row => + state.fields.slice(bounds.minCol, bounds.maxCol + 1).map(f => escapeTSV(row[f.id])).join('\t') + ).join('\n') + await navigator.clipboard.writeText(text) + } + + container.addEventListener('keydown', e => { + if (e.ctrlKey || e.metaKey) { + if (e.key === 'c') { e.preventDefault(); container.copy() } + } + }) + + const onGlobalMouseUp = () => selection.endSelect() + const onGlobalMouseDown = e => { + if (!container.contains(e.target)) selection.clearAllActive() + } + + window.addEventListener('mouseup', onGlobalMouseUp) + document.addEventListener('mousedown', onGlobalMouseDown) + + container._onUnload = () => { + document.removeEventListener('mousedown', onGlobalMouseDown) + window.removeEventListener('mouseup', onGlobalMouseUp) } }, Util.makeDom(/*html*/` -