@openpresentation/opf-editor 0.27.0 → 0.29.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 +23 -9
- package/dist/canvas.d.ts +13 -1
- package/dist/canvas.js +39 -9
- package/dist/chart-options-panel.js +16 -16
- package/dist/codemirror-guard.js +69 -0
- package/dist/content-controls.js +127 -74
- package/dist/data-grid.js +15 -7
- package/dist/design-controls.d.ts +13 -0
- package/dist/design-controls.js +136 -125
- package/dist/design-options.js +1 -1
- package/dist/exact-source.d.ts +30 -0
- package/dist/font-gate.js +20 -1
- package/dist/json-editor.d.ts +33 -0
- package/dist/json-editor.js +91 -4
- package/dist/json-options.d.ts +2 -1
- package/dist/json-options.js +7 -6
- package/dist/layout-adapter.js +96 -0
- package/dist/layout-canvas.js +122 -232
- package/dist/layout-editor-ui.js +46 -185
- package/dist/layout-inspector.js +36 -99
- package/dist/layout-layers.js +75 -92
- package/dist/layout-model.js +62 -103
- package/dist/layout-templates.d.ts +8 -3
- package/dist/layout-templates.js +30 -16
- package/dist/numbering-panel.js +2 -3
- package/dist/react.d.ts +16 -4
- package/dist/react.js +47 -6
- package/dist/slide-adapter.d.ts +49 -0
- package/dist/slide-adapter.js +289 -0
- package/dist/slide-edits.d.ts +46 -0
- package/dist/slide-edits.js +629 -0
- package/dist/slide-manager.js +2 -1
- package/dist/slide-scene.d.ts +96 -0
- package/dist/slide-scene.js +413 -0
- package/dist/source-checker.js +45 -6
- package/dist/source-color-scheme.js +60 -0
- package/dist/source-completions.js +6 -4
- package/dist/source-pane.d.ts +103 -6
- package/dist/source-pane.js +196 -9
- package/dist/source-worker.js +8 -4
- package/dist/studio-canvas.d.ts +34 -0
- package/dist/studio-canvas.js +586 -0
- package/dist/studio-commands.d.ts +15 -0
- package/dist/studio-commands.js +155 -0
- package/dist/studio-design.d.ts +16 -0
- package/dist/studio-design.js +354 -0
- package/dist/studio-kit/commands.d.ts +38 -0
- package/dist/studio-kit/commands.js +148 -0
- package/dist/studio-kit/dom.d.ts +52 -0
- package/dist/studio-kit/dom.js +198 -0
- package/dist/studio-kit/drag.d.ts +26 -0
- package/dist/studio-kit/drag.js +127 -0
- package/dist/studio-kit/gesture.d.ts +29 -0
- package/dist/studio-kit/gesture.js +87 -0
- package/dist/studio-kit/index.d.ts +29 -0
- package/dist/studio-kit/index.js +12 -0
- package/dist/studio-kit/inspector.d.ts +22 -0
- package/dist/studio-kit/inspector.js +119 -0
- package/dist/studio-kit/overlay.d.ts +29 -0
- package/dist/studio-kit/overlay.js +182 -0
- package/dist/studio-kit/selection.d.ts +30 -0
- package/dist/studio-kit/selection.js +106 -0
- package/dist/studio-kit/tree.d.ts +9 -0
- package/dist/studio-kit/tree.js +82 -0
- package/dist/studio-kit/viewport.d.ts +29 -0
- package/dist/studio-kit/viewport.js +153 -0
- package/dist/studio-layers.d.ts +12 -0
- package/dist/studio-layers.js +218 -0
- package/dist/studio.d.ts +31 -0
- package/dist/studio.js +1608 -0
- package/dist/styles.css +1573 -123
- package/dist/thumbnail-cache.d.ts +2 -0
- package/dist/thumbnail-cache.js +10 -1
- package/dist/workspace-measure.js +6 -4
- package/dist/workspace-parts.js +20 -0
- package/dist/workspace-shell.js +25 -15
- package/dist/workspace-theme.js +3 -0
- package/dist/workspace.d.ts +114 -10
- package/dist/workspace.js +223 -49
- package/package.json +30 -19
package/dist/design-options.js
CHANGED
|
@@ -62,7 +62,7 @@ const ENUMS = Object.freeze({
|
|
|
62
62
|
contentAlignment: ["left", "center", "right"],
|
|
63
63
|
contentDirection: ["horizontal", "vertical"],
|
|
64
64
|
chartPrimary: ["none", "top", "bottom", "left", "right"],
|
|
65
|
-
listBullet: ["character", "image"],
|
|
65
|
+
listBullet: ["character", "image", "none"],
|
|
66
66
|
imageFit: ["cover", "contain", "stretch"],
|
|
67
67
|
});
|
|
68
68
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@openpresentation/opf-editor/exact-source`: the writer the editor uses to put a changed document back into the JSON text it was read from,
|
|
3
|
+
* keeping the author's bytes. Only the tokens of values that changed are rewritten; indentation, key order, escapes (`"\/"`, `"é"`), line
|
|
4
|
+
* endings and every unrelated value stay byte for byte. A memory of the texts it has seen brings an authored spelling back when a value returns
|
|
5
|
+
* (Escape, Undo, Redo) instead of re-serializing it.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Texts longer than this many characters are not remembered (edits still work; a returning value is then written normally). */
|
|
9
|
+
export declare const MAX_EXACT_SOURCE_LENGTH: number;
|
|
10
|
+
|
|
11
|
+
/** The memory of exact source texts, keyed by the parsed value. Give each document (each editor, each file) its own. */
|
|
12
|
+
export interface SourceMemory {
|
|
13
|
+
/** True when the last source given was too long to remember (see `MAX_EXACT_SOURCE_LENGTH`): tell the author a returning value may be respelled. */
|
|
14
|
+
limited: boolean;
|
|
15
|
+
readonly limit: number;
|
|
16
|
+
readonly maxBytes: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A new memory: at most `limit` texts (default 64) and `maxBytes` characters in total (default 8,000,000), the oldest evicted first. */
|
|
20
|
+
export declare function createSourceMemory(limit?: number, maxBytes?: number): SourceMemory;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Write `document` into `source` (JSON text) and return the new text. Only the affected values change; everything else is kept byte for byte.
|
|
24
|
+
* Throws when `source` is not valid JSON. The result always parses to `document`: when an edit in place cannot achieve that (the source
|
|
25
|
+
* repeats a key, say), the whole document is written as JSON indented like `source`. Without `memory` a memory shared by the module is used.
|
|
26
|
+
*/
|
|
27
|
+
export declare function updateJsonSource(source: string, document: unknown, memory?: SourceMemory): string;
|
|
28
|
+
|
|
29
|
+
/** The first object key that JSON text repeats (which `JSON.parse` silently collapses to the last), as `{ key, path }`; null when there is none. */
|
|
30
|
+
export declare function findDuplicateKey(source: string): { key: string; path: (string | number)[] } | null;
|
package/dist/font-gate.js
CHANGED
|
@@ -30,8 +30,27 @@ const GATE = Symbol("opf-editor.fontGate");
|
|
|
30
30
|
export function fontGate(fonts) {
|
|
31
31
|
if (!fonts || typeof fonts !== "object") return undefined;
|
|
32
32
|
if (fonts[GATE]) return fonts;
|
|
33
|
+
// CE-20: "nothing is pending" for one document object and the same options stays true (faces are never unloaded), so it is remembered:
|
|
34
|
+
// the canvas, the workspace and the thumbnails ask about the same frozen snapshot several times per edit, and on a long deck each
|
|
35
|
+
// answer composes every slide.
|
|
36
|
+
const settled = new WeakMap();
|
|
37
|
+
const sameOptions = (a = {}, b = {}) => {
|
|
38
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
39
|
+
for (const key of keys) {
|
|
40
|
+
const x = a[key], y = b[key];
|
|
41
|
+
if (x === y) continue;
|
|
42
|
+
if (Array.isArray(x) && Array.isArray(y) && x.length === y.length && x.every((entry, at) => entry === y[at])) continue;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
};
|
|
33
47
|
const pending = (presentation, renderOptions) => {
|
|
34
|
-
|
|
48
|
+
const known = presentation && typeof presentation === "object" ? settled.get(presentation) : undefined;
|
|
49
|
+
if (known?.some((options) => sameOptions(options, renderOptions))) return [];
|
|
50
|
+
let found;
|
|
51
|
+
try { found = fonts.pending?.(presentation, renderOptions) ?? []; } catch { return []; /* the renderer reports the document itself when it renders */ }
|
|
52
|
+
if (!found.length && presentation && typeof presentation === "object" && Object.isFrozen(presentation)) settled.set(presentation, [...(known ?? []), renderOptions ?? {}].slice(-4));
|
|
53
|
+
return found;
|
|
35
54
|
};
|
|
36
55
|
const gate = {
|
|
37
56
|
[GATE]: true,
|
package/dist/json-editor.d.ts
CHANGED
|
@@ -29,6 +29,18 @@ export interface EditorProblem {
|
|
|
29
29
|
fixes?: EditorFix[];
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/** One host decoration of the code editor (CE-17): a mark over `from..to`, or the class on every line it touches. Offsets are source offsets. */
|
|
33
|
+
export interface EditorDecoration {
|
|
34
|
+
from: number;
|
|
35
|
+
to?: number;
|
|
36
|
+
className: string;
|
|
37
|
+
/** The browser tooltip of a mark. */
|
|
38
|
+
title?: string;
|
|
39
|
+
line?: boolean;
|
|
40
|
+
/** Kept as given, for `getDecorations`. */
|
|
41
|
+
data?: unknown;
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
/** All offsets address the original source in UTF-16, including CRLF pairs. */
|
|
33
45
|
export interface JsonEditorApi {
|
|
34
46
|
/** Say `message` in the editor's live region (screen readers). */
|
|
@@ -37,6 +49,8 @@ export interface JsonEditorApi {
|
|
|
37
49
|
applyEdits(edits: SourceEdit[], options?: { caret?: number; select?: [number, number]; userEvent?: string }): void;
|
|
38
50
|
getValue(): string;
|
|
39
51
|
getSelection(): [number, number];
|
|
52
|
+
/** The end of the main selection the caret is at (equal to both ends of `getSelection()` for a plain caret), in source offsets. */
|
|
53
|
+
getHead(): number;
|
|
40
54
|
hasFocus(): boolean;
|
|
41
55
|
focus(): void;
|
|
42
56
|
setSelection(from: number, to?: number): void;
|
|
@@ -115,6 +129,10 @@ export interface JsonCodeEditorOptions {
|
|
|
115
129
|
/** Cmd/Ctrl+Shift+M: move focus to the host's problems list. Answer true when it did. */
|
|
116
130
|
onFocusProblems?(): boolean;
|
|
117
131
|
onFocus?(): void;
|
|
132
|
+
/** The selection or the text changed (every change, also one the host made). */
|
|
133
|
+
onSelectionChange?(): void;
|
|
134
|
+
/** The text changed (also by `update`). */
|
|
135
|
+
onDocChange?(): void;
|
|
118
136
|
onHoverField?(path: string | null): void;
|
|
119
137
|
onError?(error: unknown): void;
|
|
120
138
|
}
|
|
@@ -153,6 +171,21 @@ export interface JsonCodeEditor {
|
|
|
153
171
|
resetHistory(): void;
|
|
154
172
|
/** How many typed edit groups CodeMirror can still undo. */
|
|
155
173
|
undoDepth(): number;
|
|
174
|
+
/**
|
|
175
|
+
* CE-17: scroll the start of `from..to` (source offsets) into view without moving the selection or the focus. `align` is "center" (default),
|
|
176
|
+
* "start" or "nearest" (the least scrolling). `select` also selects the range and `focus` also focuses the editor. Folded text around the start is opened.
|
|
177
|
+
*/
|
|
178
|
+
reveal(from: number, to?: number, options?: { align?: "center" | "start" | "nearest"; select?: boolean; focus?: boolean }): void;
|
|
179
|
+
/**
|
|
180
|
+
* CE-17: draw decorations a host owns, kept apart from the editor's own. `key` names a set and a call replaces that set; an empty list
|
|
181
|
+
* removes it. Each item is a mark over `from..to` (source offsets), or with `line` the class on every line it touches. The decorations
|
|
182
|
+
* move with the text as it is edited.
|
|
183
|
+
*/
|
|
184
|
+
setDecorations(key: string, list: readonly EditorDecoration[]): void;
|
|
185
|
+
/** CE-17: remove the decoration set `key`, or every set when `key` is omitted. */
|
|
186
|
+
clearDecorations(key?: string): void;
|
|
187
|
+
/** CE-17: the decorations drawn now, at their current source offsets, in position order. A `line` decoration is one entry over all its lines. */
|
|
188
|
+
getDecorations(key?: string): (EditorDecoration & { key: string; to: number })[];
|
|
156
189
|
/** The CodeMirror `EditorView`. */
|
|
157
190
|
readonly view: import("@codemirror/view").EditorView;
|
|
158
191
|
openOptions(left?: number, top?: number): Promise<void>;
|
package/dist/json-editor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Annotation, ChangeSet, Compartment, EditorSelection, EditorState, Prec, StateEffect, StateField, Transaction, countColumn } from "@codemirror/state";
|
|
1
|
+
import { Annotation, ChangeSet, Compartment, EditorSelection, EditorState, Prec, RangeSet, StateEffect, StateField, Transaction, countColumn } from "@codemirror/state";
|
|
2
2
|
import { Decoration, EditorView, activateHover, closeHoverTooltips, crosshairCursor, drawSelection, hasHoverTooltips, hoverTooltip, keymap, lineNumbers, rectangularSelection } from "@codemirror/view";
|
|
3
3
|
import { defaultKeymap, deleteCharBackward, history, historyKeymap, indentLess, indentMore, insertNewlineAndIndent, isolateHistory, redo, selectAll, selectLine, selectParentSyntax, toggleTabFocusMode, undo, undoDepth } from "@codemirror/commands";
|
|
4
|
-
import { HighlightStyle, StreamLanguage, bracketMatching, codeFolding, indentOnInput, indentUnit, syntaxHighlighting } from "@codemirror/language";
|
|
4
|
+
import { HighlightStyle, StreamLanguage, bracketMatching, codeFolding, foldedRanges, indentOnInput, indentUnit, syntaxHighlighting, unfoldEffect } from "@codemirror/language";
|
|
5
5
|
import { json } from "@codemirror/lang-json";
|
|
6
6
|
import { yaml } from "@codemirror/lang-yaml";
|
|
7
7
|
import { acceptCompletion, autocompletion, closeBrackets, closeBracketsKeymap, closeCompletion, completionStatus, currentCompletions, deleteBracketPair, hasNextSnippetField, hasPrevSnippetField, moveCompletionSelection, nextSnippetField, prevSnippetField, startCompletion } from "@codemirror/autocomplete";
|
|
@@ -13,11 +13,13 @@ import {textInputMap} from './text-input.js';
|
|
|
13
13
|
import {detectIndent,formatJsonSource,whitespaceEdits} from './json-format.js';
|
|
14
14
|
import { redoPair, undoPair } from "./source-undo-groups.js";
|
|
15
15
|
import {a11yExtension,createAnnouncer} from './source-a11y.js';
|
|
16
|
+
import { checkCodeMirrorCopies } from './codemirror-guard.js';
|
|
16
17
|
import { forEachDiagnostic } from "@codemirror/lint";
|
|
17
18
|
import {createDiagnostics,editsTransaction} from './source-diagnostics.js';
|
|
18
19
|
import { createKeymap, domKeymap, strokeText } from './source-keymap.js';
|
|
19
20
|
import { fold, foldEverything, foldGutterExtension, foldLevel, foldRecursively, toggleFoldHere, unfold, unfoldEverything, unfoldRecursively } from './source-folding.js';
|
|
20
21
|
import { hangingIndent } from './source-wrap.js';
|
|
22
|
+
import { colorSchemeExtension } from './source-color-scheme.js';
|
|
21
23
|
import {languageDataFor,markdownBackspace,markdownEnter,markdownFolding,markdownTab,markdownToggleComment,markdownWrapSelection,yamlEnter} from './source-editing.js';
|
|
22
24
|
const external = Annotation.define();
|
|
23
25
|
const hoverField = StateEffect.define();
|
|
@@ -228,6 +230,23 @@ function sourceDecorations(state) {
|
|
|
228
230
|
for (const from of lines) ranges.push(Decoration.line({ class: "is-hover" }).range(from));
|
|
229
231
|
return Decoration.set(ranges, true);
|
|
230
232
|
}
|
|
233
|
+
// CE-17: decorations a host owns, kept apart from the editor's own (field highlights, problems): one set per key, mapped through every edit, so a
|
|
234
|
+
// mark stays on its text while the author types. `setHostDecorations.of({ key, set })` replaces the set of `key` (`set` null removes it).
|
|
235
|
+
const setHostDecorations = StateEffect.define();
|
|
236
|
+
const hostDecorations = StateField.define({
|
|
237
|
+
create: () => new Map(),
|
|
238
|
+
update(value, tr) {
|
|
239
|
+
let next = value;
|
|
240
|
+
if (tr.docChanged) next = new Map([...value].map(([key, set]) => [key, set.map(tr.changes)]));
|
|
241
|
+
for (const effect of tr.effects) {
|
|
242
|
+
if (!effect.is(setHostDecorations)) continue;
|
|
243
|
+
if (next === value) next = new Map(value);
|
|
244
|
+
if (effect.value.set) next.set(effect.value.key, effect.value.set); else next.delete(effect.value.key);
|
|
245
|
+
}
|
|
246
|
+
return next;
|
|
247
|
+
},
|
|
248
|
+
provide: (field) => EditorView.decorations.from(field, (value) => RangeSet.join([...value.values()])),
|
|
249
|
+
});
|
|
231
250
|
const fieldDecorations = StateField.define({
|
|
232
251
|
create: sourceDecorations,
|
|
233
252
|
update: (value, tr) => tr.docChanged || tr.effects.some((effect) => effect.is(hoverField)) ? sourceDecorations(tr.state) : value,
|
|
@@ -512,6 +531,8 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
512
531
|
const shortcutAttribute = (ids) => ids.flatMap((id) => km.shortcuts().find((row) => row.id === id)?.strokes.slice(0, 1) ?? []).map((stroke) => strokeText(stroke, km.platform).replace(/Cmd/g, "Meta").replace(/Ctrl/g, "Control")).join(" ");
|
|
513
532
|
const contentAttributes = (invalid = options.invalid, describedBy = options.describedBy) => EditorView.contentAttributes.of({
|
|
514
533
|
"aria-label": options.label,
|
|
534
|
+
// CE-20: the scroller around the text is a scrolling region; its text is focusable (axe scrollable-region-focusable).
|
|
535
|
+
tabindex: "0",
|
|
515
536
|
"aria-invalid": String(Boolean(invalid)),
|
|
516
537
|
"aria-describedby": describedBy ?? "",
|
|
517
538
|
"aria-keyshortcuts": shortcutAttribute(["suggest.trigger", "problem.next", "problem.previous", "problem.quickFix", "format.document", "nav.symbol"]),
|
|
@@ -520,7 +541,7 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
520
541
|
autocapitalize: "off",
|
|
521
542
|
autocorrect: "off"
|
|
522
543
|
});
|
|
523
|
-
const
|
|
544
|
+
const mountExtensions = [
|
|
524
545
|
// The problem menu's keys come before the list's: the two are never open together, and Enter must reach whichever is.
|
|
525
546
|
diagnostics.extension,
|
|
526
547
|
language.of(languageExtension(languageName, completionExtension)),
|
|
@@ -535,6 +556,8 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
535
556
|
indentOnInput(),
|
|
536
557
|
historyField.of(history()),
|
|
537
558
|
drawSelection(),
|
|
559
|
+
// The dark flag follows the surface's color scheme: CodeMirror paints the selection (and the tooltips and panels) by it.
|
|
560
|
+
colorSchemeExtension(parent),
|
|
538
561
|
// Multiple cursors, as in VS Code: Cmd/Ctrl+D, Cmd/Ctrl+Shift+L, Cmd/Ctrl+Alt+Up and Down, Alt+click, and Shift+Alt+drag for a column.
|
|
539
562
|
EditorState.allowMultipleSelections.of(true),
|
|
540
563
|
// Alt+click adds a cursor (VS Code's default); Cmd/Ctrl+click goes to the definition.
|
|
@@ -549,6 +572,7 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
549
572
|
highlightSelectionMatches(),
|
|
550
573
|
syntaxHighlighting(highlightStyle),
|
|
551
574
|
activeField,
|
|
575
|
+
hostDecorations,
|
|
552
576
|
attributes.of(contentAttributes()),
|
|
553
577
|
options.lineNumbers ? lineNumbers() : [],
|
|
554
578
|
EditorView.lineWrapping,
|
|
@@ -594,12 +618,18 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
594
618
|
if (update.docChanged && !update.transactions.every((tr) => tr.annotation(external))) options.onChange(update.state.field(jsonSource));
|
|
595
619
|
if (update.docChanged) scheduleSyntax();
|
|
596
620
|
if (update.selectionSet || update.docChanged) options.onSelectionChange?.();
|
|
621
|
+
if (update.docChanged) options.onDocChange?.();
|
|
597
622
|
if (update.docChanged && update.transactions.some((tr) => tr.isUserEvent("input.type"))) autoHint(update.view);
|
|
598
623
|
})
|
|
599
|
-
]
|
|
624
|
+
];
|
|
625
|
+
// Two copies of @codemirror/view break the tooltips, completions, problems and find without an error: say so once (codemirror-guard.js).
|
|
626
|
+
checkCodeMirrorCopies(mountExtensions, options.onError);
|
|
627
|
+
const view = new EditorView({ parent, state: EditorState.create({ doc: options.code, extensions: mountExtensions }) });
|
|
600
628
|
const api = {
|
|
601
629
|
getValue: () => view.state.field(jsonSource),
|
|
602
630
|
getSelection: () => {const map=textInputMap(api.getValue());return [map.toSource(view.state.selection.main.from),map.toSource(view.state.selection.main.to)];},
|
|
631
|
+
// The end of the selection the caret is at (the head), in source offsets: where a path under the cursor is read.
|
|
632
|
+
getHead: () => textInputMap(api.getValue()).toSource(view.state.selection.main.head),
|
|
603
633
|
hasFocus: () => view.hasFocus,
|
|
604
634
|
focus: () => view.focus(),
|
|
605
635
|
setSelection: (from, to=from) => {const map=textInputMap(api.getValue());view.dispatch({ selection: EditorSelection.single(map.toInput(from),map.toInput(to)), scrollIntoView: true });},
|
|
@@ -707,6 +737,63 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
707
737
|
/** How many typed edit groups CodeMirror can still undo. */
|
|
708
738
|
undoDepth: () => undoDepth(view.state),
|
|
709
739
|
setReadOnly(value){view.dispatch({ effects: editable.reconfigure(editableExtension(value)) });},
|
|
740
|
+
/**
|
|
741
|
+
* CE-17: scroll the start of `from..to` (source offsets) into view without moving the selection or the focus. `align`: "center" (default),
|
|
742
|
+
* "start" or "nearest" (the least scrolling). `select` also selects the range, `focus` also focuses the editor; folded text around the start is opened.
|
|
743
|
+
*/
|
|
744
|
+
reveal(from, to = from, { align = "center", select = false, focus = false } = {}) {
|
|
745
|
+
if (disposed) return;
|
|
746
|
+
const source = api.getValue(), map = textInputMap(source);
|
|
747
|
+
const a = map.toInput(Math.min(source.length, Math.max(0, from))), b = map.toInput(Math.min(source.length, Math.max(0, to)));
|
|
748
|
+
const effects = [];
|
|
749
|
+
foldedRanges(view.state).between(a, a, (start, end) => { effects.push(unfoldEffect.of({ from: start, to: end })); });
|
|
750
|
+
effects.push(EditorView.scrollIntoView(a, { y: align === "nearest" ? "nearest" : align === "start" ? "start" : "center" }));
|
|
751
|
+
view.dispatch({ effects, ...(select ? { selection: EditorSelection.single(a, b) } : {}) });
|
|
752
|
+
if (focus) view.focus();
|
|
753
|
+
},
|
|
754
|
+
/**
|
|
755
|
+
* CE-17: draw decorations a host owns, kept apart from the editor's own. `key` names a set (a call replaces that set); `list` is
|
|
756
|
+
* `{ from, to, className, title?, line?, data? }` in source offsets: a mark over the range, or with `line` the class on every line it touches.
|
|
757
|
+
* `data` is kept for `getDecorations`. An empty list removes the set.
|
|
758
|
+
*/
|
|
759
|
+
setDecorations(key, list) {
|
|
760
|
+
if (disposed) return;
|
|
761
|
+
const source = api.getValue(), map = textInputMap(source), ranges = [];
|
|
762
|
+
const at = (offset) => map.toInput(Math.min(source.length, Math.max(0, offset)));
|
|
763
|
+
(list ?? []).forEach((item, id) => {
|
|
764
|
+
const a = at(item.from), b = Math.max(a, at(item.to ?? item.from));
|
|
765
|
+
const spec = { opf: { id, className: item.className, title: item.title, line: Boolean(item.line), data: item.data } };
|
|
766
|
+
if (item.line) {
|
|
767
|
+
const last = view.state.doc.lineAt(b).number;
|
|
768
|
+
for (let number = view.state.doc.lineAt(a).number; number <= last; number++) ranges.push(Decoration.line({ class: item.className, ...spec }).range(view.state.doc.line(number).from));
|
|
769
|
+
} else if (b > a) {
|
|
770
|
+
ranges.push(Decoration.mark({ class: item.className, attributes: item.title ? { title: item.title } : undefined, ...spec }).range(a, b));
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
view.dispatch({ effects: setHostDecorations.of({ key, set: ranges.length ? Decoration.set(ranges, true) : null }) });
|
|
774
|
+
},
|
|
775
|
+
/** CE-17: remove the decoration set `key`, or every host set when `key` is omitted. */
|
|
776
|
+
clearDecorations(key) {
|
|
777
|
+
if (disposed) return;
|
|
778
|
+
const keys = key === undefined ? [...view.state.field(hostDecorations).keys()] : [key];
|
|
779
|
+
view.dispatch({ effects: keys.map((name) => setHostDecorations.of({ key: name, set: null })) });
|
|
780
|
+
},
|
|
781
|
+
/** CE-17: the decorations drawn now at their current source offsets, `{ key, from, to, className, title, line, data }`, in position order (a `line` one is one entry over all its lines). */
|
|
782
|
+
getDecorations(key) {
|
|
783
|
+
const map = textInputMap(api.getValue()), out = [], lines = new Map();
|
|
784
|
+
for (const [name, set] of view.state.field(hostDecorations)) {
|
|
785
|
+
if (key !== undefined && name !== key) continue;
|
|
786
|
+
set.between(0, view.state.doc.length, (from, to, value) => {
|
|
787
|
+
const info = value.spec.opf;
|
|
788
|
+
if (!info) return;
|
|
789
|
+
const entry = { key: name, from: map.toSource(from), to: map.toSource(info.line ? view.state.doc.lineAt(from).to : to), className: info.className, title: info.title, line: info.line, data: info.data };
|
|
790
|
+
if (!info.line) { out.push(entry); return; }
|
|
791
|
+
const id = `${name}\0${info.id}`, seen = lines.get(id);
|
|
792
|
+
if (seen) seen.to = Math.max(seen.to, entry.to); else { lines.set(id, entry); out.push(entry); }
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
return out.sort((a, b) => a.from - b.from || a.to - b.to);
|
|
796
|
+
},
|
|
710
797
|
/** The CodeMirror view, for hosts that scroll or measure it. */
|
|
711
798
|
view,
|
|
712
799
|
update(code, invalid=false, field=null, describedBy) {
|
package/dist/json-options.d.ts
CHANGED
|
@@ -9,7 +9,8 @@ export interface JsonFieldOption {
|
|
|
9
9
|
/** User-facing provenance; source remains stable for existing consumers. */
|
|
10
10
|
sourceLabel?: string;
|
|
11
11
|
sourceDescription?: string;
|
|
12
|
-
|
|
12
|
+
/** `Current layout`, the layout record's catalog group (for the Gallery: Openers and closers, Text, Groups of items, Numbers, Visuals or Mixed), or `Custom layouts`. */
|
|
13
|
+
layoutGroup?: string;
|
|
13
14
|
/** Region names, accepted kinds and flow from the v2 template. */
|
|
14
15
|
regions?: string;
|
|
15
16
|
}
|
package/dist/json-options.js
CHANGED
|
@@ -2,7 +2,7 @@ import { checkFormat, firstErrorMessage } from "./checks.js";
|
|
|
2
2
|
import { listCatalogRecords } from "./catalogs.js";
|
|
3
3
|
import { resolveSchema, schemaAtPath, schemaVariants } from "./schema.js";
|
|
4
4
|
import { findNodeAtOffset, getNodePath, parse as parseJsonc, parseTree } from "jsonc-parser";
|
|
5
|
-
import {
|
|
5
|
+
import { CUSTOM_LAYOUTS_GROUP, layoutGroupName, layoutGroupOrder, layoutRegionSummary } from "./layout-templates.js";
|
|
6
6
|
import { catalogKindAt } from "@openpresentation/opf/schema";
|
|
7
7
|
const sourceDetails = {
|
|
8
8
|
"Loaded catalog": ["Provided by app", "Records from a catalog this app registered; nothing is fetched from a URL. Saving embeds the record in the document."],
|
|
@@ -10,16 +10,17 @@ const sourceDetails = {
|
|
|
10
10
|
"Current value": ["Custom value", "The current value is not in the available catalogs."],
|
|
11
11
|
"Schema": ["OPF option", "Allowed by the OPF schema."],
|
|
12
12
|
};
|
|
13
|
-
const layoutGroups = ["Current layout", ...LAYOUT_INTENT_GROUPS.map(group => group.name), "Custom layouts"];
|
|
14
13
|
function describeOptions(choices, records, current, catalog) {
|
|
14
|
+
// A layout's group is its catalog record's (see layoutGroupName); the groups follow the catalog's own order.
|
|
15
|
+
const layoutGroups = catalog === "layouts" ? ["Current layout", ...layoutGroupOrder([...records.values()]), CUSTOM_LAYOUTS_GROUP] : [];
|
|
15
16
|
for (const option of choices) {
|
|
16
17
|
const [label, description] = sourceDetails[option.source];
|
|
17
18
|
option.sourceLabel = label;
|
|
18
19
|
option.sourceDescription = description;
|
|
19
20
|
if (catalog !== "layouts") continue;
|
|
20
|
-
const
|
|
21
|
-
option.regions = layoutRegionSummary(
|
|
22
|
-
option.layoutGroup = option.value === current ? "Current layout" :
|
|
21
|
+
const entry = records.get(String(option.value));
|
|
22
|
+
option.regions = layoutRegionSummary(entry?.record);
|
|
23
|
+
option.layoutGroup = option.value === current ? "Current layout" : entry ? layoutGroupName(entry) : CUSTOM_LAYOUTS_GROUP;
|
|
23
24
|
}
|
|
24
25
|
const priority = option => option.source === "Document catalog" ? 2 : option.source === "Loaded catalog" ? 1 : 0;
|
|
25
26
|
return choices.sort((a, b) => (catalog === "layouts" ? layoutGroups.indexOf(a.layoutGroup) - layoutGroups.indexOf(b.layoutGroup) : priority(b) - priority(a)) || a.label.localeCompare(b.label, "en", {numeric: true}) || String(a.value).localeCompare(String(b.value), "en"));
|
|
@@ -42,7 +43,7 @@ function fieldOptions(presentation, path, loadedCatalogs, { string, boolean }) {
|
|
|
42
43
|
const attempt = (document) => { try { return listCatalogRecords(document, catalog, { catalogs: hostCatalogs }); } catch { return undefined; } };
|
|
43
44
|
const available = attempt(presentation) ?? attempt({ ...presentation, catalogs: undefined }) ?? [];
|
|
44
45
|
for (const option of available) {
|
|
45
|
-
records.set(option.id, option
|
|
46
|
+
records.set(option.id, option);
|
|
46
47
|
options.set(option.id, { value: option.id, label: option.label, description: option.description,
|
|
47
48
|
source: option.origin === "document" ? "Document catalog" : "Loaded catalog" });
|
|
48
49
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// CE-18: Layout Studio's document adapter for the studio kit. The kit works on keys, boxes, containers, trial moves and gestures; this file
|
|
2
|
+
// says what those are for a layout record: a key is a node's index path (`[]` is the layout itself, `[1, 0]` is `regions[1].regions[0]`),
|
|
3
|
+
// boxes and dividers come from `layoutBoxes`, a trial move is `moveNodes` run without applying it, and a gesture is the model's. The slide
|
|
4
|
+
// adapter (CE-19) answers the same questions for a slide of a deck. No DOM.
|
|
5
|
+
import { LayoutOperationError, isHeading, isPrefix, isSplit, layoutBoxes, listAt, moveNodes, nodeAt, pathKey, samePath } from "./layout-tree.js";
|
|
6
|
+
import { boxesAt } from "./studio-kit/overlay.js";
|
|
7
|
+
import { clickTarget } from "./studio-kit/selection.js";
|
|
8
|
+
|
|
9
|
+
/** The kit selection's adapter over the record `getRecord()` returns: keys are index paths, the layout itself is never selected. */
|
|
10
|
+
export function layoutSelectionAdapter(getRecord) {
|
|
11
|
+
return {
|
|
12
|
+
equals: samePath,
|
|
13
|
+
copy: (path) => [...path],
|
|
14
|
+
exists: (path) => path.length > 0 && Boolean(nodeAt(getRecord(), path)),
|
|
15
|
+
parent: (path) => (path.length > 1 ? path.slice(0, -1) : null),
|
|
16
|
+
firstChild(path) {
|
|
17
|
+
const node = nodeAt(getRecord(), path);
|
|
18
|
+
return node && isSplit(node) && node.regions?.length ? [...path, 0] : null;
|
|
19
|
+
},
|
|
20
|
+
sibling(path, delta) {
|
|
21
|
+
const list = listAt(getRecord(), path.slice(0, -1));
|
|
22
|
+
const at = path[path.length - 1] + delta;
|
|
23
|
+
return !list || at < 0 || at >= list.length ? null : [...path.slice(0, -1), at];
|
|
24
|
+
},
|
|
25
|
+
first: () => (getRecord().regions.length ? [0] : null),
|
|
26
|
+
siblings(path) {
|
|
27
|
+
const list = listAt(getRecord(), path.slice(0, -1)) ?? [];
|
|
28
|
+
return list.map((_, index) => [...path.slice(0, -1), index]);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Where moving `paths` to `index` of the list at `listPath` would go: `{ ok, reason, noop }` (a refusal's reason is the operation's message). */
|
|
34
|
+
export function trialMove(record, paths, listPath, index) {
|
|
35
|
+
try {
|
|
36
|
+
const moved = moveNodes(record, paths, listPath, index);
|
|
37
|
+
return { ok: true, reason: "", noop: JSON.stringify(moved.record) === JSON.stringify(record) };
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (!(error instanceof LayoutOperationError)) throw error;
|
|
40
|
+
return { ok: false, reason: error.message, noop: false };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The studio document adapter of a layout model (`createLayoutModel`). `geometry()` is `layoutBoxes` of the current record (computed once per
|
|
46
|
+
* record); `nodes()` lists `{ key, path, kind, parent, depth, label, findings }`; `boxOf`, `containerOf`, `hitChain`, `target` and
|
|
47
|
+
* `trialMove` answer the canvas and the layers; `begin`, `preview`, `commit` and `cancel` are the model's gesture; `run(name, ...args)` runs
|
|
48
|
+
* one of the model's commands; `undo` and `redo` its history.
|
|
49
|
+
*/
|
|
50
|
+
export function createLayoutAdapter(model) {
|
|
51
|
+
let cached = null, cachedFor = null;
|
|
52
|
+
const geometry = () => {
|
|
53
|
+
if (cachedFor !== model.record) { cached = layoutBoxes(model.record); cachedFor = model.record; }
|
|
54
|
+
return cached;
|
|
55
|
+
};
|
|
56
|
+
const boxOf = (path) => geometry().nodes.find((box) => samePath(box.path, path));
|
|
57
|
+
return {
|
|
58
|
+
geometry,
|
|
59
|
+
nodes() {
|
|
60
|
+
return geometry().nodes.map((box) => ({
|
|
61
|
+
key: pathKey(box.path),
|
|
62
|
+
path: box.path,
|
|
63
|
+
kind: isSplit(box.entry) ? "split" : isHeading(box.entry) ? "heading" : "region",
|
|
64
|
+
parent: box.path.length > 1 ? pathKey(box.path.slice(0, -1)) : null,
|
|
65
|
+
depth: box.depth,
|
|
66
|
+
label: model.describe(box.path),
|
|
67
|
+
findings: model.findingsFor(box.path),
|
|
68
|
+
}));
|
|
69
|
+
},
|
|
70
|
+
boxOf,
|
|
71
|
+
/** The direction and the tracks of the layout (`[]`) or of a split, for its dividers and drop lines. */
|
|
72
|
+
containerOf(path) {
|
|
73
|
+
const entry = path.length ? nodeAt(model.record, path) : model.record;
|
|
74
|
+
if (!entry || (path.length && !isSplit(entry))) return undefined;
|
|
75
|
+
const direction = entry.direction === "row" ? "row" : "column";
|
|
76
|
+
const tracks = geometry().nodes.filter((box) => box.path.length === path.length + 1 && isPrefix(path, box.path)).map((box) => (direction === "row" ? { start: box.x, length: box.w } : { start: box.y, length: box.h }));
|
|
77
|
+
return { direction, tracks };
|
|
78
|
+
},
|
|
79
|
+
/** The boxes under a unit point, from the layout's outermost split to the deepest region (`skip(box)` leaves boxes out). */
|
|
80
|
+
hitChain: (point, skip) => boxesAt(geometry().nodes, point, { skip }),
|
|
81
|
+
/** The box a click on `chain` selects: the deepest, or one level into a selected split. */
|
|
82
|
+
target: (chain, { shift = false } = {}) => clickTarget(chain, model.primary, { shift, equals: samePath, keyOf: (box) => box.path, isContainer: (box) => isSplit(box.entry) }),
|
|
83
|
+
trialMove: (paths, listPath, index) => trialMove(model.record, paths, listPath, index),
|
|
84
|
+
begin: (label) => model.beginGesture(label),
|
|
85
|
+
preview: (record, message) => model.updateGesture(record, message),
|
|
86
|
+
commit: (message) => model.endGesture(message),
|
|
87
|
+
cancel: () => model.cancelGesture(),
|
|
88
|
+
run(name, ...args) {
|
|
89
|
+
if (!COMMANDS.has(name)) throw new TypeError(`Unknown layout command ${name}.`);
|
|
90
|
+
return model[name](...args);
|
|
91
|
+
},
|
|
92
|
+
undo: () => model.undo(),
|
|
93
|
+
redo: () => model.redo(),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const COMMANDS = new Set(["splitRegion", "insertRegion", "toggleHeading", "wrap", "unwrap", "duplicate", "remove", "move", "reorder", "moveInto", "setProperty", "setLayoutProperty", "removeDesignHint", "equalize", "nudge"]);
|