{"_id":"@apigo.cc/datatable","_rev":"5-ea987bf3da3cc9b134007d5d10e7e565","name":"@apigo.cc/datatable","dist-tags":{"latest":"1.0.16"},"versions":{"1.0.15":{"name":"@apigo.cc/datatable","version":"1.0.15","_id":"@apigo.cc/datatable@1.0.15","maintainers":[{"name":"star3s","email":"isstar3@126.com"}],"dist":{"shasum":"7ef332de315219f547b9be3ab028b1891602a89c","tarball":"https://registry.npmjs.org/@apigo.cc/datatable/-/datatable-1.0.15.tgz","fileCount":4,"integrity":"sha512-n+D5JaLwnX36gCLgaZpYIKfoTIC0QvsVGIp3hiZgPg4vj0BO7HfNtv3CnfKKN9kUXl+qHTdTK963Jm1ZHNAvcw==","signatures":[{"sig":"MEYCIQCxckMjapSNWM1iaJIICxDDx/YR0MKZeu0R4UeHOMfpYgIhAOY3MKKl5xDw8U/KfCG63gFpcE9MkgIYh0JHQRyz/QGe","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":96959},"main":"dist/datatable.js","type":"module","module":"dist/datatable.js","gitHead":"bf745013b2f1e9bec2d1acdae7c91e711b47dbae","scripts":{"dev":"vite","pub":"node scripts/publish.js","test":"playwright test","build":"vite build"},"_npmUser":{"name":"star3s","email":"isstar3@126.com"},"_npmVersion":"10.7.0","description":"`@apigo.cc/datatable` 是一个高性能、无状态的 Web 数据表格组件，支持万级数据虚拟滚动、多维编辑及固定列。","directories":{},"_nodeVersion":"20.15.0","_hasShrinkwrap":false,"devDependencies":{"vite":"^5.0.0","terser":"^5.47.1","@playwright/test":"^1.40.0","@rollup/plugin-terser":"^1.0.0"},"_npmOperationalInternal":{"tmp":"tmp/datatable_1.0.15_1782130070436_0.3702796380723945","host":"s3://npm-registry-packages-npm-production"}},"1.0.16":{"name":"@apigo.cc/datatable","version":"1.0.16","type":"module","main":"dist/datatable.js","module":"dist/datatable.js","scripts":{"dev":"vite","build":"vite build","test":"playwright test","pub":"node scripts/publish.js"},"devDependencies":{"@playwright/test":"^1.40.0","@rollup/plugin-terser":"^1.0.0","terser":"^5.47.1","vite":"^5.0.0"},"_id":"@apigo.cc/datatable@1.0.16","gitHead":"a033b2dbb39a6eecb7a843d12e81f722d078a19d","description":"`@apigo.cc/datatable` 是建立在 `@apigo.cc/base` 上的同步数据表格组件，重点不是“能显示表格”，而是“能直接被页面首屏加载并完成渲染、筛选、编辑和保存”。","_nodeVersion":"20.15.0","_npmVersion":"10.7.0","dist":{"integrity":"sha512-Vjzkd4MJZLJrvhIYPb4YkPIrHXghvE4PJMEyxHQX1ruo28FT2UIo8XxbyLLFWRHCz112Ptnj7vr0voL4hlC27w==","shasum":"3cd51ac1c70a9a792ed49137acc7841001b95116","tarball":"https://registry.npmjs.org/@apigo.cc/datatable/-/datatable-1.0.16.tgz","fileCount":4,"unpackedSize":109359,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCtCXs2yFfzcz6vf8zxgIZDv6yM7gqc7/7oidCKTyRtVAIgC/jrQvfHq39ZG4bmurpv4HUtWhDxXNEBm3qbYfnfGlU="}]},"_npmUser":{"name":"star3s","email":"isstar3@126.com"},"directories":{},"maintainers":[{"name":"star3s","email":"isstar3@126.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/datatable_1.0.16_1783354667787_0.8924752636957078"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-22T12:07:50.336Z","modified":"2026-07-06T16:17:48.067Z","1.0.6":"2026-06-02T12:44:47.062Z","1.0.7":"2026-06-04T11:27:01.589Z","1.0.15":"2026-06-22T12:07:50.627Z","1.0.16":"2026-07-06T16:17:47.929Z"},"description":"`@apigo.cc/datatable` 是建立在 `@apigo.cc/base` 上的同步数据表格组件，重点不是“能显示表格”，而是“能直接被页面首屏加载并完成渲染、筛选、编辑和保存”。","maintainers":[{"name":"star3s","email":"isstar3@126.com"}],"readme":"# @apigo.cc/datatable\n\n`@apigo.cc/datatable` 是建立在 `@apigo.cc/base` 上的同步数据表格组件，重点不是“能显示表格”，而是“能直接被页面首屏加载并完成渲染、筛选、编辑和保存”。\n\n## 目录\n\n- [先看依赖](#先看依赖) - 同步加载顺序\n- [全局对象](#全局对象) - 字段类型注册入口\n- [`<DataTable>`](#datatable) - 高性能表格组件\n- [字段定义](#字段定义)\n- [内置编辑能力](#内置编辑能力)\n- [筛选、排序、固定列](#筛选排序固定列)\n- [复制粘贴与批量编辑](#复制粘贴与批量编辑)\n- [自定义字段类型](#自定义字段类型)\n- [推荐示例](#推荐示例)\n\n## 先看依赖\n\n`datatable` 依赖 `state`、`bootstrap` 和 `base`，加载顺序仍然必须是同步的。\n\n### 推荐加载顺序\n\n```html\n<script src=\"https://cdn.jsdelivr.net/npm/@apigo.cc/state@1.0.23/dist/state.min.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/@apigo.cc/bootstrap@1.0.9/dist/bootstrap.min.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/@apigo.cc/base@1.0.23/dist/base.min.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/@apigo.cc/datatable@1.0.16/dist/datatable.min.js\"></script>\n```\n\n## 全局对象\n\n### `DataTable`\n\n`DataTable` 是本包挂到全局的对象。\n\n```html\n<script>\n  console.log(DataTable.getFieldTypes())\n</script>\n```\n\n### 方法\n\n| 方法 | 说明 |\n|---|---|\n| `registerFieldType(config)` | 注册字段类型 |\n| `getFieldTypes()` | 获取当前所有已注册字段类型 |\n\n## `<DataTable>`\n\n`<DataTable>` 负责渲染、编辑、筛选、排序和虚拟滚动。内部会用到 `<AutoForm>`, `<Modal>`, `<Resizer>`，这些都来自 `base`。\n\n### 绑定方式\n\n| 名称 | 写法示例 | 说明 |\n|---|---|---|\n| `editable` | `<DataTable editable>` | 开启编辑、删除、字段管理、保存按钮 |\n| `$.state.fields` | `$.state.fields=\"window.fields\"` | 字段定义数组 |\n| `$.state.list` | `$.state.list=\"window.list\"` | 数据行数组 |\n\n### 事件\n\n| 事件 | 触发时机 | `detail` |\n|---|---|---|\n| `save` | 点击底部保存按钮 | `{ list, fields }` |\n| `savefields` | 字段新增 / 编辑 / 删除后 | `fields` |\n| `remove` | 删除行后 | `{ items }` |\n\n### 常用状态\n\n| 名称 | 说明 |\n|---|---|\n| `editable` | 是否开启编辑模式 |\n| `list` | 当前生效的数据列表，排序和筛选后会更新这里 |\n| `fields` | 字段定义数组 |\n| `_renderedList` | 当前 DOM 实际渲染的片段 |\n| `prevHeight` | 虚拟滚动顶部占位高度 |\n| `postHeight` | 虚拟滚动底部占位高度 |\n| `_listStartIndex` | 当前渲染片段在原列表中的起始索引 |\n| `selectedRowCount` | 当前选中行数 |\n| `_originalList` | 原始数据快照，排序 / 筛选前的数据源 |\n| `sortConfig` | `{ fieldId, direction }` |\n| `filterConfig` | `{ [fieldId]: { mode, value, value2, selectedValues } }` |\n| `activeFieldId` | 当前打开列菜单的字段 id |\n| `activeField` | 当前打开列菜单的字段对象 |\n| `activeModes` | 当前列允许的筛选模式 |\n| `_columnStats` | 当前列高频值统计 |\n| `_internalUpdate` | 内部同步标记，避免列表回写时触发二次筛选 |\n| `_appliedHash` | 内部哈希缓存 |\n| `_fieldsDirty` | 字段变更标记 |\n| `_masterCellNodes` | 用于动态重建列模板的内部缓存 |\n| `isDirty` | 是否存在未保存修改 |\n| `isBulkEdit` | 是否处于批量编辑状态 |\n\n### 方法\n\n| 方法 | 写法示例 | 说明 |\n|---|---|---|\n| `addRow()` | `table.addRow()` | 新增一行空数据 |\n| `deleteSelectedRow()` | `table.deleteSelectedRow()` | 删除当前选区对应的行 |\n| `saveChanges()` | `table.saveChanges()` | 派发 `save` 事件 |\n| `addField()` | `table.addField()` | 新增字段 |\n| `editField()` | `table.editField()` | 编辑当前打开菜单的字段 |\n| `deleteField()` | `table.deleteField()` | 删除当前打开菜单的字段 |\n| `showColumnMenu(field, event)` | `table.showColumnMenu(field, event)` | 打开列菜单 |\n| `hideColumnMenu()` | `table.hideColumnMenu()` | 关闭列菜单 |\n| `editCell(row, field, cellNode)` | `table.editCell(row, field, cellNode)` | 打开单元格编辑器 |\n| `onScroll()` | `table.onScroll()` | 滚动时刷新虚拟列表 |\n\n### 推荐示例\n\n```html\n<script>\n  window.fields = [\n    { id: 'id', name: 'ID', type: 'string', settings: { width: 70, formType: 'text', pinned: 'left' } },\n    { id: 'name', name: 'Name', type: 'string', settings: { width: 160, formType: 'text', pinned: 'left' } },\n    { id: 'role', name: 'Role', type: 'string', settings: { width: 120, formType: 'select', options: [{ label: 'Admin', value: 'admin' }, { label: 'Editor', value: 'editor' }] } },\n    { id: 'active', name: 'Active', type: 'boolean', settings: { width: 90, formType: 'switch', labelOn: 'YES', labelOff: 'NO' } },\n    { id: 'score', name: 'Score', type: 'number', settings: { width: 120, formType: 'number', prefix: '$', decimals: 2, thousandSep: true } },\n    { id: 'tags', name: 'Tags', type: 'object', settings: { width: 180, formType: 'TagsInput' } },\n    { id: 'created', name: 'Created', type: 'date', settings: { width: 140, formType: 'date' } },\n    { id: 'actions', name: 'Actions', type: 'string', settings: { width: 120, formType: 'text', pinned: 'right' }, formatter: () => 'Edit' }\n  ]\n\n  window.list = [\n    { id: 1, name: 'Alice', role: 'admin', active: true, score: 12800.5, tags: ['Vue', 'AI'], created: '2026-06-01', actions: '' },\n    { id: 2, name: 'Bob', role: 'editor', active: false, score: 980.25, tags: ['Docs'], created: '2026-06-02', actions: '' }\n  ]\n\n  DataTable.registerFieldType({\n    value: 'TagsInput',\n    label: '{#Tags#}',\n    typeForDB: 'v4096',\n    schema: [{ name: 'placeholder', label: 'Placeholder', type: 'text' }],\n    formatter: val => Array.isArray(val) ? val.join(', ') : val\n  })\n\n</script>\n\n<div style=\"height: 600px\" class=\"border rounded overflow-hidden bg-body\">\n  <DataTable editable\n    $.state.fields=\"window.fields\"\n    $.state.list=\"window.list\"\n    $onsave=\"UI.toast('save: ' + JSON.stringify(event.detail.list))\"\n    $onsavefields=\"UI.toast('fields changed')\"\n    $onremove=\"UI.toast('removed: ' + event.detail.items.length)\">\n  </DataTable>\n</div>\n```\n\n## 字段定义\n\n`fields` 是表格最重要的配置。每个字段都建议写完整，不要只写一个 `id` 就了事。\n\n### 字段结构\n\n| 字段 | 写法示例 | 说明 |\n|---|---|---|\n| `id` | `id: 'name'` | 列唯一标识，同时也是数据行中的字段名 |\n| `name` | `name: 'Name'` | 表头显示名 |\n| `type` | `type: 'string'` | 列的原始类型 |\n| `memo` | `memo: 'User name'` | 字段备注，字段编辑器可见 |\n| `isIndex` | `isIndex: true` | 是否索引字段 |\n| `settings` | `settings: { width: 160, formType: 'text' }` | 编辑与展示配置 |\n| `formatter` | `formatter: v => String(v)` | 自定义显示格式 |\n\n### `settings` 字段\n\n| 字段 | 写法示例 | 说明 |\n|---|---|---|\n| `width` | `width: 160` | 列宽，默认 `150` |\n| `pinned` | `pinned: 'left'` | 固定列，`left` 或 `right` |\n| `formType` | `formType: 'select'` | 编辑器类型 |\n| `options` | `options: [{ label: 'A', value: 'a' }]` | 选择类选项 |\n| `decimals` | `decimals: 2` | 数字小数位 |\n| `prefix` | `prefix: '$'` | 数字前缀 |\n| `suffix` | `suffix: '%'` | 数字后缀 |\n| `thousandSep` | `thousandSep: true` | 数字千分位 |\n| `labelOn` | `labelOn: 'YES'` | `switch` 为真时显示 |\n| `labelOff` | `labelOff: 'NO'` | `switch` 为假时显示 |\n| `format` | `format: 'YYYY-MM-DD'` | 日期时间格式说明 |\n| `placeholder` | `placeholder: 'Enter value'` | 文本占位符 |\n\n### 字段示例\n\n```js\n{\n  id: 'score',\n  name: 'Score',\n  type: 'number',\n  settings: {\n    width: 120,\n    formType: 'number',\n    decimals: 2,\n    prefix: '$',\n    thousandSep: true\n  },\n  formatter: value => value == null ? '' : String(value)\n}\n```\n\n## 内置编辑能力\n\n### 单元格编辑支持的 `formType`\n\n`editCell()` 会把 `settings.formType || type || 'text'` 交给 `AutoForm`，因此这些类型都能直接编辑：\n\n- `text`\n- `number`\n- `select`\n- `checkbox`\n- `radio`\n- `switch`\n- `date`\n- `datetime`\n- `textarea`\n- `TagsInput`\n- `DatePicker`\n- `ColorPicker`\n- `IconPicker`\n\n### 字段编辑器默认可选类型\n\n字段管理弹窗里，默认由 `DataTable.getFieldTypes()` 提供的类型是：\n\n- `text`\n- `number`\n- `select`\n- `checkbox`\n- `switch`\n- `datetime`\n- `textarea`\n\n### 重要区别\n\n- `date`、`radio` 这些类型在单元格编辑里是可用的\n- 但默认字段编辑器列表里不一定出现\n- 如果你要让它们出现在字段管理弹窗里，直接用 `registerFieldType()` 补进去\n\n### 默认字段类型的格式化规则\n\n| 类型 | 显示规则 |\n|---|---|\n| `text` | 原样字符串化 |\n| `number` | 支持 `decimals`、`prefix`、`suffix`、`thousandSep` |\n| `select` | 显示匹配项的 `label` |\n| `checkbox` | 把多选项 label 用逗号拼接 |\n| `switch` | `true` 显示 `labelOn` 或 `Yes`，`false` 显示 `labelOff` 或 `No` |\n| `datetime` | 默认字符串化 |\n| `textarea` | 默认字符串化 |\n\n## 筛选、排序、固定列\n\n### 排序\n\n- 点击表头右侧下拉按钮\n- 选择 `ASC` 或 `DESC`\n- 排序状态写在 `state.sortConfig`\n\n### 筛选\n\n列菜单支持三类筛选：\n\n- 文本类：`contains`, `equals`, `starts`, `ends`\n- 数字类：`=`, `>`, `<`, `between`\n- 值列表：直接勾选统计出来的高频值\n\n### 支持的列筛选模式\n\n| `formType` | 可用模式 |\n|---|---|\n| `text` | `contains`, `equals`, `starts`, `ends` |\n| `textarea` | `contains`, `equals`, `starts`, `ends` |\n| `number` | `=`, `>`, `<`, `between` |\n| `date` | `=`, `>`, `<`, `between` |\n| `select` | `contains`, `equals` |\n| `TagsInput` | `contains`, `equals`, `starts`, `ends` |\n| `switch`, `checkbox`, `radio` | 选项勾选，不走文本筛选模式 |\n\n### 固定列\n\n字段上设置：\n\n```js\nsettings: { pinned: 'left' }\n```\n\n或：\n\n```js\nsettings: { pinned: 'right' }\n```\n\n就会固定在对应侧边。\n\n## 复制粘贴与批量编辑\n\n### 选择\n\n- 鼠标拖选单元格区域\n- `selectedRowCount` 会同步更新\n- 选区会用于复制、粘贴和批量编辑\n\n### 复制\n\n- `Ctrl/Cmd + C` 把选区复制成 TSV\n- 带引号、换行、制表符的值会自动转义\n\n### 粘贴\n\n- `Ctrl/Cmd + V` 会把剪贴板 TSV 回填到选区\n- 单格选中时会把后续数据按行列铺开\n\n### 批量编辑\n\n- 在已选中的区域内双击一个单元格\n- 如果选区跨多行，会进入批量编辑\n- 保存时会把新值写入范围内每一行\n\n## 自定义字段类型\n\n### `DataTable.registerFieldType(config)`\n\n```js\nDataTable.registerFieldType({\n  value: 'MyTags',\n  label: '{#Tags#}',\n  typeForDB: 'v4096',\n  schema: [\n    { name: 'placeholder', label: 'Placeholder', type: 'text' }\n  ],\n  formatter: val => Array.isArray(val) ? val.join(' | ') : val\n})\n```\n\n### `schema` 说明\n\n`schema` 的格式和 `base` 里的 `AutoForm.schema` 一样，所以这里也能用：\n\n- `name`\n- `label`\n- `type`\n- `setting`\n- `options`\n- `if`\n\n### 适合注册的场景\n\n- 你要让字段编辑器认识一个新的 `formType`\n- 你要给某种值定义专用 `formatter`\n- 你要把扩展控件复用到表格内联编辑里\n\n## 推荐示例\n\n### 可编辑表格\n\n```html\n<script>\n  window.fields = [\n    { id: 'id', name: 'ID', type: 'string', settings: { width: 70, formType: 'text', pinned: 'left' } },\n    { id: 'name', name: 'Name', type: 'string', settings: { width: 160, formType: 'text', pinned: 'left' } },\n    { id: 'role', name: 'Role', type: 'string', settings: { width: 120, formType: 'select', options: [{ label: 'Admin', value: 'admin' }, { label: 'Editor', value: 'editor' }] } },\n    { id: 'active', name: 'Active', type: 'boolean', settings: { width: 90, formType: 'switch', labelOn: 'YES', labelOff: 'NO' } },\n    { id: 'score', name: 'Score', type: 'number', settings: { width: 120, formType: 'number', prefix: '$', decimals: 2, thousandSep: true } },\n    { id: 'created', name: 'Created', type: 'date', settings: { width: 140, formType: 'date' } }\n  ]\n\n  window.list = [\n    { id: 1, name: 'Alice', role: 'admin', active: true, score: 12800.5, created: '2026-06-01' },\n    { id: 2, name: 'Bob', role: 'editor', active: false, score: 980.25, created: '2026-06-02' }\n  ]\n</script>\n\n<div style=\"height: 560px\" class=\"border rounded overflow-hidden\">\n  <DataTable editable\n    $.state.fields=\"window.fields\"\n    $.state.list=\"window.list\"\n    $onsave=\"UI.toast('save')\"\n    $onremove=\"UI.toast('remove: ' + event.detail.items.length)\">\n  </DataTable>\n</div>\n```\n\n### 只读大表 + 虚拟滚动\n\n```html\n<script>\n  window.fields = [\n    { id: 'id', name: 'ID', type: 'string', settings: { width: 70, formType: 'text', pinned: 'left' } },\n    { id: 'name', name: 'Name', type: 'string', settings: { width: 160, formType: 'text', pinned: 'left' } },\n    { id: 'role', name: 'Role', type: 'string', settings: { width: 120, formType: 'select', options: [{ label: 'Admin', value: 'admin' }, { label: 'Editor', value: 'editor' }] } },\n    { id: 'active', name: 'Active', type: 'boolean', settings: { width: 90, formType: 'switch', labelOn: 'YES', labelOff: 'NO' } },\n    { id: 'score', name: 'Score', type: 'number', settings: { width: 120, formType: 'number', prefix: '$', decimals: 2, thousandSep: true } },\n    { id: 'created', name: 'Created', type: 'date', settings: { width: 140, formType: 'date' } }\n  ]\n\n  window.bigList = Array.from({ length: 2000 }, (_, i) => ({\n    id: i + 1,\n    name: 'User ' + (i + 1),\n    role: i % 2 ? 'admin' : 'editor',\n    active: i % 3 === 0,\n    score: Math.round(Math.random() * 10000) / 100,\n    created: '2026-06-01'\n  }))\n</script>\n\n<div style=\"height: 560px\" class=\"border rounded overflow-hidden\">\n  <DataTable\n    $.state.fields=\"window.fields\"\n    $.state.list=\"window.bigList\">\n  </DataTable>\n</div>\n```\n\n### 自定义字段类型\n\n```html\n<script>\n  DataTable.registerFieldType({\n    value: 'TagsInput',\n    label: '{#Tags#}',\n    typeForDB: 'v4096',\n    schema: [{ name: 'placeholder', label: 'Placeholder', type: 'text' }],\n    formatter: val => Array.isArray(val) ? val.join(', ') : val\n  })\n\n  window.fields = [\n    { id: 'tags', name: 'Tags', type: 'object', settings: { width: 180, formType: 'TagsInput' } }\n  ]\n\n  window.list = [\n    { id: 1, tags: ['Vue', 'AI'] }\n  ]\n</script>\n\n<DataTable $.state.fields=\"window.fields\" $.state.list=\"window.list\"></DataTable>\n```\n\n### 备注\n\n- 字段定义和数据列表建议放在 JS 里\n- 外层容器最好明确给高度\n- 需要保存时，监听 `save`；需要字段变更时，监听 `savefields`\n","readmeFilename":"README.md"}