@openpresentation/opf-editor 0.11.3 → 0.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +82 -58
  2. package/dist/annotations.d.ts +16 -16
  3. package/dist/annotations.js +71 -79
  4. package/dist/assets.d.ts +7 -7
  5. package/dist/assets.js +15 -15
  6. package/dist/background-options.d.ts +2 -2
  7. package/dist/background-options.js +6 -6
  8. package/dist/block-controls.js +7 -7
  9. package/dist/block-convert.d.ts +7 -7
  10. package/dist/block-convert.js +23 -21
  11. package/dist/blocks.d.ts +8 -8
  12. package/dist/blocks.js +41 -40
  13. package/dist/canvas-fields.js +35 -12
  14. package/dist/canvas.d.ts +22 -30
  15. package/dist/canvas.js +57 -64
  16. package/dist/chart-data.d.ts +12 -10
  17. package/dist/chart-data.js +22 -21
  18. package/dist/chart-options-panel.d.ts +2 -2
  19. package/dist/chart-options-panel.js +82 -8
  20. package/dist/chart-options.d.ts +35 -9
  21. package/dist/chart-options.js +133 -40
  22. package/dist/checks.js +21 -0
  23. package/dist/color-authoring.js +1 -1
  24. package/dist/content-actions.d.ts +15 -15
  25. package/dist/content-actions.js +71 -70
  26. package/dist/content-controls.js +12 -12
  27. package/dist/data-grid.js +179 -10
  28. package/dist/data.d.ts +23 -2
  29. package/dist/data.js +40 -1
  30. package/dist/dataset-refs.d.ts +17 -0
  31. package/dist/dataset-refs.js +45 -0
  32. package/dist/design-controls.js +66 -47
  33. package/dist/design-options.d.ts +16 -14
  34. package/dist/design-options.js +86 -74
  35. package/dist/edit-helpers.js +8 -7
  36. package/dist/exact-source.js +5 -5
  37. package/dist/export.d.ts +5 -7
  38. package/dist/export.js +40 -14
  39. package/dist/find-panel.js +5 -5
  40. package/dist/find-replace.d.ts +3 -3
  41. package/dist/find-replace.js +35 -20
  42. package/dist/font-gate.js +49 -58
  43. package/dist/galleries.js +36 -52
  44. package/dist/grid-model.d.ts +99 -24
  45. package/dist/grid-model.js +577 -101
  46. package/dist/image-crop.d.ts +4 -4
  47. package/dist/image-crop.js +20 -20
  48. package/dist/image-cropper.js +3 -3
  49. package/dist/index.d.ts +28 -28
  50. package/dist/index.js +74 -90
  51. package/dist/json-options.js +17 -16
  52. package/dist/layout-handles.js +5 -5
  53. package/dist/layout-placeholders.js +4 -4
  54. package/dist/layout.d.ts +2 -2
  55. package/dist/layout.js +8 -7
  56. package/dist/numbering-panel.js +3 -5
  57. package/dist/numbering.d.ts +3 -5
  58. package/dist/numbering.js +14 -26
  59. package/dist/outline-view.js +24 -24
  60. package/dist/outline.d.ts +12 -12
  61. package/dist/outline.js +52 -50
  62. package/dist/persistence.d.ts +1 -1
  63. package/dist/persistence.js +19 -18
  64. package/dist/presentation-ids.js +2 -2
  65. package/dist/react.js +3 -3
  66. package/dist/review-panel.d.ts +30 -13
  67. package/dist/review-panel.js +154 -53
  68. package/dist/review.d.ts +36 -43
  69. package/dist/review.js +92 -53
  70. package/dist/rich-text-input.js +4 -3
  71. package/dist/rich-text-toolbar.js +5 -3
  72. package/dist/rich-text.js +5 -2
  73. package/dist/schema-inspector.d.ts +2 -2
  74. package/dist/schema-inspector.js +16 -11
  75. package/dist/schema.d.ts +1 -1
  76. package/dist/schema.js +25 -5
  77. package/dist/slide-manager.d.ts +2 -2
  78. package/dist/slide-manager.js +18 -18
  79. package/dist/slides.d.ts +20 -20
  80. package/dist/slides.js +99 -97
  81. package/dist/svelte.js +1 -1
  82. package/dist/switches.d.ts +10 -10
  83. package/dist/switches.js +136 -109
  84. package/dist/table-options.d.ts +8 -8
  85. package/dist/table-options.js +41 -30
  86. package/dist/table-structure.d.ts +10 -10
  87. package/dist/table-structure.js +21 -21
  88. package/dist/template-panel.d.ts +3 -3
  89. package/dist/template-panel.js +37 -13
  90. package/dist/templates.d.ts +22 -11
  91. package/dist/templates.js +48 -55
  92. package/dist/transfer.d.ts +4 -4
  93. package/dist/transfer.js +74 -54
  94. package/package.json +10 -6
  95. package/dist/font-defaults.js +0 -25
@@ -1,26 +1,47 @@
1
1
  // The data model behind the data grid (RR-24): one table-shaped view of a chart's inline data and of a
2
2
  // table, and every edit to it as one validated, undoable patch.
3
3
  //
4
- // A chart is `{ type, data: { columns, rows } }`: `columns` are labels, `rows` are arrays aligned to them. The
5
- // first column holds the categories (the x axis, or the slice names) and every further column is one series,
6
- // named by its column label, in the order the renderer draws them. (A scatter chart reads its x values from the
7
- // second column when it has three or more; a chart type that needs one column only reads the first.) A table is
8
- // `{ columns?, rows }` whose cells are plain values, rich runs or styled cells with spans, as table-options.js
9
- // describes.
4
+ // A chart is `{ type, data: { columns, rows }, mapping? }`: `columns` are labels (a string, or a `DataColumn`
5
+ // `{ name, format? }`), `rows` are arrays aligned to them. By default the first column holds the categories (the x axis, or
6
+ // the slice names) and every further column is one series, named by its column label, in the order the renderer draws them.
7
+ // (A scatter chart reads its x values from the second column; a chart type that needs one column only reads the first.)
8
+ // `mapping` names the category, X and series columns instead. A table is `{ columns?, rows }` whose cells are plain values,
9
+ // rich runs or styled cells with spans, as table-options.js describes.
10
+ //
11
+ // RR-54: a chart (`data: { dataset, fields? }`) or table (`{ dataset, fields? }`) may take its data from a shared top-level
12
+ // dataset. The grid then edits `/datasets/<id>` through the `fields` selection: edits map back to the dataset's own column
13
+ // indices, a column added while `fields` is set goes to the dataset and to `fields`, and a deleted column leaves `fields` (the
14
+ // dataset keeps it). Renaming a column keeps every `fields` and `mapping` that names it in step.
10
15
  //
11
16
  // Both are read as "lines": the header line (when there is one) followed by the body rows. The operations below
12
- // change lines and columns, keep merged cells whole or refuse with a reason, and return a `{ document, patches,
17
+ // change lines and columns, keep merged cells whole or refuse with a reason, and return a `{ presentation, patches,
13
18
  // changed }` description like table-options.js does. Nothing here touches a DOM.
14
- import { getValueAtPath, opfPathToJsonPointer, splitOpfPath, validateOpfDocument } from "./index.js";
15
- import { checkedDocument, fail } from "./edit-helpers.js";
19
+ import { chartNumber, formatDataNumber, inlineChartData, inlineTableData, isXYChartType, numberFormatError } from "@openpresentation/opf";
20
+ import { getValueAtPath, opfPathToJsonPointer, splitOpfPath } from "./index.js";
21
+ import { checkedDocument, fail, same } from "./edit-helpers.js";
22
+ import { datasetUsage, isDatasetRef, walkDatasetItems } from "./dataset-refs.js";
16
23
  import { richTextContent, updateRichTextInput } from "./rich-text.js";
17
24
  import { formatGridNumber, isCanonicalNumber, parseDelimited, parseGridNumber, resolveNumberFormat, toDelimited } from "./grid-text.js";
18
25
  import { applyTableStyleToTable, readTableStyleOfTable } from "./table-options.js";
26
+ import { checkFormat } from "./checks.js";
19
27
 
20
28
  const isObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
21
29
  const isStyled = (cell) => isObject(cell) && Object.hasOwn(cell, "value");
22
- const valueOf = (cell) => (isStyled(cell) ? cell.value : cell);
30
+ // A header may be a DataColumn `{ name, format? }`: its text is `name`.
31
+ const isColumnObject = (cell) => isObject(cell) && Object.hasOwn(cell, "name") && !Object.hasOwn(cell, "value");
32
+ const valueOf = (cell) => (isStyled(cell) ? cell.value : isColumnObject(cell) ? cell.name : cell);
33
+ const withValue = (raw, value) => (isStyled(raw) ? { ...raw, value } : isColumnObject(raw) ? { ...raw, name: value === null || value === undefined ? "" : String(value) } : value);
34
+ const formatOf = (raw) => ((isStyled(raw) || isColumnObject(raw)) && typeof raw.format === "string" ? raw.format : undefined);
23
35
  const clone = (value) => structuredClone(value);
36
+ const columnNameOf = (column) => (typeof column === "string" ? column : isObject(column) && typeof column.name === "string" ? column.name : "");
37
+ // A body row remembers which dataset row it came from, so a row that moves, sorts or is deleted keeps the columns `fields` hides.
38
+ const LINE_ORIGIN = Symbol("opf.grid.row");
39
+ const cloneLines = (lines) =>
40
+ lines.map((line) => {
41
+ const copy = clone(line);
42
+ if (line[LINE_ORIGIN] !== undefined) copy[LINE_ORIGIN] = line[LINE_ORIGIN];
43
+ return copy;
44
+ });
24
45
  const spansOf = (raw) => ({ rs: isStyled(raw) ? (raw.rowSpan ?? 1) : 1, cs: isStyled(raw) ? (raw.colSpan ?? 1) : 1 });
