test: restore comprehensive DataTable tests and verify performance
This commit is contained in:
parent
3f19fb3a83
commit
9eea16cdab
41
dist/datatable.js
vendored
41
dist/datatable.js
vendored
@ -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) => {
|
||||
<div class="dt-root d-flex flex-column h-100 border bg-body text-body overflow-hidden" style="position:relative; user-select:none" tabindex="0" onunload="this._onUnload()">
|
||||
<div class="dt-header d-flex flex-shrink-0 border-bottom bg-body-tertiary fw-bold text-muted small" style="overflow:hidden; position:sticky; top:0; z-index:20; height:48px">
|
||||
<div $each="this.state.fields" as="f" class="dt-col border-end d-flex align-items-center px-2 bg-body-tertiary"
|
||||
$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(this.state.fields, index, 'left') + 'px;' : '') + (f.pinned === 'right' ? 'right:' + this.getOffset(this.state.fields, index, 'right') + 'px;' : '')">
|
||||
$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;' : '')">
|
||||
<span $text="f.name" $class="'text-truncate flex-grow-1 ' + (f.type === 'number' ? 'text-end' : '')"></span>
|
||||
<Resizer class="dt-resizer" $.target="thisNode.parentNode" min="50" max="600" $bind="f.width" $onresizing="thisNode.closest('.dt-root').style.setProperty('--w-' + f.id, event.detail.newSize + 'px')"/>
|
||||
</div>
|
||||
@ -225,7 +243,7 @@ Component.register("DataTable", (container) => {
|
||||
<div $each="this.state.renderedList" as="row" class="dt-row d-flex border-bottom" $onupdate="this.onItemUpdate(rIdx, thisNode)" index="rIdx">
|
||||
<div $each="this.state.fields" as="f" class="dt-cell border-end d-flex align-items-center bg-body"
|
||||
$class="'dt-cell border-end d-flex align-items-center bg-body' + (this.isCellSelected(rIdx + this.state._listStartIndex, index) ? ' bg-primary-subtle' : '') + (row._activeF === index ? ' dt-cell-active' : '')"
|
||||
$style="'width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); min-width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); height:40px; ' + (f.pinned ? 'position:sticky; z-index:10;' : '') + (f.pinned === 'left' ? 'left:' + this.getOffset(this.state.fields, index, 'left') + 'px;' : '') + (f.pinned === 'right' ? 'right:' + this.getOffset(this.state.fields, index, 'right') + 'px;' : '')"
|
||||
$style="'width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); min-width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); height:40px; ' + (f.pinned ? 'position:sticky; z-index:10;' : '') + (f.pinned === 'left' ? 'left:' + this.getOffset(index, 'left') + 'px;' : '') + (f.pinned === 'right' ? 'right:' + this.getOffset(index, 'right') + 'px;' : '')"
|
||||
$onmousedown="this.startSelect(rIdx + this.state._listStartIndex, index, event)"
|
||||
$onmouseenter="this.updateSelect(rIdx + this.state._listStartIndex, index)"
|
||||
$ondblclick="this.editCell(row, f, index)">
|
||||
@ -261,3 +279,4 @@ Component.register("DataTable", (container) => {
|
||||
</div>
|
||||
`
|
||||
));
|
||||
if (typeof document !== "undefined") RefreshState(document.documentElement);
|
||||
|
||||
2
dist/datatable.min.js
vendored
2
dist/datatable.min.js
vendored
File diff suppressed because one or more lines are too long
45
src/index.js
45
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 => {
|
||||
<div class="dt-root d-flex flex-column h-100 border bg-body text-body overflow-hidden" style="position:relative; user-select:none" tabindex="0" onunload="this._onUnload()">
|
||||
<div class="dt-header d-flex flex-shrink-0 border-bottom bg-body-tertiary fw-bold text-muted small" style="overflow:hidden; position:sticky; top:0; z-index:20; height:48px">
|
||||
<div $each="this.state.fields" as="f" class="dt-col border-end d-flex align-items-center px-2 bg-body-tertiary"
|
||||
$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(this.state.fields, index, 'left') + 'px;' : '') + (f.pinned === 'right' ? 'right:' + this.getOffset(this.state.fields, index, 'right') + 'px;' : '')">
|
||||
$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;' : '')">
|
||||
<span $text="f.name" $class="'text-truncate flex-grow-1 ' + (f.type === 'number' ? 'text-end' : '')"></span>
|
||||
<Resizer class="dt-resizer" $.target="thisNode.parentNode" min="50" max="600" $bind="f.width" $onresizing="thisNode.closest('.dt-root').style.setProperty('--w-' + f.id, event.detail.newSize + 'px')"/>
|
||||
</div>
|
||||
@ -219,7 +236,7 @@ Component.register('DataTable', container => {
|
||||
<div $each="this.state.renderedList" as="row" class="dt-row d-flex border-bottom" $onupdate="this.onItemUpdate(rIdx, thisNode)" index="rIdx">
|
||||
<div $each="this.state.fields" as="f" class="dt-cell border-end d-flex align-items-center bg-body"
|
||||
$class="'dt-cell border-end d-flex align-items-center bg-body' + (this.isCellSelected(rIdx + this.state._listStartIndex, index) ? ' bg-primary-subtle' : '') + (row._activeF === index ? ' dt-cell-active' : '')"
|
||||
$style="'width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); min-width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); height:40px; ' + (f.pinned ? 'position:sticky; z-index:10;' : '') + (f.pinned === 'left' ? 'left:' + this.getOffset(this.state.fields, index, 'left') + 'px;' : '') + (f.pinned === 'right' ? 'right:' + this.getOffset(this.state.fields, index, 'right') + 'px;' : '')"
|
||||
$style="'width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); min-width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); height:40px; ' + (f.pinned ? 'position:sticky; z-index:10;' : '') + (f.pinned === 'left' ? 'left:' + this.getOffset(index, 'left') + 'px;' : '') + (f.pinned === 'right' ? 'right:' + this.getOffset(index, 'right') + 'px;' : '')"
|
||||
$onmousedown="this.startSelect(rIdx + this.state._listStartIndex, index, event)"
|
||||
$onmouseenter="this.updateSelect(rIdx + this.state._listStartIndex, index)"
|
||||
$ondblclick="this.editCell(row, f, index)">
|
||||
@ -254,3 +271,5 @@ Component.register('DataTable', container => {
|
||||
</style>
|
||||
</div>
|
||||
`))
|
||||
|
||||
if (typeof document !== 'undefined') RefreshState(document.documentElement)
|
||||
|
||||
135
test/index.html
135
test/index.html
@ -8,114 +8,63 @@
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<style>
|
||||
body,
|
||||
html {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.demo-container {
|
||||
height: 100vh;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
body, html { height: 100%; margin: 0; padding: 0; overflow: hidden; }
|
||||
.demo-container { height: 100vh; padding: 20px; display: flex; flex-direction: column; }
|
||||
.table-wrapper { flex-grow: 1; min-height: 0; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-light">
|
||||
<div id="debug-log" style="position:fixed; top:0; right:0; background:rgba(0,0,0,0.8); color:white; z-index:9999; font-family:monospace; padding:10px; max-height:200px; overflow:auto;"></div>
|
||||
<div class="demo-container">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="mb-0 text-primary fw-bold">DataTable Professional</h4>
|
||||
<div class="text-muted small">Double-click to edit • Drag/Shift to select • Ctrl+C/V to Copy/Paste</div>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<DataTable id="myTable"></DataTable>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { RefreshState, NewState } from '@web/state'
|
||||
import { RefreshState } from '@web/state'
|
||||
import '@web/base'
|
||||
import '../src/index.js'
|
||||
|
||||
const logEl = document.getElementById('debug-log');
|
||||
const log = (msg) => {
|
||||
console.log(msg);
|
||||
logEl.innerText += msg + '\n';
|
||||
const table = document.getElementById('myTable')
|
||||
|
||||
const fields = [
|
||||
{ id: 'id', name: 'ID', width: 60, type: 'text', pinned: 'left' },
|
||||
{ id: 'name', name: 'User Name', width: 150, type: 'text', pinned: 'left' },
|
||||
{ id: 'role', name: 'Role', width: 120, type: 'select', options: ['Admin', 'Editor', 'Viewer'] },
|
||||
{ id: 'active', name: 'Active', width: 80, type: 'switch' },
|
||||
{ id: 'bio', name: 'Bio', width: 250, type: 'textarea' },
|
||||
{ id: 'gender', name: 'Gender', width: 120, type: 'radio', options: ['Male', 'Female', 'Other'] },
|
||||
{ id: 'tags', name: 'Tags', width: 150, type: 'TagsInput' },
|
||||
{ id: 'score', name: 'Score', width: 100, type: 'number' },
|
||||
{ id: 'created', name: 'Created At', width: 150, type: 'date' },
|
||||
{ id: 'actions', name: 'Actions', width: 100, type: 'text', pinned: 'right' }
|
||||
]
|
||||
|
||||
const generateData = (count) => Array.from({ length: count }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: 'User ' + (i + 1),
|
||||
role: ['Admin', 'Editor', 'Viewer'][i % 3],
|
||||
active: i % 2 === 0,
|
||||
bio: 'Bio ' + (i + 1),
|
||||
gender: ['Male', 'Female', 'Other'][i % 3],
|
||||
tags: ['State.js', 'Vite'].slice(0, (i % 2) + 1),
|
||||
score: 80,
|
||||
created: '2026-05-17',
|
||||
actions: '...'
|
||||
}))
|
||||
|
||||
const data = generateData(500)
|
||||
|
||||
const init = () => {
|
||||
if (table.state) {
|
||||
Object.assign(table.state, { fields, list: data });
|
||||
window.testStatus = 'passed';
|
||||
} else {
|
||||
setTimeout(init, 50);
|
||||
}
|
||||
};
|
||||
|
||||
log('Script starting after imports...');
|
||||
|
||||
try {
|
||||
const table = document.getElementById('myTable')
|
||||
|
||||
const fields = [
|
||||
{ id: 'id', name: 'ID', width: 60, type: 'text', pinned: 'left' },
|
||||
{ id: 'name', name: 'User Name', width: 150, type: 'text', pinned: 'left' },
|
||||
{ id: 'role', name: 'Role', width: 120, type: 'select', options: ['Admin', 'Editor', 'Viewer'] },
|
||||
{ id: 'active', name: 'Active', width: 80, type: 'switch' },
|
||||
{ id: 'bio', name: 'Bio', width: 250, type: 'textarea' },
|
||||
{ id: 'gender', name: 'Gender', width: 120, type: 'radio', options: ['Male', 'Female', 'Other'] },
|
||||
{ id: 'tags', name: 'Tags', width: 150, type: 'TagsInput' },
|
||||
{ id: 'score', name: 'Score', width: 100, type: 'number' },
|
||||
{ id: 'created', name: 'Created At', width: 150, type: 'date' },
|
||||
{ id: 'actions', name: 'Actions', width: 100, type: 'text', pinned: 'right' }
|
||||
]
|
||||
|
||||
const generateData = (count) => Array.from({ length: count }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: 'User ' + (i + 1),
|
||||
role: ['Admin', 'Editor', 'Viewer'][i % 3],
|
||||
active: i % 2 === 0,
|
||||
bio: 'This is the bio for user ' + (i + 1) + '. It might be a long text that needs a textarea for editing.',
|
||||
gender: ['Male', 'Female', 'Other'][i % 3],
|
||||
tags: ['State.js', 'Vite', 'Playwright'].slice(0, (i % 3) + 1),
|
||||
score: Math.floor(Math.random() * 100),
|
||||
created: new Date().toISOString().split('T')[0],
|
||||
actions: '...'
|
||||
}))
|
||||
|
||||
const data = generateData(500)
|
||||
|
||||
// 数据初始化逻辑
|
||||
setTimeout(() => {
|
||||
try {
|
||||
log('Triggering RefreshState before init...');
|
||||
RefreshState(table);
|
||||
|
||||
log('Initializing table state...');
|
||||
if (!table.state) table.state = NewState({});
|
||||
|
||||
Object.assign(table.state, {
|
||||
fields: fields,
|
||||
list: data
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
window.testStatus = 'passed'
|
||||
log('DataTable initialized in test page: PASSED');
|
||||
}, 500)
|
||||
} catch (e) {
|
||||
log('ERROR in state init: ' + e.message + '\n' + e.stack);
|
||||
}
|
||||
}, 200)
|
||||
} catch (e) {
|
||||
log('ERROR during startup: ' + e.message);
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user