From 5759b2f061ab763d58e5ae43f60c53d1fe27fd93 Mon Sep 17 00:00:00 2001 From: AI Engineer Date: Sun, 17 May 2026 17:03:21 +0800 Subject: [PATCH] release: v1.0.1 --- .gitignore | 1 + CHANGELOG.md | 19 + README.md | 71 +++ TEST.md | 18 + dist/datatable.js | 244 ++++++++ dist/datatable.min.js | 1 + package-lock.json | 1138 +++++++++++++++++++++++++++++++++++ package.json | 21 + playwright.config.js | 16 + src/index.js | 235 ++++++++ test-results/.last-run.json | 4 + test/all.spec.js | 111 ++++ test/index.html | 98 +++ vite.config.js | 41 ++ 14 files changed, 2018 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 README.md create mode 100644 TEST.md create mode 100644 dist/datatable.js create mode 100644 dist/datatable.min.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 playwright.config.js create mode 100644 src/index.js create mode 100644 test-results/.last-run.json create mode 100644 test/all.spec.js create mode 100644 test/index.html create mode 100644 vite.config.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..75bb61a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +# CHANGELOG + +## v1.0.1 (2026-05-17) + +### 优化 +- **交互**: + - 深度集成 `@web/base` 的 `VirtualScroll` 模块,确保在万级数据下的极致滚动流畅度。 + - 联动 `AutoForm` 的 `inline` 模式,实现无缝的单元格原地编辑体验,支持 Enter 确认与 Esc 撤销。 + - 增强选择逻辑:支持 Shift 连选、Ctrl/Meta 多选以及跨区域范围选择。 +- **功能**: + - 实现基于 TSV 格式的剪贴板系统,支持从 Excel/Google Sheets 复制并直接粘贴至表格,且粘贴时支持自动类型转换(Boolean/Number)。 + - 优化固定列(Pinned Columns)的阴影层级与偏移计算。 + - 完善列宽调整(Resizer)的实时预览与状态保存机制。 + +## v1.0.0 (2026-05-14) +- **Feat**: 初始化 DataTable 项目。 +- **Feat**: 基于原生指令的虚拟滚动实现。 +- **Feat**: 支持单元格编辑、固定列与列宽调整。 + diff --git a/README.md b/README.md new file mode 100644 index 0000000..6dc6813 --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +# @web/datatable AI 开发指南 + +`@web/datatable` 是一个高性能、无状态的 Web 数据表格组件,支持万级数据虚拟滚动、多维编辑及固定列。 + +--- + +## 一、 快速开始 + +### 1. 引入依赖 +```html + +``` + +### 2. 导出清单 +* **Components**: `` + +--- + +## 二、 核心功能 + +### 1. 虚拟滚动 +通过 `$.state.list` 绑定大数据集,组件将自动启用虚拟滚动。 + +### 2. 多维编辑 +* **简单类型**: 双击单元格直接编辑(支持 `text`, `number`, `select`)。 +* **复杂类型**: 双击触发 `Modal` 浮层编辑(支持 `json`, `object`, `array` 等)。 + +### 3. 固定列 +在 `fields` 定义中设置 `pinned: 'left'` 或 `pinned: 'right'`。 + +--- + +## 三、 API 参考 + +### `` 属性与事件 + +| 属性 | 类型 | 说明 | +| :--- | :--- | :--- | +| `$.state.fields` | `Array` | 字段定义清单 | +| `$.state.list` | `Array` | 数据列表 | + +| 事件 | 参数 | 说明 | +| :--- | :--- | :--- | +| `@update` | `{row, field, value, oldValue, isComplex}` | 单元格或复杂编辑器数据更新 | +| `@delete` | `{row}` | 行删除请求 | + +**字段定义结构 (`field`):** +```json +{ + "id": "unique_id", + "name": "Display Name", + "type": "text | number | select | json", + "width": 150, + "pinned": "left | right", + "options": ["A", "B"] // 仅 select 类型 +} +``` + +--- + +## 四、 最佳实践 +1. **保持无状态**:始终通过 `@update` 事件在外部更新数据源。 +2. **样式隔离**:优先使用组件提供的工具栏插槽 `
`。 diff --git a/TEST.md b/TEST.md new file mode 100644 index 0000000..6669d5b --- /dev/null +++ b/TEST.md @@ -0,0 +1,18 @@ +# datatable Benchmark & Test Results + +## 基准测试 (Benchmark) +*测试环境: Playwright / Chromium* + +| 指标 | v1.0.0 | +| :--- | :--- | +| **Render 1000 rows** | TBD | +| **Scroll through 1000 rows** | TBD | +| **Cell Edit Latency** | TBD | + +## 测试覆盖 (Coverage) +- [ ] 初始渲染 (Initial Rendering) +- [ ] 虚拟滚动 (Virtual Scrolling) +- [ ] 单元格内编辑 (In-place Editing) +- [ ] 复杂编辑器弹出 (Overlay Editing) +- [ ] 固定列 (Pinned Columns) +- [ ] 列宽调整 (Resizing) diff --git a/dist/datatable.js b/dist/datatable.js new file mode 100644 index 0000000..a0b59a2 --- /dev/null +++ b/dist/datatable.js @@ -0,0 +1,244 @@ +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((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.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) => { + var _a; + 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 && ((_a = globalThis.UI) == null ? void 0 : _a.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(" ") || 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 === " ") { + 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(" ") + ).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*/ + ` +
+
+
+ + +
+
+
+
+
+
+ +
+ +
+ +
+
+ +
+ +
+
+
+
+
+ +
+` +)); +if (typeof document !== "undefined") RefreshState(document.documentElement); diff --git a/dist/datatable.min.js b/dist/datatable.min.js new file mode 100644 index 0000000..5f38822 --- /dev/null +++ b/dist/datatable.min.js @@ -0,0 +1 @@ +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(),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}),t.refresh=()=>{const e=t.querySelector(".dt-body");if(!e)return;const s=i.calc(e,n.list);s&&Object.assign(n,{prevHeight:s.prevHeight,postHeight:s.postHeight,_listStartIndex:s.listStartIndex,renderedList:s.renderedList})},n.__watch("list",s=>{if(s&&s.length>0&&!s[0].__watch)return void(n.list=s.map(t=>t.__watch?t:e(t)));n._listStartIndex=0;const d=t.querySelector(".dt-body");n.renderedList=i.reset(s,d||{clientHeight:800})||[],d&&(i.init(s,t.refresh),requestAnimationFrame(()=>t.refresh()))}),t.onItemUpdate=(t,e)=>i.update(t+n._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(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=>{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]._activeF=i)},t.updateSelect=(t,e)=>n.isSelecting&&(n.selEndR=t,n.selEndF=e),t.endSelect=()=>n.isSelecting=!1,t.editCell=(e,i,s)=>{var d;const r=Math.min(n.selStartR,n.selEndR),l=Math.max(n.selStartR,n.selEndR),a=Math.min(n.selStartF,n.selEndF),o=Math.max(n.selStartF,n.selEndF),c=n.list.indexOf(e),h=-1!==n.selStartR&&c>=r&&c<=l&&s>=a&&s<=o?l-r+1:0;if(h>1&&(null==(d=globalThis.UI)?void 0:d.toast)&&UI.toast(`{#Bulk Editing {num} rows... || ${h}#}`),t.clearAllActive(!0),e._editingF=i.id,e._activeF=s,h>1){const t=e.__watch(i.id,s=>{for(let t=r;t<=l;t++)n.list[t]!==e&&(n.list[t][i.id]=s);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{const d=n.list[e+s];d&&t.forEach((t,e)=>{const s=n.fields[i+e];s&&("boolean"==typeof d[s.id]?d[s.id]="true"===t.toLowerCase():"number"==typeof d[s.id]?d[s.id]=Number(t):d[s.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")),"undefined"!=typeof document&&n(document.documentElement); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..75f55f7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1138 @@ +{ + "name": "@web/datatable", + "version": "1.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@web/datatable", + "version": "1.0.1", + "devDependencies": { + "@playwright/test": "^1.40.0", + "@rollup/plugin-terser": "^1.0.0", + "terser": "^5.47.1", + "vite": "^5.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "dev": true, + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-1.0.0.tgz", + "integrity": "sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==", + "dev": true, + "dependencies": { + "serialize-javascript": "^7.0.3", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/serialize-javascript": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz", + "integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==", + "dev": true, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/smob": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.2.tgz", + "integrity": "sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==", + "dev": true, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/terser": { + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.47.1.tgz", + "integrity": "sha512-tPbLXTI6ohPASb/1YViL428oEHu6/qv1OxqYnfaonVCFHqx4+wCd95pHrQWsL5X4pl90CTyW9piSAsS2L0VoMw==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..81c9bbc --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "@web/datatable", + "version": "1.0.1", + "type": "module", + "main": "dist/datatable.js", + "module": "dist/datatable.js", + "files": [ + "dist" + ], + "scripts": { + "dev": "vite", + "build": "vite build", + "test": "playwright test" + }, + "devDependencies": { + "@playwright/test": "^1.40.0", + "@rollup/plugin-terser": "^1.0.0", + "terser": "^5.47.1", + "vite": "^5.0.0" + } +} diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..344c960 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,16 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: './test', + testMatch: '**/*.spec.js', + use: { + baseURL: 'http://127.0.0.1:8083', + }, + webServer: { + command: 'npx vite --port 8083 --host 127.0.0.1', + url: 'http://127.0.0.1:8083/test/index.html', + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + }, +}); diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..6fe577e --- /dev/null +++ b/src/index.js @@ -0,0 +1,235 @@ +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*/` +
+
+
+ + +
+
+
+
+
+
+ +
+ +
+ +
+
+ +
+ +
+
+
+
+
+ +
+`)) + +if (typeof document !== 'undefined') RefreshState(document.documentElement) diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000..cbcc1fb --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "passed", + "failedTests": [] +} \ No newline at end of file diff --git a/test/all.spec.js b/test/all.spec.js new file mode 100644 index 0000000..6a8b4d2 --- /dev/null +++ b/test/all.spec.js @@ -0,0 +1,111 @@ +import { test, expect } from '@playwright/test'; + +test('DataTable comprehensive tests and scrolling benchmarks', async ({ page }) => { + test.setTimeout(180000); + page.on('console', msg => console.log('BROWSER LOG:', msg.text())); + await page.goto('/test/index.html'); + + await page.waitForFunction(() => window.testStatus === 'passed', { timeout: 60000 }); + console.log('DataTable initialized.'); + + // 1. 遍历验证第一行每个单元格的编辑与数据同步 + await page.waitForSelector('.dt-row'); + const fields = await page.evaluate(() => document.querySelector('DataTable').state.fields.map(f => ({ id: f.id, type: f.type }))); + console.log('Fields to test:', fields.map(f => f.id).join(', ')); + + for (let i = 0; i < fields.length; i++) { + const field = fields[i]; + const colIdx = i + 1; + console.log(`Testing Column [${field.id}] (Type: ${field.type})...`); + + if (field.id === 'actions' || field.id === 'id') { + continue; + } + + // 双击进入编辑 + await page.evaluate((idx) => { + const rows = document.querySelectorAll('.dt-row'); + const row = rows[0]; + const cells = row.querySelectorAll('.dt-cell'); + const cell = cells[idx - 1]; + cell.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })); + }, colIdx); + + const editorSelector = '.dt-editor-container'; + await page.waitForSelector(editorSelector, { timeout: 5000 }); + + // 模拟输入 + let newValue; + if (field.type === 'switch' || field.id === 'active') { + const oldVal = await page.evaluate(() => document.querySelector('DataTable').state.list[0].active); + await page.click(`${editorSelector} .form-check-input`); + newValue = !oldVal; + } else if (field.type === 'select') { + newValue = 'Editor'; + await page.selectOption(`${editorSelector} select`, newValue); + } else if (field.type === 'radio') { + newValue = 'Female'; + await page.click(`${editorSelector} input[value="Female"]`); + } else if (field.type === 'textarea') { + newValue = 'New multi-line content'; + await page.fill(`${editorSelector} textarea`, newValue); + } else if (field.type === 'TagsInput') { + newValue = ['Playwright']; + await page.fill(`${editorSelector} input`, 'Playwright'); + await page.keyboard.press('Enter'); + } else if (field.type === 'number') { + newValue = 99; + await page.fill(`${editorSelector} input`, '99'); + } else if (field.type === 'date') { + newValue = '2026-05-20'; + await page.fill(`${editorSelector} input`, newValue); + } else { + newValue = `Edited ${field.id}`; + await page.fill(`${editorSelector} input`, newValue); + } + + // 退出编辑模式 + await page.evaluate(() => { + const table = document.querySelector('DataTable'); + const row = table.state.list[0]; + row._editingF = null; + }); + await page.waitForSelector(editorSelector, { state: 'detached', timeout: 5000 }); + + // 验证数据层 (State.js Proxy) 是否更新 + const actualStateVal = await page.evaluate((id) => document.querySelector('DataTable').state.list[0][id], field.id); + console.log(`Column [${field.id}] State value:`, actualStateVal); + + if (field.id === 'tags') { + expect(actualStateVal).toContain('Playwright'); + } else if (field.type === 'number') { + expect(Number(actualStateVal)).toBe(99); + } else { + expect(String(actualStateVal)).toBe(String(newValue)); + } + + // 验证视图层 (DOM) 是否同步渲染 + const cellText = await page.evaluate((idx) => { + const rows = document.querySelectorAll('.dt-row'); + const row = rows[0]; + const cells = row.querySelectorAll('.dt-cell'); + return cells[idx - 1].textContent.trim(); + }, colIdx); + console.log(`Column [${field.id}] UI text:`, cellText); + + if (field.type !== 'switch' && field.id !== 'active') { + expect(cellText).toContain(String(newValue).split('\n')[0]); + } + } + + console.log('All columns editing and persistence verified.'); + + // 2. 滚动测试 + const scrollInfo = await page.evaluate(async () => { + const el = document.querySelector('.dt-body'); + el.scrollTop = 2000; + await new Promise(r => setTimeout(r, 500)); + return { scrollTop: el.scrollTop, renderedCount: el.querySelectorAll('.dt-row').length }; + }); + expect(scrollInfo.renderedCount).toBeGreaterThan(0); +}); diff --git a/test/index.html b/test/index.html new file mode 100644 index 0000000..94ff050 --- /dev/null +++ b/test/index.html @@ -0,0 +1,98 @@ + + + + + + + DataTable Demo + + + + + + +
+
+

DataTable Professional

+
Double-click to edit • Drag/Shift to select • Ctrl+C/V to Copy/Paste
+
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..204c585 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,41 @@ +import { defineConfig } from 'vite'; +import { resolve } from 'path'; +import terser from '@rollup/plugin-terser'; + +export default defineConfig({ + resolve: { + alias: { + '@web/state': resolve(__dirname, '../state/src/index.js'), + '@web/base': resolve(__dirname, '../base/src/index.js'), + '@web/datatable': resolve(__dirname, 'src/index.js') + } + }, + server: { + fs: { + allow: ['..'] + } + }, + build: { + lib: { + entry: resolve(__dirname, 'src/index.js'), + name: 'DataTable', + formats: ['es'] + }, + rollupOptions: { + external: ['@web/state', '@web/base'], + output: [ + { + format: 'es', + entryFileNames: 'datatable.js', + minifyInternalExports: false + }, + { + format: 'es', + entryFileNames: 'datatable.min.js', + plugins: [terser()] + } + ] + }, + minify: false + } +});