import { Component, NewState, Util, RefreshState } from '@web/state' import { VirtualScroll } from '@web/base' Component.register('DataTable', container => { const vs = VirtualScroll() // 如果 state 还未由框架自动创建,则手动创建 if (!container.state) container.state = NewState({}) 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 }) const refresh = () => { const scrollEl = container.querySelector('.dt-body') if (!scrollEl) return const res = vs.calc(scrollEl, state.list) if (res) { res.renderedList.forEach((item, i) => { if (item && !item.__watch) { const wrapped = NewState(item) res.renderedList[i] = wrapped state.list[res.listStartIndex + i] = wrapped } }) Object.assign(state, { prevHeight: res.prevHeight, postHeight: res.postHeight, _listStartIndex: res.listStartIndex, renderedList: res.renderedList }) } } container.refresh = refresh state.__watch('fields', fields => { if (!fields) return const leftOffsets = [], rightOffsets = [] let lSum = 0 fields.forEach((f, i) => { if (f.pinned === 'left') { leftOffsets[i] = lSum lSum += (f.width || 150) } }) fields.forEach((f, i) => { if (f.pinned === 'right') { let rs = 0 for (let j = i + 1; j < fields.length; j++) { if (fields[j].pinned === 'right') rs += (fields[j].width || 150) } rightOffsets[i] = rs } }) state._leftOffsets = leftOffsets state._rightOffsets = rightOffsets }) state.__watch('list', list => { state._listStartIndex = 0 const scrollEl = container.querySelector('.dt-body') state.renderedList = vs.reset(list, scrollEl || { clientHeight: 800 }) || [] if (scrollEl) { vs.init(list, refresh) requestAnimationFrame(refresh) } }) container.onItemUpdate = (rIdx, node) => vs.update(rIdx + state._listStartIndex, node) container.getOffset = (index, side) => (state._leftOffsets || [])[index] || (state._rightOffsets || [])[index] || 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 && row.__watch) { 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 if (state.list[r] && state.list[r].__watch) 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++) { const cur = state.list[r] if (cur !== row) { const wrapped = cur.__watch ? cur : NewState(cur) state.list[r] = wrapped wrapped[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) => { let row = state.list[rStart + i] if (row) { if (!row.__watch) { row = NewState(row) state.list[rStart + i] = 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*/`