import { Component, NewState, Util, RefreshState } from '@web/state' import { VirtualScroll } from '@web/base' Component.register('DataTable', container => { const vs = VirtualScroll() const state = container.state Object.assign(state, { list: [], fields: [], renderedList: [], prevHeight: 0, postHeight: 0, _listStartIndex: 0, selStartR: -1, selStartF: -1, selEndR: -1, selEndF: -1, multiSelections: [], isSelecting: false }) container.refresh = () => { const scrollEl = container.querySelector('.dt-body') if (!scrollEl) return const res = vs.calc(scrollEl, state.list) if (res) Object.assign(state, { prevHeight: res.prevHeight, postHeight: res.postHeight, _listStartIndex: res.listStartIndex, renderedList: res.renderedList }) } state.__watch('list', list => { if (list && list.length > 0 && !list[0].__watch) { state.list = list.map(item => item.__watch ? item : NewState(item)) return } state._listStartIndex = 0 const scrollEl = container.querySelector('.dt-body') state.renderedList = vs.reset(list, scrollEl || { clientHeight: 800 }) || [] if (scrollEl) { vs.init(list, container.refresh) requestAnimationFrame(() => container.refresh()) } }) container.onItemUpdate = (rIdx, node) => vs.update(rIdx + state._listStartIndex, node) container.getOffset = (fields, index, side) => { const f = fields.slice(0, index).filter(f => f.pinned === 'left') const r = fields.slice(index + 1).filter(f => f.pinned === 'right') return (side === 'left' ? f : r).reduce((sum, f) => sum + (f.width || 150), 0) } container.isCellSelected = (r, f) => { const rMin = Math.min(state.selStartR, state.selEndR), rMax = Math.max(state.selStartR, state.selEndR) const fMin = Math.min(state.selStartF, state.selEndF), fMax = Math.max(state.selStartF, state.selEndF) if (r >= rMin && r <= rMax && f >= fMin && f <= fMax) return true return state.multiSelections.some(s => r >= s.r1 && r <= s.r2 && f >= s.f1 && f <= s.f2) } container.clearAllActive = (keepSelection = false) => { state.list.forEach(row => { if (row._editingF !== null) row._editingF = null if (row._activeF !== null) row._activeF = null }) if (!keepSelection) { state.selStartR = -1; state.multiSelections = [] } } container.startSelect = (r, f, e) => { const alreadySelected = container.isCellSelected(r, f) if (state.editingCell) state.editingCell = null if (e.shiftKey && state.selStartR !== -1) { state.selEndR = r; state.selEndF = f } else { if (!alreadySelected) { if (!e.ctrlKey && !e.metaKey) container.clearAllActive() else if (state.selStartR !== -1) { state.multiSelections.push({ r1: Math.min(state.selStartR, state.selEndR), r2: Math.max(state.selStartR, state.selEndR), f1: Math.min(state.selStartF, state.selEndF), f2: Math.max(state.selStartF, state.selEndF) }) } state.selStartR = state.selEndR = r state.selStartF = state.selEndF = f } state.isSelecting = true state.list[r]._activeF = f } } container.updateSelect = (r, f) => state.isSelecting && (state.selEndR = r, state.selEndF = f) container.endSelect = () => state.isSelecting = false container.editCell = (row, f, fIdx) => { const rMin = Math.min(state.selStartR, state.selEndR), rMax = Math.max(state.selStartR, state.selEndR) const fMin = Math.min(state.selStartF, state.selEndF), fMax = Math.max(state.selStartF, state.selEndF) const rIdx = state.list.indexOf(row) const isInRange = state.selStartR !== -1 && rIdx >= rMin && rIdx <= rMax && fIdx >= fMin && fIdx <= fMax const count = isInRange ? (rMax - rMin + 1) : 0 if (count > 1 && globalThis.UI?.toast) { UI.toast(`{#Bulk Editing {num} rows... || ${count}#}`) } container.clearAllActive(true) row._editingF = f.id row._activeF = fIdx if (count > 1) { const unwatch = row.__watch(f.id, (val) => { for (let r = rMin; r <= rMax; r++) { if (state.list[r] !== row) state.list[r][f.id] = val } unwatch() }) } } const escapeTSV = val => { const str = String(val ?? '') return (str.includes('\t') || str.includes('\n') || str.includes('"')) ? '"' + str.replace(/"/g, '""') + '"' : str } const parseTSV = text => { const rows = [] let curRow = [], curCell = '', inQuote = false for (let i = 0; i < text.length; i++) { const c = text[i], next = text[i+1] if (inQuote) { if (c === '"' && next === '"') { curCell += '"'; i++ } else if (c === '"') inQuote = false else curCell += c } else { if (c === '"') inQuote = true else if (c === '\t') { curRow.push(curCell); curCell = '' } else if (c === '\n') { curRow.push(curCell); rows.push(curRow); curRow = []; curCell = '' } else if (c !== '\r') curCell += c } } curRow.push(curCell); rows.push(curRow) return rows } container.copy = async () => { const rMin = Math.min(state.selStartR, state.selEndR), rMax = Math.max(state.selStartR, state.selEndR) const fMin = Math.min(state.selStartF, state.selEndF), fMax = Math.max(state.selStartF, state.selEndF) if (rMin === -1) return const text = state.list.slice(rMin, rMax + 1).map(row => state.fields.slice(fMin, fMax + 1).map(f => escapeTSV(row[f.id])).join('\t') ).join('\n') await navigator.clipboard.writeText(text) } container.paste = async () => { const text = await navigator.clipboard.readText() const rows = parseTSV(text) const rStart = Math.min(state.selStartR, state.selEndR) const fStart = Math.min(state.selStartF, state.selEndF) if (rStart === -1) return rows.forEach((rowData, i) => { const row = state.list[rStart + i] if (row) rowData.forEach((val, j) => { const field = state.fields[fStart + j] if (field) { if (typeof row[field.id] === 'boolean') row[field.id] = val.toLowerCase() === 'true' else if (typeof row[field.id] === 'number') row[field.id] = Number(val) else row[field.id] = val } }) }) } container.addEventListener('keydown', e => { if (e.ctrlKey || e.metaKey) { if (e.key === 'c') { e.preventDefault(); container.copy() } if (e.key === 'v') { e.preventDefault(); container.paste() } } }) const onGlobalMouseDown = e => !container.contains(e.target) && container.clearAllActive() document.addEventListener('mousedown', onGlobalMouseDown) window.addEventListener('mouseup', container.endSelect) container._onUnload = () => { document.removeEventListener('mousedown', onGlobalMouseDown) window.removeEventListener('mouseup', container.endSelect) } }, Util.makeDom(/*html*/`