@openpresentation/opf-editor 0.12.1 → 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.
- package/README.md +71 -60
- package/dist/annotations.d.ts +16 -16
- package/dist/annotations.js +71 -79
- package/dist/assets.d.ts +7 -7
- package/dist/assets.js +15 -15
- package/dist/background-options.d.ts +2 -2
- package/dist/background-options.js +6 -6
- package/dist/block-controls.js +7 -7
- package/dist/block-convert.d.ts +7 -7
- package/dist/block-convert.js +22 -21
- package/dist/blocks.d.ts +8 -8
- package/dist/blocks.js +41 -40
- package/dist/canvas-fields.js +35 -12
- package/dist/canvas.d.ts +22 -30
- package/dist/canvas.js +57 -64
- package/dist/chart-data.d.ts +10 -10
- package/dist/chart-data.js +21 -21
- package/dist/chart-options-panel.d.ts +2 -2
- package/dist/chart-options-panel.js +82 -8
- package/dist/chart-options.d.ts +35 -9
- package/dist/chart-options.js +133 -40
- package/dist/checks.js +21 -0
- package/dist/color-authoring.js +1 -1
- package/dist/content-actions.d.ts +15 -15
- package/dist/content-actions.js +71 -70
- package/dist/content-controls.js +12 -12
- package/dist/data-grid.js +7 -8
- package/dist/data.d.ts +4 -4
- package/dist/data.js +5 -5
- package/dist/dataset-refs.d.ts +2 -2
- package/dist/dataset-refs.js +5 -5
- package/dist/design-controls.js +61 -44
- package/dist/design-options.d.ts +16 -14
- package/dist/design-options.js +86 -74
- package/dist/edit-helpers.js +8 -7
- package/dist/exact-source.js +5 -5
- package/dist/export.d.ts +5 -7
- package/dist/export.js +40 -14
- package/dist/find-panel.js +5 -5
- package/dist/find-replace.d.ts +3 -3
- package/dist/find-replace.js +23 -20
- package/dist/font-gate.js +49 -58
- package/dist/galleries.js +36 -52
- package/dist/grid-model.d.ts +25 -27
- package/dist/grid-model.js +92 -102
- package/dist/image-crop.d.ts +4 -4
- package/dist/image-crop.js +20 -20
- package/dist/image-cropper.js +3 -3
- package/dist/index.d.ts +28 -28
- package/dist/index.js +74 -90
- package/dist/json-options.js +17 -16
- package/dist/layout-handles.js +5 -5
- package/dist/layout-placeholders.js +4 -4
- package/dist/layout.d.ts +2 -2
- package/dist/layout.js +8 -7
- package/dist/numbering-panel.js +3 -5
- package/dist/numbering.d.ts +3 -5
- package/dist/numbering.js +14 -26
- package/dist/outline-view.js +24 -24
- package/dist/outline.d.ts +12 -12
- package/dist/outline.js +52 -50
- package/dist/persistence.d.ts +1 -1
- package/dist/persistence.js +19 -18
- package/dist/presentation-ids.js +2 -2
- package/dist/react.js +3 -3
- package/dist/review-panel.d.ts +30 -13
- package/dist/review-panel.js +154 -53
- package/dist/review.d.ts +36 -43
- package/dist/review.js +92 -53
- package/dist/rich-text-input.js +4 -3
- package/dist/rich-text-toolbar.js +5 -3
- package/dist/rich-text.js +5 -2
- package/dist/schema-inspector.d.ts +2 -2
- package/dist/schema-inspector.js +16 -11
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +25 -5
- package/dist/slide-manager.d.ts +2 -2
- package/dist/slide-manager.js +18 -18
- package/dist/slides.d.ts +20 -20
- package/dist/slides.js +99 -97
- package/dist/svelte.js +1 -1
- package/dist/switches.d.ts +10 -10
- package/dist/switches.js +124 -114
- package/dist/table-options.d.ts +8 -8
- package/dist/table-options.js +28 -27
- package/dist/table-structure.d.ts +10 -10
- package/dist/table-structure.js +21 -21
- package/dist/template-panel.d.ts +3 -3
- package/dist/template-panel.js +37 -13
- package/dist/templates.d.ts +22 -11
- package/dist/templates.js +48 -55
- package/dist/transfer.d.ts +4 -4
- package/dist/transfer.js +49 -59
- package/package.json +8 -6
- package/dist/font-defaults.js +0 -25
package/dist/grid-model.js
CHANGED
|
@@ -14,15 +14,16 @@
|
|
|
14
14
|
// dataset keeps it). Renaming a column keeps every `fields` and `mapping` that names it in step.
|
|
15
15
|
//
|
|
16
16
|
// Both are read as "lines": the header line (when there is one) followed by the body rows. The operations below
|
|
17
|
-
// change lines and columns, keep merged cells whole or refuse with a reason, and return a `{
|
|
17
|
+
// change lines and columns, keep merged cells whole or refuse with a reason, and return a `{ presentation, patches,
|
|
18
18
|
// changed }` description like table-options.js does. Nothing here touches a DOM.
|
|
19
|
-
import
|
|
20
|
-
import { getValueAtPath, opfPathToJsonPointer, splitOpfPath
|
|
19
|
+
import { chartNumber, formatDataNumber, inlineChartData, inlineTableData, isXYChartType, numberFormatError } from "@openpresentation/opf";
|
|
20
|
+
import { getValueAtPath, opfPathToJsonPointer, splitOpfPath } from "./index.js";
|
|
21
21
|
import { checkedDocument, fail, same } from "./edit-helpers.js";
|
|
22
22
|
import { datasetUsage, isDatasetRef, walkDatasetItems } from "./dataset-refs.js";
|
|
23
23
|
import { richTextContent, updateRichTextInput } from "./rich-text.js";
|
|
24
24
|
import { formatGridNumber, isCanonicalNumber, parseDelimited, parseGridNumber, resolveNumberFormat, toDelimited } from "./grid-text.js";
|
|
25
25
|
import { applyTableStyleToTable, readTableStyleOfTable } from "./table-options.js";
|
|
26
|
+
import { checkFormat } from "./checks.js";
|
|
26
27
|
|
|
27
28
|
const isObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
28
29
|
const isStyled = (cell) => isObject(cell) && Object.hasOwn(cell, "value");
|
|
@@ -58,12 +59,12 @@ const decimalOf = (options = {}) => (options.decimal === "," || options.decimal
|
|
|
58
59
|
|
|
59
60
|
// --- locating ----------------------------------------------------------------------------------
|
|
60
61
|
|
|
61
|
-
const datasetsOf = (
|
|
62
|
+
const datasetsOf = (presentation) => (isObject(presentation) && isObject(presentation.datasets) ? presentation.datasets : {});
|
|
62
63
|
|
|
63
64
|
/** The dataset a chart or table reference points at, and the dataset columns its `fields` select (by index). */
|
|
64
|
-
function datasetOf(
|
|
65
|
+
function datasetOf(presentation, ref, path, refParts) {
|
|
65
66
|
const id = ref.dataset;
|
|
66
|
-
const datasets = datasetsOf(
|
|
67
|
+
const datasets = datasetsOf(presentation);
|
|
67
68
|
const data = Object.hasOwn(datasets, id) ? datasets[id] : undefined;
|
|
68
69
|
if (!isObject(data) || !Array.isArray(data.columns) || !Array.isArray(data.rows))
|
|
69
70
|
throw fail("dataset-unavailable", `This item uses the shared dataset '${id}', which the document does not hold.`, { path, dataset: id });
|
|
@@ -80,13 +81,12 @@ function datasetOf(document, ref, path, refParts) {
|
|
|
80
81
|
|
|
81
82
|
/**
|
|
82
83
|
* Find the data a path points at. A chart path ends in `chart` (or `chart.data`) and a table path in `table`.
|
|
83
|
-
* Throws `grid-target-not-found
|
|
84
|
-
* has no inline data to edit) or `dataset-unavailable` (the shared dataset or one of its `fields` is missing).
|
|
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
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
86
|
* is `["datasets", id]` and `dataset` is `{ id, fields, indices, refParts, ref }` (`indices` maps each shown column to the
|
|
87
87
|
* dataset's own column).
|
|
88
88
|
*/
|
|
89
|
-
export function locateGridData(
|
|
89
|
+
export function locateGridData(presentation, path) {
|
|
90
90
|
let parts;
|
|
91
91
|
try {
|
|
92
92
|
parts = Array.isArray(path) ? [...path] : splitOpfPath(path);
|
|
@@ -94,22 +94,22 @@ export function locateGridData(document, path) {
|
|
|
94
94
|
throw fail("grid-target-not-found", "Choose a chart or a table.", { path });
|
|
95
95
|
}
|
|
96
96
|
if (parts.at(-1) === "data" && parts.at(-2) === "chart") parts = parts.slice(0, -1);
|
|
97
|
-
const owner = getValueAtPath(
|
|
97
|
+
const owner = getValueAtPath(presentation, parts);
|
|
98
98
|
if (parts.at(-1) === "chart") {
|
|
99
99
|
if (!isObject(owner) || !isObject(owner.data)) throw fail("grid-target-not-found", "This chart has no data.", { path });
|
|
100
100
|
const data = owner.data;
|
|
101
101
|
const chartType = typeof owner.type === "string" ? owner.type : undefined;
|
|
102
102
|
if (isDatasetRef(data)) {
|
|
103
|
-
const found = datasetOf(
|
|
103
|
+
const found = datasetOf(presentation, data, path, [...parts, "data"]);
|
|
104
104
|
return { kind: "chart", parts, dataParts: ["datasets", data.dataset], data: found.data, chartType, owner, dataset: found.dataset };
|
|
105
105
|
}
|
|
106
106
|
if (!Array.isArray(data.columns) || !Array.isArray(data.rows))
|
|
107
|
-
throw fail("
|
|
107
|
+
throw fail("grid-target-not-found", "This chart has no inline columns and rows. The data grid edits inline chart data.", { path });
|
|
108
108
|
return { kind: "chart", parts, dataParts: [...parts, "data"], data, chartType, owner };
|
|
109
109
|
}
|
|
110
110
|
if (parts.at(-1) === "table") {
|
|
111
111
|
if (isObject(owner) && isDatasetRef(owner) && !Array.isArray(owner.rows)) {
|
|
112
|
-
const found = datasetOf(
|
|
112
|
+
const found = datasetOf(presentation, owner, path, parts);
|
|
113
113
|
return { kind: "table", parts, dataParts: ["datasets", owner.dataset], data: found.data, owner, dataset: found.dataset };
|
|
114
114
|
}
|
|
115
115
|
if (!isObject(owner) || !Array.isArray(owner.rows)) throw fail("table-not-found", "Choose a table (a path ending in .table).", { path });
|
|
@@ -125,7 +125,7 @@ export function locateGridData(document, path) {
|
|
|
125
125
|
* `dataset: { id, fields, count, items }` (`items` are the paths of every chart and table that uses the dataset).
|
|
126
126
|
* Returns undefined when the path is not inside a chart or table.
|
|
127
127
|
*/
|
|
128
|
-
export function resolveDataGridTarget(
|
|
128
|
+
export function resolveDataGridTarget(presentation, selectedPath) {
|
|
129
129
|
let parts;
|
|
130
130
|
try {
|
|
131
131
|
parts = splitOpfPath(selectedPath);
|
|
@@ -136,15 +136,15 @@ export function resolveDataGridTarget(document, selectedPath) {
|
|
|
136
136
|
if (parts[index] !== "chart" && parts[index] !== "table") continue;
|
|
137
137
|
const head = parts.slice(0, index + 1);
|
|
138
138
|
try {
|
|
139
|
-
const found = locateGridData(
|
|
139
|
+
const found = locateGridData(presentation, head);
|
|
140
140
|
const target = { kind: found.kind, path: head.join(".") };
|
|
141
141
|
if (found.dataset) {
|
|
142
|
-
const items = datasetUsage(
|
|
142
|
+
const items = datasetUsage(presentation, found.dataset.id).map((entry) => entry.path);
|
|
143
143
|
target.dataset = { id: found.dataset.id, ...(found.dataset.fields ? { fields: [...found.dataset.fields] } : {}), count: items.length, items };
|
|
144
144
|
}
|
|
145
145
|
return target;
|
|
146
146
|
} catch (error) {
|
|
147
|
-
if (error.code === "
|
|
147
|
+
if (error.code === "dataset-unavailable") return { kind: parts[index], path: head.join("."), editable: false, reason: error.message };
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
return undefined;
|
|
@@ -296,13 +296,6 @@ export function cellText(raw, kind, decimal = ".") {
|
|
|
296
296
|
|
|
297
297
|
const isScatter = (type) => typeof type === "string" && /^(scatter|bubble)/.test(type);
|
|
298
298
|
|
|
299
|
-
// RR-54: core's strict chart number and XY test. A core that predates them (the installed range still allows one) keeps the
|
|
300
|
-
// editor working on documents that use none of the new fields: numbers and scatter-like type ids read as the editor always read them.
|
|
301
|
-
const chartNumber = (value) =>
|
|
302
|
-
typeof core.chartNumber === "function" ? core.chartNumber(value) : typeof value === "number" ? (Number.isFinite(value) ? value : null) : typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value)) ? Number(value) : null;
|
|
303
|
-
/** Whether the installed core has the RR-54 chart and table data contract (number formats, datasets, `chartNumber`); an older core edits documents that use none of it. */
|
|
304
|
-
export const supportsChartTableData = ["chartNumber", "numberFormatError", "inlineChartData", "inlineTableData", "isXYChartType"].every((name) => typeof core[name] === "function");
|
|
305
|
-
const isXYType = (type) => (typeof core.isXYChartType === "function" ? core.isXYChartType(type) : isScatter(type));
|
|
306
299
|
|
|
307
300
|
/**
|
|
308
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
|
|
@@ -311,15 +304,14 @@ const isXYType = (type) => (typeof core.isXYChartType === "function" ? core.isXY
|
|
|
311
304
|
* replaced: editing and copying use it.
|
|
312
305
|
*/
|
|
313
306
|
function displayText(model, raw, column, role, text) {
|
|
314
|
-
if (!supportsChartTableData || typeof core.formatDataNumber !== "function") return undefined;
|
|
315
307
|
const format = (model.kind === "table" ? formatOf(raw) : undefined) ?? (model.hasHeader ? formatOf(model.lines[0]?.[column]) : undefined);
|
|
316
|
-
if (format === undefined ||
|
|
308
|
+
if (format === undefined || numberFormatError(format) !== undefined) return undefined;
|
|
317
309
|
const value = valueOf(raw);
|
|
318
310
|
let number = null;
|
|
319
311
|
if (model.kind === "chart") number = isNumericRole(role) && (typeof value === "number" || typeof value === "string") ? chartNumber(value) : null;
|
|
320
312
|
else if (typeof value === "number" && Number.isFinite(value)) number = value;
|
|
321
313
|
if (number === null) return undefined;
|
|
322
|
-
const shown =
|
|
314
|
+
const shown = formatDataNumber(number, format);
|
|
323
315
|
return shown === text ? undefined : shown;
|
|
324
316
|
}
|
|
325
317
|
|
|
@@ -367,7 +359,7 @@ function mappingColumns(count, xy, mapping, names) {
|
|
|
367
359
|
*/
|
|
368
360
|
export function chartColumnRoles(columnCount, chartType, mapping, names) {
|
|
369
361
|
if (isObject(mapping) && Array.isArray(names) && columnCount > 0) {
|
|
370
|
-
const xy =
|
|
362
|
+
const xy = isXYChartType(chartType);
|
|
371
363
|
const { category, x, series } = mappingColumns(columnCount, xy, mapping, names);
|
|
372
364
|
const roles = Array(columnCount).fill("other");
|
|
373
365
|
for (const index of series) roles[index] = "series";
|
|
@@ -391,8 +383,8 @@ const isNumericRole = (role) => role === undefined || role === "series" || role
|
|
|
391
383
|
* cell has `covered: true` and its anchor in `owner`; its `text` is "". `warnings` lists data problems the document
|
|
392
384
|
* already has (text in a series column the renderer draws as a gap, an empty or repeated series name, a missing category).
|
|
393
385
|
*/
|
|
394
|
-
export function describeDataGrid(
|
|
395
|
-
const found = locateGridData(
|
|
386
|
+
export function describeDataGrid(presentation, path, options = {}) {
|
|
387
|
+
const found = locateGridData(presentation, path);
|
|
396
388
|
const decimal = decimalOf(options);
|
|
397
389
|
const model = toModel(found);
|
|
398
390
|
padLines(model);
|
|
@@ -458,7 +450,7 @@ export function describeDataGrid(document, path, options = {}) {
|
|
|
458
450
|
columnRoles: roles,
|
|
459
451
|
columnFormats: Array.from({ length: model.width }, (_, column) => (model.hasHeader ? formatOf(model.lines[0]?.[column]) : undefined)),
|
|
460
452
|
...(model.hasHeader ? { columnNames: namesOfModel(model) } : {}),
|
|
461
|
-
...(found.dataset ? { dataset: { id: found.dataset.id, ...(found.dataset.fields ? { fields: [...found.dataset.fields] } : {}), items: datasetUsage(
|
|
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) } } : {}),
|
|
462
454
|
...(model.mapping ? { mapping: clone(model.mapping) } : {}),
|
|
463
455
|
lines,
|
|
464
456
|
warnings,
|
|
@@ -577,7 +569,7 @@ function inlineChanges(found, model, style) {
|
|
|
577
569
|
* `fields` selects leaves only `fields` when deleted; without `fields` it leaves the dataset, which another item's `fields` or
|
|
578
570
|
* `mapping` that names it forbids.
|
|
579
571
|
*/
|
|
580
|
-
function datasetChanges(
|
|
572
|
+
function datasetChanges(presentation, found, model) {
|
|
581
573
|
const { dataset, data } = found;
|
|
582
574
|
padLines(model);
|
|
583
575
|
const header = model.lines[0];
|
|
@@ -615,7 +607,7 @@ function datasetChanges(document, found, model) {
|
|
|
615
607
|
const renames = renamesOf(model);
|
|
616
608
|
const others = [];
|
|
617
609
|
const ownPath = found.parts.join(".");
|
|
618
|
-
walkDatasetItems(
|
|
610
|
+
walkDatasetItems(presentation, (entry) => {
|
|
619
611
|
if (entry.id === dataset.id && entry.parts.join(".") !== ownPath) others.push(entry);
|
|
620
612
|
});
|
|
621
613
|
if (!dataset.fields) {
|
|
@@ -650,8 +642,8 @@ function datasetChanges(document, found, model) {
|
|
|
650
642
|
return changes;
|
|
651
643
|
}
|
|
652
644
|
|
|
653
|
-
function transact(
|
|
654
|
-
const found = locateGridData(
|
|
645
|
+
function transact(presentation, path, action, mutate, extra = {}) {
|
|
646
|
+
const found = locateGridData(presentation, path);
|
|
655
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 });
|
|
656
648
|
const model = toModel(found);
|
|
657
649
|
const problemsBefore = mergeProblems(model).length;
|
|
@@ -659,14 +651,14 @@ function transact(document, path, action, mutate, extra = {}) {
|
|
|
659
651
|
const style = info.restyle && found.kind === "table" && !found.dataset ? readTableStyleOfTable(found.data) : undefined;
|
|
660
652
|
if (info.touchesStructure) assertMerges(model, problemsBefore, info.what ?? "This change");
|
|
661
653
|
if (style && style.preset !== "custom") padLines(model);
|
|
662
|
-
const changes = found.dataset ? datasetChanges(
|
|
654
|
+
const changes = found.dataset ? datasetChanges(presentation, found, model) : inlineChanges(found, model, style);
|
|
663
655
|
const patches = changes.flatMap((entry) => valuePatches(entry.parts, entry.before, entry.after));
|
|
664
656
|
const changed = patches.length > 0;
|
|
665
|
-
const before =
|
|
666
|
-
const result = changed ? checkedDocument(
|
|
657
|
+
const before = checkFormat(presentation);
|
|
658
|
+
const result = changed ? checkedDocument(presentation, patches, before) : presentation;
|
|
667
659
|
// `restyle`, `touchesStructure` and `what` steer this function only; the rest of what the operation reports is the caller's.
|
|
668
660
|
const { restyle: _restyle, touchesStructure: _structure, what: _what, ...summary } = info;
|
|
669
|
-
return { action, ...summary, kind: found.kind, path: found.parts.join("."), ...(found.dataset ? { dataset: found.dataset.id } : {}),
|
|
661
|
+
return { action, ...summary, kind: found.kind, path: found.parts.join("."), ...(found.dataset ? { dataset: found.dataset.id } : {}), presentation: clone(result), patches, changed };
|
|
670
662
|
}
|
|
671
663
|
|
|
672
664
|
// --- row and column structure ------------------------------------------------------------------
|
|
@@ -1030,8 +1022,8 @@ function pasteRows(model, anchor, matrix, options) {
|
|
|
1030
1022
|
* The problems `edits` would have, without applying or validating the document (cheap enough to run on every keystroke): a list of
|
|
1031
1023
|
* `{ section, row, column, message }`, empty when every edit is fine.
|
|
1032
1024
|
*/
|
|
1033
|
-
export function gridCellIssues(
|
|
1034
|
-
const model = toModel(locateGridData(
|
|
1025
|
+
export function gridCellIssues(presentation, path, edits, options = {}) {
|
|
1026
|
+
const model = toModel(locateGridData(presentation, path));
|
|
1035
1027
|
return applyEdits(model, edits, decimalOf(options), ownersOf(model));
|
|
1036
1028
|
}
|
|
1037
1029
|
|
|
@@ -1044,8 +1036,8 @@ const countOf = (count) => {
|
|
|
1044
1036
|
const kindOf = (options) => (options?.kind ? { kind: options.kind } : {});
|
|
1045
1037
|
|
|
1046
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. */
|
|
1047
|
-
export function prepareGridCells(
|
|
1048
|
-
return transact(
|
|
1039
|
+
export function prepareGridCells(presentation, path, edits, options = {}) {
|
|
1040
|
+
return transact(presentation, path, "set-cells", (model) => setCells(model, edits, options), kindOf(options));
|
|
1049
1041
|
}
|
|
1050
1042
|
|
|
1051
1043
|
/**
|
|
@@ -1054,54 +1046,54 @@ export function prepareGridCells(document, path, edits, options = {}) {
|
|
|
1054
1046
|
* `options` (`decimal`, or `numberFormat` and `locale`); when that fails and the other format reads all of the paste, that is used and
|
|
1055
1047
|
* reported as `decimal`. A table paste over a merged cell's covered positions is refused. One patch.
|
|
1056
1048
|
*/
|
|
1057
|
-
export function preparePaste(
|
|
1049
|
+
export function preparePaste(presentation, path, anchor, source, options = {}) {
|
|
1058
1050
|
const rows = typeof source === "string" ? parseDelimited(source, { delimiter: options.delimiter }).rows : source;
|
|
1059
|
-
return transact(
|
|
1051
|
+
return transact(presentation, path, "paste", (model) => pasteRows(model, anchor, rows, options), kindOf(options));
|
|
1060
1052
|
}
|
|
1061
1053
|
|
|
1062
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. */
|
|
1063
|
-
export function prepareInsertRows(
|
|
1064
|
-
return transact(
|
|
1055
|
+
export function prepareInsertRows(presentation, path, at, count = 1, options = {}) {
|
|
1056
|
+
return transact(presentation, path, "insert-rows", (model) => {
|
|
1065
1057
|
insertRows(model, at, countOf(count));
|
|
1066
1058
|
return { at, count, restyle: true, touchesStructure: true, what: "Inserting rows" };
|
|
1067
1059
|
}, kindOf(options));
|
|
1068
1060
|
}
|
|
1069
1061
|
|
|
1070
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. */
|
|
1071
|
-
export function prepareDeleteRows(
|
|
1072
|
-
return transact(
|
|
1063
|
+
export function prepareDeleteRows(presentation, path, indices, options = {}) {
|
|
1064
|
+
return transact(presentation, path, "delete-rows", (model) => {
|
|
1073
1065
|
deleteRows(model, indices);
|
|
1074
1066
|
return { indices: [...indices], restyle: true, touchesStructure: true, what: "Deleting rows" };
|
|
1075
1067
|
}, kindOf(options));
|
|
1076
1068
|
}
|
|
1077
1069
|
|
|
1078
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. */
|
|
1079
|
-
export function prepareMoveRows(
|
|
1080
|
-
return transact(
|
|
1071
|
+
export function prepareMoveRows(presentation, path, from, to, count = 1, options = {}) {
|
|
1072
|
+
return transact(presentation, path, "move-rows", (model) => {
|
|
1081
1073
|
moveRows(model, from, count, to);
|
|
1082
1074
|
return { from, to, count, restyle: true, touchesStructure: true, what: "Moving rows" };
|
|
1083
1075
|
}, kindOf(options));
|
|
1084
1076
|
}
|
|
1085
1077
|
|
|
1086
1078
|
/** Insert `count` empty columns so the first is column `at`. A merged cell that spans the position grows. */
|
|
1087
|
-
export function prepareInsertColumns(
|
|
1088
|
-
return transact(
|
|
1079
|
+
export function prepareInsertColumns(presentation, path, at, count = 1, options = {}) {
|
|
1080
|
+
return transact(presentation, path, "insert-columns", (model) => {
|
|
1089
1081
|
insertColumns(model, at, countOf(count));
|
|
1090
1082
|
return { at, count, restyle: true, touchesStructure: true, what: "Inserting columns" };
|
|
1091
1083
|
}, kindOf(options));
|
|
1092
1084
|
}
|
|
1093
1085
|
|
|
1094
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. */
|
|
1095
|
-
export function prepareDeleteColumns(
|
|
1096
|
-
return transact(
|
|
1087
|
+
export function prepareDeleteColumns(presentation, path, indices, options = {}) {
|
|
1088
|
+
return transact(presentation, path, "delete-columns", (model) => {
|
|
1097
1089
|
deleteColumns(model, indices);
|
|
1098
1090
|
return { indices: [...indices], restyle: true, touchesStructure: true, what: "Deleting columns" };
|
|
1099
1091
|
}, kindOf(options));
|
|
1100
1092
|
}
|
|
1101
1093
|
|
|
1102
1094
|
/** Move `count` columns starting at `from` so the first is at `to`. Refused with the reason when it would split a merged cell. */
|
|
1103
|
-
export function prepareMoveColumns(
|
|
1104
|
-
return transact(
|
|
1095
|
+
export function prepareMoveColumns(presentation, path, from, to, count = 1, options = {}) {
|
|
1096
|
+
return transact(presentation, path, "move-columns", (model) => {
|
|
1105
1097
|
moveColumns(model, from, count, to);
|
|
1106
1098
|
return { from, to, count, restyle: true, touchesStructure: true, what: "Moving columns" };
|
|
1107
1099
|
}, kindOf(options));
|
|
@@ -1112,18 +1104,18 @@ export function prepareMoveColumns(document, path, from, to, count = 1, options
|
|
|
1112
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
|
|
1113
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.
|
|
1114
1106
|
*/
|
|
1115
|
-
export function prepareSortRows(
|
|
1116
|
-
return transact(
|
|
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));
|
|
1117
1109
|
}
|
|
1118
1110
|
|
|
1119
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"`). */
|
|
1120
|
-
export function prepareSetHeader(
|
|
1121
|
-
return transact(
|
|
1112
|
+
export function prepareSetHeader(presentation, path, enabled, options = {}) {
|
|
1113
|
+
return transact(presentation, path, "set-header", (model) => setHeader(model, Boolean(enabled), options), { kind: "table" });
|
|
1122
1114
|
}
|
|
1123
1115
|
|
|
1124
1116
|
/** Swap a chart's categories and series: the first column's values become the series names and the series become the rows. */
|
|
1125
|
-
export function prepareTranspose(
|
|
1126
|
-
return transact(
|
|
1117
|
+
export function prepareTranspose(presentation, path) {
|
|
1118
|
+
return transact(presentation, path, "transpose", (model) => transposeChart(model), { kind: "chart" });
|
|
1127
1119
|
}
|
|
1128
1120
|
|
|
1129
1121
|
// --- number format of a column (RR-54) ---------------------------------------------------------
|
|
@@ -1131,8 +1123,7 @@ export function prepareTranspose(document, path) {
|
|
|
1131
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). */
|
|
1132
1124
|
export function columnFormatError(format) {
|
|
1133
1125
|
if (format === undefined || format === null || format === "") return undefined;
|
|
1134
|
-
|
|
1135
|
-
return core.numberFormatError(format);
|
|
1126
|
+
return numberFormatError(format);
|
|
1136
1127
|
}
|
|
1137
1128
|
|
|
1138
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. */
|
|
@@ -1168,8 +1159,8 @@ function setColumnFormat(model, column, format) {
|
|
|
1168
1159
|
}
|
|
1169
1160
|
|
|
1170
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. */
|
|
1171
|
-
export function prepareGridColumnFormat(
|
|
1172
|
-
return transact(
|
|
1162
|
+
export function prepareGridColumnFormat(presentation, path, column, format, options = {}) {
|
|
1163
|
+
return transact(presentation, path, "set-column-format", (model) => setColumnFormat(model, column, format), kindOf(options));
|
|
1173
1164
|
}
|
|
1174
1165
|
|
|
1175
1166
|
// --- chart mapping (RR-54) -----------------------------------------------------------------------
|
|
@@ -1180,13 +1171,13 @@ export function prepareGridColumnFormat(document, path, column, format, options
|
|
|
1180
1171
|
* with an X axis (scatter); `authored` is the chart's own `mapping`, when it has one. Roles are "category", "label", "x", "series"
|
|
1181
1172
|
* and "other" (a column nothing plots).
|
|
1182
1173
|
*/
|
|
1183
|
-
export function describeChartMapping(
|
|
1184
|
-
const found = locateGridData(
|
|
1174
|
+
export function describeChartMapping(presentation, path) {
|
|
1175
|
+
const found = locateGridData(presentation, path);
|
|
1185
1176
|
if (found.kind !== "chart") throw fail("grid-wrong-kind", "Only a chart has a series mapping.", { path });
|
|
1186
1177
|
const model = toModel(found);
|
|
1187
1178
|
padLines(model);
|
|
1188
1179
|
const names = namesOfModel(model);
|
|
1189
|
-
const xy =
|
|
1180
|
+
const xy = isXYChartType(found.chartType);
|
|
1190
1181
|
const { category, x, series } = mappingColumns(names.length, xy, model.mapping, names);
|
|
1191
1182
|
const roles = chartColumnRoles(names.length, found.chartType, { ...(model.mapping ?? {}) }, names);
|
|
1192
1183
|
return {
|
|
@@ -1205,13 +1196,13 @@ export function describeChartMapping(document, path) {
|
|
|
1205
1196
|
* current value. A field that equals the default (the first column is the category, the second the X column, every other column a
|
|
1206
1197
|
* series, in order) is not written, and `chart.mapping` is removed when nothing is left. One patch.
|
|
1207
1198
|
*/
|
|
1208
|
-
export function prepareChartMapping(
|
|
1209
|
-
const found = locateGridData(
|
|
1199
|
+
export function prepareChartMapping(presentation, path, wanted = {}) {
|
|
1200
|
+
const found = locateGridData(presentation, path);
|
|
1210
1201
|
if (found.kind !== "chart") throw fail("grid-wrong-kind", "Only a chart has a series mapping.", { path });
|
|
1211
1202
|
const model = toModel(found);
|
|
1212
1203
|
padLines(model);
|
|
1213
1204
|
const names = namesOfModel(model);
|
|
1214
|
-
const xy =
|
|
1205
|
+
const xy = isXYChartType(found.chartType);
|
|
1215
1206
|
const current = mappingColumns(names.length, xy, model.mapping, names);
|
|
1216
1207
|
const pick = (name, what) => {
|
|
1217
1208
|
const index = typeof name === "string" ? names.indexOf(name) : -1;
|
|
@@ -1234,11 +1225,11 @@ export function prepareChartMapping(document, path, wanted = {}) {
|
|
|
1234
1225
|
if (xy && x !== undefined && x !== defaultX(category, names.length)) next.x = names[x];
|
|
1235
1226
|
if (series.length !== defaultSeries.length || series.some((index, position) => index !== defaultSeries[position])) next.series = series.map((index) => names[index]);
|
|
1236
1227
|
const after = Object.keys(next).length ? next : undefined;
|
|
1237
|
-
const before =
|
|
1228
|
+
const before = checkFormat(presentation);
|
|
1238
1229
|
const patches = valuePatches([...found.parts, "mapping"], found.owner.mapping, after);
|
|
1239
1230
|
const changed = patches.length > 0;
|
|
1240
|
-
const result = changed ? checkedDocument(
|
|
1241
|
-
return { action: "set-mapping", kind: "chart", path: found.parts.join("."), mapping: after ? clone(after) : null,
|
|
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 };
|
|
1242
1233
|
}
|
|
1243
1234
|
|
|
1244
1235
|
// --- shared datasets (RR-54) -----------------------------------------------------------------------
|
|
@@ -1248,15 +1239,14 @@ export function prepareChartMapping(document, path, wanted = {}) {
|
|
|
1248
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
|
|
1249
1240
|
* reach the shared dataset.
|
|
1250
1241
|
*/
|
|
1251
|
-
export function prepareDetachDataset(
|
|
1252
|
-
const found = locateGridData(
|
|
1242
|
+
export function prepareDetachDataset(presentation, path) {
|
|
1243
|
+
const found = locateGridData(presentation, path);
|
|
1253
1244
|
if (!found.dataset) throw fail("grid-not-dataset", "This chart or table does not use a shared dataset.", { path });
|
|
1254
|
-
|
|
1255
|
-
const inline = found.kind === "chart" ? core.inlineChartData(found.owner, document).data : core.inlineTableData(found.owner, document);
|
|
1245
|
+
const inline = found.kind === "chart" ? inlineChartData(found.owner, presentation).data : inlineTableData(found.owner, presentation);
|
|
1256
1246
|
const pointer = opfPathToJsonPointer(found.dataset.refParts);
|
|
1257
1247
|
const patches = [{ op: "test", path: pointer, value: clone(found.dataset.ref) }, { op: "replace", path: pointer, value: clone(inline) }];
|
|
1258
|
-
const result = checkedDocument(
|
|
1259
|
-
return { action: "detach-dataset", kind: found.kind, path: found.parts.join("."), dataset: found.dataset.id,
|
|
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 };
|
|
1260
1250
|
}
|
|
1261
1251
|
|
|
1262
1252
|
export { datasetUsage };
|
|
@@ -1268,8 +1258,8 @@ export { datasetUsage };
|
|
|
1268
1258
|
* another `delimiter`). `range` is `{ from, to }` of `{ section, row, column }` addresses; omit it for the whole grid. A merged cell's text is
|
|
1269
1259
|
* in its first position and its covered positions are empty, as a spreadsheet copies them.
|
|
1270
1260
|
*/
|
|
1271
|
-
export function gridRangeText(
|
|
1272
|
-
const grid = describeDataGrid(
|
|
1261
|
+
export function gridRangeText(presentation, path, range, options = {}) {
|
|
1262
|
+
const grid = describeDataGrid(presentation, path, options);
|
|
1273
1263
|
const find = (address) => {
|
|
1274
1264
|
const u = address.section === "header" ? 0 : address.row + (grid.hasHeader ? 1 : 0);
|
|
1275
1265
|
if (address.section === "header" && !grid.hasHeader) throw fail("grid-cell-not-found", "This table has no header row.", { cell: address });
|
|
@@ -1289,34 +1279,34 @@ export function gridRangeText(document, path, range, options = {}) {
|
|
|
1289
1279
|
|
|
1290
1280
|
function commit(editor, prepared, meta = {}) {
|
|
1291
1281
|
if (!editor || typeof editor.applyPatch !== "function") throw fail("invalid-editor", "Expected an editor session created by createEditorSession.");
|
|
1292
|
-
const {
|
|
1293
|
-
void
|
|
1294
|
-
if (!prepared.changed) return { ...summary,
|
|
1282
|
+
const { presentation, patches, ...summary } = prepared;
|
|
1283
|
+
void presentation;
|
|
1284
|
+
if (!prepared.changed) return { ...summary, presentation: editor.presentation, patches: [], inversePatches: [], validation: editor.validation };
|
|
1295
1285
|
const change = editor.applyPatch(patches, { ...meta, source: meta.source ?? "data-grid", action: prepared.action, path: prepared.path });
|
|
1296
1286
|
return { ...change, ...summary };
|
|
1297
1287
|
}
|
|
1298
1288
|
|
|
1299
1289
|
const run = (editor, prepare, options = {}) => {
|
|
1300
1290
|
if (!editor || typeof editor.applyPatch !== "function" || typeof editor.subscribe !== "function") throw fail("invalid-editor", "Expected an editor session created by createEditorSession.");
|
|
1301
|
-
return commit(editor, prepare(editor.
|
|
1291
|
+
return commit(editor, prepare(editor.presentation), options.meta);
|
|
1302
1292
|
};
|
|
1303
1293
|
|
|
1304
1294
|
/** Set cells as one undoable transaction. See {@link prepareGridCells}. */
|
|
1305
|
-
export const setGridCells = (editor, path, edits, options = {}) => run(editor, (
|
|
1295
|
+
export const setGridCells = (editor, path, edits, options = {}) => run(editor, (presentation) => prepareGridCells(presentation, path, edits, options), options);
|
|
1306
1296
|
/** Paste text or rows as one undoable transaction. See {@link preparePaste}. */
|
|
1307
|
-
export const pasteGridText = (editor, path, anchor, source, options = {}) => run(editor, (
|
|
1308
|
-
export const insertGridRows = (editor, path, at, count = 1, options = {}) => run(editor, (
|
|
1309
|
-
export const deleteGridRows = (editor, path, indices, options = {}) => run(editor, (
|
|
1310
|
-
export const moveGridRows = (editor, path, from, to, count = 1, options = {}) => run(editor, (
|
|
1311
|
-
export const insertGridColumns = (editor, path, at, count = 1, options = {}) => run(editor, (
|
|
1312
|
-
export const deleteGridColumns = (editor, path, indices, options = {}) => run(editor, (
|
|
1313
|
-
export const moveGridColumns = (editor, path, from, to, count = 1, options = {}) => run(editor, (
|
|
1314
|
-
export const sortGridRows = (editor, path, column, options = {}) => run(editor, (
|
|
1315
|
-
export const setGridHeader = (editor, path, enabled, options = {}) => run(editor, (
|
|
1316
|
-
export const transposeGridData = (editor, path, options = {}) => run(editor, (
|
|
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);
|
|
1317
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}. */
|
|
1318
|
-
export const setGridColumnFormat = (editor, path, column, format, options = {}) => run(editor, (
|
|
1308
|
+
export const setGridColumnFormat = (editor, path, column, format, options = {}) => run(editor, (presentation) => prepareGridColumnFormat(presentation, path, column, format, options), options);
|
|
1319
1309
|
/** Set a chart's category, X and series columns as one undoable transaction. See {@link prepareChartMapping}. */
|
|
1320
|
-
export const setChartMapping = (editor, path, wanted, options = {}) => run(editor, (
|
|
1310
|
+
export const setChartMapping = (editor, path, wanted, options = {}) => run(editor, (presentation) => prepareChartMapping(presentation, path, wanted), options);
|
|
1321
1311
|
/** Give a chart or table its own copy of a shared dataset's data, as one undoable transaction. See {@link prepareDetachDataset}. */
|
|
1322
|
-
export const detachGridDataset = (editor, path, options = {}) => run(editor, (
|
|
1312
|
+
export const detachGridDataset = (editor, path, options = {}) => run(editor, (presentation) => prepareDetachDataset(presentation, path), options);
|
package/dist/image-crop.d.ts
CHANGED
|
@@ -41,14 +41,14 @@ export interface ImageDescription {
|
|
|
41
41
|
entry?: unknown;
|
|
42
42
|
}
|
|
43
43
|
/** What a path holds when it is a picture, or `{ error }` with the reason it is not croppable (not a picture, a missing asset, SVG). */
|
|
44
|
-
export declare function describeImage(
|
|
45
|
-
export declare function countAssetReferences(
|
|
44
|
+
export declare function describeImage(presentation: unknown, path: string): ImageDescription | { error: string };
|
|
45
|
+
export declare function countAssetReferences(presentation: unknown, id: string): number;
|
|
46
46
|
export interface CroppedPixels { dataUri: string; mediaType: string; width: number; height: number; bytes?: number }
|
|
47
47
|
export interface PreparedCrop { patches: JsonPatchOperation[]; assetId: string; reference: string; origin?: string; image: ImageDescription }
|
|
48
48
|
/** The patches that apply a cropped picture: a new asset and the image pointing at it (pure). Throws `not-croppable`. */
|
|
49
|
-
export declare function prepareCrop(
|
|
49
|
+
export declare function prepareCrop(presentation: unknown, path: string, pixels: CroppedPixels): PreparedCrop;
|
|
50
50
|
/** The patches that put a cropped picture back to its original asset, or null. */
|
|
51
|
-
export declare function prepareRestore(
|
|
51
|
+
export declare function prepareRestore(presentation: unknown, path: string): { patches: JsonPatchOperation[]; origin: string; image: ImageDescription } | null;
|
|
52
52
|
/** Restore the original as one undoable change; null when there is no original. */
|
|
53
53
|
export declare function restoreOriginal(editor: EditorSession, path: string, meta?: Record<string, unknown>): (EditorChange & { origin: string }) | null;
|
|
54
54
|
export interface LoadedImage { image: HTMLImageElement; width: number; height: number }
|
package/dist/image-crop.js
CHANGED
|
@@ -156,11 +156,11 @@ export const isFullRect = (rect, bounds) => rect.x < 0.5 && rect.y < 0.5 && Math
|
|
|
156
156
|
* (the field is `{ src, ... }`); `assetSrc` is what `src` resolves to (the asset's own `src`), and `origin` is the asset a
|
|
157
157
|
* crop of this picture was made from, when there is one.
|
|
158
158
|
*/
|
|
159
|
-
export function describeImage(
|
|
159
|
+
export function describeImage(presentation, path) {
|
|
160
160
|
const segments = String(path).split(".");
|
|
161
161
|
const last = segments.at(-1);
|
|
162
162
|
if (!["image", "slideImage"].includes(last)) return { error: "This is not a picture." };
|
|
163
|
-
const value = getValueAtPath(
|
|
163
|
+
const value = getValueAtPath(presentation, path);
|
|
164
164
|
const pointer = opfPathToJsonPointer(segments);
|
|
165
165
|
let form;
|
|
166
166
|
let src;
|
|
@@ -177,7 +177,7 @@ export function describeImage(document, path) {
|
|
|
177
177
|
let assetSrc = src;
|
|
178
178
|
let entry;
|
|
179
179
|
if (assetId !== undefined) {
|
|
180
|
-
entry = getValueAtPath(
|
|
180
|
+
entry = getValueAtPath(presentation, ["assets", assetId]);
|
|
181
181
|
if (entry === undefined) return { error: `The picture asset "${assetId}" is missing from this presentation.` };
|
|
182
182
|
assetSrc = typeof entry === "string" ? entry : entry?.src;
|
|
183
183
|
if (typeof assetSrc !== "string") return { error: `The picture asset "${assetId}" has no source.` };
|
|
@@ -193,22 +193,22 @@ export function describeImage(document, path) {
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
/** How many places in the document use `asset:<id>` (the `assets` map itself is not counted). */
|
|
196
|
-
export function countAssetReferences(
|
|
196
|
+
export function countAssetReferences(presentation, id) {
|
|
197
197
|
const reference = `asset:${id}`;
|
|
198
198
|
let count = 0;
|
|
199
199
|
const walk = (value) => {
|
|
200
200
|
if (typeof value === "string") count += value === reference ? 1 : 0;
|
|
201
201
|
else if (Array.isArray(value)) value.forEach(walk);
|
|
202
|
-
else if (isObject(value)) for (const [key, child] of Object.entries(value)) if (key !== "assets" || value !==
|
|
202
|
+
else if (isObject(value)) for (const [key, child] of Object.entries(value)) if (key !== "assets" || value !== presentation) walk(child);
|
|
203
203
|
};
|
|
204
|
-
walk(
|
|
204
|
+
walk(presentation);
|
|
205
205
|
return count;
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
// ---- the change --------------------------------------------------------------------------------------------------------
|
|
209
209
|
|
|
210
|
-
function assetPatches(
|
|
211
|
-
const assets =
|
|
210
|
+
function assetPatches(presentation, id, entry) {
|
|
211
|
+
const assets = presentation?.assets;
|
|
212
212
|
return isObject(assets)
|
|
213
213
|
? [{ op: "add", path: opfPathToJsonPointer(["assets", id]), value: entry }]
|
|
214
214
|
: [{ op: "add", path: "/assets", value: { [id]: entry } }];
|
|
@@ -219,33 +219,33 @@ function assetPatches(document, id, entry) {
|
|
|
219
219
|
* pointing at it. `pixels` is `{ dataUri, mediaType, width, height }` (from `cropImagePixels`). A previous crop's asset that
|
|
220
220
|
* nothing else uses is removed in the same change. Pure.
|
|
221
221
|
*/
|
|
222
|
-
export function prepareCrop(
|
|
223
|
-
const image = describeImage(
|
|
222
|
+
export function prepareCrop(presentation, path, pixels) {
|
|
223
|
+
const image = describeImage(presentation, path);
|
|
224
224
|
if (image.error) throw fail("not-croppable", image.error, { path });
|
|
225
225
|
const origin = image.origin ?? image.assetId;
|
|
226
|
-
const id = uniqueAssetId(
|
|
226
|
+
const id = uniqueAssetId(presentation, `${origin ?? "image"}-crop`);
|
|
227
227
|
const title = (isObject(image.entry) && typeof image.entry.title === "string" ? image.entry.title : origin ?? "Picture").replace(/ \(cropped\)$/, "");
|
|
228
228
|
const entry = { src: pixels.dataUri, mediaType: pixels.mediaType, title: `${title} (cropped)` };
|
|
229
229
|
if (image.alt) entry.alt = image.alt;
|
|
230
230
|
if (origin) entry.description = `Cropped from asset:${origin}`;
|
|
231
|
-
const patches = assetPatches(
|
|
231
|
+
const patches = assetPatches(presentation, id, entry);
|
|
232
232
|
patches.push({ op: "replace", path: image.srcPointer, value: `asset:${id}` });
|
|
233
|
-
if (image.origin && image.assetId && countAssetReferences(
|
|
233
|
+
if (image.origin && image.assetId && countAssetReferences(presentation, image.assetId) === 1) patches.push({ op: "remove", path: opfPathToJsonPointer(["assets", image.assetId]) });
|
|
234
234
|
return { patches, assetId: id, reference: `asset:${id}`, origin, image };
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
/** The patches that put a cropped picture back to the asset it was cropped from, or null when there is none. */
|
|
238
|
-
export function prepareRestore(
|
|
239
|
-
const image = describeImage(
|
|
240
|
-
if (image.error || !image.origin || getValueAtPath(
|
|
238
|
+
export function prepareRestore(presentation, path) {
|
|
239
|
+
const image = describeImage(presentation, path);
|
|
240
|
+
if (image.error || !image.origin || getValueAtPath(presentation, ["assets", image.origin]) === undefined) return null;
|
|
241
241
|
const patches = [{ op: "replace", path: image.srcPointer, value: `asset:${image.origin}` }];
|
|
242
|
-
if (countAssetReferences(
|
|
242
|
+
if (countAssetReferences(presentation, image.assetId) === 1) patches.push({ op: "remove", path: opfPathToJsonPointer(["assets", image.assetId]) });
|
|
243
243
|
return { patches, origin: image.origin, image };
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
/** Put a cropped picture back to its original as one undoable change. Returns null when there is no original to restore. */
|
|
247
247
|
export function restoreOriginal(editor, path, meta = {}) {
|
|
248
|
-
const prepared = prepareRestore(editor.
|
|
248
|
+
const prepared = prepareRestore(editor.presentation, path);
|
|
249
249
|
if (!prepared) return null;
|
|
250
250
|
return { ...editor.applyPatch(prepared.patches, { ...meta, source: meta.source ?? "image-restore", path }), origin: prepared.origin };
|
|
251
251
|
}
|
|
@@ -325,12 +325,12 @@ export async function cropImagePixels(loaded, rect, { mediaType, maxBytes = DEFA
|
|
|
325
325
|
* `{ assetId, reference, width, height }`.
|
|
326
326
|
*/
|
|
327
327
|
export async function applyCrop(editor, path, rect, options = {}) {
|
|
328
|
-
const image = describeImage(editor.
|
|
328
|
+
const image = describeImage(editor.presentation, path);
|
|
329
329
|
if (image.error) throw fail("not-croppable", image.error, { path });
|
|
330
330
|
const loaded = options.loaded ?? await loadImagePixels(image.assetSrc);
|
|
331
331
|
const pixels = await cropImagePixels(loaded, rect, { mediaType: image.mediaType, maxBytes: options.maxBytes });
|
|
332
332
|
// The document may have changed while the picture was encoding.
|
|
333
|
-
const prepared = prepareCrop(editor.
|
|
333
|
+
const prepared = prepareCrop(editor.presentation, path, pixels);
|
|
334
334
|
const change = editor.applyPatch(prepared.patches, { ...options.meta, source: options.meta?.source ?? "image-crop", path, assetId: prepared.assetId });
|
|
335
335
|
return { ...change, assetId: prepared.assetId, reference: prepared.reference, width: pixels.width, height: pixels.height };
|
|
336
336
|
}
|