25
46
 
26
47
  /** Spreadsheet column letters: 0 is A, 25 is Z, 26 is AA. */
@@ -38,12 +59,34 @@ const decimalOf = (options = {}) => (options.decimal === "," || options.decimal
38
59
 
39
60
  // --- locating ----------------------------------------------------------------------------------
40
61
 
62
+ const datasetsOf = (presentation) => (isObject(presentation) && isObject(presentation.datasets) ? presentation.datasets : {});
63
+
64
+ /** The dataset a chart or table reference points at, and the dataset columns its `fields` select (by index). */
65
+ function datasetOf(presentation, ref, path, refParts) {
66
+ const id = ref.dataset;
67
+ const datasets = datasetsOf(presentation);
68
+ const data = Object.hasOwn(datasets, id) ? datasets[id] : undefined;
69
+ if (!isObject(data) || !Array.isArray(data.columns) || !Array.isArray(data.rows))
70
+ throw fail("dataset-unavailable", `This item uses the shared dataset '${id}', which the document does not hold.`, { path, dataset: id });
71
+ const names = data.columns.map(columnNameOf);
72
+ const fields = Array.isArray(ref.fields) ? ref.fields : undefined;
73
+ let indices = names.map((_, index) => index);
74
+ if (fields) {
75
+ indices = fields.map((field) => names.indexOf(field));
76
+ const missing = fields.find((_, index) => indices[index] < 0);
77
+ if (missing !== undefined) throw fail("dataset-unavailable", `This item selects the column ${JSON.stringify(missing)}, which dataset '${id}' does not have.`, { path, dataset: id, field: missing });
78
+ }
79
+ return { data, dataset: { id, fields, indices, refParts, ref } };
80
+ }
81
+
41
82
  /**
42
83
  * Find the data a path points at. A chart path ends in `chart` (or `chart.data`) and a table path in `table`.
43
- * Throws `grid-target-not-found`, `table-not-found` or `chart-data-source` (a chart that reads its data from a source
44
- * has no inline data to edit).
84
+ * Throws `grid-target-not-found` (also for a chart without inline columns and rows), `table-not-found` or `dataset-unavailable` (the shared dataset or one of its `fields` is missing).
85
+ * A chart or table that takes its data from a dataset (RR-54) is located at the dataset: `data` is the dataset, `dataParts`
86
+ * is `["datasets", id]` and `dataset` is `{ id, fields, indices, refParts, ref }` (`indices` maps each shown column to the
87
+ * dataset's own column).
45
88
  */
46
- export function locateGridData(document, path) {
89
+ export function locateGridData(presentation, path) {
47
90
  let parts;
48
91
  try {
49
92
  parts = Array.isArray(path) ? [...path] : splitOpfPath(path);
@@ -51,27 +94,38 @@ export function locateGridData(document, path) {
51
94
  throw fail("grid-target-not-found", "Choose a chart or a table.", { path });
52
95
  }
53
96
  if (parts.at(-1) === "data" && parts.at(-2) === "chart") parts = parts.slice(0, -1);
54
- const owner = getValueAtPath(document, parts);
97
+ const owner = getValueAtPath(presentation, parts);
55
98
  if (parts.at(-1) === "chart") {
56
99
  if (!isObject(owner) || !isObject(owner.data)) throw fail("grid-target-not-found", "This chart has no data.", { path });
57
100
  const data = owner.data;
101
+ const chartType = typeof owner.type === "string" ? owner.type : undefined;
102
+ if (isDatasetRef(data)) {
103
+ const found = datasetOf(presentation, data, path, [...parts, "data"]);
104
+ return { kind: "chart", parts, dataParts: ["datasets", data.dataset], data: found.data, chartType, owner, dataset: found.dataset };
105
+ }
58
106
  if (!Array.isArray(data.columns) || !Array.isArray(data.rows))
59
- throw fail("chart-data-source", "This chart reads its data from a source (src), not from inline columns and rows. The data grid edits inline chart data.", { path });
60
- return { kind: "chart", parts, dataParts: [...parts, "data"], data, chartType: typeof owner.type === "string" ? owner.type : undefined };
107
+ throw fail("grid-target-not-found", "This chart has no inline columns and rows. The data grid edits inline chart data.", { path });
108
+ return { kind: "chart", parts, dataParts: [...parts, "data"], data, chartType, owner };
61
109
  }
62
110
  if (parts.at(-1) === "table") {
111
+ if (isObject(owner) && isDatasetRef(owner) && !Array.isArray(owner.rows)) {
112
+ const found = datasetOf(presentation, owner, path, parts);
113
+ return { kind: "table", parts, dataParts: ["datasets", owner.dataset], data: found.data, owner, dataset: found.dataset };
114
+ }
63
115
  if (!isObject(owner) || !Array.isArray(owner.rows)) throw fail("table-not-found", "Choose a table (a path ending in .table).", { path });
64
- return { kind: "table", parts, dataParts: parts, data: owner };
116
+ return { kind: "table", parts, dataParts: parts, data: owner, owner };
65
117
  }
66
118
  throw fail("grid-target-not-found", "Choose a chart or a table (a path ending in .chart or .table).", { path });
67
119
  }
68
120
 
69
121
  /**
70
122
  * The chart or table a selection path belongs to: `{ kind, path }` for `…chart`, `…chart.data.rows.1.0`, `…table`,
71
- * `…table.rows.1.2.value`, and so on. A chart whose data comes from a source is `{ kind: "chart", path, editable: false,
72
- * reason }`. Returns undefined when the path is not inside a chart or table.
123
+ * `…table.rows.1.2.value`, and so on. A chart whose data comes from a source (or from a dataset the document lacks) is
124
+ * `{ kind: "chart", path, editable: false, reason }`. A chart or table that takes its data from a shared dataset also has
125
+ * `dataset: { id, fields, count, items }` (`items` are the paths of every chart and table that uses the dataset).
126
+ * Returns undefined when the path is not inside a chart or table.
73
127
  */
74
- export function resolveDataGridTarget(document, selectedPath) {
128
+ export function resolveDataGridTarget(presentation, selectedPath) {
75
129
  let parts;
76
130
  try {
77
131
  parts = splitOpfPath(selectedPath);
@@ -82,10 +136,15 @@ export function resolveDataGridTarget(document, selectedPath) {
82
136
  if (parts[index] !== "chart" && parts[index] !== "table") continue;
83
137
  const head = parts.slice(0, index + 1);
84
138
  try {
85
- const found = locateGridData(document, head);
86
- return { kind: found.kind, path: head.join(".") };
139
+ const found = locateGridData(presentation, head);
140
+ const target = { kind: found.kind, path: head.join(".") };
141
+ if (found.dataset) {
142
+ const items = datasetUsage(presentation, found.dataset.id).map((entry) => entry.path);
143
+ target.dataset = { id: found.dataset.id, ...(found.dataset.fields ? { fields: [...found.dataset.fields] } : {}), count: items.length, items };
144
+ }
145
+ return target;
87
146
  } catch (error) {
88
- if (error.code === "chart-data-source") return { kind: "chart", path: head.join("."), editable: false, reason: error.message };
147
+ if (error.code === "dataset-unavailable") return { kind: parts[index], path: head.join("."), editable: false, reason: error.message };
89
148
  }
90
149
  }
91
150
  return undefined;
@@ -94,14 +153,29 @@ export function resolveDataGridTarget(document, selectedPath) {
94
153
  // --- lines -------------------------------------------------------------------------------------
95
154
 
96
155
  function toModel(found) {
97
- const { kind, data } = found;
98
- const hasHeader = kind === "chart" ? true : Array.isArray(data.columns);
99
- const lines = [...(hasHeader ? [clone(data.columns)] : []), ...clone(data.rows).map((row) => (Array.isArray(row) ? row : [row]))];
100
- const model = { kind, hasHeader, lines, width: 0, chartType: found.chartType };
156
+ const { kind, data, dataset } = found;
157
+ const hasHeader = kind === "chart" || Boolean(dataset) ? true : Array.isArray(data.columns);
158
+ let lines;
159
+ if (dataset) {
160
+ // The dataset as the item shows it: only the columns `fields` selects, in that order. Each body row remembers its dataset row.
161
+ const pick = (row) => dataset.indices.map((index) => (Array.isArray(row) && index < row.length ? clone(row[index]) : null));
162
+ lines = [clone(dataset.indices.map((index) => data.columns[index])), ...data.rows.map((row, r) => Object.assign(pick(row), { [LINE_ORIGIN]: r }))];
163
+ } else lines = [...(hasHeader ? [clone(data.columns)] : []), ...clone(data.rows).map((row) => (Array.isArray(row) ? row : [row]))];
164
+ const mapping = kind === "chart" && isObject(found.owner?.mapping) ? clone(found.owner.mapping) : undefined;
165
+ // `datasetNames` are every column of the shared dataset, also the ones `fields` hides: a new column's name must differ from them.
166
+ const model = { kind, hasHeader, lines, width: 0, chartType: found.chartType, dataset, mapping, colOrigin: [], origNames: [], datasetNames: dataset ? data.columns.map(columnNameOf) : [] };
101
167
  model.width = widthOf(model);
168
+ // Where each column came from, so renames, deletions and moves can be followed after an operation (null: a new column).
169
+ model.colOrigin = Array.from({ length: model.width }, (_, index) => index);
170
+ model.origNames = namesOfModel(model);
102
171
  return model;
103
172
  }
104
173
 
174
+ /** The header texts of a model (the column names `fields` and `mapping` address). */
175
+ function namesOfModel(model) {
176
+ return model.hasHeader ? Array.from({ length: model.width }, (_, column) => String(valueOf(model.lines[0]?.[column]) ?? "")) : [];
177
+ }
178
+
105
179
  function fromModel(model, original) {
106
180
  const rest = Object.fromEntries(Object.entries(original).filter(([key]) => key !== "columns" && key !== "rows"));
107
181
  const out = {};
@@ -113,13 +187,25 @@ function fromModel(model, original) {
113
187
  const widthOf = (model) => model.lines.reduce((most, line) => Math.max(most, line.length), 0);
114
188
  const offset = (model) => (model.hasHeader ? 1 : 0);
115
189
  const bodyCount = (model) => model.lines.length - offset(model);
116
- const filler = (model, u) => (model.kind === "chart" && !(model.hasHeader && u === 0) ? null : "");
190
+ // Column names must be unique where a dataset, `fields` or a chart `mapping` addresses them, so a new column of such a
191
+ // target gets a distinct placeholder name instead of "".
192
+ const needsNames = (model) => Boolean(model.dataset) || model.mapping !== undefined;
193
+ function uniqueName(model, taken = []) {
194
+ const used = new Set([...namesOfModel(model), ...model.datasetNames, ...taken]);
195
+ let n = 1;
196
+ while (used.has(n === 1 ? "New column" : `New column ${n}`)) n += 1;
197
+ return n === 1 ? "New column" : `New column ${n}`;
198
+ }
199
+ // What a new, empty cell holds: a chart's and a dataset's cells are null (a gap), a table's are "".
200
+ const emptyCell = (model) => (model.kind === "chart" || model.dataset ? null : "");
201
+ const filler = (model, u) => (model.hasHeader && u === 0 ? (needsNames(model) ? uniqueName(model) : "") : emptyCell(model));
117
202
 
118
203
  function padLines(model) {
119
204
  model.width = widthOf(model);
120
205
  model.lines.forEach((line, u) => {
121
206
  while (line.length < model.width) line.push(filler(model, u));
122
207
  });
208
+ while (model.colOrigin.length < model.width) model.colOrigin.push(null);
123
209
  }
124
210
 
125
211
  const addressOf = (model, u) => (model.hasHeader && u === 0 ? { section: "header", row: 0 } : { section: "body", row: u - offset(model) });
@@ -208,17 +294,86 @@ export function cellText(raw, kind, decimal = ".") {
208
294
  return String(value);
209
295
  }
210
296
 
211
- const looksNumeric = (value) => typeof value === "number" ? Number.isFinite(value) : typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value));
212
297
  const isScatter = (type) => typeof type === "string" && /^(scatter|bubble)/.test(type);
213
298
 
214
- /** What each column of a chart is for: "category" (column A), "series", and for a scatter chart with three or more columns "label" and "x". */
215
- export function chartColumnRoles(columnCount, chartType) {
299
+
300
+ /**
301
+ * A number as the slide draws it: the column's format (a table body cell's own format wins) through core's `formatDataNumber`, or undefined when
302
+ * the cell is not a number, has no valid format, or the format changes nothing. A chart cell is read by `chartNumber`, as the renderer does, and
303
+ * only in a column the chart plots as numbers; a table cell is formatted only when it holds a number. The cell's `text` (the raw value) is never
304
+ * replaced: editing and copying use it.
305
+ */
306
+ function displayText(model, raw, column, role, text) {
307
+ const format = (model.kind === "table" ? formatOf(raw) : undefined) ?? (model.hasHeader ? formatOf(model.lines[0]?.[column]) : undefined);
308
+ if (format === undefined || numberFormatError(format) !== undefined) return undefined;
309
+ const value = valueOf(raw);
310
+ let number = null;
311
+ if (model.kind === "chart") number = isNumericRole(role) && (typeof value === "number" || typeof value === "string") ? chartNumber(value) : null;
312
+ else if (typeof value === "number" && Number.isFinite(value)) number = value;
313
+ if (number === null) return undefined;
314
+ const shown = formatDataNumber(number, format);
315
+ return shown === text ? undefined : shown;
316
+ }
317
+
318
+ /**
319
+ * The default X column of an XY chart (core's rule): the second column, or the first when the category is the second, and only
320
+ * with three or more columns. With two, the second column is the one series, plotted against row numbers.
321
+ */
322
+ const defaultX = (category, count) => (count > 2 ? (category === 1 ? 0 : 1) : undefined);
323
+
324
+ /**
325
+ * The columns a chart plots, by index: `{ category, x, series }`. `mapping` names them (`{ category?, x?, series? }`); a missing or
326
+ * unknown name falls back to core's positional default. `x` is only set for an XY chart.
327
+ */
328
+ function mappingColumns(count, xy, mapping, names) {
329
+ const known = (name) => (typeof name === "string" ? names.indexOf(name) : -1);
330
+ const map = isObject(mapping) ? mapping : {};
331
+ let category = known(map.category);
332
+ if (category < 0) category = 0;
333
+ let x;
334
+ if (xy) {
335
+ x = known(map.x);
336
+ if (x < 0 || x === category) x = defaultX(category, count);
337
+ if (x !== undefined && (x >= count || x === category)) x = undefined;
338
+ }
339
+ let series;
340
+ if (Array.isArray(map.series)) {
341
+ series = [];
342
+ for (const name of map.series) {
343
+ const index = known(name);
344
+ if (index >= 0 && index !== category && index !== x && !series.includes(index)) series.push(index);
345
+ }
346
+ } else series = Array.from({ length: count }, (_, index) => index).filter((index) => index !== category && index !== x);
347
+ // An X column needs a series beside it; otherwise core plots it as the series against row numbers.
348
+ if (x !== undefined && !series.length) {
349
+ series = [x];
350
+ x = undefined;
351
+ }
352
+ return { category, x, series };
353
+ }
354
+
355
+ /**
356
+ * What each column of a chart is for: "category" (column A), "series", and for a scatter chart with three or more columns
357
+ * "label" and "x". With a `mapping` (and the column `names` it addresses) the roles follow it: "category" (the label column of a
358
+ * chart with no X axis), "label" and "x" for an XY chart, "series" for a plotted column and "other" for a column nothing plots.
359
+ */
360
+ export function chartColumnRoles(columnCount, chartType, mapping, names) {
361
+ if (isObject(mapping) && Array.isArray(names) && columnCount > 0) {
362
+ const xy = isXYChartType(chartType);
363
+ const { category, x, series } = mappingColumns(columnCount, xy, mapping, names);
364
+ const roles = Array(columnCount).fill("other");
365
+ for (const index of series) roles[index] = "series";
366
+ roles[category] = xy ? "label" : "category";
367
+ if (x !== undefined) roles[x] = "x";
368
+ return roles;
369
+ }
216
370
  if (columnCount <= 1) return Array(columnCount).fill("series");
217
371
  if (isScatter(chartType) && columnCount >= 3) return ["label", "x", ...Array(columnCount - 2).fill("series")];
218
372
  return ["category", ...Array(columnCount - 1).fill("series")];
219
373
  }
220
374
 
221
- const isNumericColumn = (model, column) => model.kind === "chart" && !["category", "label"].includes(chartColumnRoles(model.width, model.chartType)[column] ?? "series");
375
+ const rolesOf = (model) => (model.kind === "chart" ? chartColumnRoles(model.width, model.chartType, model.mapping, namesOfModel(model)) : []);
376
+ const isNumericRole = (role) => role === undefined || role === "series" || role === "x";
222
377
 
223
378
  // --- describing --------------------------------------------------------------------------------
224
379
 
@@ -228,13 +383,13 @@ const isNumericColumn = (model, column) => model.kind === "chart" && !["category
228
383
  * cell has `covered: true` and its anchor in `owner`; its `text` is "". `warnings` lists data problems the document
229
384
  * already has (text in a series column the renderer draws as a gap, an empty or repeated series name, a missing category).
230
385
  */
231
- export function describeDataGrid(document, path, options = {}) {
232
- const found = locateGridData(document, path);
386
+ export function describeDataGrid(presentation, path, options = {}) {
387
+ const found = locateGridData(presentation, path);
233
388
  const decimal = decimalOf(options);
234
389
  const model = toModel(found);
235
390
  padLines(model);
236
391
  const owners = ownersOf(model);
237
- const roles = model.kind === "chart" ? chartColumnRoles(model.width, model.chartType) : [];
392
+ const roles = rolesOf(model);
238
393
  const warnings = [];
239
394
  const names = new Map();
240
395
  const lines = model.lines.map((line, u) =>
@@ -256,21 +411,27 @@ export function describeDataGrid(document, path, options = {}) {
256
411
  };
257
412
  if (Array.isArray(value)) cell.runs = value;
258
413
  if (isStyled(raw) && raw.style) cell.style = raw.style;
414
+ if (!owner && address.section === "body") {
415
+ const display = displayText(model, raw, column, roles[column], cell.text);
416
+ if (display !== undefined) cell.display = display;
417
+ }
418
+ if (address.section === "header" && formatOf(raw) !== undefined) cell.format = formatOf(raw);
259
419
  if (owner) cell.owner = { line: owner.u, column: owner.c };
260
420
  if (model.kind === "chart") {
261
421
  let warning;
262
422
  if (u === 0) {
263
423
  const name = String(value ?? "");
264
- if (column > 0 || model.width === 1) {
424
+ if (roles[column] !== "category" && roles[column] !== "label") {
265
425
  if (name.trim() === "") warning = "This series has no name.";
266
426
  else if (names.has(name)) warning = `Another series is also named "${name}".`;
267
427
  names.set(name, column);
268
428
  }
269
429
  } else if (roles[column] === "category" || roles[column] === "label") {
270
430
  if (roles[column] === "category" && (value === null || value === undefined || value === "")) warning = "This row has no category label.";
431
+ } else if (roles[column] === "other") {
432
+ // A column the chart does not plot may hold anything.
271
433
  } else if (typeof value === "boolean") warning = "A yes/no value is not a number, so the chart draws a gap here.";
272
- else if (value !== null && value !== undefined && !(typeof value === "number" || looksNumeric(value)) && String(value).trim() !== "")
273
- warning = "This is not a number, so the chart draws a gap here.";
434
+ else if (value !== null && value !== undefined && chartNumber(value) === null && String(value).trim() !== "") warning = "This is not a number, so the chart draws a gap here.";
274
435
  if (warning) {
275
436
  cell.warning = warning;
276
437
  warnings.push({ section: address.section, row: address.row, column, message: `${describeAddress({ ...address, column })}: ${warning}` });
@@ -287,6 +448,10 @@ export function describeDataGrid(document, path, options = {}) {
287
448
  rowCount: bodyCount(model),
288
449
  columnCount: model.width,
289
450
  columnRoles: roles,
451
+ columnFormats: Array.from({ length: model.width }, (_, column) => (model.hasHeader ? formatOf(model.lines[0]?.[column]) : undefined)),
452
+ ...(model.hasHeader ? { columnNames: namesOfModel(model) } : {}),
453
+ ...(found.dataset ? { dataset: { id: found.dataset.id, ...(found.dataset.fields ? { fields: [...found.dataset.fields] } : {}), items: datasetUsage(presentation, found.dataset.id).map((entry) => entry.path) } } : {}),
454
+ ...(model.mapping ? { mapping: clone(model.mapping) } : {}),
290
455
  lines,
291
456
  warnings,
292
457
  merges: rectsOf(model).map((rect) => ({ ...addressOf(model, rect.u), column: rect.c, rowSpan: rect.rs, colSpan: rect.cs })),
@@ -336,24 +501,164 @@ export function gridPatches(parts, before, after) {
336
501
  return patches;
337
502
  }
338
503
 
339
- function transact(document, path, action, mutate, extra = {}) {
340
- const found = locateGridData(document, path);
504
+ /** Like {@link gridPatches}, and also for a value that is added (`before` is undefined) or removed (`after` is undefined). */
505
+ function valuePatches(parts, before, after) {
506
+ if (before === undefined && after === undefined) return [];
507
+ const pointer = opfPathToJsonPointer(parts);
508
+ if (before === undefined) return [{ op: "add", path: pointer, value: clone(after) }];
509
+ if (after === undefined) return [{ op: "test", path: pointer, value: clone(before) }, { op: "remove", path: pointer }];
510
+ return gridPatches(parts, before, after);
511
+ }
512
+
513
+ // --- following columns across an operation -------------------------------------------------------
514
+
515
+ /** The column renames an operation made: a Map from the old name to the new one, for the columns that survived with another name. */
516
+ function renamesOf(model) {
517
+ const names = namesOfModel(model);
518
+ const renames = new Map();
519
+ model.colOrigin.forEach((origin, column) => {
520
+ if (origin === null || origin === undefined) return;
521
+ const from = model.origNames[origin];
522
+ const to = names[column];
523
+ if (from !== undefined && to !== undefined && from !== to) renames.set(from, to);
524
+ });
525
+ return renames;
526
+ }
527
+
528
+ /**
529
+ * A chart `mapping` after an operation: a renamed column keeps its role, a column that no longer exists leaves the mapping
530
+ * (an emptied `series` is dropped, which restores "every other column"). A name the data never had is left alone. Returns
531
+ * undefined when nothing is left.
532
+ */
533
+ function reconcileMapping(mapping, renames, before, after) {
534
+ if (!isObject(mapping)) return mapping;
535
+ const keep = (name) => {
536
+ const next = renames.has(name) ? renames.get(name) : name;
537
+ if (after.includes(next)) return next;
538
+ return before.includes(name) ? undefined : name;
539
+ };
540
+ const out = {};
541
+ for (const key of ["category", "x"]) {
542
+ if (typeof mapping[key] !== "string") continue;
543
+ const name = keep(mapping[key]);
544
+ if (name !== undefined) out[key] = name;
545
+ }
546
+ if (Array.isArray(mapping.series)) {
547
+ const series = mapping.series.map(keep).filter((name) => name !== undefined);
548
+ if (series.length) out.series = series;
549
+ }
550
+ return Object.keys(out).length ? out : undefined;
551
+ }
552
+
553
+ /** The changes to an inline chart or table: its data object, and the chart's `mapping` when a rename or deletion touches it. */
554
+ function inlineChanges(found, model, style) {
555
+ const after = fromModel(model, found.data);
556
+ if (style && style.preset !== "custom") applyTableStyleToTable(after, style.preset);
557
+ const changes = [{ parts: found.dataParts, before: found.data, after }];
558
+ if (found.kind === "chart" && found.owner.mapping !== undefined) {
559
+ const next = reconcileMapping(found.owner.mapping, renamesOf(model), model.origNames, namesOfModel(model));
560
+ if (!same(found.owner.mapping, next)) changes.push({ parts: [...found.parts, "mapping"], before: found.owner.mapping, after: next });
561
+ }
562
+ return changes;
563
+ }
564
+
565
+ /**
566
+ * The changes to a chart or table that shows a dataset: the dataset itself (the shown columns mapped back to its own column
567
+ * indices, each row to the dataset row it came from, so columns `fields` hides keep their data), the item's `fields` and `mapping`,
568
+ * and the `fields` and `mapping` of every other item that shares the dataset when a column is renamed. A column that
569
+ * `fields` selects leaves only `fields` when deleted; without `fields` it leaves the dataset, which another item's `fields` or
570
+ * `mapping` that names it forbids.
571
+ */
572
+ function datasetChanges(presentation, found, model) {
573
+ const { dataset, data } = found;
574
+ padLines(model);
575
+ const header = model.lines[0];
576
+ const names = namesOfModel(model);
577
+ const body = model.lines.slice(1);
578
+ const origin = model.colOrigin;
579
+ let columns;
580
+ let rows;
581
+ if (!dataset.fields) {
582
+ columns = header.map((raw) => raw);
583
+ rows = body.map((line) => line.map((raw) => raw));
584
+ } else {
585
+ const survivors = new Map();
586
+ origin.forEach((o, column) => {
587
+ if (o !== null && o !== undefined) survivors.set(dataset.indices[o], column);
588
+ });
589
+ columns = data.columns.map((column, index) => (survivors.has(index) ? header[survivors.get(index)] : column));
590
+ const position = new Map();
591
+ origin.forEach((o, column) => {
592
+ if (o !== null && o !== undefined) position.set(column, dataset.indices[o]);
593
+ else {
594
+ position.set(column, columns.length);
595
+ columns.push(header[column]);
596
+ }
597
+ });
598
+ rows = body.map((line) => {
599
+ const source = line[LINE_ORIGIN] !== undefined && Array.isArray(data.rows[line[LINE_ORIGIN]]) ? [...data.rows[line[LINE_ORIGIN]]] : [];
600
+ while (source.length < columns.length) source.push(null);
601
+ line.forEach((raw, column) => {
602
+ source[position.get(column)] = raw;
603
+ });
604
+ return source;
605
+ });
606
+ }
607
+ const renames = renamesOf(model);
608
+ const others = [];
609
+ const ownPath = found.parts.join(".");
610
+ walkDatasetItems(presentation, (entry) => {
611
+ if (entry.id === dataset.id && entry.parts.join(".") !== ownPath) others.push(entry);
612
+ });
613
+ if (!dataset.fields) {
614
+ const gone = model.origNames.filter((_, index) => !origin.includes(index));
615
+ for (const name of gone) {
616
+ const users = others.filter((entry) => (Array.isArray(entry.ref.fields) && entry.ref.fields.includes(name)) || (isObject(entry.item.mapping) && [entry.item.mapping.category, entry.item.mapping.x, ...(entry.item.mapping.series ?? [])].includes(name)));
617
+ if (users.length)
618
+ throw fail("dataset-column-in-use", `The column ${JSON.stringify(name)} is used by ${users.length === 1 ? "another chart or table" : `${users.length} other charts and tables`} that share the dataset '${dataset.id}'. Remove it there first, or use a copy of the data here.`, { column: name, items: users.map((entry) => entry.parts.join(".")) });
619
+ }
620
+ }
621
+ const changes = [{ parts: found.dataParts, before: data, after: { ...data, columns, rows } }];
622
+ if (dataset.fields) changes.push({ parts: [...dataset.refParts, "fields"], before: dataset.ref.fields, after: names });
623
+ if (found.kind === "chart" && found.owner.mapping !== undefined) {
624
+ const next = reconcileMapping(found.owner.mapping, renames, model.origNames, names);
625
+ if (!same(found.owner.mapping, next)) changes.push({ parts: [...found.parts, "mapping"], before: found.owner.mapping, after: next });
626
+ }
627
+ if (renames.size) {
628
+ for (const entry of others) {
629
+ if (Array.isArray(entry.ref.fields) && entry.ref.fields.some((name) => renames.has(name)))
630
+ changes.push({ parts: [...entry.refParts, "fields"], before: entry.ref.fields, after: entry.ref.fields.map((name) => (renames.has(name) ? renames.get(name) : name)) });
631
+ if (entry.kind === "chart" && isObject(entry.item.mapping)) {
632
+ const mapping = entry.item.mapping;
633
+ const rename = (name) => (renames.has(name) ? renames.get(name) : name);
634
+ const next = { ...mapping };
635
+ if (typeof mapping.category === "string") next.category = rename(mapping.category);
636
+ if (typeof mapping.x === "string") next.x = rename(mapping.x);
637
+ if (Array.isArray(mapping.series)) next.series = mapping.series.map(rename);
638
+ if (!same(mapping, next)) changes.push({ parts: [...entry.parts, "mapping"], before: mapping, after: next });
639
+ }
640
+ }
641
+ }
642
+ return changes;
643
+ }
644
+
645
+ function transact(presentation, path, action, mutate, extra = {}) {
646
+ const found = locateGridData(presentation, path);
341
647
  if (extra.kind && extra.kind !== found.kind) throw fail("grid-wrong-kind", `This operation works on a ${extra.kind}, and the path points at a ${found.kind}.`, { path });
342
648
  const model = toModel(found);
343
649
  const problemsBefore = mergeProblems(model).length;
344
650
  const info = mutate(model, found) ?? {};
345
- const style = info.restyle && found.kind === "table" ? readTableStyleOfTable(found.data) : undefined;
651
+ const style = info.restyle && found.kind === "table" && !found.dataset ? readTableStyleOfTable(found.data) : undefined;
346
652
  if (info.touchesStructure) assertMerges(model, problemsBefore, info.what ?? "This change");
347
653
  if (style && style.preset !== "custom") padLines(model);
348
- const after = fromModel(model, found.data);
349
- if (style && style.preset !== "custom") applyTableStyleToTable(after, style.preset);
350
- const patches = gridPatches(found.dataParts, found.data, after);
654
+ const changes = found.dataset ? datasetChanges(presentation, found, model) : inlineChanges(found, model, style);
655
+ const patches = changes.flatMap((entry) => valuePatches(entry.parts, entry.before, entry.after));
351
656
  const changed = patches.length > 0;
352
- const before = validateOpfDocument(document);
353
- const result = changed ? checkedDocument(document, patches, before) : document;
657
+ const before = checkFormat(presentation);
658
+ const result = changed ? checkedDocument(presentation, patches, before) : presentation;
354
659
  // `restyle`, `touchesStructure` and `what` steer this function only; the rest of what the operation reports is the caller's.
355
660
  const { restyle: _restyle, touchesStructure: _structure, what: _what, ...summary } = info;
356
- return { action, ...summary, kind: found.kind, path: found.parts.join("."), document: clone(result), patches, changed };
661
+ return { action, ...summary, kind: found.kind, path: found.parts.join("."), ...(found.dataset ? { dataset: found.dataset.id } : {}), presentation: clone(result), patches, changed };
357
662
  }
358
663
 
359
664
  // --- row and column structure ------------------------------------------------------------------
@@ -370,7 +675,7 @@ function insertRows(model, at, count) {
370
675
  const u = at + offset(model);
371
676
  const crossing = rectsOf(model).filter((rect) => rect.u < u && u < rect.u + rect.rs);
372
677
  const reference = u - 1 >= offset(model) ? model.lines[u - 1] : u < model.lines.length ? model.lines[u] : undefined;
373
- const fresh = Array.from({ length: count }, () => Array.from({ length: model.width }, (_, column) => (model.kind === "chart" ? null : referenceCell(model, reference, column))));
678
+ const fresh = Array.from({ length: count }, () => Array.from({ length: model.width }, (_, column) => (model.kind === "chart" || model.dataset ? null : referenceCell(model, reference, column))));
374
679
  model.lines.splice(u, 0, ...fresh);
375
680
  for (const rect of crossing) {
376
681
  model.lines[rect.u][rect.c] = withSpans(model.lines[rect.u][rect.c], rect.rs + count, rect.cs);
@@ -403,10 +708,14 @@ function insertColumns(model, at, count) {
403
708
  if (!Number.isInteger(at) || at < 0 || at > model.width) throw fail("grid-column-out-of-range", `A column can be inserted at positions 1 to ${model.width + 1}.`, { at });
404
709
  const crossing = rectsOf(model).filter((rect) => rect.c < at && at < rect.c + rect.cs);
405
710
  const reference = at - 1 >= 0 ? at - 1 : at < model.width ? at : -1;
711
+ // New columns of a dataset (or of a chart with a mapping) are named at once: names must stay unique.
712
+ const named = [];
713
+ if (model.hasHeader && needsNames(model)) for (let i = 0; i < count; i += 1) named.push(uniqueName(model, named));
406
714
  model.lines.forEach((line, u) => {
407
- const fresh = Array.from({ length: count }, () => (model.kind === "chart" || (model.hasHeader && u === 0) ? filler(model, u) : referenceCell(model, line, reference)));
715
+ const fresh = Array.from({ length: count }, (_, i) => (model.hasHeader && u === 0 && needsNames(model) ? named[i] : model.kind === "chart" || model.dataset || (model.hasHeader && u === 0) ? filler(model, u) : referenceCell(model, line, reference)));
408
716
  line.splice(at, 0, ...fresh);
409
717
  });
718
+ model.colOrigin.splice(at, 0, ...Array(count).fill(null));
410
719
  for (const rect of crossing) {
411
720
  model.lines[rect.u][rect.c] = withSpans(model.lines[rect.u][rect.c], rect.rs, rect.cs + count);
412
721
  for (let y = rect.u; y < rect.u + rect.rs; y += 1) for (let i = 0; i < count; i += 1) model.lines[y][at + i] = null;
@@ -422,6 +731,7 @@ function deleteColumn(model, x) {
422
731
  else model.lines[rect.u][x + 1] = withSpans(anchor, rect.rs, rect.cs - 1);
423
732
  }
424
733
  for (const line of model.lines) line.splice(x, 1);
734
+ model.colOrigin.splice(x, 1);
425
735
  model.width -= 1;
426
736
  }
427
737
 
@@ -453,6 +763,7 @@ function moveColumns(model, from, count, to) {
453
763
  throw fail("grid-column-out-of-range", "That move leaves the table.", { from, count, to });
454
764
  if (from === to) return;
455
765
  for (const line of model.lines) moveBlock(line, from, count, to);
766
+ moveBlock(model.colOrigin, from, count, to);
456
767
  }
457
768
 
458
769
  // --- sorting -----------------------------------------------------------------------------------
@@ -529,6 +840,7 @@ function sortRows(model, column, options) {
529
840
 
530
841
  function setHeader(model, enabled, options) {
531
842
  if (model.kind !== "table") throw fail("grid-wrong-kind", "Only a table has a header row to turn on or off. A chart's first line always names its series.", {});
843
+ if (model.dataset) throw fail("grid-dataset-shared", `A table that shows the dataset '${model.dataset.id}' always has a header row: the dataset's column names.`, { dataset: model.dataset.id });
532
844
  if (enabled === model.hasHeader) return {};
533
845
  padLines(model);
534
846
  if (enabled) {
@@ -547,17 +859,23 @@ function setHeader(model, enabled, options) {
547
859
  model.hasHeader = true;
548
860
  return { header: true, use, restyle: true, touchesStructure: true, what: "A header row" };
549
861
  }
862
+ // A DataColumn header (`{ name, format }`) is not a body cell: it becomes its name. Its column format has nowhere to go and is dropped.
863
+ const dropped = model.lines[0].filter((raw) => isColumnObject(raw) && formatOf(raw) !== undefined).length;
864
+ model.lines[0] = model.lines[0].map((raw) => (isColumnObject(raw) ? raw.name : raw));
550
865
  model.hasHeader = false;
551
- return { header: false, restyle: true };
866
+ return { header: false, restyle: true, ...(dropped ? { droppedFormats: dropped } : {}) };
552
867
  }
553
868
 
554
869
  // --- transpose ---------------------------------------------------------------------------------
555
870
 
556
871
  function transposeChart(model) {
557
872
  if (model.kind !== "chart") throw fail("grid-wrong-kind", "Only chart data can be transposed.", {});
873
+ if (model.dataset) throw fail("grid-dataset-shared", `Swapping categories and series would reshape the shared dataset '${model.dataset.id}' for every chart and table that uses it. Use a copy of the data first.`, { dataset: model.dataset.id });
558
874
  padLines(model);
559
875
  if (model.width < 2) throw fail("chart-transpose-empty", "Transposing needs at least one series column besides the categories.", {});
560
- const [names, ...rows] = model.lines;
876
+ const [headers, ...rows] = model.lines;
877
+ // A column's number format belongs to its numbers: the old series names become category labels, as text.
878
+ const names = headers.map((cell) => String(valueOf(cell) ?? ""));
561
879
  let relabelled = 0;
562
880
  const categories = rows.map((row) => {
563
881
  const category = row[0];
@@ -568,6 +886,8 @@ function transposeChart(model) {
568
886
  const lines = [[names[0], ...categories], ...names.slice(1).map((name, index) => [name, ...rows.map((row) => row[index + 1] ?? null)])];
569
887
  model.lines = lines;
570
888
  model.width = widthOf(model);
889
+ // Every column is new: a mapping that named the old columns no longer applies.
890
+ model.colOrigin = Array(model.width).fill(null);
571
891
  return { rowsBefore: rows.length, columnsBefore: names.length, relabelled };
572
892
  }
573
893
 
@@ -575,20 +895,29 @@ function transposeChart(model) {
575
895
 
576
896
  function growTo(model, lines, width) {
577
897
  padLines(model);
578
- while (model.lines.length < lines) model.lines.push(Array.from({ length: model.width }, () => (model.kind === "chart" ? null : "")));
898
+ while (model.lines.length < lines) model.lines.push(Array.from({ length: model.width }, () => emptyCell(model)));
579
899
  while (model.width < width) {
580
900
  model.lines.forEach((line, u) => line.push(filler(model, u)));
581
901
  model.width += 1;
902
+ model.colOrigin.push(null);
582
903
  }
583
904
  }
584
905
 
585
- function newRaw(model, raw, text, decimal, address) {
906
+ // The column names of a shared dataset (and of a chart with a mapping) are how `fields` and `mapping` address columns: never blank.
907
+ const blankNameError = (model, address, text) =>
908
+ address.section === "header" && model.hasHeader && needsNames(model) && String(text ?? "").trim() === ""
909
+ ? `A column name cannot be empty here: ${model.dataset ? "the shared dataset" : "the chart's column mapping"} addresses columns by name.`
910
+ : undefined;
911
+
912
+ function newRaw(model, raw, text, decimal, address, roles) {
586
913
  const value = valueOf(raw);
914
+ const blank = blankNameError(model, address, text);
915
+ if (blank) return { error: blank };
587
916
  // Text that is already what the cell shows is left alone: a number kept as text stays text, runs keep their styling.
588
917
  const shown = cellText(raw, model.kind, decimal);
589
918
  if (Array.isArray(value)) {
590
919
  if (text.replace(/\r\n?/g, "\n") === shown.replace(/\r\n?/g, "\n")) return { skip: true };
591
- return { raw: isStyled(raw) ? { ...raw, value: updateRichTextInput(value, text) } : updateRichTextInput(value, text) };
920
+ return { raw: withValue(raw, updateRichTextInput(value, text)) };
592
921
  }
593
922
  if (text === shown) return { skip: true };
594
923
  let next;
@@ -596,17 +925,21 @@ function newRaw(model, raw, text, decimal, address) {
596
925
  if (model.hasHeader && address.section === "header") next = text;
597
926
  else next = text === "" ? "" : isCanonicalNumber(text) ? Number(text) : text;
598
927
  } else if (address.section === "header") next = text;
599
- else if (isNumericColumn(model, address.column)) {
928
+ else if (isNumericRole(roles[address.column])) {
929
+ // A chart number is read in the grid's number format and kept as a number. Text that is not a number is refused with
930
+ // a reason; it is never stored as a guess (core's chartNumber would draw it as a gap).
600
931
  const parsed = parseGridNumber(text, { decimal });
601
932
  if (parsed.error) return { error: parsed.error };
602
933
  next = parsed.empty ? null : parsed.value;
603
934
  if (next === null && (value === null || value === undefined)) return { skip: true };
604
- } else next = text === "" ? null : text;
605
- return { raw: isStyled(raw) ? { ...raw, value: next } : next };
935
+ } else if (roles[address.column] === "other") next = text === "" ? null : isCanonicalNumber(text) ? Number(text) : text;
936
+ else next = text === "" ? null : text;
937
+ return { raw: withValue(raw, next) };
606
938
  }
607
939
 
608
940
  function applyEdits(model, edits, decimal, owners) {
609
941
  const issues = [];
942
+ const roles = rolesOf(model);
610
943
  for (const edit of edits) {
611
944
  const address = { section: edit.section, row: edit.row ?? 0, column: edit.column };
612
945
  try {
@@ -620,11 +953,14 @@ function applyEdits(model, edits, decimal, owners) {
620
953
  const value = edit.value;
621
954
  if (model.kind === "chart" && !(value === null || ["string", "number", "boolean"].includes(typeof value))) throw fail("invalid-grid-value", "A chart cell is text, a number, true or false, or empty.", { cell: address });
622
955
  if (typeof value === "number" && !Number.isFinite(value)) throw fail("invalid-grid-value", "A cell cannot hold Infinity or NaN.", { cell: address });
623
- model.lines[u][address.column] = isStyled(raw) ? { ...raw, value } : value;
956
+ if (model.dataset && address.section === "header" && typeof value !== "string") throw fail("invalid-grid-value", "A dataset column name is text.", { cell: address });
957
+ const blank = blankNameError(model, address, value);
958
+ if (blank && !(valueOf(raw) === "" && value === "")) throw fail("invalid-grid-value", blank, { cell: address });
959
+ model.lines[u][address.column] = withValue(raw, value);
624
960
  continue;
625
961
  }
626
962
  if (typeof edit.text !== "string") throw fail("invalid-grid-value", "Give the cell's new text.", { cell: address });
627
- const outcome = newRaw(model, raw, edit.text, decimal, address);
963
+ const outcome = newRaw(model, raw, edit.text, decimal, address, roles);
628
964
  if (outcome.error) issues.push({ ...address, message: `${describeAddress(address)}: ${outcome.error}` });
629
965
  else if (!outcome.skip) model.lines[u][address.column] = outcome.raw;
630
966
  } catch (error) {
@@ -651,7 +987,7 @@ function pasteRows(model, anchor, matrix, options) {
651
987
  if (!Array.isArray(matrix) || !matrix.length) throw fail("invalid-grid-value", "There is nothing to paste.", {});
652
988
  const first = decimalOf(options);
653
989
  const attempt = (decimal) => {
654
- const trial = { ...model, lines: clone(model.lines) };
990
+ const trial = { ...model, lines: cloneLines(model.lines), colOrigin: [...model.colOrigin] };
655
991
  const u0 = lineIndex(trial, anchor);
656
992
  const width = matrix.reduce((most, row) => Math.max(most, row.length), 0);
657
993
  growTo(trial, u0 + matrix.length, anchor.column + width);
@@ -678,6 +1014,7 @@ function pasteRows(model, anchor, matrix, options) {
678
1014
  if (result.issues.length) throw refuse(result.issues);
679
1015
  model.lines = result.trial.lines;
680
1016
  model.width = result.trial.width;
1017
+ model.colOrigin = result.trial.colOrigin;
681
1018
  return { cells: result.cells, decimal, rows: matrix.length, restyle: true, touchesStructure: model.kind === "table", what: "Pasting" };
682
1019
  }
683
1020
 
@@ -685,8 +1022,8 @@ function pasteRows(model, anchor, matrix, options) {
685
1022
  * The problems `edits` would have, without applying or validating the document (cheap enough to run on every keystroke): a list of
686
1023
  * `{ section, row, column, message }`, empty when every edit is fine.
687
1024
  */
688
- export function gridCellIssues(document, path, edits, options = {}) {
689
- const model = toModel(locateGridData(document, path));
1025
+ export function gridCellIssues(presentation, path, edits, options = {}) {
1026
+ const model = toModel(locateGridData(presentation, path));
690
1027
  return applyEdits(model, edits, decimalOf(options), ownersOf(model));
691
1028
  }
692
1029
 
@@ -699,8 +1036,8 @@ const countOf = (count) => {
699
1036
  const kindOf = (options) => (options?.kind ? { kind: options.kind } : {});
700
1037
 
701
1038
  /** Set cells from text (or typed `value`s): `edits` are `{ section, row, column, text | value }`. One patch; every problem is listed in `error.issues` and nothing is applied. */
702
- export function prepareGridCells(document, path, edits, options = {}) {
703
- return transact(document, path, "set-cells", (model) => setCells(model, edits, options), kindOf(options));
1039
+ export function prepareGridCells(presentation, path, edits, options = {}) {
1040
+ return transact(presentation, path, "set-cells", (model) => setCells(model, edits, options), kindOf(options));
704
1041
  }
705
1042
 
706
1043
  /**
@@ -709,54 +1046,54 @@ export function prepareGridCells(document, path, edits, options = {}) {
709
1046
  * `options` (`decimal`, or `numberFormat` and `locale`); when that fails and the other format reads all of the paste, that is used and
710
1047
  * reported as `decimal`. A table paste over a merged cell's covered positions is refused. One patch.
711
1048
  */
712
- export function preparePaste(document, path, anchor, source, options = {}) {
1049
+ export function preparePaste(presentation, path, anchor, source, options = {}) {
713
1050
  const rows = typeof source === "string" ? parseDelimited(source, { delimiter: options.delimiter }).rows : source;
714
- return transact(document, path, "paste", (model) => pasteRows(model, anchor, rows, options), kindOf(options));
1051
+ return transact(presentation, path, "paste", (model) => pasteRows(model, anchor, rows, options), kindOf(options));
715
1052
  }
716
1053
 
717
1054
  /** Insert `count` empty rows so the first is body row `at` (0 is above the first row; the row count appends). A merged cell that spans the position grows. */
718
- export function prepareInsertRows(document, path, at, count = 1, options = {}) {
719
- return transact(document, path, "insert-rows", (model) => {
1055
+ export function prepareInsertRows(presentation, path, at, count = 1, options = {}) {
1056
+ return transact(presentation, path, "insert-rows", (model) => {
720
1057
  insertRows(model, at, countOf(count));
721
1058
  return { at, count, restyle: true, touchesStructure: true, what: "Inserting rows" };
722
1059
  }, kindOf(options));
723
1060
  }
724
1061
 
725
1062
  /** Delete the body rows at `indices`. A merged cell that spans a deleted row shrinks and keeps its text. The last row cannot be deleted. */
726
- export function prepareDeleteRows(document, path, indices, options = {}) {
727
- return transact(document, path, "delete-rows", (model) => {
1063
+ export function prepareDeleteRows(presentation, path, indices, options = {}) {
1064
+ return transact(presentation, path, "delete-rows", (model) => {
728
1065
  deleteRows(model, indices);
729
1066
  return { indices: [...indices], restyle: true, touchesStructure: true, what: "Deleting rows" };
730
1067
  }, kindOf(options));
731
1068
  }
732
1069
 
733
1070
  /** Move `count` rows starting at body row `from` so the first is at `to`. Refused with the reason when it would split a merged cell. */
734
- export function prepareMoveRows(document, path, from, to, count = 1, options = {}) {
735
- return transact(document, path, "move-rows", (model) => {
1071
+ export function prepareMoveRows(presentation, path, from, to, count = 1, options = {}) {
1072
+ return transact(presentation, path, "move-rows", (model) => {
736
1073
  moveRows(model, from, count, to);
737
1074
  return { from, to, count, restyle: true, touchesStructure: true, what: "Moving rows" };
738
1075
  }, kindOf(options));
739
1076
  }
740
1077
 
741
1078
  /** Insert `count` empty columns so the first is column `at`. A merged cell that spans the position grows. */
742
- export function prepareInsertColumns(document, path, at, count = 1, options = {}) {
743
- return transact(document, path, "insert-columns", (model) => {
1079
+ export function prepareInsertColumns(presentation, path, at, count = 1, options = {}) {
1080
+ return transact(presentation, path, "insert-columns", (model) => {
744
1081
  insertColumns(model, at, countOf(count));
745
1082
  return { at, count, restyle: true, touchesStructure: true, what: "Inserting columns" };
746
1083
  }, kindOf(options));
747
1084
  }
748
1085
 
749
1086
  /** Delete the columns at `indices`. A merged cell that spans a deleted column shrinks and keeps its text. The last column cannot be deleted. */
750
- export function prepareDeleteColumns(document, path, indices, options = {}) {
751
- return transact(document, path, "delete-columns", (model) => {
1087
+ export function prepareDeleteColumns(presentation, path, indices, options = {}) {
1088
+ return transact(presentation, path, "delete-columns", (model) => {
752
1089
  deleteColumns(model, indices);
753
1090
  return { indices: [...indices], restyle: true, touchesStructure: true, what: "Deleting columns" };
754
1091
  }, kindOf(options));
755
1092
  }
756
1093
 
757
1094
  /** Move `count` columns starting at `from` so the first is at `to`. Refused with the reason when it would split a merged cell. */
758
- export function prepareMoveColumns(document, path, from, to, count = 1, options = {}) {
759
- return transact(document, path, "move-columns", (model) => {
1095
+ export function prepareMoveColumns(presentation, path, from, to, count = 1, options = {}) {
1096
+ return transact(presentation, path, "move-columns", (model) => {
760
1097
  moveColumns(model, from, count, to);
761
1098
  return { from, to, count, restyle: true, touchesStructure: true, what: "Moving columns" };
762
1099
  }, kindOf(options));
@@ -767,20 +1104,153 @@ export function prepareMoveColumns(document, path, from, to, count = 1, options
767
1104
  * before text, "text" compares everything as text with numbers inside the text in numeric order, "number" and "date" read that type only. Empty cells
768
1105
  * always sort last, in either direction. Rows joined by a merged cell that spans rows move as one block. Equal keys keep their order.
769
1106
  */
770
- export function prepareSortRows(document, path, column, options = {}) {
771
- return transact(document, path, "sort-rows", (model) => ({ ...sortRows(model, column, options), column, direction: options.direction ?? "asc", restyle: true }), kindOf(options));
1107
+ export function prepareSortRows(presentation, path, column, options = {}) {
1108
+ return transact(presentation, path, "sort-rows", (model) => ({ ...sortRows(model, column, options), column, direction: options.direction ?? "asc", restyle: true }), kindOf(options));
772
1109
  }
773
1110
 
774
1111
  /** Turn a table's header row on or off. On uses the first row as the header (`use: "first-row"`, the default when there are two or more rows) or adds a new empty one (`use: "new"`). */
775
- export function prepareSetHeader(document, path, enabled, options = {}) {
776
- return transact(document, path, "set-header", (model) => setHeader(model, Boolean(enabled), options), { kind: "table" });
1112
+ export function prepareSetHeader(presentation, path, enabled, options = {}) {
1113
+ return transact(presentation, path, "set-header", (model) => setHeader(model, Boolean(enabled), options), { kind: "table" });
777
1114
  }
778
1115
 
779
1116
  /** Swap a chart's categories and series: the first column's values become the series names and the series become the rows. */
780
- export function prepareTranspose(document, path) {
781
- return transact(document, path, "transpose", (model) => transposeChart(model), { kind: "chart" });
1117
+ export function prepareTranspose(presentation, path) {
1118
+ return transact(presentation, path, "transpose", (model) => transposeChart(model), { kind: "chart" });
1119
+ }
1120
+
1121
+ // --- number format of a column (RR-54) ---------------------------------------------------------
1122
+
1123
+ /** Why `format` is not a valid number format ("#,##0", "0.0%", "$#,##0.00"), or undefined when it is valid or empty (empty clears the format). */
1124
+ export function columnFormatError(format) {
1125
+ if (format === undefined || format === null || format === "") return undefined;
1126
+ return numberFormatError(format);
1127
+ }
1128
+
1129
+ /** The header `raw` with `format` set, or cleared (`undefined`). A string header becomes `{ name, format }` and goes back to a string when the format is cleared. */
1130
+ function withColumnFormat(raw, format) {
1131
+ if (isColumnObject(raw)) {
1132
+ const next = { ...raw };
1133
+ if (format === undefined) delete next.format;
1134
+ else next.format = format;
1135
+ return Object.keys(next).length === 1 ? next.name : next;
1136
+ }
1137
+ if (isStyled(raw)) {
1138
+ const next = { ...raw };
1139
+ if (format === undefined) delete next.format;
1140
+ else next.format = format;
1141
+ return Object.keys(next).length === 1 ? next.value : next;
1142
+ }
1143
+ if (format === undefined) return raw;
1144
+ if (Array.isArray(raw)) return { value: raw, format };
1145
+ return { name: raw === null || raw === undefined ? "" : String(raw), format };
1146
+ }
1147
+
1148
+ function setColumnFormat(model, column, format) {
1149
+ if (!model.hasHeader) throw fail("grid-no-header", "This table has no header row, so there is no column heading to hold a number format. Turn on the header row first.", {});
1150
+ padLines(model);
1151
+ if (!Number.isInteger(column) || column < 0 || column >= model.width) throw fail("grid-column-out-of-range", "Choose a column.", { column });
1152
+ if (format !== undefined && format !== null && typeof format !== "string") throw fail("number-format-invalid", 'A number format is text such as "#,##0" or "0.0%".', { format });
1153
+ const next = typeof format === "string" && format !== "" ? format : undefined;
1154
+ const error = columnFormatError(next);
1155
+ if (error) throw fail("number-format-invalid", error, { format: next });
1156
+ if (ownersOf(model).has(`0:${column}`)) throw fail("table-cell-covered", `${describeAddress({ section: "header", row: 0, column })} is covered by a merged cell. Choose the merged heading.`, { column });
1157
+ model.lines[0][column] = withColumnFormat(model.lines[0][column], next);
1158
+ return { column, format: next ?? null };
782
1159
  }
783
1160
 
1161
+ /** Set or clear (`null` or "") the number format of column `column` (its header cell). A string header becomes `{ name, format }` and returns to a string when the format is cleared. Refused with the reason when the format is invalid or the table has no header row. */
1162
+ export function prepareGridColumnFormat(presentation, path, column, format, options = {}) {
1163
+ return transact(presentation, path, "set-column-format", (model) => setColumnFormat(model, column, format), kindOf(options));
1164
+ }
1165
+
1166
+ // --- chart mapping (RR-54) -----------------------------------------------------------------------
1167
+
1168
+ /**
1169
+ * The columns a chart plots and how they got that role: `{ path, xy, columns: [{ name, format?, role }], category, x?, series,
1170
+ * authored? }`. `category`, `x` and `series` are column names (the mapping's, or core's positional default); `xy` is true for a chart
1171
+ * with an X axis (scatter); `authored` is the chart's own `mapping`, when it has one. Roles are "category", "label", "x", "series"
1172
+ * and "other" (a column nothing plots).
1173
+ */
1174
+ export function describeChartMapping(presentation, path) {
1175
+ const found = locateGridData(presentation, path);
1176
+ if (found.kind !== "chart") throw fail("grid-wrong-kind", "Only a chart has a series mapping.", { path });
1177
+ const model = toModel(found);
1178
+ padLines(model);
1179
+ const names = namesOfModel(model);
1180
+ const xy = isXYChartType(found.chartType);
1181
+ const { category, x, series } = mappingColumns(names.length, xy, model.mapping, names);
1182
+ const roles = chartColumnRoles(names.length, found.chartType, { ...(model.mapping ?? {}) }, names);
1183
+ return {
1184
+ path: found.parts.join("."),
1185
+ xy,
1186
+ columns: names.map((name, index) => ({ name, ...(formatOf(model.lines[0][index]) ? { format: formatOf(model.lines[0][index]) } : {}), role: roles[index] })),
1187
+ category: names[category],
1188
+ ...(x === undefined ? {} : { x: names[x] }),
1189
+ series: series.map((index) => names[index]),
1190
+ ...(model.mapping ? { authored: clone(model.mapping) } : {}),
1191
+ };
1192
+ }
1193
+
1194
+ /**
1195
+ * Set a chart's category, X and series columns by name. `wanted` is `{ category?, x?, series? }`; what it leaves out keeps its
1196
+ * current value. A field that equals the default (the first column is the category, the second the X column, every other column a
1197
+ * series, in order) is not written, and `chart.mapping` is removed when nothing is left. One patch.
1198
+ */
1199
+ export function prepareChartMapping(presentation, path, wanted = {}) {
1200
+ const found = locateGridData(presentation, path);
1201
+ if (found.kind !== "chart") throw fail("grid-wrong-kind", "Only a chart has a series mapping.", { path });
1202
+ const model = toModel(found);
1203
+ padLines(model);
1204
+ const names = namesOfModel(model);
1205
+ const xy = isXYChartType(found.chartType);
1206
+ const current = mappingColumns(names.length, xy, model.mapping, names);
1207
+ const pick = (name, what) => {
1208
+ const index = typeof name === "string" ? names.indexOf(name) : -1;
1209
+ if (index < 0) throw fail("chart-mapping-unknown-column", `The ${what} column ${JSON.stringify(name)} is not a column of this chart's data. Choose ${names.map((entry) => JSON.stringify(entry)).join(", ")}.`, { column: name });
1210
+ return index;
1211
+ };
1212
+ if (wanted.x !== undefined && !xy) throw fail("chart-mapping-x-unsupported", `A '${found.chartType ?? "chart"}' chart has no X axis, so it has no X column. Only a scatter chart does.`, { x: wanted.x });
1213
+ const category = wanted.category !== undefined ? pick(wanted.category, "category") : current.category;
1214
+ let x;
1215
+ if (xy) x = wanted.x !== undefined ? pick(wanted.x, "X") : current.x === category || current.x === undefined ? defaultX(category, names.length) : current.x;
1216
+ if (xy && x === category) throw fail("chart-mapping-conflict", "The X column and the category column must be different columns.", { category: names[category] });
1217
+ if (xy && x !== undefined && x >= names.length) throw fail("chart-mapping-conflict", "An XY chart needs a column for X besides the category.", {});
1218
+ const defaultSeries = names.map((_, index) => index).filter((index) => index !== category && index !== x);
1219
+ // Without its own `series`, a chart plots every other column: that follows a new category or X column.
1220
+ let series = wanted.series !== undefined ? wanted.series.map((name) => pick(name, "series")) : Array.isArray(model.mapping?.series) ? current.series : defaultSeries;
1221
+ series = series.filter((index, position) => index !== category && index !== x && series.indexOf(index) === position);
1222
+ if (!series.length) throw fail("chart-mapping-no-series", "Choose at least one series column to plot.", {});
1223
+ const next = {};
1224
+ if (category !== 0) next.category = names[category];
1225
+ if (xy && x !== undefined && x !== defaultX(category, names.length)) next.x = names[x];
1226
+ if (series.length !== defaultSeries.length || series.some((index, position) => index !== defaultSeries[position])) next.series = series.map((index) => names[index]);
1227
+ const after = Object.keys(next).length ? next : undefined;
1228
+ const before = checkFormat(presentation);
1229
+ const patches = valuePatches([...found.parts, "mapping"], found.owner.mapping, after);
1230
+ const changed = patches.length > 0;
1231
+ const result = changed ? checkedDocument(presentation, patches, before) : presentation;
1232
+ return { action: "set-mapping", kind: "chart", path: found.parts.join("."), mapping: after ? clone(after) : null, presentation: clone(result), patches, changed };
1233
+ }
1234
+
1235
+ // --- shared datasets (RR-54) -----------------------------------------------------------------------
1236
+
1237
+ /**
1238
+ * Replace a chart's or table's dataset reference with its own inline copy of the data (the columns `fields` selects, formats and
1239
+ * the dataset's `source` kept). The dataset stays in the document for the other items. One patch; after it the item's edits no longer
1240
+ * reach the shared dataset.
1241
+ */
1242
+ export function prepareDetachDataset(presentation, path) {
1243
+ const found = locateGridData(presentation, path);
1244
+ if (!found.dataset) throw fail("grid-not-dataset", "This chart or table does not use a shared dataset.", { path });
1245
+ const inline = found.kind === "chart" ? inlineChartData(found.owner, presentation).data : inlineTableData(found.owner, presentation);
1246
+ const pointer = opfPathToJsonPointer(found.dataset.refParts);
1247
+ const patches = [{ op: "test", path: pointer, value: clone(found.dataset.ref) }, { op: "replace", path: pointer, value: clone(inline) }];
1248
+ const result = checkedDocument(presentation, patches, checkFormat(presentation));
1249
+ return { action: "detach-dataset", kind: found.kind, path: found.parts.join("."), dataset: found.dataset.id, presentation: clone(result), patches, changed: true };
1250
+ }
1251
+
1252
+ export { datasetUsage };
1253
+
784
1254
  // --- copy --------------------------------------------------------------------------------------
785
1255
 
786
1256
  /**
@@ -788,8 +1258,8 @@ export function prepareTranspose(document, path) {
788
1258
  * another `delimiter`). `range` is `{ from, to }` of `{ section, row, column }` addresses; omit it for the whole grid. A merged cell's text is
789
1259
  * in its first position and its covered positions are empty, as a spreadsheet copies them.
790
1260
  */
791
- export function gridRangeText(document, path, range, options = {}) {
792
- const grid = describeDataGrid(document, path, options);
1261
+ export function gridRangeText(presentation, path, range, options = {}) {
1262
+ const grid = describeDataGrid(presentation, path, options);
793
1263
  const find = (address) => {
794
1264
  const u = address.section === "header" ? 0 : address.row + (grid.hasHeader ? 1 : 0);
795
1265
  if (address.section === "header" && !grid.hasHeader) throw fail("grid-cell-not-found", "This table has no header row.", { cell: address });
@@ -809,28 +1279,34 @@ export function gridRangeText(document, path, range, options = {}) {
809
1279
 
810
1280
  function commit(editor, prepared, meta = {}) {
811
1281
  if (!editor || typeof editor.applyPatch !== "function") throw fail("invalid-editor", "Expected an editor session created by createEditorSession.");
812
- const { document, patches, ...summary } = prepared;
813
- void document;
814
- if (!prepared.changed) return { ...summary, document: editor.document, patches: [], inversePatches: [], validation: editor.validation };
1282
+ const { presentation, patches, ...summary } = prepared;
1283
+ void presentation;
1284
+ if (!prepared.changed) return { ...summary, presentation: editor.presentation, patches: [], inversePatches: [], validation: editor.validation };
815
1285
  const change = editor.applyPatch(patches, { ...meta, source: meta.source ?? "data-grid", action: prepared.action, path: prepared.path });
816
1286
  return { ...change, ...summary };
817
1287
  }
818
1288
 
819
1289
  const run = (editor, prepare, options = {}) => {
820
1290
  if (!editor || typeof editor.applyPatch !== "function" || typeof editor.subscribe !== "function") throw fail("invalid-editor", "Expected an editor session created by createEditorSession.");
821
- return commit(editor, prepare(editor.document), options.meta);
1291
+ return commit(editor, prepare(editor.presentation), options.meta);
822
1292
  };
823
1293
 
824
1294
  /** Set cells as one undoable transaction. See {@link prepareGridCells}. */
825
- export const setGridCells = (editor, path, edits, options = {}) => run(editor, (document) => prepareGridCells(document, path, edits, options), options);
1295
+ export const setGridCells = (editor, path, edits, options = {}) => run(editor, (presentation) => prepareGridCells(presentation, path, edits, options), options);
826
1296
  /** Paste text or rows as one undoable transaction. See {@link preparePaste}. */
827
- export const pasteGridText = (editor, path, anchor, source, options = {}) => run(editor, (document) => preparePaste(document, path, anchor, source, options), options);
828
- export const insertGridRows = (editor, path, at, count = 1, options = {}) => run(editor, (document) => prepareInsertRows(document, path, at, count, options), options);
829
- export const deleteGridRows = (editor, path, indices, options = {}) => run(editor, (document) => prepareDeleteRows(document, path, indices, options), options);
830
- export const moveGridRows = (editor, path, from, to, count = 1, options = {}) => run(editor, (document) => prepareMoveRows(document, path, from, to, count, options), options);
831
- export const insertGridColumns = (editor, path, at, count = 1, options = {}) => run(editor, (document) => prepareInsertColumns(document, path, at, count, options), options);
832
- export const deleteGridColumns = (editor, path, indices, options = {}) => run(editor, (document) => prepareDeleteColumns(document, path, indices, options), options);
833
- export const moveGridColumns = (editor, path, from, to, count = 1, options = {}) => run(editor, (document) => prepareMoveColumns(document, path, from, to, count, options), options);
834
- export const sortGridRows = (editor, path, column, options = {}) => run(editor, (document) => prepareSortRows(document, path, column, options), options);
835
- export const setGridHeader = (editor, path, enabled, options = {}) => run(editor, (document) => prepareSetHeader(document, path, enabled, options), options);
836
- export const transposeGridData = (editor, path, options = {}) => run(editor, (document) => prepareTranspose(document, path), options);
1297
+ export const pasteGridText = (editor, path, anchor, source, options = {}) => run(editor, (presentation) => preparePaste(presentation, path, anchor, source, options), options);
1298
+ export const insertGridRows = (editor, path, at, count = 1, options = {}) => run(editor, (presentation) => prepareInsertRows(presentation, path, at, count, options), options);
1299
+ export const deleteGridRows = (editor, path, indices, options = {}) => run(editor, (presentation) => prepareDeleteRows(presentation, path, indices, options), options);
1300
+ export const moveGridRows = (editor, path, from, to, count = 1, options = {}) => run(editor, (presentation) => prepareMoveRows(presentation, path, from, to, count, options), options);
1301
+ export const insertGridColumns = (editor, path, at, count = 1, options = {}) => run(editor, (presentation) => prepareInsertColumns(presentation, path, at, count, options), options);
1302
+ export const deleteGridColumns = (editor, path, indices, options = {}) => run(editor, (presentation) => prepareDeleteColumns(presentation, path, indices, options), options);
1303
+ export const moveGridColumns = (editor, path, from, to, count = 1, options = {}) => run(editor, (presentation) => prepareMoveColumns(presentation, path, from, to, count, options), options);
1304
+ export const sortGridRows = (editor, path, column, options = {}) => run(editor, (presentation) => prepareSortRows(presentation, path, column, options), options);
1305
+ export const setGridHeader = (editor, path, enabled, options = {}) => run(editor, (presentation) => prepareSetHeader(presentation, path, enabled, options), options);
1306
+ export const transposeGridData = (editor, path, options = {}) => run(editor, (presentation) => prepareTranspose(presentation, path), options);
1307
+ /** Set (or clear, with `null` or "") the number format of a column's header as one undoable transaction. Throws `number-format-invalid` with the reason. See {@link columnFormatError}. */
1308
+ export const setGridColumnFormat = (editor, path, column, format, options = {}) => run(editor, (presentation) => prepareGridColumnFormat(presentation, path, column, format, options), options);
1309
+ /** Set a chart's category, X and series columns as one undoable transaction. See {@link prepareChartMapping}. */
1310
+ export const setChartMapping = (editor, path, wanted, options = {}) => run(editor, (presentation) => prepareChartMapping(presentation, path, wanted), options);
1311
+ /** Give a chart or table its own copy of a shared dataset's data, as one undoable transaction. See {@link prepareDetachDataset}. */
1312
+ export const detachGridDataset = (editor, path, options = {}) => run(editor, (presentation) => prepareDetachDataset(presentation, path), options);