@openpresentation/opf-editor 0.12.1 → 0.15.0
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 +122 -80
- package/dist/annotations.d.ts +16 -16
- package/dist/annotations.js +71 -79
- package/dist/assets.d.ts +7 -7
- package/dist/assets.js +16 -16
- package/dist/background-options.d.ts +12 -5
- package/dist/background-options.js +44 -19
- 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 +30 -31
- package/dist/canvas.js +64 -68
- package/dist/catalogs.d.ts +48 -0
- package/dist/catalogs.js +217 -0
- 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 +23 -0
- package/dist/color-authoring.js +1 -1
- package/dist/content-actions.d.ts +17 -17
- package/dist/content-actions.js +74 -73
- package/dist/content-controls.js +21 -21
- 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.d.ts +12 -5
- package/dist/design-controls.js +312 -111
- package/dist/design-options.d.ts +20 -18
- package/dist/design-options.js +91 -99
- package/dist/edit-helpers.js +8 -7
- package/dist/exact-source.js +5 -5
- package/dist/export.d.ts +7 -7
- package/dist/export.js +48 -17
- 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 +50 -101
- 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 +28 -29
- package/dist/image-cropper.js +3 -3
- package/dist/image-options.d.ts +45 -0
- package/dist/image-options.js +195 -0
- package/dist/index.d.ts +48 -31
- package/dist/index.js +154 -126
- package/dist/json-editor.js +2 -2
- package/dist/json-menu.js +2 -2
- package/dist/json-options.d.ts +3 -3
- package/dist/json-options.js +44 -54
- 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 +160 -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 +26 -6
- package/dist/slide-manager.d.ts +2 -2
- package/dist/slide-manager.js +19 -19
- package/dist/slides.d.ts +21 -21
- package/dist/slides.js +104 -101
- package/dist/svelte.js +2 -3
- package/dist/switches.d.ts +37 -22
- package/dist/switches.js +282 -171
- 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 +15 -4
- package/dist/transfer.js +107 -161
- package/package.json +12 -6
- package/dist/font-defaults.js +0 -25
package/dist/index.js
CHANGED
|
@@ -1,38 +1,42 @@
|
|
|
1
1
|
import { paginateSlide } from "@openpresentation/opf/pagination";
|
|
2
|
-
import {
|
|
2
|
+
import { OPFPatchError, applyPatch as applyCorePatch, applyPatchWithInverse, formatPointer, invertPatch, jsonEqual, parsePointer, readPointer } from "@openpresentation/opf/patch";
|
|
3
3
|
import { collectReservedPresentationIds } from "./presentation-ids.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
4
|
+
import { composeSlide } from "@openpresentation/opf/composition";
|
|
5
|
+
import { catalogKinds, resolveSlideContext, stats } from "@openpresentation/opf";
|
|
6
|
+
import { checkFormat, errorFindings } from "./checks.js";
|
|
7
|
+
import { forkEditedRecords, forkNotice, listCatalogRecords, mergeCatalogs } from "./catalogs.js";
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
REFERENCE_FINDING_CODES,
|
|
11
|
+
applyCatalogUpdate,
|
|
12
|
+
catalogRecordAt,
|
|
13
|
+
catalogRecordLabel,
|
|
14
|
+
catalogsFor,
|
|
15
|
+
checkCatalogUpdates,
|
|
16
|
+
editedCatalogRecords,
|
|
17
|
+
FORKED_RECORD_KINDS,
|
|
18
|
+
forkEditedRecords,
|
|
19
|
+
forkNotice,
|
|
20
|
+
listCatalogRecords,
|
|
21
|
+
mergeCatalogs,
|
|
22
|
+
moveToCustom,
|
|
23
|
+
prepareMoveToCustom,
|
|
24
|
+
prepareSave,
|
|
25
|
+
referenceFindings,
|
|
26
|
+
saveDocument,
|
|
27
|
+
} from "./catalogs.js";
|
|
28
|
+
|
|
29
|
+
// The options `composeSlide` and `paginateSlide` take for one slide of the open deck. Core's `resolveSlideContext` resolves the
|
|
30
|
+
// slide's canvas, layout, theme, colour scheme and font families the one way every engine does (slide design, deck design,
|
|
31
|
+
// theme, engine default), with the session's registered catalogs, so the editor measures and composes what the renderer draws
|
|
32
|
+
// and the exporter writes. `options.fonts` is the renderer's fonts handle (its `textMeasurement` measures); `options.catalogs`
|
|
33
|
+
// adds catalogs for this call; `options.onDiagnostic` hears each diagnostic (`unresolved-reference`);
|
|
34
|
+
// any other option overrides the resolved one (`layout`, ...).
|
|
35
|
+
function slideContext(presentation, slideIndex, catalogs, { fonts, catalogs: _extra, onDiagnostic, ...overrides } = {}) {
|
|
36
|
+
if (!Number.isInteger(slideIndex) || !presentation.slides?.[slideIndex]) throw new OPFEditorError("slide-index-out-of-range", "Slide index is out of range.");
|
|
37
|
+
const { options, diagnostics } = resolveSlideContext(presentation, slideIndex, { fonts, catalogs });
|
|
38
|
+
for (const diagnostic of diagnostics) onDiagnostic?.(diagnostic);
|
|
39
|
+
return { ...options, ...overrides };
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
export const packageName = "@openpresentation/opf-editor";
|
|
@@ -85,20 +89,20 @@ export function jsonPointerToOpfPath(pointer) {
|
|
|
85
89
|
return parseJsonPointer(pointer).join(".");
|
|
86
90
|
}
|
|
87
91
|
|
|
88
|
-
export function getValueAtPath(
|
|
89
|
-
const found = readPointer(
|
|
92
|
+
export function getValueAtPath(presentation, path, fallback) {
|
|
93
|
+
const found = readPointer(presentation, splitOpfPath(path));
|
|
90
94
|
return found.found ? found.value : fallback;
|
|
91
95
|
}
|
|
92
96
|
|
|
93
|
-
export function hasValueAtPath(
|
|
94
|
-
return readPointer(
|
|
97
|
+
export function hasValueAtPath(presentation, path) {
|
|
98
|
+
return readPointer(presentation, splitOpfPath(path)).found;
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
export function createValuePatch(
|
|
101
|
+
export function createValuePatch(presentation, path, value) {
|
|
98
102
|
const pointer = normalizePatchPath(path);
|
|
99
103
|
return [
|
|
100
104
|
{
|
|
101
|
-
op: hasValueAtPath(
|
|
105
|
+
op: hasValueAtPath(presentation, pointer) ? "replace" : "add",
|
|
102
106
|
path: pointer,
|
|
103
107
|
value: clone(value)
|
|
104
108
|
}
|
|
@@ -108,40 +112,43 @@ export function createValuePatch(document, path, value) {
|
|
|
108
112
|
// Patch semantics (RFC 6902, pointers, inverse patches) live in core's
|
|
109
113
|
// "@openpresentation/opf/patch"; the editor adds its path spellings (dotted OPF
|
|
110
114
|
// paths), its error type and its clone policy on top.
|
|
111
|
-
export function applyJsonPatch(
|
|
115
|
+
export function applyJsonPatch(presentation, operations) {
|
|
112
116
|
assertPatchOperations(operations);
|
|
113
117
|
const patch = operations.map(normalizeOperation);
|
|
114
118
|
try {
|
|
115
|
-
return applyCorePatch(
|
|
119
|
+
return applyCorePatch(presentation, patch);
|
|
116
120
|
} catch (error) {
|
|
117
121
|
throw editorPatchError(error);
|
|
118
122
|
}
|
|
119
123
|
}
|
|
120
124
|
|
|
121
|
-
export function invertJsonPatch(
|
|
125
|
+
export function invertJsonPatch(presentation, operations) {
|
|
122
126
|
assertPatchOperations(operations);
|
|
123
127
|
const patch = operations.map(normalizeOperation);
|
|
124
128
|
try {
|
|
125
|
-
return invertPatch(
|
|
129
|
+
return invertPatch(presentation, patch);
|
|
126
130
|
} catch (error) {
|
|
127
131
|
throw editorPatchError(error);
|
|
128
132
|
}
|
|
129
133
|
}
|
|
130
134
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Facts about the open presentation, from core's `stats`: slide, layout and section counts, payload kinds, words and notes coverage,
|
|
137
|
+
* images and their alt text, charts, tables, citations, fonts and an estimated speaking time (`options`: `perSlide`, `values`,
|
|
138
|
+
* `wordsPerMinute`). They are facts, never findings: no severities and no judgment about whether a number is too high, so a deck-info
|
|
139
|
+
* view can show them next to the Review panel. It reads the JSON only, so it also works on a document that fails validation.
|
|
140
|
+
*/
|
|
141
|
+
export function deckStats(editor, options) {
|
|
142
|
+
assertEditorSession(editor);
|
|
143
|
+
return stats(editor.presentation, options);
|
|
140
144
|
}
|
|
141
145
|
|
|
142
146
|
export function createEditorSession(input, options = {}) {
|
|
143
|
-
let
|
|
144
|
-
|
|
147
|
+
let presentation = parseInput(input);
|
|
148
|
+
// The host's registered catalogs (FA-23): one list, handed to every core call, the renderer and the exporter.
|
|
149
|
+
let catalogs = mergeCatalogs(options.catalogs);
|
|
150
|
+
const check = (document) => checkFormat(document, { catalogs });
|
|
151
|
+
let validation = check(presentation);
|
|
145
152
|
const undoStack = [];
|
|
146
153
|
const redoStack = [];
|
|
147
154
|
const listeners = new Set();
|
|
@@ -156,27 +163,44 @@ export function createEditorSession(input, options = {}) {
|
|
|
156
163
|
|
|
157
164
|
function commitPatch(operations, meta = {}) {
|
|
158
165
|
assertPatchOperations(operations);
|
|
159
|
-
|
|
160
|
-
const before =
|
|
166
|
+
let patches = operations.map(normalizeOperation);
|
|
167
|
+
const before = presentation;
|
|
161
168
|
let next, inversePatches;
|
|
162
169
|
try {
|
|
163
|
-
({
|
|
170
|
+
({ presentation: next, inverse: inversePatches } = applyPatchWithInverse(before, patches));
|
|
164
171
|
} catch (error) {
|
|
165
172
|
throw editorPatchError(error);
|
|
166
173
|
}
|
|
167
|
-
|
|
174
|
+
// Owner rule (FA-23): an edit of a record under catalogs.default or a named group forks it into catalogs.custom (every reference
|
|
175
|
+
// follows, the original is dropped), in this same undo step. Catalog updates and moves change catalog records on purpose.
|
|
176
|
+
let forks = [];
|
|
177
|
+
if (meta.catalogRecordEdit !== "in-place") {
|
|
178
|
+
const forked = forkEditedRecords(before, next, { catalogs, forkIds: meta.forkIds });
|
|
179
|
+
if (forked.forks.length) {
|
|
180
|
+
forks = forked.forks;
|
|
181
|
+
for (const key of new Set([...Object.keys(next), ...Object.keys(forked.document)]))
|
|
182
|
+
if (!jsonEqual(next[key], forked.document[key])) patches = [...patches, forked.document[key] === undefined ? { op: "remove", path: formatPointer([key]) } : { op: Object.hasOwn(next, key) ? "replace" : "add", path: formatPointer([key]), value: clone(forked.document[key]) }];
|
|
183
|
+
try {
|
|
184
|
+
({ presentation: next, inverse: inversePatches } = applyPatchWithInverse(before, patches));
|
|
185
|
+
} catch (error) {
|
|
186
|
+
throw editorPatchError(error);
|
|
187
|
+
}
|
|
188
|
+
meta = { ...meta, forked: forks, notice: forks.map(forkNotice).join(" ") };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const nextValidation = check(next);
|
|
168
192
|
|
|
169
193
|
if ((meta.rejectInvalid ?? rejectInvalid) && !nextValidation.valid) {
|
|
170
194
|
throw new OPFEditorError("invalid-opf-edit", "OPF edit produced an invalid document.", {
|
|
171
|
-
issues: nextValidation
|
|
195
|
+
issues: errorFindings(nextValidation),
|
|
172
196
|
patches
|
|
173
197
|
});
|
|
174
198
|
}
|
|
175
199
|
|
|
176
200
|
if (patches.every(patch => patch.op === "test")) return {
|
|
177
|
-
|
|
201
|
+
presentation: clone(presentation), patches: clonePatchOperations(patches), inversePatches: [], validation: nextValidation
|
|
178
202
|
};
|
|
179
|
-
|
|
203
|
+
presentation = next;
|
|
180
204
|
validation = nextValidation;
|
|
181
205
|
undoStack.push({ patches: clonePatchOperations(patches), inversePatches, meta });
|
|
182
206
|
redoStack.length = 0;
|
|
@@ -189,20 +213,35 @@ export function createEditorSession(input, options = {}) {
|
|
|
189
213
|
});
|
|
190
214
|
|
|
191
215
|
return {
|
|
192
|
-
|
|
216
|
+
presentation: clone(presentation),
|
|
193
217
|
patches: clonePatchOperations(patches),
|
|
194
218
|
inversePatches: clonePatchOperations(inversePatches),
|
|
195
|
-
validation
|
|
219
|
+
validation,
|
|
220
|
+
...(forks.length ? { forked: forks, notice: meta.notice } : {})
|
|
196
221
|
};
|
|
197
222
|
}
|
|
198
223
|
|
|
199
224
|
const editor = {
|
|
200
|
-
get
|
|
201
|
-
return clone(
|
|
225
|
+
get presentation() {
|
|
226
|
+
return clone(presentation);
|
|
202
227
|
},
|
|
203
228
|
get validation() {
|
|
204
229
|
return validation;
|
|
205
230
|
},
|
|
231
|
+
/** The catalogs the host registered (`Catalog[]`, frozen): the first is the host default. */
|
|
232
|
+
get catalogs() {
|
|
233
|
+
return catalogs;
|
|
234
|
+
},
|
|
235
|
+
/**
|
|
236
|
+
* Replace the registered catalogs (for example after the host loads a company catalog). The document is not changed; it is
|
|
237
|
+
* re-validated against the new list (reference findings depend on it) and one `catalogs` event tells every view to redraw.
|
|
238
|
+
*/
|
|
239
|
+
setCatalogs(next, meta = {}) {
|
|
240
|
+
catalogs = mergeCatalogs(next);
|
|
241
|
+
validation = check(presentation);
|
|
242
|
+
emit({ type: "catalogs", patches: [], validation, meta });
|
|
243
|
+
return catalogs;
|
|
244
|
+
},
|
|
206
245
|
get canUndo() {
|
|
207
246
|
return undoStack.length > 0;
|
|
208
247
|
},
|
|
@@ -211,7 +250,7 @@ export function createEditorSession(input, options = {}) {
|
|
|
211
250
|
},
|
|
212
251
|
snapshot() {
|
|
213
252
|
return snapshotCache ??= freezeSnapshot({
|
|
214
|
-
|
|
253
|
+
presentation: clone(presentation),
|
|
215
254
|
validation: clone(validation),
|
|
216
255
|
canUndo: undoStack.length > 0,
|
|
217
256
|
canRedo: redoStack.length > 0,
|
|
@@ -227,33 +266,37 @@ export function createEditorSession(input, options = {}) {
|
|
|
227
266
|
return () => listeners.delete(listener);
|
|
228
267
|
},
|
|
229
268
|
get(path, fallback) {
|
|
230
|
-
return getValueAtPath(
|
|
269
|
+
return getValueAtPath(presentation, path, fallback);
|
|
231
270
|
},
|
|
232
271
|
set(path, value, meta = {}) {
|
|
233
|
-
return commitPatch(createValuePatch(
|
|
272
|
+
return commitPatch(createValuePatch(presentation, path, value), {
|
|
234
273
|
...meta,
|
|
235
274
|
path: normalizePatchPath(path)
|
|
236
275
|
});
|
|
237
276
|
},
|
|
238
277
|
composeSlide(slideIndex, options = {}) {
|
|
239
|
-
return composeSlide(
|
|
278
|
+
return composeSlide(presentation.slides?.[slideIndex], slideContext(presentation, slideIndex, mergeCatalogs(catalogs, options.catalogs), options));
|
|
240
279
|
},
|
|
241
280
|
paginateSlide(slideIndex, options = {}, meta = {}) {
|
|
242
|
-
const
|
|
243
|
-
|
|
281
|
+
const list = mergeCatalogs(catalogs, options.catalogs);
|
|
282
|
+
// Core pagination reads the measurement from `fonts`, so the resolved context hands it over that way.
|
|
283
|
+
const { textMeasurement, ...resolved } = slideContext(presentation, slideIndex, list, options);
|
|
284
|
+
const pagination = paginateSlide(presentation.slides[slideIndex], {
|
|
244
285
|
...resolved,
|
|
245
|
-
|
|
286
|
+
catalogs: list,
|
|
287
|
+
fonts: options.fonts ?? (textMeasurement ? { textMeasurement } : undefined),
|
|
288
|
+
reservedIds: collectReservedPresentationIds(presentation),
|
|
246
289
|
});
|
|
247
290
|
// Pagination can persist a readability policy without adding a page. Commit
|
|
248
291
|
// that change too, so preview/export use the same minimum that was evaluated.
|
|
249
|
-
if (pagination.slides.length === 1 && jsonEqual(pagination.slides[0],
|
|
250
|
-
const slides = [...
|
|
292
|
+
if (pagination.slides.length === 1 && jsonEqual(pagination.slides[0], presentation.slides[slideIndex])) return { change: null, pagination };
|
|
293
|
+
const slides = [...presentation.slides];
|
|
251
294
|
slides.splice(slideIndex, 1, ...pagination.slides);
|
|
252
295
|
const change = editor.set('slides', slides, { ...meta, rejectInvalid: true, pagination: pagination.pages });
|
|
253
296
|
return { change, pagination };
|
|
254
297
|
},
|
|
255
298
|
setComposition(slideIndex, composition, meta = {}) {
|
|
256
|
-
if (!Number.isInteger(slideIndex) || !
|
|
299
|
+
if (!Number.isInteger(slideIndex) || !presentation.slides?.[slideIndex]) throw new OPFEditorError("slide-index-out-of-range", "Slide index is out of range.");
|
|
257
300
|
return editor.set(`slides.${slideIndex}.composition`, composition, { ...meta, rejectInvalid: true });
|
|
258
301
|
},
|
|
259
302
|
setGroupComposition(path, composition, meta = {}) {
|
|
@@ -268,7 +311,7 @@ export function createEditorSession(input, options = {}) {
|
|
|
268
311
|
applyPatch: commitPatch,
|
|
269
312
|
/**
|
|
270
313
|
* The undo and redo stacks as plain data (oldest entry first): `{ undo: [{ patches, inversePatches, meta }], redo: [...] }`.
|
|
271
|
-
* Together with `
|
|
314
|
+
* Together with `presentation` this is everything `restoreState` needs to bring a session back, for hosts that persist work.
|
|
272
315
|
*/
|
|
273
316
|
exportHistory() {
|
|
274
317
|
const entry = (item) => ({ patches: clonePatchOperations(item.patches), inversePatches: clonePatchOperations(item.inversePatches), meta: clone(item.meta ?? {}) });
|
|
@@ -281,11 +324,11 @@ export function createEditorSession(input, options = {}) {
|
|
|
281
324
|
* before anything changes, so a stale or damaged copy never corrupts the session. Emits one `restore` event.
|
|
282
325
|
*/
|
|
283
326
|
restoreState(state, meta = {}) {
|
|
284
|
-
if (!state || typeof state !== "object") throw new OPFEditorError("invalid-state", "restoreState needs {
|
|
285
|
-
const next = parseInput(state.
|
|
286
|
-
const nextValidation =
|
|
327
|
+
if (!state || typeof state !== "object") throw new OPFEditorError("invalid-state", "restoreState needs { presentation, undo?, redo? }.");
|
|
328
|
+
const next = parseInput(state.presentation);
|
|
329
|
+
const nextValidation = check(next);
|
|
287
330
|
if ((meta.rejectInvalid ?? rejectInvalid) && !nextValidation.valid) {
|
|
288
|
-
throw new OPFEditorError("invalid-opf-edit", "The restored document is not valid OPF.", { issues: nextValidation
|
|
331
|
+
throw new OPFEditorError("invalid-opf-edit", "The restored document is not valid OPF.", { issues: errorFindings(nextValidation) });
|
|
289
332
|
}
|
|
290
333
|
const undo = state.undo ?? [], redo = state.redo ?? [];
|
|
291
334
|
const wellFormed = (entries) => Array.isArray(entries) && entries.every((item) => item && Array.isArray(item.patches) && Array.isArray(item.inversePatches));
|
|
@@ -307,20 +350,20 @@ export function createEditorSession(input, options = {}) {
|
|
|
307
350
|
throw new OPFEditorError("invalid-history", "The undo history does not belong to this document.", { cause: error instanceof Error ? error.message : String(error) });
|
|
308
351
|
}
|
|
309
352
|
const entry = (item) => ({ patches: item.patches.map(normalizeOperation), inversePatches: item.inversePatches.map(normalizeOperation), meta: clone(item.meta ?? {}) });
|
|
310
|
-
|
|
353
|
+
presentation = next;
|
|
311
354
|
validation = nextValidation;
|
|
312
355
|
undoStack.length = 0;
|
|
313
356
|
redoStack.length = 0;
|
|
314
357
|
undoStack.push(...undo.map(entry));
|
|
315
358
|
redoStack.push(...redo.map(entry));
|
|
316
359
|
emit({ type: "restore", patches: [], validation, meta });
|
|
317
|
-
return {
|
|
360
|
+
return { presentation: clone(presentation), patches: [], validation };
|
|
318
361
|
},
|
|
319
362
|
undo(meta = {}) {
|
|
320
363
|
const entry = undoStack.pop();
|
|
321
364
|
if (!entry) return null;
|
|
322
|
-
|
|
323
|
-
validation =
|
|
365
|
+
presentation = applyJsonPatch(presentation, entry.inversePatches);
|
|
366
|
+
validation = check(presentation);
|
|
324
367
|
redoStack.push(entry);
|
|
325
368
|
emit({
|
|
326
369
|
type: "undo",
|
|
@@ -330,7 +373,7 @@ export function createEditorSession(input, options = {}) {
|
|
|
330
373
|
meta
|
|
331
374
|
});
|
|
332
375
|
return {
|
|
333
|
-
|
|
376
|
+
presentation: clone(presentation),
|
|
334
377
|
patches: clonePatchOperations(entry.inversePatches),
|
|
335
378
|
redoPatches: clonePatchOperations(entry.patches),
|
|
336
379
|
validation
|
|
@@ -339,8 +382,8 @@ export function createEditorSession(input, options = {}) {
|
|
|
339
382
|
redo(meta = {}) {
|
|
340
383
|
const entry = redoStack.pop();
|
|
341
384
|
if (!entry) return null;
|
|
342
|
-
|
|
343
|
-
validation =
|
|
385
|
+
presentation = applyJsonPatch(presentation, entry.patches);
|
|
386
|
+
validation = check(presentation);
|
|
344
387
|
undoStack.push(entry);
|
|
345
388
|
emit({
|
|
346
389
|
type: "redo",
|
|
@@ -350,7 +393,7 @@ export function createEditorSession(input, options = {}) {
|
|
|
350
393
|
meta
|
|
351
394
|
});
|
|
352
395
|
return {
|
|
353
|
-
|
|
396
|
+
presentation: clone(presentation),
|
|
354
397
|
patches: clonePatchOperations(entry.patches),
|
|
355
398
|
inversePatches: clonePatchOperations(entry.inversePatches),
|
|
356
399
|
validation
|
|
@@ -431,29 +474,29 @@ export function createSvgTraceBinding(root, editor, options = {}) {
|
|
|
431
474
|
};
|
|
432
475
|
}
|
|
433
476
|
|
|
477
|
+
/**
|
|
478
|
+
* The records a catalog control can offer for `catalogKind`, from core's `catalogRecords`: the document's embedded records,
|
|
479
|
+
* then the registered catalogs' (`options.catalogs`, merged with `options.editor`'s). Each is the record itself.
|
|
480
|
+
*/
|
|
434
481
|
export function getCatalogRecords(catalogKind, options = {}) {
|
|
435
|
-
|
|
436
|
-
const presentationCatalog = options.presentation?.catalogs?.[catalogKind];
|
|
437
|
-
const explicitCatalog = options.catalogs?.[catalogKind] ?? options.catalogSources?.[catalogKind];
|
|
438
|
-
|
|
439
|
-
return firstRecords(presentationCatalog)
|
|
440
|
-
?? firstRecords(explicitCatalog)
|
|
441
|
-
?? firstRecords(bundledCatalogs[catalogKind])
|
|
442
|
-
?? [];
|
|
482
|
+
return getCatalogOptions(catalogKind, options).map((option) => option.record);
|
|
443
483
|
}
|
|
444
484
|
|
|
485
|
+
/**
|
|
486
|
+
* Picker options for `catalogKind`: `{ id, reference, label, record, group, source, origin }`, where `id` (= `reference`) is the
|
|
487
|
+
* reference to write, `id` or `name:id`. `options.presentation` is the document (default: `options.editor`'s), `options.catalogs`
|
|
488
|
+
* the host catalogs (merged after the session's registered ones).
|
|
489
|
+
*/
|
|
445
490
|
export function getCatalogOptions(catalogKind, options = {}) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
record
|
|
450
|
-
}));
|
|
491
|
+
assertCatalogKind(catalogKind);
|
|
492
|
+
const presentation = options.presentation ?? options.editor?.presentation ?? {};
|
|
493
|
+
return listCatalogRecords(presentation, catalogKind, { catalogs: mergeCatalogs(options.editor?.catalogs, options.catalogs) });
|
|
451
494
|
}
|
|
452
495
|
|
|
453
496
|
export function assertCatalogId(catalogKind, id, options = {}) {
|
|
454
497
|
assertCatalogKind(catalogKind);
|
|
455
498
|
if (typeof id !== "string" || id.length === 0) {
|
|
456
|
-
throw new OPFEditorError("invalid-catalog-id", "Catalog control values must be non-empty catalog
|
|
499
|
+
throw new OPFEditorError("invalid-catalog-id", "Catalog control values must be non-empty catalog references.", {
|
|
457
500
|
catalogKind,
|
|
458
501
|
id
|
|
459
502
|
});
|
|
@@ -461,7 +504,7 @@ export function assertCatalogId(catalogKind, id, options = {}) {
|
|
|
461
504
|
|
|
462
505
|
const optionsById = new Set(getCatalogOptions(catalogKind, options).map((option) => option.id));
|
|
463
506
|
if (!optionsById.has(id)) {
|
|
464
|
-
throw new OPFEditorError("unknown-catalog-id", `Unknown ${catalogKind} catalog
|
|
507
|
+
throw new OPFEditorError("unknown-catalog-id", `Unknown ${catalogKind} catalog reference: ${id}.`, {
|
|
465
508
|
catalogKind,
|
|
466
509
|
id
|
|
467
510
|
});
|
|
@@ -470,16 +513,16 @@ export function assertCatalogId(catalogKind, id, options = {}) {
|
|
|
470
513
|
return id;
|
|
471
514
|
}
|
|
472
515
|
|
|
516
|
+
/** Write the catalog reference `id` at `path` (an object reference keeps its overrides and takes the new `id`). */
|
|
473
517
|
export function setCatalogId(editor, path, catalogKind, id, meta = {}) {
|
|
474
518
|
assertEditorSession(editor);
|
|
475
519
|
const catalogId = assertCatalogId(catalogKind, id, {
|
|
476
|
-
presentation: meta.presentation ?? editor.
|
|
477
|
-
catalogs: meta.catalogs
|
|
478
|
-
catalogSources: meta.catalogSources
|
|
520
|
+
presentation: meta.presentation ?? editor.presentation,
|
|
521
|
+
catalogs: mergeCatalogs(editor.catalogs, meta.catalogs)
|
|
479
522
|
});
|
|
480
523
|
const current = editor.get(path);
|
|
481
524
|
const value = catalogReferenceValue(current, catalogId);
|
|
482
|
-
const { catalogs
|
|
525
|
+
const { catalogs: _catalogs, presentation: _presentation, ...commitMeta } = meta;
|
|
483
526
|
return editor.set(path, value, {
|
|
484
527
|
...commitMeta,
|
|
485
528
|
catalogKind,
|
|
@@ -533,8 +576,8 @@ export function createCatalogSelect(editor, options) {
|
|
|
533
576
|
if (options?.label) select.setAttribute("aria-label", options.label);
|
|
534
577
|
|
|
535
578
|
for (const option of getCatalogOptions(catalogKind, {
|
|
536
|
-
|
|
537
|
-
presentation: options?.presentation ?? editor.
|
|
579
|
+
catalogs: mergeCatalogs(editor.catalogs, options?.catalogs),
|
|
580
|
+
presentation: options?.presentation ?? editor.presentation
|
|
538
581
|
})) {
|
|
539
582
|
const optionElement = documentRef.createElement("option");
|
|
540
583
|
optionElement.value = option.id;
|
|
@@ -549,7 +592,6 @@ export function createCatalogSelect(editor, options) {
|
|
|
549
592
|
const onChange = () => {
|
|
550
593
|
setCatalogId(editor, path, catalogKind, select.value, {
|
|
551
594
|
catalogs: options?.catalogs,
|
|
552
|
-
catalogSources: options?.catalogSources,
|
|
553
595
|
presentation: options?.presentation,
|
|
554
596
|
source: "catalog-select"
|
|
555
597
|
});
|
|
@@ -636,12 +678,12 @@ function normalizeOperation(operation) {
|
|
|
636
678
|
|
|
637
679
|
// Core patch errors keep their stable codes; the editor reports them as OPFEditorError.
|
|
638
680
|
function editorPatchError(error) {
|
|
639
|
-
if (!(error instanceof
|
|
681
|
+
if (!(error instanceof OPFPatchError)) return error;
|
|
640
682
|
const details = {};
|
|
641
683
|
if (error.path !== undefined) details.path = error.path;
|
|
642
684
|
if (error.operation !== undefined) details.operation = error.operation;
|
|
643
685
|
if (error.index !== undefined) details.index = error.index;
|
|
644
|
-
if (error.validation) details.issues = error.validation
|
|
686
|
+
if (error.validation) details.issues = errorFindings(error.validation);
|
|
645
687
|
return new OPFEditorError(error.code, error.message, details);
|
|
646
688
|
}
|
|
647
689
|
|
|
@@ -693,20 +735,6 @@ function assertCatalogKind(catalogKind) {
|
|
|
693
735
|
}
|
|
694
736
|
}
|
|
695
737
|
|
|
696
|
-
function firstRecords(value) {
|
|
697
|
-
if (Array.isArray(value)) return value.filter(isCatalogRecord);
|
|
698
|
-
if (Array.isArray(value?.records)) return value.records.filter(isCatalogRecord);
|
|
699
|
-
return null;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
function isCatalogRecord(record) {
|
|
703
|
-
return Boolean(record && typeof record === "object" && typeof record.id === "string");
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
function catalogLabel(record) {
|
|
707
|
-
return String(record.name ?? record.title ?? record.label ?? record.id);
|
|
708
|
-
}
|
|
709
|
-
|
|
710
738
|
function catalogReferenceValue(current, id) {
|
|
711
739
|
if (current && typeof current === "object" && !Array.isArray(current)) {
|
|
712
740
|
return { ...current, id };
|
package/dist/json-editor.js
CHANGED
|
@@ -103,7 +103,7 @@ function skipDelimiter(view, delimiter) {
|
|
|
103
103
|
}
|
|
104
104
|
function mountJsonCodeEditor(parent, options) {
|
|
105
105
|
const attributes = new Compartment(), indent = new Compartment();
|
|
106
|
-
let disposed=false, menu=null, catalogs=options.catalogs??
|
|
106
|
+
let disposed=false, menu=null, catalogs=options.catalogs??[], generation=0;
|
|
107
107
|
function closeOptions(){generation++;menu?.destroy();menu=null;}
|
|
108
108
|
async function openOptions(left,top){
|
|
109
109
|
closeOptions();
|
|
@@ -244,7 +244,7 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
244
244
|
return {
|
|
245
245
|
api,
|
|
246
246
|
openOptions,
|
|
247
|
-
setCatalogs(value){catalogs=value??
|
|
247
|
+
setCatalogs(value){catalogs=value??[];closeOptions();},
|
|
248
248
|
update(code, invalid=false, field=null, describedBy) {
|
|
249
249
|
if(code!==api.getValue())closeOptions();
|
|
250
250
|
const current = view.state.doc.toString(), normalized=textInputMap(code).text;
|
package/dist/json-menu.js
CHANGED
|
@@ -26,9 +26,9 @@ export function mountJsonFieldMenu(parent,api,catalogs,left,top,onClose){
|
|
|
26
26
|
const error=add('p',undefined,'json-field-empty');error.setAttribute('role','alert');error.hidden=true;
|
|
27
27
|
if(isLayout){
|
|
28
28
|
const help=add('details',undefined,'json-field-catalog-help');add('summary','Where do these layouts come from?',undefined,help);
|
|
29
|
-
add('p','
|
|
29
|
+
add('p','Provided by app: records from the catalogs the host app registered. In this document: records embedded in your JSON, which take precedence. Nothing is fetched; saving embeds every record the document uses.',undefined,help);
|
|
30
30
|
}
|
|
31
|
-
add('p',context.unloadedSource?'Using
|
|
31
|
+
add('p',context.unloadedSource?'Using the embedded records: a catalog this document names is not registered here.':'Choose an option or press Escape to keep editing JSON.','json-field-footnote');
|
|
32
32
|
let selected=Math.max(0,context.options.findIndex(option=>option.value===context.value)),choices=[],disposed=false;
|
|
33
33
|
function close(restore=false){if(disposed)return;disposed=true;observer.disconnect();root.remove();doc.removeEventListener('pointerdown',outside);doc.removeEventListener('wheel',outside,true);doc.removeEventListener('touchmove',outside,true);win.removeEventListener('resize',resize);onClose();if(restore){api.focus();api.setSelection(...selection);}}
|
|
34
34
|
function outside(event){if(!root.contains(event.target))close();}
|
package/dist/json-options.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export type JsonCatalogContext =
|
|
1
|
+
/** The host's catalogs (core `Catalog[]`): records offered after the document's embedded ones. */
|
|
2
|
+
export type JsonCatalogContext = readonly import("@openpresentation/opf").Catalog[];
|
|
3
3
|
export type JsonOptionValue = string | number | boolean;
|
|
4
4
|
export interface JsonFieldOption {
|
|
5
5
|
value: JsonOptionValue;
|
|
6
6
|
label: string;
|
|
7
7
|
description?: string;
|
|
8
|
-
source: '
|
|
8
|
+
source: 'Loaded catalog' | 'Document catalog' | 'Schema' | 'Current value';
|
|
9
9
|
suggested?: boolean;
|
|
10
10
|
/** User-facing provenance; source remains stable for existing consumers. */
|
|
11
11
|
sourceLabel?: string;
|