@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.
Files changed (52) hide show
  1. package/README.md +60 -6
  2. package/dist/canvas-variables.d.ts +15 -0
  3. package/dist/canvas-variables.js +52 -0
  4. package/dist/canvas.js +33 -5
  5. package/dist/design-controls.js +3 -2
  6. package/dist/design-options.js +1 -1
  7. package/dist/index.d.ts +37 -1
  8. package/dist/index.js +50 -3
  9. package/dist/json-editor.d.ts +33 -0
  10. package/dist/json-editor.js +83 -2
  11. package/dist/layout-adapter.js +96 -0
  12. package/dist/layout-canvas.js +122 -232
  13. package/dist/layout-editor-ui.js +46 -185
  14. package/dist/layout-inspector.js +36 -99
  15. package/dist/layout-layers.js +75 -92
  16. package/dist/layout-model.js +62 -103
  17. package/dist/react.d.ts +38 -0
  18. package/dist/react.js +127 -0
  19. package/dist/slide-retention.js +13 -0
  20. package/dist/source-color-scheme.js +60 -0
  21. package/dist/source-pane.d.ts +85 -0
  22. package/dist/source-pane.js +151 -9
  23. package/dist/studio-kit/commands.d.ts +38 -0
  24. package/dist/studio-kit/commands.js +148 -0
  25. package/dist/studio-kit/dom.d.ts +36 -0
  26. package/dist/studio-kit/dom.js +162 -0
  27. package/dist/studio-kit/drag.d.ts +26 -0
  28. package/dist/studio-kit/drag.js +127 -0
  29. package/dist/studio-kit/gesture.d.ts +29 -0
  30. package/dist/studio-kit/gesture.js +87 -0
  31. package/dist/studio-kit/index.d.ts +29 -0
  32. package/dist/studio-kit/index.js +12 -0
  33. package/dist/studio-kit/inspector.d.ts +22 -0
  34. package/dist/studio-kit/inspector.js +119 -0
  35. package/dist/studio-kit/overlay.d.ts +29 -0
  36. package/dist/studio-kit/overlay.js +182 -0
  37. package/dist/studio-kit/selection.d.ts +30 -0
  38. package/dist/studio-kit/selection.js +106 -0
  39. package/dist/studio-kit/tree.d.ts +9 -0
  40. package/dist/studio-kit/tree.js +82 -0
  41. package/dist/studio-kit/viewport.d.ts +29 -0
  42. package/dist/studio-kit/viewport.js +153 -0
  43. package/dist/styles.css +325 -7
  44. package/dist/workspace-layouts.d.ts +31 -0
  45. package/dist/workspace-layouts.js +84 -0
  46. package/dist/workspace-measure.js +35 -0
  47. package/dist/workspace-shell.js +96 -0
  48. package/dist/workspace-source.js +83 -0
  49. package/dist/workspace-theme.js +142 -0
  50. package/dist/workspace.d.ts +413 -0
  51. package/dist/workspace.js +1274 -0
  52. package/package.json +29 -11
@@ -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"]);