From 9eea16cdaba30fac02adb19fb73b96f3df0c37f2 Mon Sep 17 00:00:00 2001 From: AI Engineer Date: Sun, 17 May 2026 20:11:20 +0800 Subject: [PATCH] test: restore comprehensive DataTable tests and verify performance --- dist/datatable.js | 41 +++++++++---- dist/datatable.min.js | 2 +- src/index.js | 45 ++++++++++---- test/index.html | 135 +++++++++++++----------------------------- 4 files changed, 105 insertions(+), 118 deletions(-) diff --git a/dist/datatable.js b/dist/datatable.js index 4d779a0..0bdff7a 100644 --- a/dist/datatable.js +++ b/dist/datatable.js @@ -1,7 +1,8 @@ -import { Component, NewState, Util } from "@web/state"; +import { Component, NewState, Util, RefreshState } from "@web/state"; import { VirtualScroll } from "@web/base"; Component.register("DataTable", (container) => { const vs = VirtualScroll(); + if (!container.state) container.state = NewState({}); const state = container.state; Object.assign(state, { list: [], @@ -23,7 +24,7 @@ Component.register("DataTable", (container) => { const res = vs.calc(scrollEl, state.list); if (res) { res.renderedList.forEach((item, i) => { - if (!item.__watch) { + if (item && !item.__watch) { const wrapped = NewState(item); res.renderedList[i] = wrapped; state.list[res.listStartIndex + i] = wrapped; @@ -38,21 +39,39 @@ Component.register("DataTable", (container) => { } }; 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()); + requestAnimationFrame(refresh); } }); container.onItemUpdate = (rIdx, node) => vs.update(rIdx + state._listStartIndex, node); - container.getOffset = (fields, index, side) => { - const f = fields.slice(0, index).filter((f2) => f2.pinned === "left"); - const r = fields.slice(index + 1).filter((f2) => f2.pinned === "right"); - return (side === "left" ? f : r).reduce((sum, f2) => sum + (f2.width || 150), 0); - }; + 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); @@ -99,7 +118,6 @@ Component.register("DataTable", (container) => { container.endSelect = () => state.isSelecting = false; container.editCell = (row, f, fIdx) => { var _a; - if (!row.__watch) return; 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); @@ -215,7 +233,7 @@ Component.register("DataTable", (container) => {
+ $style="'width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); min-width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); ' + (f.pinned ? 'position:sticky; z-index:11;' : '') + (f.pinned === 'left' ? 'left:' + this.getOffset(index, 'left') + 'px;' : '') + (f.pinned === 'right' ? 'right:' + this.getOffset(index, 'right') + 'px;' : '')">
@@ -225,7 +243,7 @@ Component.register("DataTable", (container) => {
@@ -261,3 +279,4 @@ Component.register("DataTable", (container) => {
` )); +if (typeof document !== "undefined") RefreshState(document.documentElement); diff --git a/dist/datatable.min.js b/dist/datatable.min.js index 3831b2e..515e9b8 100644 --- a/dist/datatable.min.js +++ b/dist/datatable.min.js @@ -1 +1 @@ -import{Component as t,NewState as e,Util as i}from"@web/state";import{VirtualScroll as n}from"@web/base";t.register("DataTable",t=>{const i=n(),s=t.state;Object.assign(s,{list:[],fields:[],renderedList:[],prevHeight:0,postHeight:0,_listStartIndex:0,selStartR:-1,selStartF:-1,selEndR:-1,selEndF:-1,multiSelections:[],isSelecting:!1});const r=()=>{const n=t.querySelector(".dt-body");if(!n)return;const r=i.calc(n,s.list);r&&(r.renderedList.forEach((t,i)=>{if(!t.__watch){const n=e(t);r.renderedList[i]=n,s.list[r.listStartIndex+i]=n}}),Object.assign(s,{prevHeight:r.prevHeight,postHeight:r.postHeight,_listStartIndex:r.listStartIndex,renderedList:r.renderedList}))};t.refresh=r,s.__watch("list",e=>{s._listStartIndex=0;const n=t.querySelector(".dt-body");s.renderedList=i.reset(e,n||{clientHeight:800})||[],n&&(i.init(e,r),requestAnimationFrame(()=>r()))}),t.onItemUpdate=(t,e)=>i.update(t+s._listStartIndex,e),t.getOffset=(t,e,i)=>{const n=t.slice(0,e).filter(t=>"left"===t.pinned),s=t.slice(e+1).filter(t=>"right"===t.pinned);return("left"===i?n:s).reduce((t,e)=>t+(e.width||150),0)},t.isCellSelected=(t,e)=>{const i=Math.min(s.selStartR,s.selEndR),n=Math.max(s.selStartR,s.selEndR),r=Math.min(s.selStartF,s.selEndF),d=Math.max(s.selStartF,s.selEndF);return t>=i&&t<=n&&e>=r&&e<=d||s.multiSelections.some(i=>t>=i.r1&&t<=i.r2&&e>=i.f1&&e<=i.f2)},t.clearAllActive=(t=!1)=>{s.list.forEach(t=>{t&&t.__watch&&(null!==t._editingF&&(t._editingF=null),null!==t._activeF&&(t._activeF=null))}),t||(s.selStartR=-1,s.multiSelections=[])},t.startSelect=(e,i,n)=>{const r=t.isCellSelected(e,i);s.editingCell&&(s.editingCell=null),n.shiftKey&&-1!==s.selStartR?(s.selEndR=e,s.selEndF=i):(r||(n.ctrlKey||n.metaKey?-1!==s.selStartR&&s.multiSelections.push({r1:Math.min(s.selStartR,s.selEndR),r2:Math.max(s.selStartR,s.selEndR),f1:Math.min(s.selStartF,s.selEndF),f2:Math.max(s.selStartF,s.selEndF)}):t.clearAllActive(),s.selStartR=s.selEndR=e,s.selStartF=s.selEndF=i),s.isSelecting=!0,s.list[e]&&s.list[e].__watch&&(s.list[e]._activeF=i))},t.updateSelect=(t,e)=>s.isSelecting&&(s.selEndR=t,s.selEndF=e),t.endSelect=()=>s.isSelecting=!1,t.editCell=(i,n,r)=>{var d;if(!i.__watch)return;const l=Math.min(s.selStartR,s.selEndR),a=Math.max(s.selStartR,s.selEndR),o=Math.min(s.selStartF,s.selEndF),c=Math.max(s.selStartF,s.selEndF),h=s.list.indexOf(i),f=-1!==s.selStartR&&h>=l&&h<=a&&r>=o&&r<=c?a-l+1:0;if(f>1&&(null==(d=globalThis.UI)?void 0:d.toast)&&UI.toast(`{#Bulk Editing {num} rows... || ${f}#}`),t.clearAllActive(!0),i._editingF=n.id,i._activeF=r,f>1){const t=i.__watch(n.id,r=>{for(let t=l;t<=a;t++){const d=s.list[t];if(d!==i){const i=d.__watch?d:e(d);s.list[t]=i,i[n.id]=r}}t()})}};t.copy=async()=>{const t=Math.min(s.selStartR,s.selEndR),e=Math.max(s.selStartR,s.selEndR),i=Math.min(s.selStartF,s.selEndF),n=Math.max(s.selStartF,s.selEndF);if(-1===t)return;const r=s.list.slice(t,e+1).map(t=>s.fields.slice(i,n+1).map(e=>(t=>{const e=String(t??"");return e.includes("\t")||e.includes("\n")||e.includes('"')?'"'+e.replace(/"/g,'""')+'"':e})(t[e.id])).join("\t")).join("\n");await navigator.clipboard.writeText(r)},t.paste=async()=>{const t=(t=>{const e=[];let i=[],n="",s=!1;for(let r=0;r{let d=s.list[i+r];d&&(d.__watch||(d=e(d),s.list[i+r]=d),t.forEach((t,e)=>{const i=s.fields[n+e];i&&("boolean"==typeof d[i.id]?d[i.id]="true"===t.toLowerCase():"number"==typeof d[i.id]?d[i.id]=Number(t):d[i.id]=t)}))})},t.addEventListener("keydown",e=>{(e.ctrlKey||e.metaKey)&&("c"===e.key&&(e.preventDefault(),t.copy()),"v"===e.key&&(e.preventDefault(),t.paste()))});const d=e=>!t.contains(e.target)&&t.clearAllActive();document.addEventListener("mousedown",d),window.addEventListener("mouseup",t.endSelect),t._onUnload=()=>{document.removeEventListener("mousedown",d),window.removeEventListener("mouseup",t.endSelect)}},i.makeDom("\n
\n\t
\n\t\t
\n\t\t\t\n\t\t\t\n\t\t
\n\t
\n\t
this.refresh())\" style=\"overflow-anchor:none\">\n\t\t
0\" $style=\"'height:' + this.state.prevHeight + 'px;'\">
\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t
\n\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\t\t
\n\t\t
0\" $style=\"'height:' + this.state.postHeight + 'px;'\">
\n\t
\n\t\n
\n")); +import{Component as t,NewState as e,Util as i,RefreshState as n}from"@web/state";import{VirtualScroll as s}from"@web/base";t.register("DataTable",t=>{const i=s();t.state||(t.state=e({}));const n=t.state;Object.assign(n,{list:[],fields:[],renderedList:[],prevHeight:0,postHeight:0,_listStartIndex:0,selStartR:-1,selStartF:-1,selEndR:-1,selEndF:-1,multiSelections:[],isSelecting:!1});const d=()=>{const s=t.querySelector(".dt-body");if(!s)return;const d=i.calc(s,n.list);d&&(d.renderedList.forEach((t,i)=>{if(t&&!t.__watch){const s=e(t);d.renderedList[i]=s,n.list[d.listStartIndex+i]=s}}),Object.assign(n,{prevHeight:d.prevHeight,postHeight:d.postHeight,_listStartIndex:d.listStartIndex,renderedList:d.renderedList}))};t.refresh=d,n.__watch("fields",t=>{if(!t)return;const e=[],i=[];let s=0;t.forEach((t,i)=>{"left"===t.pinned&&(e[i]=s,s+=t.width||150)}),t.forEach((e,n)=>{if("right"===e.pinned){let e=0;for(let i=n+1;i{n._listStartIndex=0;const s=t.querySelector(".dt-body");n.renderedList=i.reset(e,s||{clientHeight:800})||[],s&&(i.init(e,d),requestAnimationFrame(d))}),t.onItemUpdate=(t,e)=>i.update(t+n._listStartIndex,e),t.getOffset=(t,e)=>(n._leftOffsets||[])[t]||(n._rightOffsets||[])[t]||0,t.isCellSelected=(t,e)=>{const i=Math.min(n.selStartR,n.selEndR),s=Math.max(n.selStartR,n.selEndR),d=Math.min(n.selStartF,n.selEndF),r=Math.max(n.selStartF,n.selEndF);return t>=i&&t<=s&&e>=d&&e<=r||n.multiSelections.some(i=>t>=i.r1&&t<=i.r2&&e>=i.f1&&e<=i.f2)},t.clearAllActive=(t=!1)=>{n.list.forEach(t=>{t&&t.__watch&&(null!==t._editingF&&(t._editingF=null),null!==t._activeF&&(t._activeF=null))}),t||(n.selStartR=-1,n.multiSelections=[])},t.startSelect=(e,i,s)=>{const d=t.isCellSelected(e,i);n.editingCell&&(n.editingCell=null),s.shiftKey&&-1!==n.selStartR?(n.selEndR=e,n.selEndF=i):(d||(s.ctrlKey||s.metaKey?-1!==n.selStartR&&n.multiSelections.push({r1:Math.min(n.selStartR,n.selEndR),r2:Math.max(n.selStartR,n.selEndR),f1:Math.min(n.selStartF,n.selEndF),f2:Math.max(n.selStartF,n.selEndF)}):t.clearAllActive(),n.selStartR=n.selEndR=e,n.selStartF=n.selEndF=i),n.isSelecting=!0,n.list[e]&&n.list[e].__watch&&(n.list[e]._activeF=i))},t.updateSelect=(t,e)=>n.isSelecting&&(n.selEndR=t,n.selEndF=e),t.endSelect=()=>n.isSelecting=!1,t.editCell=(i,s,d)=>{var r;const l=Math.min(n.selStartR,n.selEndR),a=Math.max(n.selStartR,n.selEndR),o=Math.min(n.selStartF,n.selEndF),c=Math.max(n.selStartF,n.selEndF),h=n.list.indexOf(i),f=-1!==n.selStartR&&h>=l&&h<=a&&d>=o&&d<=c?a-l+1:0;if(f>1&&(null==(r=globalThis.UI)?void 0:r.toast)&&UI.toast(`{#Bulk Editing {num} rows... || ${f}#}`),t.clearAllActive(!0),i._editingF=s.id,i._activeF=d,f>1){const t=i.__watch(s.id,d=>{for(let t=l;t<=a;t++){const r=n.list[t];if(r!==i){const i=r.__watch?r:e(r);n.list[t]=i,i[s.id]=d}}t()})}};t.copy=async()=>{const t=Math.min(n.selStartR,n.selEndR),e=Math.max(n.selStartR,n.selEndR),i=Math.min(n.selStartF,n.selEndF),s=Math.max(n.selStartF,n.selEndF);if(-1===t)return;const d=n.list.slice(t,e+1).map(t=>n.fields.slice(i,s+1).map(e=>(t=>{const e=String(t??"");return e.includes("\t")||e.includes("\n")||e.includes('"')?'"'+e.replace(/"/g,'""')+'"':e})(t[e.id])).join("\t")).join("\n");await navigator.clipboard.writeText(d)},t.paste=async()=>{const t=(t=>{const e=[];let i=[],n="",s=!1;for(let d=0;d{let r=n.list[i+d];r&&(r.__watch||(r=e(r),n.list[i+d]=r),t.forEach((t,e)=>{const i=n.fields[s+e];i&&("boolean"==typeof r[i.id]?r[i.id]="true"===t.toLowerCase():"number"==typeof r[i.id]?r[i.id]=Number(t):r[i.id]=t)}))})},t.addEventListener("keydown",e=>{(e.ctrlKey||e.metaKey)&&("c"===e.key&&(e.preventDefault(),t.copy()),"v"===e.key&&(e.preventDefault(),t.paste()))});const r=e=>!t.contains(e.target)&&t.clearAllActive();document.addEventListener("mousedown",r),window.addEventListener("mouseup",t.endSelect),t._onUnload=()=>{document.removeEventListener("mousedown",r),window.removeEventListener("mouseup",t.endSelect)}},i.makeDom("\n
\n\t
\n\t\t
\n\t\t\t\n\t\t\t\n\t\t
\n\t
\n\t
this.refresh())\" style=\"overflow-anchor:none\">\n\t\t
0\" $style=\"'height:' + this.state.prevHeight + 'px;'\">
\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t
\n\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\t\t
\n\t\t
0\" $style=\"'height:' + this.state.postHeight + 'px;'\">
\n\t
\n\t\n
\n")),"undefined"!=typeof document&&n(document.documentElement); diff --git a/src/index.js b/src/index.js index ec47c75..168bf21 100644 --- a/src/index.js +++ b/src/index.js @@ -3,6 +3,8 @@ 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, { @@ -18,12 +20,10 @@ Component.register('DataTable', container => { if (!scrollEl) return const res = vs.calc(scrollEl, state.list) if (res) { - // 仅对渲染列表中的项进行响应式包装(如果尚未包装) res.renderedList.forEach((item, i) => { - if (!item.__watch) { + if (item && !item.__watch) { const wrapped = NewState(item) res.renderedList[i] = wrapped - // 同步回原列表 state.list[res.listStartIndex + i] = wrapped } }) @@ -35,24 +35,42 @@ Component.register('DataTable', container => { } 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()) + requestAnimationFrame(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.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) @@ -100,7 +118,6 @@ Component.register('DataTable', container => { container.endSelect = () => state.isSelecting = false container.editCell = (row, f, fIdx) => { - if (!row.__watch) return; // 理论上双击的行一定已经包装过 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) @@ -209,7 +226,7 @@ Component.register('DataTable', container => {
+ $style="'width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); min-width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); ' + (f.pinned ? 'position:sticky; z-index:11;' : '') + (f.pinned === 'left' ? 'left:' + this.getOffset(index, 'left') + 'px;' : '') + (f.pinned === 'right' ? 'right:' + this.getOffset(index, 'right') + 'px;' : '')">
@@ -219,7 +236,7 @@ Component.register('DataTable', container => {
@@ -254,3 +271,5 @@ Component.register('DataTable', container => {
`)) + +if (typeof document !== 'undefined') RefreshState(document.documentElement) diff --git a/test/index.html b/test/index.html index e4ed62d..8883b83 100644 --- a/test/index.html +++ b/test/index.html @@ -8,114 +8,63 @@ -
-
-

DataTable Professional

-
Double-click to edit • Drag/Shift to select • Ctrl+C/V to Copy/Paste
-
-