diff --git a/HISTORY.md b/HISTORY.md index 321ff5e..95d5b84 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -6,6 +6,7 @@ - Synchronized workspace package versions from `0.3.8` to `0.3.9` across root, core, wrappers, sequence, server, docs, and demos. - Updated inter-package dependency references to `0.3.9` for cross-package compatibility (`core`->`sequence`, `react`/`vue`->`core`, `docs`/demos->core or wrappers). +- Expanded `README.md` with repository badges (npm, license, CI, docs deploy, docs URL, Node requirement) and added `README.ja.md` for Japanese onboarding. - Size-first refactor: - split renderer internals into focused modules (`rendererTypes`, `rendererShared`, `htmlRenderer`, `canvasRenderer`) and kept `renderers` as a re-export surface. - extracted shared formatter and selection helper modules (`valueFormatter`, `selectionClipboard`, `selectionCoercion`, `selectionEditorFactory`, `selectionInitialValue`) to reduce duplication. diff --git a/README.ja.md b/README.ja.md new file mode 100644 index 0000000..967b9aa --- /dev/null +++ b/README.ja.md @@ -0,0 +1,36 @@ +# Extable + +固定カラムスキーマを持つ、ExcelライクなHTMLテーブルコンポーネントです。Google Sheetsのテーブルモードに近い操作感で、マルチユーザー編集にも対応しています。 + +[English](./README.md) | [日本語](./README.ja.md) + +[![npm version](https://img.shields.io/npm/v/%40extable%2Fcore)](https://www.npmjs.com/package/@extable/core) +[![License](https://img.shields.io/github/license/shibukawa/extable)](./LICENSE) +[![CI Tests](https://github.com/shibukawa/extable/actions/workflows/test.yml/badge.svg)](https://github.com/shibukawa/extable/actions/workflows/test.yml) +[![Deploy Docs](https://github.com/shibukawa/extable/actions/workflows/pages.yml/badge.svg)](https://github.com/shibukawa/extable/actions/workflows/pages.yml) +[![Docs](https://img.shields.io/badge/docs-GitHub%20Pages-1f6feb)](https://shibukawa.github.io/extable/) +[![Node](https://img.shields.io/badge/node-%3E%3D22-339933?logo=node.js&logoColor=white)](https://nodejs.org/) + +## ドキュメント + +ドキュメントサイトは `docs/pages` を元に VitePress で構築し、GitHub Pages に公開しています。 + +- Docs URL: `https://shibukawa.github.io/extable/` +- ローカル起動: `npm run docs:dev` + +## デモ + +- Vanilla デモ(ポート `5173`): `npm run dev:demo` +- React デモ(ポート `5174`): `npm run dev:demo-react` +- Vue デモ(ポート `5175`): `npm run dev:demo-vue` + +## 開発 + +- Node: `>=22` +- インストール: `npm install --cache .npm-cache` +- ビルド: `npm run build` +- テスト: `npm run test` + +## ライセンス + +Apache-2.0。`LICENSE` を参照してください。 diff --git a/README.md b/README.md index e726ee4..9f0ddab 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,15 @@ # Extable -Excel-like HTML table component with a fixed column schema (similar to Google Sheets’ table mode) and built-in multi-user editing. +Excel-like HTML table component with a fixed column schema (similar to Google Sheets' table mode) and built-in multi-user editing. + +[English](./README.md) | [日本語](./README.ja.md) + +[![npm version](https://img.shields.io/npm/v/%40extable%2Fcore)](https://www.npmjs.com/package/@extable/core) +[![License](https://img.shields.io/github/license/shibukawa/extable)](./LICENSE) +[![CI Tests](https://github.com/shibukawa/extable/actions/workflows/test.yml/badge.svg)](https://github.com/shibukawa/extable/actions/workflows/test.yml) +[![Deploy Docs](https://github.com/shibukawa/extable/actions/workflows/pages.yml/badge.svg)](https://github.com/shibukawa/extable/actions/workflows/pages.yml) +[![Docs](https://img.shields.io/badge/docs-GitHub%20Pages-1f6feb)](https://shibukawa.github.io/extable/) +[![Node](https://img.shields.io/badge/node-%3E%3D22-339933?logo=node.js&logoColor=white)](https://nodejs.org/) ![screenshot](https://github.com/shibukawa/extable/blob/main/packages/docs/public/assets/screenshot.webp?raw=true) @@ -8,7 +17,7 @@ Excel-like HTML table component with a fixed column schema (similar to Google Sh The docs site is built with VitePress from `docs/pages` and deployed to GitHub Pages. -- Repo-pages URL (typical): `https://shibukawa.github.io/extable/` +- Docs URL: `https://shibukawa.github.io/extable/` - Local dev: `npm run docs:dev` ## Demos @@ -27,4 +36,3 @@ The docs site is built with VitePress from `docs/pages` and deployed to GitHub P ## License Apache-2.0. See `LICENSE`. - diff --git a/packages/core/src/canvasRenderer.ts b/packages/core/src/canvasRenderer.ts index be97e72..cbb1f29 100644 --- a/packages/core/src/canvasRenderer.ts +++ b/packages/core/src/canvasRenderer.ts @@ -403,6 +403,13 @@ export class CanvasRenderer implements Renderer { const textAreaY = yCursor + CELL_PADDING_TOP_PX; const textAreaW = Math.max(0, w - CELL_PADDING_X_PX * 2); const textAreaH = Math.max(0, rowH - (CELL_PADDING_TOP_PX + CELL_PADDING_BOTTOM_PX)); + const buttonTextPadX = 10; + const textDrawX = + renderAction && c.type === "button" ? textAreaX + buttonTextPadX : textAreaX; + const textDrawW = + renderAction && c.type === "button" + ? Math.max(0, textAreaW - buttonTextPadX * 2) + : textAreaW; const decorations = { underline: Boolean(mergedStyle.underline) || (renderAction && c.type === "link"), strike: Boolean(mergedStyle.strike), @@ -412,9 +419,9 @@ export class CanvasRenderer implements Renderer { ? this.measureTextBounds( ctx, text, - textAreaX, + textDrawX, textAreaY, - textAreaW, + textDrawW, textAreaH, wrap, align, @@ -469,9 +476,9 @@ export class CanvasRenderer implements Renderer { this.drawCellText( ctx, text, - textAreaX, + textDrawX, textAreaY, - textAreaW, + textDrawW, textAreaH, wrap, align, @@ -916,12 +923,16 @@ export class CanvasRenderer implements Renderer { 0, hit.rect.height - (CELL_PADDING_TOP_PX + CELL_PADDING_BOTTOM_PX), ); + const buttonTextPadX = 10; + const textDrawX = col.type === "button" ? textAreaX + buttonTextPadX : textAreaX; + const textDrawW = + col.type === "button" ? Math.max(0, textAreaW - buttonTextPadX * 2) : textAreaW; const bounds = this.measureTextBounds( ctx, actionValue.label, - textAreaX, + textDrawX, textAreaY, - textAreaW, + textDrawW, textAreaH, wrap, align, @@ -1350,6 +1361,8 @@ export class CanvasRenderer implements Renderer { ): DOMRect | null { const lines = this.getTextLinesForBounds(ctx, text, width, wrap, height); if (!lines.length) return null; + const totalTextHeight = lines.length * this.lineHeight; + const baseY = y + Math.max(0, Math.floor((height - totalTextHeight) / 2)); let minX = Number.POSITIVE_INFINITY; let maxX = Number.NEGATIVE_INFINITY; let minY = Number.POSITIVE_INFINITY; @@ -1360,7 +1373,7 @@ export class CanvasRenderer implements Renderer { let startX = x; if (align === "right") startX = x + width - lineWidth; else if (align === "center") startX = x + (width - lineWidth) / 2; - const lineTop = y + this.lineHeight * idx; + const lineTop = baseY + this.lineHeight * idx; const lineBottom = lineTop + this.lineHeight; minX = Math.min(minX, startX); maxX = Math.max(maxX, startX + lineWidth); @@ -1390,6 +1403,8 @@ export class CanvasRenderer implements Renderer { ctx.rect(x - 4, y - 4, width + 8, height + 8); ctx.clip(); const fontBackup = ctx.font; + const baselineBackup = ctx.textBaseline; + ctx.textBaseline = "top"; if (isBoolean) { if (isUniqueBoolean) { ctx.font = `${UNIQUE_BOOL_FONT_SIZE_PX}px ${CANVAS_FONT_FAMILY}`; @@ -1403,26 +1418,26 @@ export class CanvasRenderer implements Renderer { const totalTextHeight = lines.length * this.lineHeight; const baseY = y + Math.max(0, Math.floor((height - totalTextHeight) / 2)); const renderLine = (ln: string, lineIdx: number) => { - const baseline = baseY + this.lineHeight * lineIdx; + const lineTop = baseY + this.lineHeight * lineIdx; let startX = x; let endX = x; if (align === "right") { ctx.textAlign = "right"; endX = x + width; startX = endX - ctx.measureText(ln).width; - ctx.fillText(ln, endX, baseline); + ctx.fillText(ln, endX, lineTop); } else if (align === "center") { ctx.textAlign = "center"; const center = x + width / 2; const w = ctx.measureText(ln).width; startX = center - w / 2; endX = center + w / 2; - ctx.fillText(ln, center, baseline); + ctx.fillText(ln, center, lineTop); } else { ctx.textAlign = "left"; startX = x; endX = x + ctx.measureText(ln).width; - ctx.fillText(ln, x, baseline); + ctx.fillText(ln, x, lineTop); } if (decorations?.underline || decorations?.strike) { const strokeBackup = ctx.strokeStyle; @@ -1431,12 +1446,12 @@ export class CanvasRenderer implements Renderer { ctx.lineWidth = 1; ctx.beginPath(); if (decorations.underline) { - const yUnderline = baseline + 2; + const yUnderline = lineTop + this.lineHeight - 2; ctx.moveTo(startX, yUnderline); ctx.lineTo(endX, yUnderline); } if (decorations.strike) { - const yStrike = baseline - Math.floor(this.lineHeight / 2) + 2; + const yStrike = lineTop + Math.floor(this.lineHeight / 2); ctx.moveTo(startX, yStrike); ctx.lineTo(endX, yStrike); } @@ -1446,9 +1461,10 @@ export class CanvasRenderer implements Renderer { } }; for (let idx = 0; idx < lines.length; idx += 1) { - renderLine(lines[idx] ?? "", idx + 1); + renderLine(lines[idx] ?? "", idx); } ctx.textAlign = "left"; + ctx.textBaseline = baselineBackup; ctx.font = fontBackup; ctx.restore(); } diff --git a/packages/core/src/styles.css b/packages/core/src/styles.css index 4f9d3d2..617ff33 100644 --- a/packages/core/src/styles.css +++ b/packages/core/src/styles.css @@ -305,19 +305,25 @@ & .extable-action-link { display: inline-flex; align-items: center; + justify-content: center; gap: 4px; max-width: 100%; font: inherit; + line-height: 1.2; color: inherit; background: transparent; border: none; padding: 0; + margin: 0; + box-sizing: border-box; + vertical-align: middle; cursor: pointer; text-decoration: none; } & .extable-action-button { padding: 4px 10px; + min-height: 24px; border-radius: 8px; border: 1px solid #94a3b8; background: linear-gradient(180deg, #f8fafc 0%, #e2e8f0 100%); diff --git a/packages/demo-react/src/data/fixtures.ts b/packages/demo-react/src/data/fixtures.ts index 2f21d7a..2ed6c2b 100644 --- a/packages/demo-react/src/data/fixtures.ts +++ b/packages/demo-react/src/data/fixtures.ts @@ -11,6 +11,8 @@ export interface DemoRow { description: string; longNote: string; longNoWrap: string; + actionButton: { label: string; command: string; commandfor: string }; + actionLink: { label: string; href: string; target?: string }; } export const demoRows: DemoRow[] = [ @@ -39,7 +41,17 @@ for (let i = 1; i <= 100; i += 1) { 'This is an extremely long single-line note intended to verify ellipsis when wrapping is disabled. ' + 'It should exceed the column width significantly without containing newline characters.', longNoWrap: - 'A very very long text that should stay single-line and demonstrate ellipsis behavior when wrap is off for this column.' + 'A very very long text that should stay single-line and demonstrate ellipsis behavior when wrap is off for this column.', + actionButton: { + label: i % 2 === 0 ? 'Approve' : 'Review', + command: i % 2 === 0 ? 'approve' : 'review', + commandfor: String(i), + }, + actionLink: { + label: 'Profile', + href: `https://example.com/users/${i}`, + target: '_blank', + } }); } @@ -63,7 +75,17 @@ export function makePerformanceDemoRows(count = 10000): DemoRow[] { score: Math.round((50 + (i % 50)) * 100) / 100, description: shortDescription, longNote, - longNoWrap + longNoWrap, + actionButton: { + label: i % 2 === 0 ? 'Approve' : 'Review', + command: i % 2 === 0 ? 'approve' : 'review', + commandfor: String(i), + }, + actionLink: { + label: 'Profile', + href: `https://example.com/users/${i}`, + target: '_blank', + } }); } return rows; @@ -82,7 +104,9 @@ export const demoSchema = { { key: 'score', header: 'Score', type: 'number', format: { precision: 6, scale: 2 }, style: { align: 'right' }, width: 100 }, { key: 'description', header: 'Description', type: 'string', width: 260, wrapText: true }, { key: 'longNote', header: 'Long Note (wrap)', type: 'string', width: 260, wrapText: true }, - { key: 'longNoWrap', header: 'Long Note (ellipsis)', type: 'string', width: 240 } + { key: 'longNoWrap', header: 'Long Note (ellipsis)', type: 'string', width: 240 }, + { key: 'actionButton', header: 'Action', type: 'button', width: 120 }, + { key: 'actionLink', header: 'Link', type: 'link', width: 140 } ] }; diff --git a/packages/demo-react/src/main.tsx b/packages/demo-react/src/main.tsx index 0a0c14c..7ff8e08 100644 --- a/packages/demo-react/src/main.tsx +++ b/packages/demo-react/src/main.tsx @@ -65,7 +65,7 @@ export function App() { const tableRef = useRef(null); const [tableState, setTableState] = useState(null); const [history, setHistory] = useState({ undo: [], redo: [] }); - const [asyncData, setAsyncData] = useState>(null); + const [asyncData, setAsyncData] = useState>(null); const loadGenerationRef = useRef(0); const loadTimerRef = useRef(null); const renderModeFirstRef = useRef(true); @@ -239,11 +239,6 @@ export function App() { "- conditionalStyle: (row) => StyleDelta | null | Error (warning) | throw (error)", "- Warning/Error is shown as a corner marker with hover message.", ]; - if (dataMode === "unique-bool") { - headerLines.push( - "- commit mode: unique-boolean changes show red (current) and gray (previous) dots.", - ); - } headerLines.push("", "Sources:", ""); return headerLines.join("\n") + dataNoteForSchema(cfg.schema); })(); diff --git a/packages/demo-vue/src/data/fixtures.ts b/packages/demo-vue/src/data/fixtures.ts index 6e59ae6..97b5af5 100644 --- a/packages/demo-vue/src/data/fixtures.ts +++ b/packages/demo-vue/src/data/fixtures.ts @@ -11,6 +11,8 @@ export interface DemoRow { description: string; longNote: string; longNoWrap: string; + actionButton: { label: string; command: string; commandfor: string }; + actionLink: { label: string; href: string; target?: string }; } export const demoRows: DemoRow[] = [ @@ -39,7 +41,17 @@ for (let i = 1; i <= 100; i += 1) { 'This is an extremely long single-line note intended to verify ellipsis when wrapping is disabled. ' + 'It should exceed the column width significantly without containing newline characters.', longNoWrap: - 'A very very long text that should stay single-line and demonstrate ellipsis behavior when wrap is off for this column.' + 'A very very long text that should stay single-line and demonstrate ellipsis behavior when wrap is off for this column.', + actionButton: { + label: i % 2 === 0 ? 'Approve' : 'Review', + command: i % 2 === 0 ? 'approve' : 'review', + commandfor: String(i), + }, + actionLink: { + label: 'Profile', + href: `https://example.com/users/${i}`, + target: '_blank', + } }); } @@ -63,7 +75,17 @@ export function makePerformanceDemoRows(count = 10000): DemoRow[] { score: Math.round((50 + (i % 50)) * 100) / 100, description: shortDescription, longNote, - longNoWrap + longNoWrap, + actionButton: { + label: i % 2 === 0 ? 'Approve' : 'Review', + command: i % 2 === 0 ? 'approve' : 'review', + commandfor: String(i), + }, + actionLink: { + label: 'Profile', + href: `https://example.com/users/${i}`, + target: '_blank', + } }); } return rows; @@ -82,7 +104,9 @@ export const demoSchema = { { key: 'score', header: 'Score', type: 'number', format: { precision: 6, scale: 2 }, style: { align: 'right' }, width: 100 }, { key: 'description', header: 'Description', type: 'string', width: 260, wrapText: true }, { key: 'longNote', header: 'Long Note (wrap)', type: 'string', width: 260, wrapText: true }, - { key: 'longNoWrap', header: 'Long Note (ellipsis)', type: 'string', width: 240 } + { key: 'longNoWrap', header: 'Long Note (ellipsis)', type: 'string', width: 240 }, + { key: 'actionButton', header: 'Action', type: 'button', width: 120 }, + { key: 'actionLink', header: 'Link', type: 'link', width: 140 } ] }; diff --git a/packages/demo-vue/src/env.d.ts b/packages/demo-vue/src/env.d.ts new file mode 100644 index 0000000..92c5507 --- /dev/null +++ b/packages/demo-vue/src/env.d.ts @@ -0,0 +1,5 @@ +declare module "*.vue" { + import type { DefineComponent } from "vue"; + const component: DefineComponent, Record, unknown>; + export default component; +} diff --git a/packages/demo/src/data/fixtures.ts b/packages/demo/src/data/fixtures.ts index 1b1d582..2384e65 100644 --- a/packages/demo/src/data/fixtures.ts +++ b/packages/demo/src/data/fixtures.ts @@ -11,6 +11,8 @@ export interface DemoRow { description: string; longNote: string; longNoWrap: string; + actionButton: { label: string; command: string; commandfor: string }; + actionLink: { label: string; href: string; target?: string }; } export const demoRows: DemoRow[] = [ @@ -39,7 +41,17 @@ for (let i = 1; i <= 100; i += 1) { 'This is an extremely long single-line note intended to verify ellipsis when wrapping is disabled. ' + 'It should exceed the column width significantly without containing newline characters.', longNoWrap: - 'A very very long text that should stay single-line and demonstrate ellipsis behavior when wrap is off for this column.' + 'A very very long text that should stay single-line and demonstrate ellipsis behavior when wrap is off for this column.', + actionButton: { + label: i % 2 === 0 ? 'Approve' : 'Review', + command: i % 2 === 0 ? 'approve' : 'review', + commandfor: String(i), + }, + actionLink: { + label: 'Profile', + href: `https://example.com/users/${i}`, + target: '_blank', + } }); } @@ -63,7 +75,17 @@ export function makePerformanceDemoRows(count = 10000): DemoRow[] { score: Math.round((50 + (i % 50)) * 100) / 100, description: shortDescription, longNote, - longNoWrap + longNoWrap, + actionButton: { + label: i % 2 === 0 ? 'Approve' : 'Review', + command: i % 2 === 0 ? 'approve' : 'review', + commandfor: String(i), + }, + actionLink: { + label: 'Profile', + href: `https://example.com/users/${i}`, + target: '_blank', + } }); } return rows; @@ -77,12 +99,14 @@ export const demoSchema = { { key: 'date', header: 'Date', type: 'date', width: 120 }, { key: 'time', header: 'Time', type: 'time', width: 100 }, { key: 'datetime', header: 'DateTime', type: 'datetime', width: 180 }, - { key: 'role', header: 'Role', type: 'string', edit: { lookup: { async candidates({ query }) { const options = ['viewer', 'editor', 'owner']; const filtered = options.filter(o => o.toLowerCase().includes(query.toLowerCase())); return filtered.map(value => ({ label: value, value })); } } }, width: 120 }, + { key: 'role', header: 'Role', type: 'string', edit: { lookup: { async candidates({ query }: { query: string }) { const options = ['viewer', 'editor', 'owner']; const filtered = options.filter(o => o.toLowerCase().includes(query.toLowerCase())); return filtered.map(value => ({ label: value, value })); } } }, width: 120 }, { key: 'tags', header: 'Tags', type: 'tags', tags: ['alpha', 'beta', 'priority'], width: 140 }, { key: 'score', header: 'Score', type: 'number', format: { precision: 6, scale: 2 }, style: { align: 'right' }, width: 100 }, { key: 'description', header: 'Description', type: 'string', width: 260, wrapText: true }, { key: 'longNote', header: 'Long Note (wrap)', type: 'string', width: 260, wrapText: true }, - { key: 'longNoWrap', header: 'Long Note (ellipsis)', type: 'string', width: 240 } + { key: 'longNoWrap', header: 'Long Note (ellipsis)', type: 'string', width: 240 }, + { key: 'actionButton', header: 'Action', type: 'button', width: 120 }, + { key: 'actionLink', header: 'Link', type: 'link', width: 140 } ] }; diff --git a/packages/demo/src/main.ts b/packages/demo/src/main.ts index a8bbd4c..e3d046f 100644 --- a/packages/demo/src/main.ts +++ b/packages/demo/src/main.ts @@ -61,7 +61,7 @@ const user: UserInfo = { id: "demo-user", name: "Demo User" }; let currentConfig: { data: NullableData; view: View; schema: Schema } = { data: demoRows.map((r) => ({ ...r })), view: { ...demoView }, - schema: demoSchema, + schema: demoSchema as Schema, }; const serverStub: ServerAdapter = { @@ -148,12 +148,12 @@ function renderShell() { `; } -function cloneConfig(dataMode: DataMode) { +function cloneConfig(dataMode: DataMode): { data: NullableData; view: View; schema: Schema } { if (dataMode === "loading-async") { return { data: null, - schema: demoSchema, - view: { ...demoView }, + schema: demoSchema as Schema, + view: { ...demoView } as View, }; } if (dataMode === "performance-10k") { @@ -161,63 +161,63 @@ function cloneConfig(dataMode: DataMode) { makePerformanceDemoRows(10000); return { data: (cloneConfig as unknown as Record)._perfRows as any[], - schema: demoSchema, - view: { ...demoView }, + schema: demoSchema as Schema, + view: { ...demoView } as View, }; } if (dataMode === "data-format") { return { data: dataFormatRows.map((r) => ({ ...r })), - schema: dataFormatSchema, - view: { ...dataFormatView }, + schema: dataFormatSchema as Schema, + view: { ...dataFormatView } as View, }; } if (dataMode === "numbers") { return { data: numbersRows.map((r) => ({ ...r })), - schema: numbersSchema, - view: { ...numbersView }, + schema: numbersSchema as Schema, + view: { ...numbersView } as View, }; } if (dataMode === "formula") { return { data: formulaRows.map((r) => ({ ...r })), - schema: formulaSchema, - view: { ...formulaView }, + schema: formulaSchema as Schema, + view: { ...formulaView } as View, }; } if (dataMode === "conditional-style") { return { data: conditionalStyleRows.map((r) => ({ ...r })), - schema: conditionalStyleSchema, - view: { ...conditionalStyleView }, + schema: conditionalStyleSchema as Schema, + view: { ...conditionalStyleView } as View, }; } if (dataMode === "unique-check") { return { data: uniqueCheckRows.map((r) => ({ ...r })), - schema: uniqueCheckSchema, - view: { ...uniqueCheckView }, + schema: uniqueCheckSchema as Schema, + view: { ...uniqueCheckView } as View, }; } if (dataMode === "unique-bool") { return { data: uniqueBoolRows.map((r) => ({ ...r })), - schema: uniqueBoolSchema, - view: { ...uniqueBoolView }, + schema: uniqueBoolSchema as Schema, + view: { ...uniqueBoolView } as View, }; } if (dataMode === "filter-sort") { return { data: filterSortRows.map((r) => ({ ...r })), - schema: filterSortSchema, - view: { ...filterSortView }, + schema: filterSortSchema as Schema, + view: { ...filterSortView } as View, }; } return { data: demoRows.map((r) => ({ ...r })), - schema: demoSchema, - view: { ...demoView }, + schema: demoSchema as Schema, + view: { ...demoView } as View, }; } @@ -234,7 +234,7 @@ function main() { user, }; - let core: ExtableCore | null = null; + let core: ExtableCore | null = null; let dataMode: DataMode = "standard"; let unsubscribeTable: (() => void) | null = null; let loadGeneration = 0; @@ -353,13 +353,14 @@ function main() { const config = cloneConfig(dataMode); currentConfig = config; updateDataNote(); - core = new ExtableCore({ + const nextCore = new ExtableCore({ root: tableRoot, defaultData: config.data, defaultView: { ...config.view, wrapText: config.view.wrapText ?? {} }, schema: config.schema, options: { ...options }, }); + core = nextCore; if (dataMode === "loading-async") { const gen = loadGeneration; loadTimer = window.setTimeout(() => { @@ -376,7 +377,7 @@ function main() { } const commitState = document.getElementById("commit-state"); - unsubscribeTable = core.subscribeTableState((next) => { + unsubscribeTable = nextCore.subscribeTableState((next) => { if (commitBtn) { commitBtn.toggleAttribute("disabled", !next.canCommit); if (options.editMode !== "commit") commitBtn.setAttribute("disabled", "true"); @@ -407,7 +408,7 @@ function main() { } }; - const history = core?.getUndoRedoHistory(); + const history = nextCore.getUndoRedoHistory(); if (history) { renderHistoryList(undoHistoryEl, history.undo); renderHistoryList(redoHistoryEl, history.redo); @@ -425,33 +426,33 @@ function main() { undoBtn?.addEventListener("click", () => core?.undo()); redoBtn?.addEventListener("click", () => core?.redo()); - for (const input of document.querySelectorAll('input[name="render-mode"]')) { + document.querySelectorAll('input[name="render-mode"]').forEach((input) => { input.addEventListener("change", () => { options.renderMode = input.value as Mode; rebuildCore(); }); - } + }); - for (const input of document.querySelectorAll('input[name="edit-mode"]')) { + document.querySelectorAll('input[name="edit-mode"]').forEach((input) => { input.addEventListener("change", () => { options.editMode = input.value as EditMode; rebuildCore(); }); - } + }); - for (const input of document.querySelectorAll('input[name="lock-mode"]')) { + document.querySelectorAll('input[name="lock-mode"]').forEach((input) => { input.addEventListener("change", () => { options.lockMode = input.value as LockMode; rebuildCore(); }); - } + }); - for (const input of document.querySelectorAll('input[name="data-mode"]')) { + document.querySelectorAll('input[name="data-mode"]').forEach((input) => { input.addEventListener("change", () => { dataMode = input.value as DataMode; rebuildCore(); }); - } + }); const commitBtn = document.getElementById("commit-btn"); commitBtn?.addEventListener("click", () => {