diff --git a/docs/dist/idocs.compiler.umd.js b/docs/dist/idocs.compiler.umd.js index a257c689..ae51754d 100644 --- a/docs/dist/idocs.compiler.umd.js +++ b/docs/dist/idocs.compiler.umd.js @@ -371,8 +371,8 @@ ${content} break; } case "table": { - const { dataSourceName, variableId, tabulatorOptions } = element; - const tableSpec = { dataSourceName, variableId, tabulatorOptions }; + const { dataSourceName, variableId, tabulatorOptions, editable } = element; + const tableSpec = { dataSourceName, variableId, tabulatorOptions, editable }; addSpec("tabulator", tableSpec); break; } diff --git a/docs/dist/idocs.editor.umd.js b/docs/dist/idocs.editor.umd.js index 97b62e58..a3bd9532 100644 --- a/docs/dist/idocs.editor.umd.js +++ b/docs/dist/idocs.editor.umd.js @@ -373,8 +373,8 @@ ${content} break; } case "table": { - const { dataSourceName, variableId, tabulatorOptions } = element; - const tableSpec = { dataSourceName, variableId, tabulatorOptions }; + const { dataSourceName, variableId, tabulatorOptions, editable } = element; + const tableSpec = { dataSourceName, variableId, tabulatorOptions, editable }; addSpec("tabulator", tableSpec); break; } @@ -853,6 +853,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy function pluginClassName(pluginName2) { return \`chartifact-plugin-\${pluginName2}\`; } + const newId = () => [...Date.now().toString(36) + Math.random().toString(36).slice(2)].sort(() => 0.5 - Math.random()).join(""); /*! * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. @@ -1882,17 +1883,30 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy ...flaggableJsonPlugin(pluginName$3, className$3, inspectTabulatorSpec, { style: "box-sizing: border-box;" }), hydrateComponent: async (renderer, errorHandler, specs) => { const tabulatorInstances = []; + const deleteFieldname = newId(); for (let index2 = 0; index2 < specs.length; index2++) { const specReview = specs[index2]; if (!specReview.approvedSpec) { continue; } const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + if (!container) { + continue; + } + const spec = specReview.approvedSpec; + const buttons = spec.editable ? \`
+ + +
\` : ""; + container.innerHTML = \`
+
+ \${buttons} +
\`; + const nestedDiv = container.querySelector(".tabulator-nested"); if (!Tabulator && index2 === 0) { errorHandler(new Error("Tabulator not found"), pluginName$3, index2, "init", container); continue; } - const spec = specReview.approvedSpec; if (!spec.dataSourceName || !spec.variableId) { errorHandler(new Error("Tabulator requires dataSourceName and variableId"), pluginName$3, index2, "init", container); continue; @@ -1908,8 +1922,19 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy if (spec.tabulatorOptions && Object.keys(spec.tabulatorOptions).length > 0) { options = spec.tabulatorOptions; } - const table = new Tabulator(container, options); - const tabulatorInstance = { id: \`\${pluginName$3}-\${index2}\`, spec, table, built: false }; + const selectableRows = !!(options == null ? void 0 : options.selectableRows) || false; + if (spec.editable && selectableRows) { + delete options.selectableRows; + } + const table = new Tabulator(nestedDiv, options); + const tabulatorInstance = { + id: \`\${pluginName$3}-\${index2}\`, + spec, + container, + table, + built: false, + selectableRows + }; table.on("tableBuilt", () => { table.off("tableBuilt"); tabulatorInstance.built = true; @@ -1917,65 +1942,141 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy tabulatorInstances.push(tabulatorInstance); } const instances = tabulatorInstances.map((tabulatorInstance, index2) => { - var _a; + const { container, spec, table, selectableRows } = tabulatorInstance; const initialSignals = [{ - name: tabulatorInstance.spec.dataSourceName, + name: spec.dataSourceName, value: null, priority: -1, isData: true }]; - if ((_a = tabulatorInstance.spec.tabulatorOptions) == null ? void 0 : _a.selectableRows) { + if (selectableRows || spec.editable) { initialSignals.push({ - name: tabulatorInstance.spec.variableId, + name: spec.variableId, value: [], priority: -1, isData: true }); } + const outputData = () => { + let data; + if (selectableRows) { + data = table.getSelectedData(); + } else { + data = table.getData(); + } + data.forEach((row) => { + delete row[deleteFieldname]; + }); + const value = structuredClone(data); + const batch = { + [spec.variableId]: { + value, + isData: true + } + }; + renderer.signalBus.log(tabulatorInstance.id, "sending batch", batch); + renderer.signalBus.broadcast(tabulatorInstance.id, batch); + }; + const setData = (data) => { + table.setData(data).then(() => { + let columns = table.getColumnDefinitions().filter((cd) => cd.field !== deleteFieldname).filter((cd) => cd.formatter !== "rowSelection"); + if (spec.editable) { + columns.unshift({ + headerSort: false, + title: "Delete", + field: deleteFieldname, + titleFormatter: "tickCross", + width: 40, + formatter: "tickCross", + cellClick: (e, cell) => { + cell.getRow().delete(); + outputData(); + } + }); + columns = columns.map((col) => { + if (col.editor === void 0) { + const type = col.type || col.sorter; + if (type === "number") { + return { ...col, editor: "number" }; + } + if (type === "date" || type === "datetime") { + return { ...col, editor: "date" }; + } + if (type === "boolean") { + return { ...col, editor: "tickCross" }; + } + return { ...col, editor: "input" }; + } + return col; + }); + } + table.setColumns(columns); + outputData(); + }).catch((error) => { + console.error(\`Error setting data for Tabulator \${spec.variableId}:\`, error); + }); + }; + if (spec.editable) { + const addRowBtn = container.querySelector(".tabulator-add-row"); + const resetBtn = container.querySelector(".tabulator-reset"); + if (addRowBtn) { + addRowBtn.onclick = () => { + table.addRow({}).then((row) => { + row.scrollTo(); + }); + }; + } + if (resetBtn) { + resetBtn.onclick = () => { + const value = renderer.signalBus.signalDeps[spec.dataSourceName].value; + if (Array.isArray(value)) { + setData(value); + } + }; + } + } return { ...tabulatorInstance, initialSignals, - recieveBatch: async (batch) => { - var _a2; - const newData = (_a2 = batch[tabulatorInstance.spec.dataSourceName]) == null ? void 0 : _a2.value; + recieveBatch: async (batch, from) => { + var _a; + const newData = (_a = batch[spec.dataSourceName]) == null ? void 0 : _a.value; if (newData) { if (!tabulatorInstance.built) { - tabulatorInstance.table.off("tableBuilt"); - tabulatorInstance.table.on("tableBuilt", () => { + table.off("tableBuilt"); + table.on("tableBuilt", () => { tabulatorInstance.built = true; - tabulatorInstance.table.off("tableBuilt"); - tabulatorInstance.table.setData(newData); + table.off("tableBuilt"); + setData(newData); }); } else { - tabulatorInstance.table.setData(newData); + setData(newData); } } }, beginListening(sharedSignals) { - var _a2; - if ((_a2 = tabulatorInstance.spec.tabulatorOptions) == null ? void 0 : _a2.selectableRows) { - for (const { isData, signalName } of sharedSignals) { - if (isData) { - const matchData = signalName === tabulatorInstance.spec.variableId; - if (matchData) { - tabulatorInstance.table.on("rowSelectionChanged", (e, rows) => { - const selectedData = tabulatorInstance.table.getSelectedData(); - const batch = { - [tabulatorInstance.spec.variableId]: { - value: selectedData, - isData: true - } - }; - renderer.signalBus.log(tabulatorInstance.id, "sending batch", batch); - renderer.signalBus.broadcast(tabulatorInstance.id, batch); - }); - } - } + if (selectableRows) { + const hasMatchingSignal = sharedSignals.some( + ({ isData, signalName }) => isData && signalName === spec.variableId + ); + if (hasMatchingSignal) { + table.on("rowSelectionChanged", (e, rows) => { + outputData(); + }); } } + if (spec.editable) { + table.on("cellEdited", (cell) => { + outputData(); + }); + } }, getCurrentSignalValue() { - return tabulatorInstance.table.getSelectedData(); + if (selectableRows) { + return tabulatorInstance.table.getSelectedData(); + } else { + return tabulatorInstance.table.getData(); + } }, destroy: () => { tabulatorInstance.table.destroy(); diff --git a/docs/dist/idocs.host.umd.js b/docs/dist/idocs.host.umd.js index 2368f244..0666800f 100644 --- a/docs/dist/idocs.host.umd.js +++ b/docs/dist/idocs.host.umd.js @@ -373,8 +373,8 @@ ${content} break; } case "table": { - const { dataSourceName, variableId, tabulatorOptions } = element; - const tableSpec = { dataSourceName, variableId, tabulatorOptions }; + const { dataSourceName, variableId, tabulatorOptions, editable } = element; + const tableSpec = { dataSourceName, variableId, tabulatorOptions, editable }; addSpec("tabulator", tableSpec); break; } @@ -853,6 +853,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy function pluginClassName(pluginName2) { return \`chartifact-plugin-\${pluginName2}\`; } + const newId = () => [...Date.now().toString(36) + Math.random().toString(36).slice(2)].sort(() => 0.5 - Math.random()).join(""); /*! * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. @@ -1882,17 +1883,30 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy ...flaggableJsonPlugin(pluginName$3, className$3, inspectTabulatorSpec, { style: "box-sizing: border-box;" }), hydrateComponent: async (renderer, errorHandler, specs) => { const tabulatorInstances = []; + const deleteFieldname = newId(); for (let index2 = 0; index2 < specs.length; index2++) { const specReview = specs[index2]; if (!specReview.approvedSpec) { continue; } const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + if (!container) { + continue; + } + const spec = specReview.approvedSpec; + const buttons = spec.editable ? \`
+ + +
\` : ""; + container.innerHTML = \`
+
+ \${buttons} +
\`; + const nestedDiv = container.querySelector(".tabulator-nested"); if (!Tabulator && index2 === 0) { errorHandler(new Error("Tabulator not found"), pluginName$3, index2, "init", container); continue; } - const spec = specReview.approvedSpec; if (!spec.dataSourceName || !spec.variableId) { errorHandler(new Error("Tabulator requires dataSourceName and variableId"), pluginName$3, index2, "init", container); continue; @@ -1908,8 +1922,19 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy if (spec.tabulatorOptions && Object.keys(spec.tabulatorOptions).length > 0) { options = spec.tabulatorOptions; } - const table = new Tabulator(container, options); - const tabulatorInstance = { id: \`\${pluginName$3}-\${index2}\`, spec, table, built: false }; + const selectableRows = !!(options == null ? void 0 : options.selectableRows) || false; + if (spec.editable && selectableRows) { + delete options.selectableRows; + } + const table = new Tabulator(nestedDiv, options); + const tabulatorInstance = { + id: \`\${pluginName$3}-\${index2}\`, + spec, + container, + table, + built: false, + selectableRows + }; table.on("tableBuilt", () => { table.off("tableBuilt"); tabulatorInstance.built = true; @@ -1917,65 +1942,141 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy tabulatorInstances.push(tabulatorInstance); } const instances = tabulatorInstances.map((tabulatorInstance, index2) => { - var _a; + const { container, spec, table, selectableRows } = tabulatorInstance; const initialSignals = [{ - name: tabulatorInstance.spec.dataSourceName, + name: spec.dataSourceName, value: null, priority: -1, isData: true }]; - if ((_a = tabulatorInstance.spec.tabulatorOptions) == null ? void 0 : _a.selectableRows) { + if (selectableRows || spec.editable) { initialSignals.push({ - name: tabulatorInstance.spec.variableId, + name: spec.variableId, value: [], priority: -1, isData: true }); } + const outputData = () => { + let data; + if (selectableRows) { + data = table.getSelectedData(); + } else { + data = table.getData(); + } + data.forEach((row) => { + delete row[deleteFieldname]; + }); + const value = structuredClone(data); + const batch = { + [spec.variableId]: { + value, + isData: true + } + }; + renderer.signalBus.log(tabulatorInstance.id, "sending batch", batch); + renderer.signalBus.broadcast(tabulatorInstance.id, batch); + }; + const setData = (data) => { + table.setData(data).then(() => { + let columns = table.getColumnDefinitions().filter((cd) => cd.field !== deleteFieldname).filter((cd) => cd.formatter !== "rowSelection"); + if (spec.editable) { + columns.unshift({ + headerSort: false, + title: "Delete", + field: deleteFieldname, + titleFormatter: "tickCross", + width: 40, + formatter: "tickCross", + cellClick: (e, cell) => { + cell.getRow().delete(); + outputData(); + } + }); + columns = columns.map((col) => { + if (col.editor === void 0) { + const type = col.type || col.sorter; + if (type === "number") { + return { ...col, editor: "number" }; + } + if (type === "date" || type === "datetime") { + return { ...col, editor: "date" }; + } + if (type === "boolean") { + return { ...col, editor: "tickCross" }; + } + return { ...col, editor: "input" }; + } + return col; + }); + } + table.setColumns(columns); + outputData(); + }).catch((error) => { + console.error(\`Error setting data for Tabulator \${spec.variableId}:\`, error); + }); + }; + if (spec.editable) { + const addRowBtn = container.querySelector(".tabulator-add-row"); + const resetBtn = container.querySelector(".tabulator-reset"); + if (addRowBtn) { + addRowBtn.onclick = () => { + table.addRow({}).then((row) => { + row.scrollTo(); + }); + }; + } + if (resetBtn) { + resetBtn.onclick = () => { + const value = renderer.signalBus.signalDeps[spec.dataSourceName].value; + if (Array.isArray(value)) { + setData(value); + } + }; + } + } return { ...tabulatorInstance, initialSignals, - recieveBatch: async (batch) => { - var _a2; - const newData = (_a2 = batch[tabulatorInstance.spec.dataSourceName]) == null ? void 0 : _a2.value; + recieveBatch: async (batch, from) => { + var _a; + const newData = (_a = batch[spec.dataSourceName]) == null ? void 0 : _a.value; if (newData) { if (!tabulatorInstance.built) { - tabulatorInstance.table.off("tableBuilt"); - tabulatorInstance.table.on("tableBuilt", () => { + table.off("tableBuilt"); + table.on("tableBuilt", () => { tabulatorInstance.built = true; - tabulatorInstance.table.off("tableBuilt"); - tabulatorInstance.table.setData(newData); + table.off("tableBuilt"); + setData(newData); }); } else { - tabulatorInstance.table.setData(newData); + setData(newData); } } }, beginListening(sharedSignals) { - var _a2; - if ((_a2 = tabulatorInstance.spec.tabulatorOptions) == null ? void 0 : _a2.selectableRows) { - for (const { isData, signalName } of sharedSignals) { - if (isData) { - const matchData = signalName === tabulatorInstance.spec.variableId; - if (matchData) { - tabulatorInstance.table.on("rowSelectionChanged", (e, rows) => { - const selectedData = tabulatorInstance.table.getSelectedData(); - const batch = { - [tabulatorInstance.spec.variableId]: { - value: selectedData, - isData: true - } - }; - renderer.signalBus.log(tabulatorInstance.id, "sending batch", batch); - renderer.signalBus.broadcast(tabulatorInstance.id, batch); - }); - } - } + if (selectableRows) { + const hasMatchingSignal = sharedSignals.some( + ({ isData, signalName }) => isData && signalName === spec.variableId + ); + if (hasMatchingSignal) { + table.on("rowSelectionChanged", (e, rows) => { + outputData(); + }); } } + if (spec.editable) { + table.on("cellEdited", (cell) => { + outputData(); + }); + } }, getCurrentSignalValue() { - return tabulatorInstance.table.getSelectedData(); + if (selectableRows) { + return tabulatorInstance.table.getSelectedData(); + } else { + return tabulatorInstance.table.getData(); + } }, destroy: () => { tabulatorInstance.table.destroy(); diff --git a/docs/dist/idocs.sandbox.umd.js b/docs/dist/idocs.sandbox.umd.js index e225188f..f4924cb8 100644 --- a/docs/dist/idocs.sandbox.umd.js +++ b/docs/dist/idocs.sandbox.umd.js @@ -464,6 +464,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy function pluginClassName(pluginName2) { return \`chartifact-plugin-\${pluginName2}\`; } + const newId = () => [...Date.now().toString(36) + Math.random().toString(36).slice(2)].sort(() => 0.5 - Math.random()).join(""); /*! * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. @@ -1493,17 +1494,30 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy ...flaggableJsonPlugin(pluginName$3, className$3, inspectTabulatorSpec, { style: "box-sizing: border-box;" }), hydrateComponent: async (renderer, errorHandler, specs) => { const tabulatorInstances = []; + const deleteFieldname = newId(); for (let index2 = 0; index2 < specs.length; index2++) { const specReview = specs[index2]; if (!specReview.approvedSpec) { continue; } const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + if (!container) { + continue; + } + const spec = specReview.approvedSpec; + const buttons = spec.editable ? \`
+ + +
\` : ""; + container.innerHTML = \`
+
+ \${buttons} +
\`; + const nestedDiv = container.querySelector(".tabulator-nested"); if (!Tabulator && index2 === 0) { errorHandler(new Error("Tabulator not found"), pluginName$3, index2, "init", container); continue; } - const spec = specReview.approvedSpec; if (!spec.dataSourceName || !spec.variableId) { errorHandler(new Error("Tabulator requires dataSourceName and variableId"), pluginName$3, index2, "init", container); continue; @@ -1519,8 +1533,19 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy if (spec.tabulatorOptions && Object.keys(spec.tabulatorOptions).length > 0) { options = spec.tabulatorOptions; } - const table = new Tabulator(container, options); - const tabulatorInstance = { id: \`\${pluginName$3}-\${index2}\`, spec, table, built: false }; + const selectableRows = !!(options == null ? void 0 : options.selectableRows) || false; + if (spec.editable && selectableRows) { + delete options.selectableRows; + } + const table = new Tabulator(nestedDiv, options); + const tabulatorInstance = { + id: \`\${pluginName$3}-\${index2}\`, + spec, + container, + table, + built: false, + selectableRows + }; table.on("tableBuilt", () => { table.off("tableBuilt"); tabulatorInstance.built = true; @@ -1528,65 +1553,141 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy tabulatorInstances.push(tabulatorInstance); } const instances = tabulatorInstances.map((tabulatorInstance, index2) => { - var _a; + const { container, spec, table, selectableRows } = tabulatorInstance; const initialSignals = [{ - name: tabulatorInstance.spec.dataSourceName, + name: spec.dataSourceName, value: null, priority: -1, isData: true }]; - if ((_a = tabulatorInstance.spec.tabulatorOptions) == null ? void 0 : _a.selectableRows) { + if (selectableRows || spec.editable) { initialSignals.push({ - name: tabulatorInstance.spec.variableId, + name: spec.variableId, value: [], priority: -1, isData: true }); } + const outputData = () => { + let data; + if (selectableRows) { + data = table.getSelectedData(); + } else { + data = table.getData(); + } + data.forEach((row) => { + delete row[deleteFieldname]; + }); + const value = structuredClone(data); + const batch = { + [spec.variableId]: { + value, + isData: true + } + }; + renderer.signalBus.log(tabulatorInstance.id, "sending batch", batch); + renderer.signalBus.broadcast(tabulatorInstance.id, batch); + }; + const setData = (data) => { + table.setData(data).then(() => { + let columns = table.getColumnDefinitions().filter((cd) => cd.field !== deleteFieldname).filter((cd) => cd.formatter !== "rowSelection"); + if (spec.editable) { + columns.unshift({ + headerSort: false, + title: "Delete", + field: deleteFieldname, + titleFormatter: "tickCross", + width: 40, + formatter: "tickCross", + cellClick: (e, cell) => { + cell.getRow().delete(); + outputData(); + } + }); + columns = columns.map((col) => { + if (col.editor === void 0) { + const type = col.type || col.sorter; + if (type === "number") { + return { ...col, editor: "number" }; + } + if (type === "date" || type === "datetime") { + return { ...col, editor: "date" }; + } + if (type === "boolean") { + return { ...col, editor: "tickCross" }; + } + return { ...col, editor: "input" }; + } + return col; + }); + } + table.setColumns(columns); + outputData(); + }).catch((error) => { + console.error(\`Error setting data for Tabulator \${spec.variableId}:\`, error); + }); + }; + if (spec.editable) { + const addRowBtn = container.querySelector(".tabulator-add-row"); + const resetBtn = container.querySelector(".tabulator-reset"); + if (addRowBtn) { + addRowBtn.onclick = () => { + table.addRow({}).then((row) => { + row.scrollTo(); + }); + }; + } + if (resetBtn) { + resetBtn.onclick = () => { + const value = renderer.signalBus.signalDeps[spec.dataSourceName].value; + if (Array.isArray(value)) { + setData(value); + } + }; + } + } return { ...tabulatorInstance, initialSignals, - recieveBatch: async (batch) => { - var _a2; - const newData = (_a2 = batch[tabulatorInstance.spec.dataSourceName]) == null ? void 0 : _a2.value; + recieveBatch: async (batch, from) => { + var _a; + const newData = (_a = batch[spec.dataSourceName]) == null ? void 0 : _a.value; if (newData) { if (!tabulatorInstance.built) { - tabulatorInstance.table.off("tableBuilt"); - tabulatorInstance.table.on("tableBuilt", () => { + table.off("tableBuilt"); + table.on("tableBuilt", () => { tabulatorInstance.built = true; - tabulatorInstance.table.off("tableBuilt"); - tabulatorInstance.table.setData(newData); + table.off("tableBuilt"); + setData(newData); }); } else { - tabulatorInstance.table.setData(newData); + setData(newData); } } }, beginListening(sharedSignals) { - var _a2; - if ((_a2 = tabulatorInstance.spec.tabulatorOptions) == null ? void 0 : _a2.selectableRows) { - for (const { isData, signalName } of sharedSignals) { - if (isData) { - const matchData = signalName === tabulatorInstance.spec.variableId; - if (matchData) { - tabulatorInstance.table.on("rowSelectionChanged", (e, rows) => { - const selectedData = tabulatorInstance.table.getSelectedData(); - const batch = { - [tabulatorInstance.spec.variableId]: { - value: selectedData, - isData: true - } - }; - renderer.signalBus.log(tabulatorInstance.id, "sending batch", batch); - renderer.signalBus.broadcast(tabulatorInstance.id, batch); - }); - } - } + if (selectableRows) { + const hasMatchingSignal = sharedSignals.some( + ({ isData, signalName }) => isData && signalName === spec.variableId + ); + if (hasMatchingSignal) { + table.on("rowSelectionChanged", (e, rows) => { + outputData(); + }); } } + if (spec.editable) { + table.on("cellEdited", (cell) => { + outputData(); + }); + } }, getCurrentSignalValue() { - return tabulatorInstance.table.getSelectedData(); + if (selectableRows) { + return tabulatorInstance.table.getSelectedData(); + } else { + return tabulatorInstance.table.getData(); + } }, destroy: () => { tabulatorInstance.table.destroy(); diff --git a/docs/schema/idoc_v1.d.ts b/docs/schema/idoc_v1.d.ts index e2ef81a3..4a3214a3 100644 --- a/docs/schema/idoc_v1.d.ts +++ b/docs/schema/idoc_v1.d.ts @@ -215,6 +215,7 @@ interface TableElement extends TableElementProps { interface TableElementProps extends VariableControl { /** Name of the data source to use for incoming data (output data is available via the variableId of this table element) */ dataSourceName: string; + editable?: boolean; /** Tabulator options (must be JSON stringify-able, so no callbacks allowed) */ tabulatorOptions?: object; } diff --git a/docs/schema/idoc_v1.json b/docs/schema/idoc_v1.json index b012e28c..9df07570 100644 --- a/docs/schema/idoc_v1.json +++ b/docs/schema/idoc_v1.json @@ -2824,6 +2824,9 @@ "description": "Name of the data source to use for incoming data (output data is available via the variableId of this table element)", "type": "string" }, + "editable": { + "type": "boolean" + }, "label": { "description": "optional label if the variableId is not descriptive enough", "type": "string" diff --git a/package.json b/package.json index 3bcd52dc..220e5c93 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": { "clean": "npm run clean --workspaces --if-present", "build": "npm run build:00 --workspaces --if-present && npm run build:01 --workspaces --if-present && npm run build:02 --workspaces --if-present && npm run build:03 --workspaces --if-present && npm run build:04 --workspaces --if-present && npm run build:05 --workspaces --if-present && npm run build:06 --workspaces --if-present && npm run build:07 --workspaces --if-present && npm run build:08 --workspaces --if-present", + "build-host": "npm run build -w @microsoft/interactive-document-markdown && npm run build -w @microsoft/chartifact-sandbox && npm run build -w @microsoft/interactive-document-host", "start": "concurrently \"npm run dev -w common\" \"npm run dev -w @microsoft/interactive-document-markdown\" \"npm run dev -w schema\" \"npm run dev -w @microsoft/interactive-document-compiler\" \"npm run dev -w editor\"", "package": "npm run package --workspaces --if-present" }, diff --git a/packages/compiler/src/md.ts b/packages/compiler/src/md.ts index 1e798396..9f229d73 100644 --- a/packages/compiler/src/md.ts +++ b/packages/compiler/src/md.ts @@ -174,8 +174,8 @@ function groupMarkdown(group: ElementGroup, variables: Variable[], vegaScope: Ve break; } case 'table': { - const { dataSourceName, variableId, tabulatorOptions } = element; - const tableSpec: Plugins.TabulatorSpec = { dataSourceName, variableId, tabulatorOptions }; + const { dataSourceName, variableId, tabulatorOptions, editable } = element; + const tableSpec: Plugins.TabulatorSpec = { dataSourceName, variableId, tabulatorOptions, editable }; addSpec('tabulator', tableSpec); break; } diff --git a/packages/markdown/src/plugins/tabulator.ts b/packages/markdown/src/plugins/tabulator.ts index 9c633974..d6666569 100644 --- a/packages/markdown/src/plugins/tabulator.ts +++ b/packages/markdown/src/plugins/tabulator.ts @@ -5,7 +5,7 @@ import { Batch, IInstance, Plugin, RawFlaggableSpec } from '../factory.js'; import { Tabulator as TabulatorType, Options as TabulatorOptions } from 'tabulator-tables'; -import { pluginClassName } from './util.js'; +import { newId, pluginClassName } from './util.js'; import { TableElementProps } from 'schema'; import { flaggableJsonPlugin } from './config.js'; import { PluginNames } from './interfaces.js'; @@ -13,8 +13,10 @@ import { PluginNames } from './interfaces.js'; interface TabulatorInstance { id: string; spec: TabulatorSpec; + container: Element; table: TabulatorType; built: boolean; + selectableRows: boolean; } export interface TabulatorSpec extends TableElementProps { @@ -38,20 +40,39 @@ export const tabulatorPlugin: Plugin = { ...flaggableJsonPlugin(pluginName, className, inspectTabulatorSpec, { style: 'box-sizing: border-box;' }), hydrateComponent: async (renderer, errorHandler, specs) => { const tabulatorInstances: TabulatorInstance[] = []; + + // Generate a unique field name for the delete column, used for all tables in this hydration + const deleteFieldname = newId(); + for (let index = 0; index < specs.length; index++) { const specReview = specs[index]; if (!specReview.approvedSpec) { continue; } const container = renderer.element.querySelector(`#${specReview.containerId}`); + if (!container) { + continue; + } + + const spec: TabulatorSpec = specReview.approvedSpec; + const buttons = spec.editable + ? `
+ + +
` + : ''; + + container.innerHTML = `
+
+ ${buttons} +
`; + const nestedDiv = container.querySelector('.tabulator-nested'); if (!Tabulator && index === 0) { errorHandler(new Error('Tabulator not found'), pluginName, index, 'init', container); continue; } - const spec: TabulatorSpec = specReview.approvedSpec; - if (!spec.dataSourceName || !spec.variableId) { errorHandler(new Error('Tabulator requires dataSourceName and variableId'), pluginName, index, 'init', container); continue; @@ -71,8 +92,21 @@ export const tabulatorPlugin: Plugin = { options = spec.tabulatorOptions; } - const table = new Tabulator(container as HTMLElement, options); - const tabulatorInstance: TabulatorInstance = { id: `${pluginName}-${index}`, spec, table, built: false }; + const selectableRows = !!options?.selectableRows || false; + if (spec.editable && selectableRows) { + delete options.selectableRows; //remove selectableRows from options if editable + } + + const table = new Tabulator(nestedDiv as HTMLElement, options); + + const tabulatorInstance: TabulatorInstance = { + id: `${pluginName}-${index}`, + spec, + container, + table, + built: false, + selectableRows, + }; table.on('tableBuilt', () => { table.off('tableBuilt'); tabulatorInstance.built = true; @@ -80,63 +114,169 @@ export const tabulatorPlugin: Plugin = { tabulatorInstances.push(tabulatorInstance); } const instances: IInstance[] = tabulatorInstances.map((tabulatorInstance, index) => { + const { container, spec, table, selectableRows } = tabulatorInstance; const initialSignals = [{ - name: tabulatorInstance.spec.dataSourceName, + name: spec.dataSourceName, value: null, priority: -1, isData: true, }]; - if (tabulatorInstance.spec.tabulatorOptions?.selectableRows) { + if (selectableRows || spec.editable) { initialSignals.push({ - name: tabulatorInstance.spec.variableId, + name: spec.variableId, value: [], priority: -1, isData: true, }); } + const outputData = () => { + let data: object[]; + if (selectableRows) { + data = table.getSelectedData(); + } else { + data = table.getData(); + } + + //remove the delete column if it exists + data.forEach(row => { + delete row[deleteFieldname]; + }); + + + // Use structuredClone to ensure deep copy + // vega may efficiently have symbols on data to cache a datum's values + // so this needs to appear to be new data + const value = structuredClone(data); + + const batch: Batch = { + [spec.variableId]: { + value, + isData: true, + }, + }; + renderer.signalBus.log(tabulatorInstance.id, 'sending batch', batch); + renderer.signalBus.broadcast(tabulatorInstance.id, batch); + } + const setData = (data: object[]) => { + table.setData(data).then(() => { + + // Get current column definitions + let columns = table.getColumnDefinitions() + .filter(cd => cd.field !== deleteFieldname) + .filter(cd => cd.formatter !== 'rowSelection'); + + if (spec.editable) { + + //inject a column at the beginning for delete + columns.unshift({ + headerSort: false, + title: "Delete", + field: deleteFieldname, + titleFormatter: 'tickCross', + width: 40, + formatter: "tickCross", + cellClick: (e, cell) => { + cell.getRow().delete(); + outputData(); + } + }); + + columns = columns.map(col => { + // Only set editor if not already defined + if (col.editor === undefined) { + // Prefer explicit type, fallback to sorter + const type = (col as any).type || col.sorter; + if (type === "number") { + return { ...col, editor: "number" }; + } + if (type === "date" || type === "datetime") { + return { ...col, editor: "date" }; + } + if (type === "boolean") { + return { ...col, editor: "tickCross" }; + } + // Add more types as needed + return { ...col, editor: "input" }; + } + return col; + }); + } + table.setColumns(columns); + + + outputData(); + + }).catch((error) => { + console.error(`Error setting data for Tabulator ${spec.variableId}:`, error); + // errorHandler(error, pluginName, index, 'setData', container); + }); + }; + + if (spec.editable) { + const addRowBtn = container.querySelector('.tabulator-add-row') as HTMLButtonElement; + const resetBtn = container.querySelector('.tabulator-reset') as HTMLButtonElement; + + if (addRowBtn) { + addRowBtn.onclick = () => { + table.addRow({}).then((row) => { + row.scrollTo(); + }); + }; + } + if (resetBtn) { + resetBtn.onclick = () => { + const value = renderer.signalBus.signalDeps[spec.dataSourceName].value; + if (Array.isArray(value)) { + setData(value); + } + }; + } + } + return { ...tabulatorInstance, initialSignals, - recieveBatch: async (batch) => { - const newData = batch[tabulatorInstance.spec.dataSourceName]?.value as object[]; + recieveBatch: async (batch, from) => { + const newData = batch[spec.dataSourceName]?.value as object[]; if (newData) { //make sure tabulator is ready before setting data if (!tabulatorInstance.built) { - tabulatorInstance.table.off('tableBuilt'); - tabulatorInstance.table.on('tableBuilt', () => { + table.off('tableBuilt'); + table.on('tableBuilt', () => { tabulatorInstance.built = true; - tabulatorInstance.table.off('tableBuilt'); - tabulatorInstance.table.setData(newData); + table.off('tableBuilt'); + setData(newData); }); } else { - tabulatorInstance.table.setData(newData); + setData(newData); } } }, beginListening(sharedSignals) { - if (tabulatorInstance.spec.tabulatorOptions?.selectableRows) { - for (const { isData, signalName } of sharedSignals) { - if (isData) { - const matchData = signalName === tabulatorInstance.spec.variableId; - if (matchData) { - tabulatorInstance.table.on('rowSelectionChanged', (e, rows) => { - const selectedData = tabulatorInstance.table.getSelectedData(); - const batch: Batch = { - [tabulatorInstance.spec.variableId]: { - value: selectedData, - isData: true, - }, - }; - renderer.signalBus.log(tabulatorInstance.id, 'sending batch', batch); - renderer.signalBus.broadcast(tabulatorInstance.id, batch); - }); - } - } + if (selectableRows) { + const hasMatchingSignal = sharedSignals.some(({ isData, signalName }) => + isData && signalName === spec.variableId + ); + + if (hasMatchingSignal) { + table.on('rowSelectionChanged', (e, rows) => { + outputData(); + }); } } + if (spec.editable) { + table.on('cellEdited', (cell) => { + outputData(); + }); + } }, getCurrentSignalValue() { - return tabulatorInstance.table.getSelectedData(); + if (selectableRows) { + return tabulatorInstance.table.getSelectedData(); + } else { + // When editable, return all data since that's what gets broadcast + return tabulatorInstance.table.getData(); + } }, destroy: () => { tabulatorInstance.table.destroy(); diff --git a/packages/markdown/src/plugins/util.ts b/packages/markdown/src/plugins/util.ts index 58fff829..e063e9a6 100644 --- a/packages/markdown/src/plugins/util.ts +++ b/packages/markdown/src/plugins/util.ts @@ -28,3 +28,5 @@ export function getJsonScriptTag(container: Element, errorHandler: (error: Error export function pluginClassName(pluginName: string) { return `chartifact-plugin-${pluginName}`; } + +export const newId = () => ([...Date.now().toString(36) + Math.random().toString(36).slice(2)]).sort(() => 0.5 - Math.random()).join(''); diff --git a/packages/schema/src/interactive.ts b/packages/schema/src/interactive.ts index 85840666..b631e66f 100644 --- a/packages/schema/src/interactive.ts +++ b/packages/schema/src/interactive.ts @@ -135,6 +135,8 @@ export interface TableElementProps extends VariableControl { /** Name of the data source to use for incoming data (output data is available via the variableId of this table element) */ dataSourceName: string; + + editable?: boolean; /** Tabulator options (must be JSON stringify-able, so no callbacks allowed) */ tabulatorOptions?: object;