@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/layout.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type {ComposedFlow, Composition} from '@openpresentation/opf/composition';
|
|
2
2
|
import type {JsonPatchOperation} from './index.js';
|
|
3
3
|
/** Resize adjacent tracks. Fractions clamp to 5–95%; automatic flows become explicit grids. */
|
|
4
|
-
export declare function prepareTrackResize(
|
|
5
|
-
|
|
4
|
+
export declare function prepareTrackResize(presentation:unknown,flow:ComposedFlow,boundary:number,fraction:number):{
|
|
5
|
+
presentation:unknown;patches:JsonPatchOperation[];composition:Composition;fraction:number;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
export {prepareBlockMove,listBlockContainers,prepareBlockInsert,prepareBlockDuplicate,prepareBlockRemove,prepareBlockReplace,createContentBlock} from './blocks.js';
|
package/dist/layout.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {getValueAtPath, createValuePatch, applyJsonPatch,
|
|
1
|
+
import { getValueAtPath, createValuePatch, applyJsonPatch, opfPathToJsonPointer } from './index.js';
|
|
2
|
+
import { firstErrorMessage, checkFormat } from "./checks.js";
|
|
2
3
|
|
|
3
4
|
/** Prepare a validated track resize; the test operation guards its container revision. */
|
|
4
|
-
export function prepareTrackResize(
|
|
5
|
+
export function prepareTrackResize(presentation, flow, boundary, fraction) {
|
|
5
6
|
const vertical = flow?.composition?.mode === 'column';
|
|
6
7
|
const tracks = vertical ? flow?.rows : flow?.columns;
|
|
7
8
|
if (!Array.isArray(tracks) || tracks.length < 2 || tracks.length > 12)
|
|
@@ -12,7 +13,7 @@ export function prepareTrackResize(document, flow, boundary, fraction) {
|
|
|
12
13
|
throw new RangeError('Track boundary is out of range.');
|
|
13
14
|
if (!Number.isFinite(fraction) || fraction < 0 || fraction > 1)
|
|
14
15
|
throw new RangeError('Track fraction must be between zero and one.');
|
|
15
|
-
const container = getValueAtPath(
|
|
16
|
+
const container = getValueAtPath(presentation,flow.path);
|
|
16
17
|
if (!container || typeof container !== 'object') throw new Error('The layout container no longer exists.');
|
|
17
18
|
const effective = flow.composition.weights ?? [];
|
|
18
19
|
const weights = Array.from({length:Math.max(tracks.length,effective.length)},(_,index)=>effective[index]??1);
|
|
@@ -25,10 +26,10 @@ export function prepareTrackResize(document, flow, boundary, fraction) {
|
|
|
25
26
|
const composition={...container.composition,mode:flow.composition.mode??'auto',weights};
|
|
26
27
|
// A drag expresses an explicit arrangement. Preserve the automatic engine's chosen columns.
|
|
27
28
|
if(composition.mode==='auto'){composition.mode='grid';composition.columns=flow.columns.length;}
|
|
28
|
-
const patches=[{op:'test',path:opfPathToJsonPointer(flow.path),value:structuredClone(container)},...createValuePatch(
|
|
29
|
-
const next=applyJsonPatch(
|
|
30
|
-
if(!validation.valid)throw new Error(validation
|
|
31
|
-
return {
|
|
29
|
+
const patches=[{op:'test',path:opfPathToJsonPointer(flow.path),value:structuredClone(container)},...createValuePatch(presentation,[...flow.path.split('.'),'composition'],composition)];
|
|
30
|
+
const next=applyJsonPatch(presentation,patches),validation=checkFormat(next);
|
|
31
|
+
if(!validation.valid)throw new Error(firstErrorMessage(validation, 'Track resize is not valid OPF.'));
|
|
32
|
+
return {presentation:next,patches,composition,fraction:share};
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export {prepareBlockMove,listBlockContainers,prepareBlockInsert,prepareBlockDuplicate,prepareBlockRemove,prepareBlockReplace,createContentBlock} from './blocks.js';
|
package/dist/numbering-panel.js
CHANGED
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
NUMBERING_STYLE_OPTIONS,
|
|
9
9
|
NUMBERING_SUFFIX_OPTIONS,
|
|
10
10
|
findNumberableLists,
|
|
11
|
-
numberingAvailable,
|
|
12
11
|
numberingState,
|
|
13
12
|
numberingValue,
|
|
14
13
|
setEntryStart,
|
|
@@ -36,7 +35,6 @@ function h(doc, tag, attributes = {}, ...children) {
|
|
|
36
35
|
* `onStatus(message, {error})`.
|
|
37
36
|
*/
|
|
38
37
|
export function createNumberingPanel(container, options) {
|
|
39
|
-
if (!numberingAvailable()) throw new Error("The numbering control needs a core release that ships numbering.");
|
|
40
38
|
const { editor, getTarget, getSlideIndex, onStatus } = options;
|
|
41
39
|
const doc = container.ownerDocument;
|
|
42
40
|
const id = `opf-numbering-${++panelCounter}`;
|
|
@@ -58,9 +56,9 @@ export function createNumberingPanel(container, options) {
|
|
|
58
56
|
|
|
59
57
|
const currentPath = () => {
|
|
60
58
|
const slide = getSlideIndex?.() ?? 0;
|
|
61
|
-
const lists = findNumberableLists(editor.
|
|
59
|
+
const lists = findNumberableLists(editor.presentation, slide);
|
|
62
60
|
const wanted = explicit ?? getTarget?.();
|
|
63
|
-
if (wanted && numberingState(editor.
|
|
61
|
+
if (wanted && numberingState(editor.presentation, wanted)) return { path: wanted, lists };
|
|
64
62
|
return { path: lists[0]?.path, lists };
|
|
65
63
|
};
|
|
66
64
|
|
|
@@ -100,7 +98,7 @@ export function createNumberingPanel(container, options) {
|
|
|
100
98
|
function draw(force = false) {
|
|
101
99
|
if (destroyed) return;
|
|
102
100
|
const { path, lists } = currentPath();
|
|
103
|
-
const state = path ? numberingState(editor.
|
|
101
|
+
const state = path ? numberingState(editor.presentation, path) : undefined;
|
|
104
102
|
const signature = JSON.stringify([path, state, lists.map((entry) => [entry.path, entry.numbered])]);
|
|
105
103
|
if (!force && signature === drawn) return;
|
|
106
104
|
drawn = signature;
|
package/dist/numbering.d.ts
CHANGED
|
@@ -12,8 +12,6 @@ export declare const NUMBERING_SUFFIX_OPTIONS: readonly { value: NumberingSuffix
|
|
|
12
12
|
export declare const MAX_NUMBERING_LEVELS: 9;
|
|
13
13
|
export declare const MAX_NUMBERING_START: 32767;
|
|
14
14
|
|
|
15
|
-
/** True when the core this editor runs on composes numbered lists. */
|
|
16
|
-
export declare function numberingAvailable(): boolean;
|
|
17
15
|
|
|
18
16
|
export interface ListPayloadLocation {
|
|
19
17
|
/** The payload that owns the list ("" for a document-level path is not possible; a slide, region or block). */
|
|
@@ -26,11 +24,11 @@ export interface ListPayloadLocation {
|
|
|
26
24
|
entry?: number;
|
|
27
25
|
}
|
|
28
26
|
/** Where a list lives for a path that points at it or into it (`slides.0.items`, `slides.0.items.2`, `slides.0.left.items.1.text`). */
|
|
29
|
-
export declare function listPayloadAt(
|
|
27
|
+
export declare function listPayloadAt(presentation: unknown, path: string): ListPayloadLocation | undefined;
|
|
30
28
|
|
|
31
29
|
export interface NumberableList { path: string; payloadPath: string; field: "items" | "bullets"; label: string; count: number; numbered: boolean }
|
|
32
30
|
/** Every list of a slide: path, label, entry count and whether it is numbered. */
|
|
33
|
-
export declare function findNumberableLists(
|
|
31
|
+
export declare function findNumberableLists(presentation: unknown, slideIndex: number): NumberableList[];
|
|
34
32
|
|
|
35
33
|
/** The shortest `numbering` value for per-level settings (a style name, an object of the non-default fields, or an array). */
|
|
36
34
|
export declare function numberingValue(levels: readonly Partial<NumberingLevel>[]): NumberingValue;
|
|
@@ -54,7 +52,7 @@ export interface NumberingState {
|
|
|
54
52
|
/** The entry the path selects, with its own `start` and the marker it draws. */
|
|
55
53
|
entry?: { index: number; start?: number; marker?: string };
|
|
56
54
|
}
|
|
57
|
-
export declare function numberingState(
|
|
55
|
+
export declare function numberingState(presentation: unknown, path: string): NumberingState | undefined;
|
|
58
56
|
|
|
59
57
|
/** Number a list, change its numbering, or (with `undefined` or `null`) turn it off, as one undoable validated edit. Turning it off also removes the entry `start` values. */
|
|
60
58
|
export declare function setNumbering(editor: EditorSession, path: string, value: NumberingValue | undefined | null, meta?: Record<string, unknown>): unknown;
|
package/dist/numbering.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// List numbering (RR-33): the headless model behind the numbering control. A list payload (`items` or `bullets`) can
|
|
2
2
|
// carry `numbering`: a style name, a { style, start, suffix } object, or an array with one entry per list level. Every
|
|
3
3
|
// write goes through the session as a validated JSON Patch edit, so each change is one undoable edit. Importing this
|
|
4
|
-
// module needs no DOM.
|
|
5
|
-
|
|
6
|
-
import * as core from "@openpresentation/opf";
|
|
4
|
+
// module needs no DOM.
|
|
5
|
+
import { listNumbers } from "@openpresentation/opf/composition";
|
|
7
6
|
import { OPFEditorError, getValueAtPath, opfPathToJsonPointer, splitOpfPath } from "./index.js";
|
|
8
7
|
|
|
9
8
|
/** The number styles and how each draws, in the order a picker lists them. */
|
|
@@ -41,21 +40,12 @@ function checkEditor(editor) {
|
|
|
41
40
|
}
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
/** True when the core this editor runs on composes numbered lists (the core that ships `numbering`). */
|
|
45
|
-
export function numberingAvailable() {
|
|
46
|
-
return typeof core.listNumbers === "function" && typeof core.formatListNumber === "function";
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function requireCore() {
|
|
50
|
-
if (!numberingAvailable()) throw fail("numbering-unavailable", "List numbering needs a core release that ships numbering (@openpresentation/opf after 0.11.4).");
|
|
51
|
-
}
|
|
52
|
-
|
|
53
43
|
/**
|
|
54
44
|
* Where a list lives for a path that points at it or into it: `slides.0.items`, `slides.0.items.2` or
|
|
55
45
|
* `slides.0.left.items.1.text` all resolve to the payload that owns the list. Returns undefined when the path is not
|
|
56
46
|
* inside a list.
|
|
57
47
|
*/
|
|
58
|
-
export function listPayloadAt(
|
|
48
|
+
export function listPayloadAt(presentation, path) {
|
|
59
49
|
let segments;
|
|
60
50
|
try {
|
|
61
51
|
segments = splitOpfPath(path);
|
|
@@ -67,7 +57,7 @@ export function listPayloadAt(document, path) {
|
|
|
67
57
|
const next = segments[index + 1];
|
|
68
58
|
if (next !== undefined && !/^\d+$/.test(next)) continue;
|
|
69
59
|
const payloadPath = segments.slice(0, index);
|
|
70
|
-
const payload = payloadPath.length ? getValueAtPath(
|
|
60
|
+
const payload = payloadPath.length ? getValueAtPath(presentation, payloadPath) : presentation;
|
|
71
61
|
if (!isObject(payload) || !Array.isArray(payload[segments[index]])) continue;
|
|
72
62
|
const entry = next === undefined ? undefined : Number(next);
|
|
73
63
|
return { payloadPath: payloadPath.join("."), field: segments[index], path: [...payloadPath, segments[index]].join("."), payload, entry };
|
|
@@ -78,8 +68,8 @@ export function listPayloadAt(document, path) {
|
|
|
78
68
|
const isRegionKey = (key) => /^(?:(?:top|middle|bottom)(?:\+(?:top|middle|bottom))*(?::(?:left|center|right)(?:\+(?:left|center|right))*)?|(?:left|center|right)(?:\+(?:left|center|right))*)$/.test(key);
|
|
79
69
|
|
|
80
70
|
/** Every list (`items` or `bullets`) of a slide: its path, label, entry count and whether it is numbered. */
|
|
81
|
-
export function findNumberableLists(
|
|
82
|
-
const slide =
|
|
71
|
+
export function findNumberableLists(presentation, slideIndex) {
|
|
72
|
+
const slide = presentation?.slides?.[slideIndex];
|
|
83
73
|
if (!isObject(slide)) return [];
|
|
84
74
|
const found = [];
|
|
85
75
|
const visit = (payload, base, label, depth = 0) => {
|
|
@@ -137,8 +127,8 @@ const maxLevelOf = (items) => items.reduce((max, item) => Math.max(max, isObject
|
|
|
137
127
|
* numbering applies to every level), the number of levels the list uses, the markers it draws and which entry (if any)
|
|
138
128
|
* the path selects with that entry's own `start`.
|
|
139
129
|
*/
|
|
140
|
-
export function numberingState(
|
|
141
|
-
const target = listPayloadAt(
|
|
130
|
+
export function numberingState(presentation, path) {
|
|
131
|
+
const target = listPayloadAt(presentation, path);
|
|
142
132
|
if (!target) return undefined;
|
|
143
133
|
const { payload, field, entry } = target;
|
|
144
134
|
const items = payload[field];
|
|
@@ -147,9 +137,9 @@ export function numberingState(document, path) {
|
|
|
147
137
|
const levels = numbered ? resolveLevels(numbering) : [{ ...DEFAULT_LEVEL }];
|
|
148
138
|
const depth = Math.min(MAX_NUMBERING_LEVELS, maxLevelOf(items) + 1);
|
|
149
139
|
let markers = [];
|
|
150
|
-
if (numbered
|
|
140
|
+
if (numbered) {
|
|
151
141
|
try {
|
|
152
|
-
markers =
|
|
142
|
+
markers = listNumbers(items, numbering).map((number) => ({ index: number.index, level: number.level, text: number.text, value: number.value, adapted: number.adapted }));
|
|
153
143
|
} catch {
|
|
154
144
|
markers = [];
|
|
155
145
|
}
|
|
@@ -180,14 +170,13 @@ function numberingPointer(target) {
|
|
|
180
170
|
* the entry `start` values, which mean nothing without it. `path` addresses the list or anything inside it.
|
|
181
171
|
*/
|
|
182
172
|
export function setNumbering(editor, path, value, meta = {}) {
|
|
183
|
-
requireCore();
|
|
184
173
|
checkEditor(editor);
|
|
185
|
-
const target = listPayloadAt(editor.
|
|
174
|
+
const target = listPayloadAt(editor.presentation, path);
|
|
186
175
|
if (!target) throw fail("not-a-list", "Select a list (items or bullets) to number.", { path });
|
|
187
176
|
const patches = [];
|
|
188
177
|
const present = Object.hasOwn(target.payload, "numbering");
|
|
189
178
|
if (value === undefined || value === null) {
|
|
190
|
-
if (!present) return {
|
|
179
|
+
if (!present) return { presentation: editor.presentation, patches: [], inversePatches: [], validation: editor.validation };
|
|
191
180
|
patches.push({ op: "remove", path: numberingPointer(target) });
|
|
192
181
|
target.payload[target.field].forEach((item, index) => {
|
|
193
182
|
if (isObject(item) && item.start !== undefined) patches.push({ op: "remove", path: opfPathToJsonPointer(`${target.path}.${index}.start`) });
|
|
@@ -203,9 +192,8 @@ export function setNumbering(editor, path, value, meta = {}) {
|
|
|
203
192
|
* removes the restart. A plain entry (a string or runs) becomes the object form to carry it. One undoable validated edit.
|
|
204
193
|
*/
|
|
205
194
|
export function setEntryStart(editor, itemPath, start, meta = {}) {
|
|
206
|
-
requireCore();
|
|
207
195
|
checkEditor(editor);
|
|
208
|
-
const target = listPayloadAt(editor.
|
|
196
|
+
const target = listPayloadAt(editor.presentation, itemPath);
|
|
209
197
|
if (!target || target.entry === undefined) throw fail("not-an-entry", "Select a list entry to restart its numbering.", { path: itemPath });
|
|
210
198
|
if (target.payload.numbering === undefined) throw fail("not-numbered", "Number the list first; an entry start has no effect on a bulleted list.", { path: itemPath });
|
|
211
199
|
const item = target.payload[target.field][target.entry];
|
|
@@ -218,6 +206,6 @@ export function setEntryStart(editor, itemPath, start, meta = {}) {
|
|
|
218
206
|
if (isObject(item)) patches.push({ op: item.start === undefined ? "add" : "replace", path: `${pointer}/start`, value: start });
|
|
219
207
|
else patches.push({ op: "replace", path: pointer, value: { text: structuredClone(item), start } });
|
|
220
208
|
}
|
|
221
|
-
if (!patches.length) return {
|
|
209
|
+
if (!patches.length) return { presentation: editor.presentation, patches: [], inversePatches: [], validation: editor.validation };
|
|
222
210
|
return editor.applyPatch(patches, { source: "list-numbering-start", rejectInvalid: true, ...meta });
|
|
223
211
|
}
|
package/dist/outline-view.js
CHANGED
|
@@ -92,9 +92,9 @@ export function createOutlineView(container, options) {
|
|
|
92
92
|
const firstDraw = !lastSnapshot;
|
|
93
93
|
if (snapshot === lastSnapshot && !pendingFocus) return;
|
|
94
94
|
lastSnapshot = snapshot;
|
|
95
|
-
const
|
|
96
|
-
rows = readOutline(
|
|
97
|
-
const sections = hasSections(
|
|
95
|
+
const presentation = editor.presentation;
|
|
96
|
+
rows = readOutline(presentation).rows;
|
|
97
|
+
const sections = hasSections(presentation);
|
|
98
98
|
const entries = [];
|
|
99
99
|
let lastSection;
|
|
100
100
|
for (const row of rows) {
|
|
@@ -143,18 +143,18 @@ export function createOutlineView(container, options) {
|
|
|
143
143
|
function activeRow() { return activeKey ? rowByKey(activeKey) : undefined; }
|
|
144
144
|
function updateTools() {
|
|
145
145
|
const row = activeRow();
|
|
146
|
-
const
|
|
146
|
+
const presentation = editor.presentation;
|
|
147
147
|
const can = { promote: false, demote: false, up: false, down: false, add: Boolean(row) };
|
|
148
148
|
if (row) {
|
|
149
149
|
try {
|
|
150
150
|
if (row.kind === "item") {
|
|
151
|
-
can.demote = prepareShiftOutlineItem(
|
|
152
|
-
can.promote = row.level > 1 ? prepareShiftOutlineItem(
|
|
153
|
-
can.up = prepareMoveOutlineItem(
|
|
154
|
-
can.down = prepareMoveOutlineItem(
|
|
151
|
+
can.demote = prepareShiftOutlineItem(presentation, row, 1).changed;
|
|
152
|
+
can.promote = row.level > 1 ? prepareShiftOutlineItem(presentation, row, -1).changed : row.editable;
|
|
153
|
+
can.up = prepareMoveOutlineItem(presentation, row, -1).changed;
|
|
154
|
+
can.down = prepareMoveOutlineItem(presentation, row, 1).changed;
|
|
155
155
|
} else if (row.kind === "slide") {
|
|
156
156
|
can.up = row.slideIndex > 0;
|
|
157
|
-
can.down = row.slideIndex <
|
|
157
|
+
can.down = row.slideIndex < presentation.slides.length - 1;
|
|
158
158
|
can.demote = row.slideIndex > 0;
|
|
159
159
|
}
|
|
160
160
|
} catch { /* a row that vanished mid-edit leaves the tools off */ }
|
|
@@ -167,7 +167,7 @@ export function createOutlineView(container, options) {
|
|
|
167
167
|
if (!row?.editable) return false;
|
|
168
168
|
if (input.value === row.text) return false;
|
|
169
169
|
try {
|
|
170
|
-
const prepared = prepareSetOutlineText(editor.
|
|
170
|
+
const prepared = prepareSetOutlineText(editor.presentation, row, input.value);
|
|
171
171
|
if (!prepared.changed) return false;
|
|
172
172
|
applyOutlineChange(editor, prepared);
|
|
173
173
|
render();
|
|
@@ -203,25 +203,25 @@ export function createOutlineView(container, options) {
|
|
|
203
203
|
const key = activeKey;
|
|
204
204
|
const row = settle(input ?? inputFor(key), key);
|
|
205
205
|
if (!row) return;
|
|
206
|
-
const
|
|
206
|
+
const presentation = editor.presentation;
|
|
207
207
|
if (action === "add") {
|
|
208
|
-
if (row.kind === "item") apply(() => prepareInsertOutlineItem(
|
|
209
|
-
else if (row.kind === "slide") apply(() => prepareAddOutlineBullet(
|
|
208
|
+
if (row.kind === "item") apply(() => prepareInsertOutlineItem(presentation, row), "Line added");
|
|
209
|
+
else if (row.kind === "slide") apply(() => prepareAddOutlineBullet(presentation, row.slideIndex), "Bullet added under the slide title");
|
|
210
210
|
else announce("Add a line from a title or a bullet");
|
|
211
211
|
} else if (action === "demote") {
|
|
212
|
-
if (row.kind === "item") apply(() => prepareShiftOutlineItem(
|
|
213
|
-
else if (row.kind === "slide") apply(() => prepareOutlineDemoteSlide(
|
|
212
|
+
if (row.kind === "item") apply(() => prepareShiftOutlineItem(presentation, row, 1), () => "Demoted");
|
|
213
|
+
else if (row.kind === "slide") apply(() => prepareOutlineDemoteSlide(presentation, row.slideIndex), "Slide joined the slide before it as a bullet");
|
|
214
214
|
else announce("Only titles and bullets change level");
|
|
215
215
|
} else if (action === "promote") {
|
|
216
216
|
if (row.kind === "item") {
|
|
217
|
-
if (row.level > 1) apply(() => prepareShiftOutlineItem(
|
|
218
|
-
else apply(() => prepareOutlinePromote(
|
|
217
|
+
if (row.level > 1) apply(() => prepareShiftOutlineItem(presentation, row, -1), "Promoted");
|
|
218
|
+
else apply(() => prepareOutlinePromote(presentation, row), (prepared) => `Promoted to slide ${prepared.newSlideIndex + 1}`);
|
|
219
219
|
} else if (row.kind === "slide") announce("Slide titles are already the top level");
|
|
220
220
|
else announce("Only titles and bullets change level");
|
|
221
221
|
} else if (action === "up" || action === "down") {
|
|
222
222
|
const delta = action === "up" ? -1 : 1;
|
|
223
|
-
if (row.kind === "item") apply(() => prepareMoveOutlineItem(
|
|
224
|
-
else if (row.kind === "slide") apply(() => prepareMoveOutlineSlide(
|
|
223
|
+
if (row.kind === "item") apply(() => prepareMoveOutlineItem(presentation, row, delta), `Moved ${action}`);
|
|
224
|
+
else if (row.kind === "slide") apply(() => prepareMoveOutlineSlide(presentation, row.slideIndex, delta), (prepared) => `Slide moved to position ${prepared.selection[0] + 1} of ${presentation.slides.length}`);
|
|
225
225
|
else announce("Move bullets or slides");
|
|
226
226
|
}
|
|
227
227
|
}
|
|
@@ -244,18 +244,18 @@ export function createOutlineView(container, options) {
|
|
|
244
244
|
event.preventDefault();
|
|
245
245
|
const settled = settle(input, key);
|
|
246
246
|
if (!settled) return;
|
|
247
|
-
const
|
|
248
|
-
if (settled.kind === "item") apply(() => prepareInsertOutlineItem(
|
|
249
|
-
else if (settled.kind === "slide") apply(() => prepareInsertOutlineSlide(
|
|
247
|
+
const presentation = editor.presentation;
|
|
248
|
+
if (settled.kind === "item") apply(() => prepareInsertOutlineItem(presentation, settled), "Line added");
|
|
249
|
+
else if (settled.kind === "slide") apply(() => prepareInsertOutlineSlide(presentation, settled.slideIndex), (prepared) => `Slide ${prepared.index + 1} added`);
|
|
250
250
|
else focusAt(index + 1);
|
|
251
251
|
return;
|
|
252
252
|
}
|
|
253
253
|
if (event.key === "Backspace" && input.value === "" && input.selectionStart === 0 && !input.readOnly) {
|
|
254
|
-
if (row.kind === "item") { event.preventDefault(); apply(() => prepareRemoveOutlineItem(editor.
|
|
254
|
+
if (row.kind === "item") { event.preventDefault(); apply(() => prepareRemoveOutlineItem(editor.presentation, row), "Line removed"); }
|
|
255
255
|
else if (row.kind === "slide" && !rows.some((other) => other.slideIndex === row.slideIndex && other.kind !== "slide")) {
|
|
256
256
|
event.preventDefault();
|
|
257
257
|
apply(() => {
|
|
258
|
-
const prepared = prepareRemoveSlides(editor.
|
|
258
|
+
const prepared = prepareRemoveSlides(editor.presentation, [row.slideIndex]);
|
|
259
259
|
return { ...prepared, focus: `slide:slides.${Math.max(0, row.slideIndex - 1)}` };
|
|
260
260
|
}, `Slide ${row.slideIndex + 1} removed`);
|
|
261
261
|
}
|
package/dist/outline.d.ts
CHANGED
|
@@ -35,22 +35,22 @@ export interface PreparedOutlineChange extends Omit<PreparedSlideChange, "select
|
|
|
35
35
|
reason?: string;
|
|
36
36
|
newSlideIndex?: number;
|
|
37
37
|
}
|
|
38
|
-
export type OutlineChange = Omit<EditorChange, "
|
|
38
|
+
export type OutlineChange = Omit<EditorChange, "presentation" | "patches"> & Omit<PreparedOutlineChange, "presentation" | "patches"> & { presentation: unknown; patches: JsonPatchOperation[] };
|
|
39
39
|
|
|
40
|
-
export declare function readOutline(
|
|
41
|
-
export declare function prepareSetOutlineText(
|
|
40
|
+
export declare function readOutline(presentation: unknown): Outline;
|
|
41
|
+
export declare function prepareSetOutlineText(presentation: unknown, row: OutlineRow, text: string): PreparedOutlineChange;
|
|
42
42
|
export declare function setOutlineText(editor: EditorSession, row: OutlineRow, text: string, meta?: Record<string, unknown>): OutlineChange;
|
|
43
43
|
/** Nest (`delta` 1) or un-nest (-1) a list item with the items under it. A top-level item reports `changed: false` and a `reason`. */
|
|
44
|
-
export declare function prepareShiftOutlineItem(
|
|
45
|
-
export declare function prepareMoveOutlineItem(
|
|
46
|
-
export declare function prepareInsertOutlineItem(
|
|
47
|
-
export declare function prepareAddOutlineBullet(
|
|
48
|
-
export declare function prepareRemoveOutlineItem(
|
|
44
|
+
export declare function prepareShiftOutlineItem(presentation: unknown, row: OutlineRow, delta: 1 | -1): PreparedOutlineChange;
|
|
45
|
+
export declare function prepareMoveOutlineItem(presentation: unknown, row: OutlineRow, delta: 1 | -1): PreparedOutlineChange;
|
|
46
|
+
export declare function prepareInsertOutlineItem(presentation: unknown, row: OutlineRow, text?: string): PreparedOutlineChange;
|
|
47
|
+
export declare function prepareAddOutlineBullet(presentation: unknown, slideIndex: number, text?: string): PreparedOutlineChange;
|
|
48
|
+
export declare function prepareRemoveOutlineItem(presentation: unknown, row: OutlineRow): PreparedOutlineChange;
|
|
49
49
|
/** A top-level list item becomes the title of a new slide that takes the items after it. */
|
|
50
|
-
export declare function prepareOutlinePromote(
|
|
50
|
+
export declare function prepareOutlinePromote(presentation: unknown, row: OutlineRow): PreparedOutlineChange;
|
|
51
51
|
/** A plain slide becomes a bullet (with its text nested) of the slide before it. Throws `outline-row-not-supported` rather than dropping content. */
|
|
52
|
-
export declare function prepareOutlineDemoteSlide(
|
|
53
|
-
export declare function prepareMoveOutlineSlide(
|
|
54
|
-
export declare function prepareInsertOutlineSlide(
|
|
52
|
+
export declare function prepareOutlineDemoteSlide(presentation: unknown, slideIndex: number): PreparedOutlineChange;
|
|
53
|
+
export declare function prepareMoveOutlineSlide(presentation: unknown, slideIndex: number, delta: number): PreparedOutlineChange;
|
|
54
|
+
export declare function prepareInsertOutlineSlide(presentation: unknown, slideIndex: number): PreparedOutlineChange;
|
|
55
55
|
/** Apply any prepared outline change as one undoable step. */
|
|
56
56
|
export declare function applyOutlineChange(editor: EditorSession, prepared: PreparedOutlineChange | PreparedSlideChange, meta?: Record<string, unknown>): OutlineChange;
|
package/dist/outline.js
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
// item one list item or bullet; `listPath` and `itemIndex` locate it and `level` is 1 plus its nesting level
|
|
10
10
|
// other content that is not text, shown by its kind (`label`), never edited here
|
|
11
11
|
// Text that carries formatting (a run array) is shown flattened and is not editable in the outline, so nothing is flattened away.
|
|
12
|
-
import { getValueAtPath, opfPathToJsonPointer, splitOpfPath
|
|
12
|
+
import { getValueAtPath, opfPathToJsonPointer, splitOpfPath } from "./index.js";
|
|
13
13
|
import { checkedDocument, fail } from "./edit-helpers.js";
|
|
14
14
|
import { visitContentPayloads } from "./presentation-ids.js";
|
|
15
15
|
import { prepareAddSlide, prepareMoveSlidesBy } from "./slides.js";
|
|
16
|
+
import { checkFormat } from "./checks.js";
|
|
16
17
|
|
|
17
18
|
const clone = (value) => structuredClone(value);
|
|
18
19
|
const isObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
@@ -46,14 +47,15 @@ function walkPayload(node, path, slideIndex, rows) {
|
|
|
46
47
|
* Read the deck as outline rows (see the file header). `rows` follow document order: for each slide its title, subtitle, the
|
|
47
48
|
* slide's own text and list, then every block and region in order.
|
|
48
49
|
*/
|
|
49
|
-
export function readOutline(
|
|
50
|
-
if (!
|
|
50
|
+
export function readOutline(presentation) {
|
|
51
|
+
if (!presentation || !Array.isArray(presentation.slides)) throw fail("invalid-input", "The outline needs an OPF document with a slides array.");
|
|
51
52
|
const rows = [];
|
|
52
|
-
|
|
53
|
+
presentation.slides.forEach((slide, slideIndex) => {
|
|
53
54
|
const base = `slides.${slideIndex}`;
|
|
54
|
-
|
|
55
|
+
// FA-10: a title or subtitle may be TextRun[]: the row shows its plain text and is edited as runs elsewhere (a text row writes a string).
|
|
56
|
+
const title = flatten(slide?.title);
|
|
55
57
|
rows.push({ key: `slide:${base}`, kind: "slide", slideIndex, level: 0, text: title, path: `${base}.title`, editable: slide?.title === undefined || typeof slide.title === "string", hidden: slide?.hidden === true, section: typeof slide?.section === "string" ? slide.section : undefined, id: slide?.id });
|
|
56
|
-
if (typeof slide?.subtitle === "string") rows.push({ key: `subtitle:${base}.subtitle`, kind: "subtitle", slideIndex, level: 1, text: slide.subtitle, path: `${base}.subtitle`, editable:
|
|
58
|
+
if (typeof slide?.subtitle === "string" || Array.isArray(slide?.subtitle)) rows.push({ key: `subtitle:${base}.subtitle`, kind: "subtitle", slideIndex, level: 1, text: flatten(slide.subtitle), path: `${base}.subtitle`, editable: typeof slide.subtitle === "string" });
|
|
57
59
|
if (!isObject(slide)) return;
|
|
58
60
|
walkPayload(slide, base, slideIndex, rows);
|
|
59
61
|
visitContentPayloads(slide, base, (payload, path) => walkPayload(payload, path, slideIndex, rows));
|
|
@@ -64,23 +66,23 @@ export function readOutline(document) {
|
|
|
64
66
|
// --- text ------------------------------------------------------------------------------------------
|
|
65
67
|
|
|
66
68
|
/** Compute editing the text of a row (a title, subtitle, text paragraph or list item); a row that is not plain text throws. */
|
|
67
|
-
export function prepareSetOutlineText(
|
|
69
|
+
export function prepareSetOutlineText(presentation, row, text) {
|
|
68
70
|
if (!row?.editable) throw fail("outline-row-not-editable", "This text carries formatting or is not text. Edit it on the slide.", { path: row?.path });
|
|
69
71
|
const value = String(text);
|
|
70
|
-
const current = getValueAtPath(
|
|
71
|
-
if (current === value || (current === undefined && value === "")) return {
|
|
72
|
+
const current = getValueAtPath(presentation, splitOpfPath(row.path));
|
|
73
|
+
if (current === value || (current === undefined && value === "")) return { presentation: clone(presentation), patches: [], changed: false, selection: [row.slideIndex] };
|
|
72
74
|
const pointer = opfPathToJsonPointer(row.path);
|
|
73
75
|
const patches = [{ op: current === undefined ? "add" : "replace", path: pointer, value }];
|
|
74
|
-
return finish(
|
|
76
|
+
return finish(presentation, patches, { action: "outline-text", selection: [row.slideIndex] });
|
|
75
77
|
}
|
|
76
78
|
export function setOutlineText(editor, row, text, meta) {
|
|
77
|
-
return apply(editor, prepareSetOutlineText(editor.
|
|
79
|
+
return apply(editor, prepareSetOutlineText(editor.presentation, row, text), meta, "outline-text");
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
// --- list levels -----------------------------------------------------------------------------------
|
|
81
83
|
|
|
82
|
-
function listAt(
|
|
83
|
-
const list = getValueAtPath(
|
|
84
|
+
function listAt(presentation, row) {
|
|
85
|
+
const list = getValueAtPath(presentation, splitOpfPath(row.listPath));
|
|
84
86
|
if (!Array.isArray(list) || list[row.itemIndex] === undefined) throw fail("outline-row-not-found", "That list item is no longer there.", { path: row.listPath });
|
|
85
87
|
return list;
|
|
86
88
|
}
|
|
@@ -104,17 +106,17 @@ function blockEnd(levels, index) {
|
|
|
104
106
|
* than the item above, and the first item cannot be nested. Un-nesting an item that is already at the top level is
|
|
105
107
|
* `prepareOutlinePromote`'s job (it becomes a slide); here it reports `changed: false` with a `reason`.
|
|
106
108
|
*/
|
|
107
|
-
export function prepareShiftOutlineItem(
|
|
108
|
-
const list = listAt(
|
|
109
|
+
export function prepareShiftOutlineItem(presentation, row, delta) {
|
|
110
|
+
const list = listAt(presentation, row);
|
|
109
111
|
const levels = list.map((item) => itemParts(item).level);
|
|
110
112
|
const index = row.itemIndex;
|
|
111
|
-
const none = (reason) => ({
|
|
113
|
+
const none = (reason) => ({ presentation: clone(presentation), patches: [], changed: false, reason, selection: [row.slideIndex] });
|
|
112
114
|
if (delta > 0 && (index === 0 || levels[index] > levels[index - 1])) return none(index === 0 ? "The first item cannot be nested." : "This item is already one level in from the item above.");
|
|
113
115
|
if (delta < 0 && levels[index] === 0) return none("This item is already at the top level.");
|
|
114
116
|
const end = blockEnd(levels, index);
|
|
115
117
|
const next = list.map((item, at) => (at >= index && at < end ? withLevel(item, Math.max(0, levels[at] + delta)) : item));
|
|
116
118
|
const patches = [{ op: "replace", path: opfPathToJsonPointer(row.listPath), value: next }];
|
|
117
|
-
return finish(
|
|
119
|
+
return finish(presentation, patches, { action: delta > 0 ? "outline-demote" : "outline-promote", selection: [row.slideIndex], focus: `item:${row.listPath}.${index}` });
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
// --- item order ------------------------------------------------------------------------------------
|
|
@@ -123,11 +125,11 @@ export function prepareShiftOutlineItem(document, row, delta) {
|
|
|
123
125
|
* Compute moving a list item (with the items nested under it) one place up (`delta` -1) or down (1) among the items at its level
|
|
124
126
|
* and under the same parent. At the first or last place nothing changes (`changed` is false).
|
|
125
127
|
*/
|
|
126
|
-
export function prepareMoveOutlineItem(
|
|
127
|
-
const list = listAt(
|
|
128
|
+
export function prepareMoveOutlineItem(presentation, row, delta) {
|
|
129
|
+
const list = listAt(presentation, row);
|
|
128
130
|
const levels = list.map((item) => itemParts(item).level);
|
|
129
131
|
const index = row.itemIndex, end = blockEnd(levels, index);
|
|
130
|
-
const none = {
|
|
132
|
+
const none = { presentation: clone(presentation), patches: [], changed: false, selection: [row.slideIndex] };
|
|
131
133
|
let order, focusIndex;
|
|
132
134
|
if (delta < 0) {
|
|
133
135
|
// The previous sibling starts at the nearest earlier item on the same level, provided no shallower item lies between.
|
|
@@ -143,32 +145,32 @@ export function prepareMoveOutlineItem(document, row, delta) {
|
|
|
143
145
|
focusIndex = index + (siblingEnd - end);
|
|
144
146
|
}
|
|
145
147
|
const patches = [{ op: "replace", path: opfPathToJsonPointer(row.listPath), value: order }];
|
|
146
|
-
return finish(
|
|
148
|
+
return finish(presentation, patches, { action: "outline-move", selection: [row.slideIndex], focus: `item:${row.listPath}.${focusIndex}` });
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
// --- insert and delete -----------------------------------------------------------------------------
|
|
150
152
|
|
|
151
153
|
/** Compute adding an empty list item right after the item at `row` and everything nested under it, at the same level. */
|
|
152
|
-
export function prepareInsertOutlineItem(
|
|
153
|
-
const list = listAt(
|
|
154
|
+
export function prepareInsertOutlineItem(presentation, row, text = "") {
|
|
155
|
+
const list = listAt(presentation, row);
|
|
154
156
|
const levels = list.map((item) => itemParts(item).level);
|
|
155
157
|
const end = blockEnd(levels, row.itemIndex);
|
|
156
158
|
const item = levels[row.itemIndex] > 0 ? { text, level: levels[row.itemIndex] } : text;
|
|
157
|
-
return finish(
|
|
159
|
+
return finish(presentation, [{ op: "add", path: `${opfPathToJsonPointer(row.listPath)}/${end}`, value: item }], { action: "outline-insert", selection: [row.slideIndex], focus: `item:${row.listPath}.${end}` });
|
|
158
160
|
}
|
|
159
161
|
/** Compute adding the first bullet under a slide: it goes in the slide's own list, or creates one. */
|
|
160
|
-
export function prepareAddOutlineBullet(
|
|
161
|
-
const slide =
|
|
162
|
+
export function prepareAddOutlineBullet(presentation, slideIndex, text = "") {
|
|
163
|
+
const slide = presentation.slides?.[slideIndex];
|
|
162
164
|
if (!isObject(slide)) throw fail("slide-index-out-of-range", `Slide ${slideIndex} does not exist.`, { slideIndex });
|
|
163
165
|
const key = LIST_KEYS.find((candidate) => Array.isArray(slide[candidate]));
|
|
164
166
|
const base = `/slides/${slideIndex}`;
|
|
165
|
-
if (key) return finish(
|
|
167
|
+
if (key) return finish(presentation, [{ op: "add", path: `${base}/${key}/${slide[key].length}`, value: text }], { action: "outline-insert", selection: [slideIndex], focus: `item:slides.${slideIndex}.${key}.${slide[key].length}` });
|
|
166
168
|
if (Array.isArray(slide.blocks) || slide.type !== undefined) throw fail("outline-row-not-supported", "Add a list to this slide from the Content panel; its content is arranged in blocks.", { slideIndex });
|
|
167
|
-
return finish(
|
|
169
|
+
return finish(presentation, [{ op: "add", path: `${base}/items`, value: [text] }], { action: "outline-insert", selection: [slideIndex], focus: `item:slides.${slideIndex}.items.0` });
|
|
168
170
|
}
|
|
169
171
|
/** Compute deleting a list item together with the items nested under it. */
|
|
170
|
-
export function prepareRemoveOutlineItem(
|
|
171
|
-
const list = listAt(
|
|
172
|
+
export function prepareRemoveOutlineItem(presentation, row) {
|
|
173
|
+
const list = listAt(presentation, row);
|
|
172
174
|
const levels = list.map((item) => itemParts(item).level);
|
|
173
175
|
const end = blockEnd(levels, row.itemIndex);
|
|
174
176
|
const patches = [];
|
|
@@ -178,7 +180,7 @@ export function prepareRemoveOutlineItem(document, row) {
|
|
|
178
180
|
const removeList = survivors === 0 && row.listPath.split(".").length === 3;
|
|
179
181
|
const finalPatches = removeList ? [{ op: "remove", path: opfPathToJsonPointer(row.listPath) }] : patches;
|
|
180
182
|
const focus = survivors ? `item:${row.listPath}.${Math.max(0, row.itemIndex - 1)}` : `slide:slides.${row.slideIndex}`;
|
|
181
|
-
return finish(
|
|
183
|
+
return finish(presentation, finalPatches, { action: "outline-remove", selection: [row.slideIndex], focus });
|
|
182
184
|
}
|
|
183
185
|
|
|
184
186
|
// --- promote and demote across slides --------------------------------------------------------------
|
|
@@ -187,14 +189,14 @@ export function prepareRemoveOutlineItem(document, row) {
|
|
|
187
189
|
* Compute promoting a top-level list item to a slide: the item becomes the title of a new slide right after its own, and the items that
|
|
188
190
|
* follow it in the same list move to that slide (PowerPoint's outline does the same). Items above it stay.
|
|
189
191
|
*/
|
|
190
|
-
export function prepareOutlinePromote(
|
|
191
|
-
const list = listAt(
|
|
192
|
+
export function prepareOutlinePromote(presentation, row) {
|
|
193
|
+
const list = listAt(presentation, row);
|
|
192
194
|
const title = itemParts(list[row.itemIndex]);
|
|
193
195
|
if (title.level !== 0) throw fail("outline-row-not-supported", "Only a top-level item becomes a slide.", { path: row.path });
|
|
194
196
|
if (!title.plain) throw fail("outline-row-not-editable", "This item carries formatting. Edit it on the slide.", { path: row.path });
|
|
195
197
|
const following = list.slice(row.itemIndex + 1);
|
|
196
|
-
const added = prepareAddSlide(
|
|
197
|
-
const slide = clone(added.
|
|
198
|
+
const added = prepareAddSlide(presentation, { at: row.slideIndex + 1, title: title.text });
|
|
199
|
+
const slide = clone(added.presentation.slides[row.slideIndex + 1]);
|
|
198
200
|
const key = row.listPath.endsWith(".bullets") ? "bullets" : "items";
|
|
199
201
|
if (following.length) slide[key] = clone(following);
|
|
200
202
|
const pointer = opfPathToJsonPointer(row.listPath);
|
|
@@ -204,7 +206,7 @@ export function prepareOutlinePromote(document, row) {
|
|
|
204
206
|
? [{ op: "remove", path: pointer }]
|
|
205
207
|
: Array.from({ length: list.length - row.itemIndex }, (_, offset) => ({ op: "remove", path: `${pointer}/${list.length - 1 - offset}` }));
|
|
206
208
|
const patches = [...removal, { op: "add", path: `/slides/${row.slideIndex + 1}`, value: slide }];
|
|
207
|
-
return finish(
|
|
209
|
+
return finish(presentation, patches, { action: "outline-promote-slide", selection: [row.slideIndex + 1], focus: `slide:slides.${row.slideIndex + 1}`, newSlideIndex: row.slideIndex + 1 });
|
|
208
210
|
}
|
|
209
211
|
|
|
210
212
|
const MERGEABLE = new Set(["id", "title", "text", "items", "bullets", "section", "hidden"]);
|
|
@@ -213,8 +215,8 @@ const MERGEABLE = new Set(["id", "title", "text", "items", "bullets", "section",
|
|
|
213
215
|
* beneath it. Only a plain slide qualifies: one with a title and at most text or a list (no blocks, regions, notes, design or layout),
|
|
214
216
|
* after a slide that has a list of its own or no body. Anything else throws `outline-row-not-supported` so nothing is dropped.
|
|
215
217
|
*/
|
|
216
|
-
export function prepareOutlineDemoteSlide(
|
|
217
|
-
const slides =
|
|
218
|
+
export function prepareOutlineDemoteSlide(presentation, slideIndex) {
|
|
219
|
+
const slides = presentation.slides;
|
|
218
220
|
const slide = slides?.[slideIndex];
|
|
219
221
|
if (!isObject(slide)) throw fail("slide-index-out-of-range", `Slide ${slideIndex} does not exist.`, { slideIndex });
|
|
220
222
|
if (slideIndex === 0) throw fail("outline-row-not-supported", "The first slide has no slide before it to join.", { slideIndex });
|
|
@@ -231,36 +233,36 @@ export function prepareOutlineDemoteSlide(document, slideIndex) {
|
|
|
231
233
|
const added = [slide.title, ...(typeof slide.text === "string" && slide.text ? [{ text: slide.text, level: 1 }] : []), ...nested];
|
|
232
234
|
const base = `/slides/${slideIndex - 1}`;
|
|
233
235
|
const patches = [{ op: "remove", path: `/slides/${slideIndex}` }, listKey ? { op: "replace", path: `${base}/${key}`, value: [...previous[key], ...added] } : { op: "add", path: `${base}/${key}`, value: added }];
|
|
234
|
-
return finish(
|
|
236
|
+
return finish(presentation, patches, { action: "outline-demote-slide", selection: [slideIndex - 1], focus: `item:slides.${slideIndex - 1}.${key}.${(previous[key]?.length ?? 0)}` });
|
|
235
237
|
}
|
|
236
238
|
|
|
237
239
|
// --- slides ----------------------------------------------------------------------------------------
|
|
238
240
|
|
|
239
241
|
/** Compute moving a slide (the whole slide: its text moves with its title) by `delta` places; see `prepareMoveSlidesBy`. */
|
|
240
|
-
export function prepareMoveOutlineSlide(
|
|
241
|
-
const prepared = prepareMoveSlidesBy(
|
|
242
|
+
export function prepareMoveOutlineSlide(presentation, slideIndex, delta) {
|
|
243
|
+
const prepared = prepareMoveSlidesBy(presentation, [slideIndex], delta);
|
|
242
244
|
return prepared.changed ? { ...prepared, focus: `slide:slides.${prepared.selection[0]}` } : prepared;
|
|
243
245
|
}
|
|
244
246
|
/** Compute adding a slide right after `slideIndex`, with an empty title for the outline to type into. */
|
|
245
|
-
export function prepareInsertOutlineSlide(
|
|
246
|
-
return { ...prepareAddSlide(
|
|
247
|
+
export function prepareInsertOutlineSlide(presentation, slideIndex) {
|
|
248
|
+
return { ...prepareAddSlide(presentation, { at: slideIndex + 1, title: "" }), action: "outline-insert-slide", focus: `slide:slides.${slideIndex + 1}` };
|
|
247
249
|
}
|
|
248
250
|
|
|
249
251
|
// --- apply -----------------------------------------------------------------------------------------
|
|
250
252
|
|
|
251
|
-
function finish(
|
|
252
|
-
if (!patches.length) return {
|
|
253
|
-
const before =
|
|
254
|
-
return {
|
|
253
|
+
function finish(presentation, patches, extra) {
|
|
254
|
+
if (!patches.length) return { presentation: clone(presentation), patches: [], changed: false, ...extra };
|
|
255
|
+
const before = checkFormat(presentation);
|
|
256
|
+
return { presentation: checkedDocument(presentation, patches, before), patches, changed: true, ...extra };
|
|
255
257
|
}
|
|
256
258
|
function requireEditor(editor) {
|
|
257
259
|
if (!editor || typeof editor.applyPatch !== "function") throw fail("invalid-editor", "Expected an editor session created by createEditorSession.");
|
|
258
260
|
}
|
|
259
261
|
function apply(editor, prepared, meta, action) {
|
|
260
262
|
requireEditor(editor);
|
|
261
|
-
const {
|
|
262
|
-
void
|
|
263
|
-
if (!prepared.changed) return { ...summary,
|
|
263
|
+
const { presentation, patches, ...summary } = prepared;
|
|
264
|
+
void presentation;
|
|
265
|
+
if (!prepared.changed) return { ...summary, presentation: editor.presentation, patches: [], inversePatches: [], validation: editor.validation };
|
|
264
266
|
const change = editor.applyPatch(patches, { ...meta, source: meta?.source ?? "outline", action });
|
|
265
267
|
return { ...change, ...summary };
|
|
266
268
|
}
|