@openpresentation/opf-editor 0.26.10 → 0.28.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 +60 -6
- package/dist/canvas-variables.d.ts +15 -0
- package/dist/canvas-variables.js +52 -0
- package/dist/canvas.js +33 -5
- package/dist/design-controls.js +3 -2
- package/dist/design-options.js +1 -1
- package/dist/index.d.ts +37 -1
- package/dist/index.js +50 -3
- package/dist/json-editor.d.ts +33 -0
- package/dist/json-editor.js +83 -2
- 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/react.d.ts +38 -0
- package/dist/react.js +127 -0
- package/dist/slide-retention.js +13 -0
- package/dist/source-color-scheme.js +60 -0
- package/dist/source-pane.d.ts +85 -0
- package/dist/source-pane.js +151 -9
- package/dist/studio-kit/commands.d.ts +38 -0
- package/dist/studio-kit/commands.js +148 -0
- package/dist/studio-kit/dom.d.ts +36 -0
- package/dist/studio-kit/dom.js +162 -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/styles.css +325 -7
- package/dist/workspace-layouts.d.ts +31 -0
- package/dist/workspace-layouts.js +84 -0
- package/dist/workspace-measure.js +35 -0
- package/dist/workspace-shell.js +96 -0
- package/dist/workspace-source.js +83 -0
- package/dist/workspace-theme.js +142 -0
- package/dist/workspace.d.ts +413 -0
- package/dist/workspace.js +1274 -0
- package/package.json +29 -11
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";
|
|
@@ -18,6 +18,7 @@ import {createDiagnostics,editsTransaction} from './source-diagnostics.js';
|
|
|
18
18
|
import { createKeymap, domKeymap, strokeText } from './source-keymap.js';
|
|
19
19
|
import { fold, foldEverything, foldGutterExtension, foldLevel, foldRecursively, toggleFoldHere, unfold, unfoldEverything, unfoldRecursively } from './source-folding.js';
|
|
20
20
|
import { hangingIndent } from './source-wrap.js';
|
|
21
|
+
import { colorSchemeExtension } from './source-color-scheme.js';
|
|
21
22
|
import {languageDataFor,markdownBackspace,markdownEnter,markdownFolding,markdownTab,markdownToggleComment,markdownWrapSelection,yamlEnter} from './source-editing.js';
|
|
22
23
|
const external = Annotation.define();
|
|
23
24
|
const hoverField = StateEffect.define();
|
|
@@ -228,6 +229,23 @@ function sourceDecorations(state) {
|
|
|
228
229
|
for (const from of lines) ranges.push(Decoration.line({ class: "is-hover" }).range(from));
|
|
229
230
|
return Decoration.set(ranges, true);
|
|
230
231
|
}
|
|
232
|
+
// 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
|
|
233
|
+
// mark stays on its text while the author types. `setHostDecorations.of({ key, set })` replaces the set of `key` (`set` null removes it).
|
|
234
|
+
const setHostDecorations = StateEffect.define();
|
|
235
|
+
const hostDecorations = StateField.define({
|
|
236
|
+
create: () => new Map(),
|
|
237
|
+
update(value, tr) {
|
|
238
|
+
let next = value;
|
|
239
|
+
if (tr.docChanged) next = new Map([...value].map(([key, set]) => [key, set.map(tr.changes)]));
|
|
240
|
+
for (const effect of tr.effects) {
|
|
241
|
+
if (!effect.is(setHostDecorations)) continue;
|
|
242
|
+
if (next === value) next = new Map(value);
|
|
243
|
+
if (effect.value.set) next.set(effect.value.key, effect.value.set); else next.delete(effect.value.key);
|
|
244
|
+
}
|
|
245
|
+
return next;
|
|
246
|
+
},
|
|
247
|
+
provide: (field) => EditorView.decorations.from(field, (value) => RangeSet.join([...value.values()])),
|
|
248
|
+
});
|
|
231
249
|
const fieldDecorations = StateField.define({
|
|
232
250
|
create: sourceDecorations,
|
|
233
251
|
update: (value, tr) => tr.docChanged || tr.effects.some((effect) => effect.is(hoverField)) ? sourceDecorations(tr.state) : value,
|
|
@@ -535,6 +553,8 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
535
553
|
indentOnInput(),
|
|
536
554
|
historyField.of(history()),
|
|
537
555
|
drawSelection(),
|
|
556
|
+
// The dark flag follows the surface's color scheme: CodeMirror paints the selection (and the tooltips and panels) by it.
|
|
557
|
+
colorSchemeExtension(parent),
|
|
538
558
|
// 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
559
|
EditorState.allowMultipleSelections.of(true),
|
|
540
560
|
// Alt+click adds a cursor (VS Code's default); Cmd/Ctrl+click goes to the definition.
|
|
@@ -549,6 +569,7 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
549
569
|
highlightSelectionMatches(),
|
|
550
570
|
syntaxHighlighting(highlightStyle),
|
|
551
571
|
activeField,
|
|
572
|
+
hostDecorations,
|
|
552
573
|
attributes.of(contentAttributes()),
|
|
553
574
|
options.lineNumbers ? lineNumbers() : [],
|
|
554
575
|
EditorView.lineWrapping,
|
|
@@ -594,12 +615,15 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
594
615
|
if (update.docChanged && !update.transactions.every((tr) => tr.annotation(external))) options.onChange(update.state.field(jsonSource));
|
|
595
616
|
if (update.docChanged) scheduleSyntax();
|
|
596
617
|
if (update.selectionSet || update.docChanged) options.onSelectionChange?.();
|
|
618
|
+
if (update.docChanged) options.onDocChange?.();
|
|
597
619
|
if (update.docChanged && update.transactions.some((tr) => tr.isUserEvent("input.type"))) autoHint(update.view);
|
|
598
620
|
})
|
|
599
621
|
] }) });
|
|
600
622
|
const api = {
|
|
601
623
|
getValue: () => view.state.field(jsonSource),
|
|
602
624
|
getSelection: () => {const map=textInputMap(api.getValue());return [map.toSource(view.state.selection.main.from),map.toSource(view.state.selection.main.to)];},
|
|
625
|
+
// The end of the selection the caret is at (the head), in source offsets: where a path under the cursor is read.
|
|
626
|
+
getHead: () => textInputMap(api.getValue()).toSource(view.state.selection.main.head),
|
|
603
627
|
hasFocus: () => view.hasFocus,
|
|
604
628
|
focus: () => view.focus(),
|
|
605
629
|
setSelection: (from, to=from) => {const map=textInputMap(api.getValue());view.dispatch({ selection: EditorSelection.single(map.toInput(from),map.toInput(to)), scrollIntoView: true });},
|
|
@@ -707,6 +731,63 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
707
731
|
/** How many typed edit groups CodeMirror can still undo. */
|
|
708
732
|
undoDepth: () => undoDepth(view.state),
|
|
709
733
|
setReadOnly(value){view.dispatch({ effects: editable.reconfigure(editableExtension(value)) });},
|
|
734
|
+
/**
|
|
735
|
+
* CE-17: scroll the start of `from..to` (source offsets) into view without moving the selection or the focus. `align`: "center" (default),
|
|
736
|
+
* "start" or "nearest" (the least scrolling). `select` also selects the range, `focus` also focuses the editor; folded text around the start is opened.
|
|
737
|
+
*/
|
|
738
|
+
reveal(from, to = from, { align = "center", select = false, focus = false } = {}) {
|
|
739
|
+
if (disposed) return;
|
|
740
|
+
const source = api.getValue(), map = textInputMap(source);
|
|
741
|
+
const a = map.toInput(Math.min(source.length, Math.max(0, from))), b = map.toInput(Math.min(source.length, Math.max(0, to)));
|
|
742
|
+
const effects = [];
|
|
743
|
+
foldedRanges(view.state).between(a, a, (start, end) => { effects.push(unfoldEffect.of({ from: start, to: end })); });
|
|
744
|
+
effects.push(EditorView.scrollIntoView(a, { y: align === "nearest" ? "nearest" : align === "start" ? "start" : "center" }));
|
|
745
|
+
view.dispatch({ effects, ...(select ? { selection: EditorSelection.single(a, b) } : {}) });
|
|
746
|
+
if (focus) view.focus();
|
|
747
|
+
},
|
|
748
|
+
/**
|
|
749
|
+
* 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
|
|
750
|
+
* `{ from, to, className, title?, line?, data? }` in source offsets: a mark over the range, or with `line` the class on every line it touches.
|
|
751
|
+
* `data` is kept for `getDecorations`. An empty list removes the set.
|
|
752
|
+
*/
|
|
753
|
+
setDecorations(key, list) {
|
|
754
|
+
if (disposed) return;
|
|
755
|
+
const source = api.getValue(), map = textInputMap(source), ranges = [];
|
|
756
|
+
const at = (offset) => map.toInput(Math.min(source.length, Math.max(0, offset)));
|
|
757
|
+
(list ?? []).forEach((item, id) => {
|
|
758
|
+
const a = at(item.from), b = Math.max(a, at(item.to ?? item.from));
|
|
759
|
+
const spec = { opf: { id, className: item.className, title: item.title, line: Boolean(item.line), data: item.data } };
|
|
760
|
+
if (item.line) {
|
|
761
|
+
const last = view.state.doc.lineAt(b).number;
|
|
762
|
+
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));
|
|
763
|
+
} else if (b > a) {
|
|
764
|
+
ranges.push(Decoration.mark({ class: item.className, attributes: item.title ? { title: item.title } : undefined, ...spec }).range(a, b));
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
view.dispatch({ effects: setHostDecorations.of({ key, set: ranges.length ? Decoration.set(ranges, true) : null }) });
|
|
768
|
+
},
|
|
769
|
+
/** CE-17: remove the decoration set `key`, or every host set when `key` is omitted. */
|
|
770
|
+
clearDecorations(key) {
|
|
771
|
+
if (disposed) return;
|
|
772
|
+
const keys = key === undefined ? [...view.state.field(hostDecorations).keys()] : [key];
|
|
773
|
+
view.dispatch({ effects: keys.map((name) => setHostDecorations.of({ key: name, set: null })) });
|
|
774
|
+
},
|
|
775
|
+
/** 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). */
|
|
776
|
+
getDecorations(key) {
|
|
777
|
+
const map = textInputMap(api.getValue()), out = [], lines = new Map();
|
|
778
|
+
for (const [name, set] of view.state.field(hostDecorations)) {
|
|
779
|
+
if (key !== undefined && name !== key) continue;
|
|
780
|
+
set.between(0, view.state.doc.length, (from, to, value) => {
|
|
781
|
+
const info = value.spec.opf;
|
|
782
|
+
if (!info) return;
|
|
783
|
+
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 };
|
|
784
|
+
if (!info.line) { out.push(entry); return; }
|
|
785
|
+
const id = `${name}\0${info.id}`, seen = lines.get(id);
|
|
786
|
+
if (seen) seen.to = Math.max(seen.to, entry.to); else { lines.set(id, entry); out.push(entry); }
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
return out.sort((a, b) => a.from - b.from || a.to - b.to);
|
|
790
|
+
},
|
|
710
791
|
/** The CodeMirror view, for hosts that scroll or measure it. */
|
|
711
792
|
view,
|
|
712
793
|
update(code, invalid=false, field=null, describedBy) {
|
|
@@ -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"]);
|