@openpresentation/opf-editor 0.11.3 → 0.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -58
- 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 +23 -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 +12 -10
- package/dist/chart-data.js +22 -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 +179 -10
- package/dist/data.d.ts +23 -2
- package/dist/data.js +40 -1
- package/dist/dataset-refs.d.ts +17 -0
- package/dist/dataset-refs.js +45 -0
- package/dist/design-controls.js +66 -47
- 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 +35 -20
- package/dist/font-gate.js +49 -58
- package/dist/galleries.js +36 -52
- package/dist/grid-model.d.ts +99 -24
- package/dist/grid-model.js +577 -101
- 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 +136 -109
- package/dist/table-options.d.ts +8 -8
- package/dist/table-options.js +41 -30
- 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 +74 -54
- package/package.json +10 -6
- package/dist/font-defaults.js +0 -25
package/dist/json-options.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { catalogKinds, catalogSchemaNames, schemas, validateCatalogRecord
|
|
1
|
+
import { catalogKinds, catalogSchemaNames, schemas, validateCatalogRecord } from "@openpresentation/opf";
|
|
2
|
+
import { checkFormat, firstErrorMessage } from "./checks.js";
|
|
2
3
|
import { getCatalogOptions } from "./index.js";
|
|
3
4
|
import { schemaAtPath, schemaVariants } from "./schema.js";
|
|
4
5
|
import { applyEdits, findNodeAtLocation, findNodeAtOffset, getNodePath, modify, parseTree } from "jsonc-parser";
|
|
@@ -19,8 +20,8 @@ function placeholderSummary(types) {
|
|
|
19
20
|
return "Placeholders not specified";
|
|
20
21
|
if (!types.length)
|
|
21
22
|
return "No placeholders";
|
|
22
|
-
const labels = { title: "Title", subtitle: "Subtitle",
|
|
23
|
-
const order = ["title", "subtitle", "
|
|
23
|
+
const labels = { title: "Title", subtitle: "Subtitle", tag: "Tag", text: "Text", list: "List", image: "Image", video: "Video", chart: "Chart", table: "Table", code: "Code", metric: "Metric", quote: "Quote", timeline: "Timeline" };
|
|
24
|
+
const order = ["title", "subtitle", "tag", "text", "list", "image", "video", "chart", "table", "code", "metric", "quote", "timeline"];
|
|
24
25
|
return [...new Set(types)].sort((a, b) => order.indexOf(a) - order.indexOf(b)).map(type => {
|
|
25
26
|
const count = types.filter(value => value === type).length;
|
|
26
27
|
return `${labels[type] ?? type}${count > 1 ? ` × ${count}` : ""}`;
|
|
@@ -64,9 +65,9 @@ function describeOptions(choices, records, current, catalog) {
|
|
|
64
65
|
// The installed schema identifies fields; a document field merely named "layout"
|
|
65
66
|
// inside arbitrary metadata is not mistaken for an OPF layout reference.
|
|
66
67
|
export function getJsonFieldContext(source, position, loadedCatalogs = {}) {
|
|
67
|
-
let
|
|
68
|
+
let presentation;
|
|
68
69
|
try {
|
|
69
|
-
|
|
70
|
+
presentation = JSON.parse(source);
|
|
70
71
|
}
|
|
71
72
|
catch {
|
|
72
73
|
return null;
|
|
@@ -84,18 +85,18 @@ export function getJsonFieldContext(source, position, loadedCatalogs = {}) {
|
|
|
84
85
|
const path = getNodePath(node);
|
|
85
86
|
if (!path.length)
|
|
86
87
|
return null;
|
|
87
|
-
const schema = schemaAtPath(
|
|
88
|
+
const schema = schemaAtPath(presentation, path.map(String));
|
|
88
89
|
const variants = schemaVariants(schema);
|
|
89
90
|
const description = String(schema.description ?? "");
|
|
90
91
|
let catalog = catalogFor(description);
|
|
91
92
|
// Audience arrays inherit the catalog context from the array property.
|
|
92
93
|
if (!catalog && typeof path.at(-1) === "number")
|
|
93
|
-
catalog = catalogFor(String(schemaAtPath(
|
|
94
|
+
catalog = catalogFor(String(schemaAtPath(presentation, path.slice(0, -1).map(String)).description ?? ""));
|
|
94
95
|
const options = new Map();
|
|
95
96
|
const records = new Map();
|
|
96
97
|
let unloadedSource = false;
|
|
97
98
|
if (catalog && typeof node.value === "string") {
|
|
98
|
-
const entries =
|
|
99
|
+
const entries = presentation.catalogs;
|
|
99
100
|
const local = entries?.[catalog];
|
|
100
101
|
unloadedSource = Boolean(local?.source);
|
|
101
102
|
for (const option of getCatalogOptions(catalog)) {
|
|
@@ -148,18 +149,18 @@ export function replaceFieldOption(context, value) {
|
|
|
148
149
|
if (!context.options.some(option => option.value === value))
|
|
149
150
|
throw new Error("This choice is no longer available.");
|
|
150
151
|
let next = context.source.slice(0, context.offset) + JSON.stringify(value) + context.source.slice(context.offset + context.length);
|
|
151
|
-
const
|
|
152
|
-
if (context.path.reduce((current, key) => current?.[key],
|
|
152
|
+
const presentation = JSON.parse(next);
|
|
153
|
+
if (context.path.reduce((current, key) => current?.[key], presentation) !== value)
|
|
153
154
|
throw new Error("This field has a duplicate key. Resolve it in the JSON before choosing an option.");
|
|
154
155
|
const types = context.options.find(option => option.value === value).placeholderTypes;
|
|
155
156
|
if (value !== context.value && types && context.catalog === 'layouts' && context.path.length === 3
|
|
156
|
-
&& context.path[0] === 'slides' && context.path[2] === 'layout' &&
|
|
157
|
-
const populated = populateLayoutPlaceholders(
|
|
158
|
-
next = updateSource(next,
|
|
157
|
+
&& context.path[0] === 'slides' && context.path[2] === 'layout' && checkFormat(presentation).valid) {
|
|
158
|
+
const populated = populateLayoutPlaceholders(presentation, context.path[1], types);
|
|
159
|
+
next = updateSource(next, presentation, populated);
|
|
159
160
|
}
|
|
160
|
-
const result =
|
|
161
|
-
if (!result.valid &&
|
|
162
|
-
throw new Error(result
|
|
161
|
+
const result = checkFormat(JSON.parse(next));
|
|
162
|
+
if (!result.valid && checkFormat(JSON.parse(context.source)).valid)
|
|
163
|
+
throw new Error(firstErrorMessage(result, "This option is not valid here."));
|
|
163
164
|
return next;
|
|
164
165
|
}
|
|
165
166
|
|
package/dist/layout-handles.js
CHANGED
|
@@ -22,11 +22,11 @@ export function createLayoutHandles(root, options) {
|
|
|
22
22
|
const edit=active;
|
|
23
23
|
edit.fraction=fraction;
|
|
24
24
|
try{
|
|
25
|
-
const prepared=prepareTrackResize(editor.
|
|
26
|
-
options.render(prepared.
|
|
25
|
+
const prepared=prepareTrackResize(editor.presentation,active.flow,active.boundary,fraction);
|
|
26
|
+
options.render(prepared.presentation);
|
|
27
27
|
if(active!==edit)return false;
|
|
28
28
|
edit.prepared=prepared;edit.valid=true;
|
|
29
|
-
options.onDraft?.({
|
|
29
|
+
options.onDraft?.({presentation:prepared.presentation,path:active.flow.path+'.composition',value:prepared.composition});
|
|
30
30
|
if(active!==edit)return false;
|
|
31
31
|
options.clearError?.();return true;
|
|
32
32
|
}catch(error){if(active===edit)edit.valid=false;options.onError(error);return false;}
|
|
@@ -41,7 +41,7 @@ export function createLayoutHandles(root, options) {
|
|
|
41
41
|
active=null;release(edit);committing=true;
|
|
42
42
|
try{
|
|
43
43
|
// Rebase unrelated document edits, retaining the guarded container.
|
|
44
|
-
const prepared=prepareTrackResize(editor.
|
|
44
|
+
const prepared=prepareTrackResize(editor.presentation,edit.flow,edit.boundary,edit.fraction);
|
|
45
45
|
if(edit.moved)editor.applyPatch(prepared.patches,{source:'canvas-layout',rejectInvalid:true});
|
|
46
46
|
else renderCanonical();
|
|
47
47
|
options.onCommit?.({path:edit.flow.path+'.composition',editor});return true;
|
|
@@ -56,7 +56,7 @@ export function createLayoutHandles(root, options) {
|
|
|
56
56
|
if(event)layer.setPointerCapture(event.pointerId);
|
|
57
57
|
return true;
|
|
58
58
|
}
|
|
59
|
-
function update(
|
|
59
|
+
function update(presentation,geometry) {
|
|
60
60
|
lastGeometry=geometry;
|
|
61
61
|
layer.replaceChildren();
|
|
62
62
|
if(!enabled||options.isTextEditing())return;
|
|
@@ -2,7 +2,7 @@ import {listBlockContainers,prepareBlockInsert} from './blocks.js';
|
|
|
2
2
|
|
|
3
3
|
const payloadFields=['text','items','bullets','image','video','chart','table','code','metric','quote','timeline'];
|
|
4
4
|
const headingFields=new Set(['title','subtitle','tag']);
|
|
5
|
-
const fields={text:'text',list:'items',
|
|
5
|
+
const fields={text:'text',list:'items',image:'image',video:'video',chart:'chart',table:'table',code:'code',metric:'metric',quote:'quote',timeline:'timeline'};
|
|
6
6
|
const region=key=>/^(?:(?:top|middle|bottom)(?:\+(?:top|middle|bottom))*(?::(?:left|center|right)(?:\+(?:left|center|right))*)?|(?:left|center|right)(?:\+(?:left|center|right))*)$/.test(key);
|
|
7
7
|
|
|
8
8
|
function emptyPayload(field){
|
|
@@ -16,8 +16,8 @@ function emptyPayload(field){
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/** Supply authored slots without replacing content, metadata or region placement. */
|
|
19
|
-
export function populateLayoutPlaceholders(
|
|
20
|
-
let next=structuredClone(
|
|
19
|
+
export function populateLayoutPlaceholders(presentation,slideIndex,types){
|
|
20
|
+
let next=structuredClone(presentation),slide=next.slides[slideIndex];
|
|
21
21
|
const counts=new Map();
|
|
22
22
|
function count(host){
|
|
23
23
|
if(!host||typeof host!=='object')return;
|
|
@@ -48,7 +48,7 @@ export function populateLayoutPlaceholders(document,slideIndex,types){
|
|
|
48
48
|
const path=regions.length?`/slides/${slideIndex}/${regions.at(-1)}`:`/slides/${slideIndex}`;
|
|
49
49
|
const containers=listBlockContainers(next,{slideIndex,includeImplicit:true});
|
|
50
50
|
if(!containers.some(container=>container.path===path))throw new Error('This slide has no editable content container for the new placeholder.');
|
|
51
|
-
next=prepareBlockInsert(next,path,{[field]:value}).
|
|
51
|
+
next=prepareBlockInsert(next,path,{[field]:value}).presentation;
|
|
52
52
|
slide=next.slides[slideIndex];
|
|
53
53
|
}
|
|
54
54
|
}
|
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;
|