@dxos/react-ui-editor 0.8.3 → 0.8.4-main.1da679c
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/dist/lib/browser/chunk-22UMM3QJ.mjs +22 -0
- package/dist/lib/browser/chunk-22UMM3QJ.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +2502 -1384
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +72 -2
- package/dist/lib/browser/testing/index.mjs.map +4 -4
- package/dist/lib/browser/types/index.mjs +13 -0
- package/dist/lib/browser/types/index.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-YXYQPV6R.mjs +24 -0
- package/dist/lib/node-esm/chunk-YXYQPV6R.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +2504 -1387
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +72 -2
- package/dist/lib/node-esm/testing/index.mjs.map +4 -4
- package/dist/lib/node-esm/types/index.mjs +14 -0
- package/dist/lib/node-esm/types/index.mjs.map +7 -0
- package/dist/types/src/components/{Popover → CommandMenu}/CommandMenu.d.ts +10 -6
- package/dist/types/src/components/CommandMenu/CommandMenu.d.ts.map +1 -0
- package/dist/types/src/components/CommandMenu/index.d.ts +2 -0
- package/dist/types/src/components/CommandMenu/index.d.ts.map +1 -0
- package/dist/types/src/components/Editor/Editor.d.ts +19 -0
- package/dist/types/src/components/Editor/Editor.d.ts.map +1 -0
- package/dist/types/src/components/Editor/index.d.ts +2 -0
- package/dist/types/src/components/Editor/index.d.ts.map +1 -0
- package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/blocks.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/formatting.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/headings.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/image.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/lists.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/search.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/util.d.ts +6 -5
- package/dist/types/src/components/EditorToolbar/util.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/view-mode.d.ts +1 -1
- package/dist/types/src/components/EditorToolbar/view-mode.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +2 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/defaults.d.ts.map +1 -1
- package/dist/types/src/extensions/autocomplete.d.ts +20 -7
- package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts +36 -45
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/defs.d.ts +1 -1
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/autoscroll.d.ts +10 -0
- package/dist/types/src/extensions/autoscroll.d.ts.map +1 -0
- package/dist/types/src/extensions/blast.d.ts.map +1 -1
- package/dist/types/src/extensions/command/action.d.ts +1 -1
- package/dist/types/src/extensions/command/action.d.ts.map +1 -1
- package/dist/types/src/extensions/command/command-menu.d.ts +1 -1
- package/dist/types/src/extensions/command/command-menu.d.ts.map +1 -1
- package/dist/types/src/extensions/command/command.d.ts.map +1 -1
- package/dist/types/src/extensions/command/floating-menu.d.ts.map +1 -1
- package/dist/types/src/extensions/command/hint.d.ts +2 -7
- package/dist/types/src/extensions/command/hint.d.ts.map +1 -1
- package/dist/types/src/extensions/command/index.d.ts +1 -1
- package/dist/types/src/extensions/command/index.d.ts.map +1 -1
- package/dist/types/src/extensions/command/state.d.ts +1 -1
- package/dist/types/src/extensions/command/state.d.ts.map +1 -1
- package/dist/types/src/extensions/command/typeahead.d.ts +7 -2
- package/dist/types/src/extensions/command/typeahead.d.ts.map +1 -1
- package/dist/types/src/extensions/command/useCommandMenu.d.ts +3 -4
- package/dist/types/src/extensions/command/useCommandMenu.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +1 -1
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/dnd.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts +15 -1
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +2 -0
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/action.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts +8 -2
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/changes.d.ts +1 -1
- package/dist/types/src/extensions/markdown/changes.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts +9 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.test.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/image.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
- package/dist/types/src/extensions/modes.d.ts +0 -7
- package/dist/types/src/extensions/modes.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner/outliner.d.ts +1 -1
- package/dist/types/src/extensions/outliner/outliner.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner/selection.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner/tree.d.ts +2 -2
- package/dist/types/src/extensions/outliner/tree.d.ts.map +1 -1
- package/dist/types/src/extensions/preview/preview.d.ts +3 -6
- package/dist/types/src/extensions/preview/preview.d.ts.map +1 -1
- package/dist/types/src/extensions/tags/extended-markdown.d.ts +10 -0
- package/dist/types/src/extensions/tags/extended-markdown.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/extended-markdown.test.d.ts +2 -0
- package/dist/types/src/extensions/tags/extended-markdown.test.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/index.d.ts +4 -0
- package/dist/types/src/extensions/tags/index.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/streamer.d.ts +12 -0
- package/dist/types/src/extensions/tags/streamer.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/xml-tags.d.ts +71 -0
- package/dist/types/src/extensions/tags/xml-tags.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/xml-util.d.ts +10 -0
- package/dist/types/src/extensions/tags/xml-util.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/stories/Command.stories.d.ts +12 -4
- package/dist/types/src/stories/Command.stories.d.ts.map +1 -1
- package/dist/types/src/stories/CommandMenu.stories.d.ts +11 -4
- package/dist/types/src/stories/CommandMenu.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Comments.stories.d.ts +21 -9
- package/dist/types/src/stories/Comments.stories.d.ts.map +1 -1
- package/dist/types/src/stories/EditorToolbar.stories.d.ts +40 -3
- package/dist/types/src/stories/EditorToolbar.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Experimental.stories.d.ts +22 -12
- package/dist/types/src/stories/Experimental.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Markdown.stories.d.ts +32 -42
- package/dist/types/src/stories/Markdown.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Outliner.stories.d.ts +15 -20
- package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Preview.stories.d.ts +21 -6
- package/dist/types/src/stories/Preview.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Tags.stories.d.ts +17 -0
- package/dist/types/src/stories/Tags.stories.d.ts.map +1 -0
- package/dist/types/src/stories/TextEditor.stories.d.ts +38 -51
- package/dist/types/src/stories/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/stories/components/EditorStory.d.ts +3 -6
- package/dist/types/src/stories/components/EditorStory.d.ts.map +1 -1
- package/dist/types/src/styles/theme.d.ts.map +1 -1
- package/dist/types/src/testing/PreviewPopover.d.ts +20 -0
- package/dist/types/src/testing/PreviewPopover.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +1 -0
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/util.d.ts +1 -0
- package/dist/types/src/testing/util.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +28 -29
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/index.d.ts +2 -0
- package/dist/types/src/types/index.d.ts.map +1 -0
- package/dist/types/src/types/types.d.ts +21 -0
- package/dist/types/src/types/types.d.ts.map +1 -0
- package/dist/types/src/util/cursor.d.ts.map +1 -1
- package/dist/types/src/util/debug.d.ts +1 -1
- package/dist/types/src/util/debug.d.ts.map +1 -1
- package/dist/types/src/util/decorations.d.ts +4 -0
- package/dist/types/src/util/decorations.d.ts.map +1 -0
- package/dist/types/src/util/dom.d.ts +2 -12
- package/dist/types/src/util/dom.d.ts.map +1 -1
- package/dist/types/src/util/domino.d.ts +18 -0
- package/dist/types/src/util/domino.d.ts.map +1 -0
- package/dist/types/src/util/index.d.ts +2 -0
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/react.d.ts +1 -1
- package/dist/types/src/util/react.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +65 -55
- package/src/components/{Popover → CommandMenu}/CommandMenu.tsx +93 -26
- package/src/components/{Popover → CommandMenu}/index.ts +0 -2
- package/src/components/Editor/Editor.tsx +39 -0
- package/src/components/Editor/index.ts +5 -0
- package/src/components/EditorToolbar/EditorToolbar.tsx +40 -30
- package/src/components/EditorToolbar/blocks.ts +22 -25
- package/src/components/EditorToolbar/formatting.ts +22 -25
- package/src/components/EditorToolbar/headings.ts +10 -5
- package/src/components/EditorToolbar/image.ts +8 -4
- package/src/components/EditorToolbar/lists.ts +16 -19
- package/src/components/EditorToolbar/search.ts +8 -4
- package/src/components/EditorToolbar/util.ts +21 -9
- package/src/components/EditorToolbar/view-mode.ts +12 -7
- package/src/components/index.ts +2 -1
- package/src/defaults.ts +5 -2
- package/src/extensions/autocomplete.ts +204 -54
- package/src/extensions/automerge/automerge.stories.tsx +26 -17
- package/src/extensions/automerge/automerge.ts +4 -3
- package/src/extensions/automerge/defs.ts +1 -1
- package/src/extensions/automerge/sync.ts +1 -1
- package/src/extensions/automerge/update-automerge.ts +1 -1
- package/src/extensions/autoscroll.ts +157 -0
- package/src/extensions/awareness/awareness.ts +2 -2
- package/src/extensions/blast.ts +3 -16
- package/src/extensions/command/action.ts +1 -2
- package/src/extensions/command/command-menu.ts +7 -6
- package/src/extensions/command/command.ts +3 -3
- package/src/extensions/command/floating-menu.ts +10 -15
- package/src/extensions/command/hint.ts +2 -1
- package/src/extensions/command/index.ts +1 -1
- package/src/extensions/command/placeholder.ts +1 -1
- package/src/extensions/command/state.ts +4 -3
- package/src/extensions/command/typeahead.ts +28 -15
- package/src/extensions/command/useCommandMenu.ts +6 -9
- package/src/extensions/comments.ts +18 -13
- package/src/extensions/dnd.ts +1 -1
- package/src/extensions/factories.ts +22 -15
- package/src/extensions/folding.tsx +2 -2
- package/src/extensions/index.ts +2 -0
- package/src/extensions/markdown/action.ts +2 -1
- package/src/extensions/markdown/bundle.ts +25 -3
- package/src/extensions/markdown/changes.ts +1 -1
- package/src/extensions/markdown/decorate.ts +23 -14
- package/src/extensions/markdown/formatting.test.ts +7 -7
- package/src/extensions/markdown/formatting.ts +16 -14
- package/src/extensions/markdown/highlight.ts +1 -1
- package/src/extensions/markdown/image.ts +3 -4
- package/src/extensions/markdown/link.ts +3 -0
- package/src/extensions/markdown/table.ts +7 -1
- package/src/extensions/mention.ts +1 -1
- package/src/extensions/modes.ts +0 -9
- package/src/extensions/outliner/outliner.test.ts +3 -2
- package/src/extensions/outliner/outliner.ts +6 -5
- package/src/extensions/outliner/selection.ts +1 -1
- package/src/extensions/outliner/tree.test.ts +2 -1
- package/src/extensions/outliner/tree.ts +2 -2
- package/src/extensions/preview/preview.ts +59 -62
- package/src/extensions/tags/extended-markdown.test.ts +261 -0
- package/src/extensions/tags/extended-markdown.ts +78 -0
- package/src/extensions/tags/index.ts +7 -0
- package/src/extensions/tags/streamer.ts +244 -0
- package/src/extensions/tags/xml-tags.ts +335 -0
- package/src/extensions/tags/xml-util.ts +94 -0
- package/src/hooks/useTextEditor.ts +3 -15
- package/src/index.ts +1 -1
- package/src/stories/Command.stories.tsx +24 -31
- package/src/stories/CommandMenu.stories.tsx +29 -30
- package/src/stories/Comments.stories.tsx +10 -6
- package/src/stories/EditorToolbar.stories.tsx +10 -11
- package/src/stories/Experimental.stories.tsx +12 -8
- package/src/stories/Markdown.stories.tsx +21 -17
- package/src/stories/Outliner.stories.tsx +42 -30
- package/src/stories/Preview.stories.tsx +34 -33
- package/src/stories/Tags.stories.tsx +81 -0
- package/src/stories/TextEditor.stories.tsx +41 -35
- package/src/stories/components/EditorStory.tsx +9 -10
- package/src/styles/theme.ts +11 -10
- package/src/testing/PreviewPopover.tsx +78 -0
- package/src/testing/index.ts +1 -0
- package/src/testing/util.ts +2 -0
- package/src/translations.ts +5 -3
- package/src/types/index.ts +5 -0
- package/src/types/types.ts +32 -0
- package/src/util/cursor.ts +2 -1
- package/src/util/debug.ts +2 -2
- package/src/util/decorations.ts +21 -0
- package/src/util/dom.ts +5 -27
- package/src/util/domino.ts +51 -0
- package/src/util/index.ts +2 -0
- package/src/util/react.tsx +1 -1
- package/dist/lib/node/index.cjs +0 -7754
- package/dist/lib/node/index.cjs.map +0 -7
- package/dist/lib/node/meta.json +0 -1
- package/dist/lib/node/testing/index.cjs +0 -29
- package/dist/lib/node/testing/index.cjs.map +0 -7
- package/dist/types/src/components/Popover/CommandMenu.d.ts.map +0 -1
- package/dist/types/src/components/Popover/RefDropdownMenu.d.ts +0 -21
- package/dist/types/src/components/Popover/RefDropdownMenu.d.ts.map +0 -1
- package/dist/types/src/components/Popover/RefPopover.d.ts +0 -34
- package/dist/types/src/components/Popover/RefPopover.d.ts.map +0 -1
- package/dist/types/src/components/Popover/index.d.ts +0 -4
- package/dist/types/src/components/Popover/index.d.ts.map +0 -1
- package/dist/types/src/types.d.ts +0 -14
- package/dist/types/src/types.d.ts.map +0 -1
- package/src/components/Popover/RefDropdownMenu.tsx +0 -79
- package/src/components/Popover/RefPopover.tsx +0 -99
- package/src/types.ts +0 -23
@@ -0,0 +1,244 @@
|
|
1
|
+
//
|
2
|
+
// Copyright 2025 DXOS.org
|
3
|
+
//
|
4
|
+
|
5
|
+
import { type Extension, StateEffect, StateField } from '@codemirror/state';
|
6
|
+
import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate, WidgetType } from '@codemirror/view';
|
7
|
+
|
8
|
+
import { isNotFalsy } from '@dxos/util';
|
9
|
+
|
10
|
+
import { Domino } from '../../util';
|
11
|
+
|
12
|
+
const BLINK_RATE = 2_000;
|
13
|
+
|
14
|
+
export type StreamerOptions = {
|
15
|
+
cursor?: boolean;
|
16
|
+
// When true, uses defaults. When object, allows configuring removal delay.
|
17
|
+
fadeIn?: boolean | { removalDelay?: number };
|
18
|
+
};
|
19
|
+
|
20
|
+
/**
|
21
|
+
* Extension that adds a blinking cursor widget at the end of the document.
|
22
|
+
*/
|
23
|
+
export const streamer = (options: StreamerOptions = {}): Extension => {
|
24
|
+
return [
|
25
|
+
options.cursor && cursor(),
|
26
|
+
options.fadeIn && fadeIn(typeof options.fadeIn === 'object' ? options.fadeIn : {}),
|
27
|
+
].filter(isNotFalsy);
|
28
|
+
};
|
29
|
+
|
30
|
+
/**
|
31
|
+
* State field to manage the cursor widget decoration.
|
32
|
+
*/
|
33
|
+
const cursor = (): Extension => {
|
34
|
+
const hideCursor = StateEffect.define();
|
35
|
+
|
36
|
+
// State field to track if cursor should be shown.
|
37
|
+
const showCursor = StateField.define<boolean>({
|
38
|
+
create: () => true,
|
39
|
+
update: (value, tr) => {
|
40
|
+
for (const effect of tr.effects) {
|
41
|
+
if (effect.is(hideCursor)) {
|
42
|
+
return false;
|
43
|
+
}
|
44
|
+
}
|
45
|
+
if (tr.docChanged) {
|
46
|
+
return true;
|
47
|
+
}
|
48
|
+
|
49
|
+
return value;
|
50
|
+
},
|
51
|
+
});
|
52
|
+
|
53
|
+
// View plugin to manage timer and dispatch effects.
|
54
|
+
const timerPlugin = ViewPlugin.fromClass(
|
55
|
+
class {
|
56
|
+
timer: any;
|
57
|
+
|
58
|
+
constructor(private view: EditorView) {}
|
59
|
+
|
60
|
+
update(update: ViewUpdate) {
|
61
|
+
if (update.docChanged) {
|
62
|
+
clearTimeout(this.timer);
|
63
|
+
this.timer = setTimeout(() => {
|
64
|
+
this.view.dispatch({
|
65
|
+
effects: hideCursor.of(null),
|
66
|
+
});
|
67
|
+
}, BLINK_RATE);
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
destroy() {
|
72
|
+
clearTimeout(this.timer);
|
73
|
+
}
|
74
|
+
},
|
75
|
+
);
|
76
|
+
|
77
|
+
// Decoration field that uses the showCursor state.
|
78
|
+
const cursorDecoration = StateField.define<DecorationSet>({
|
79
|
+
create: () => Decoration.none,
|
80
|
+
update: (_decorations, tr) => {
|
81
|
+
const show = tr.state.field(showCursor);
|
82
|
+
if (!show) {
|
83
|
+
return Decoration.none;
|
84
|
+
}
|
85
|
+
|
86
|
+
// Always place cursor at the end of the document.
|
87
|
+
const endPos = tr.state.doc.length;
|
88
|
+
return Decoration.set([
|
89
|
+
Decoration.widget({
|
90
|
+
widget: new CursorWidget(),
|
91
|
+
side: 1, // Place after the position.
|
92
|
+
}).range(endPos),
|
93
|
+
]);
|
94
|
+
},
|
95
|
+
provide: (f) => EditorView.decorations.from(f),
|
96
|
+
});
|
97
|
+
|
98
|
+
return [showCursor, timerPlugin, cursorDecoration];
|
99
|
+
};
|
100
|
+
|
101
|
+
/**
|
102
|
+
* Widget class for the cursor at the end of the document.
|
103
|
+
* Half
|
104
|
+
*/
|
105
|
+
class CursorWidget extends WidgetType {
|
106
|
+
toDOM() {
|
107
|
+
return Domino.of('span')
|
108
|
+
.style({ opacity: '0.8' })
|
109
|
+
.child(Domino.of('span').text('\u258F').style({ animation: 'blink 2s infinite' }))
|
110
|
+
.build();
|
111
|
+
}
|
112
|
+
}
|
113
|
+
|
114
|
+
/**
|
115
|
+
* State field to detect and decorate appended text with a fade-in effect.
|
116
|
+
* Also schedules removal of the last appended decoration after a delay.
|
117
|
+
*/
|
118
|
+
const fadeIn = (options: { removalDelay?: number } = {}): Extension => {
|
119
|
+
const FADE_IN_DURATION = 1_000; // ms.
|
120
|
+
const DEFAULT_REMOVAL_DELAY = 5_000; // ms.
|
121
|
+
const removalDelay = options.removalDelay ?? DEFAULT_REMOVAL_DELAY;
|
122
|
+
|
123
|
+
// Effect to remove a specific decoration by range.
|
124
|
+
const removeDecoration = StateEffect.define<{ from: number; to: number }>();
|
125
|
+
|
126
|
+
// Decoration field that adds fade-in marks for appended content and responds to removal effects.
|
127
|
+
const fadeField = StateField.define<DecorationSet>({
|
128
|
+
create: () => Decoration.none,
|
129
|
+
update: (decorations, tr) => {
|
130
|
+
let next = decorations;
|
131
|
+
|
132
|
+
// Apply removals first, if any.
|
133
|
+
for (const effect of tr.effects) {
|
134
|
+
if (effect.is(removeDecoration)) {
|
135
|
+
const target = effect.value;
|
136
|
+
next = next.update({
|
137
|
+
filter: (from, to) => !(from === target.from && to === target.to),
|
138
|
+
});
|
139
|
+
}
|
140
|
+
}
|
141
|
+
|
142
|
+
if (!tr.docChanged) {
|
143
|
+
return next;
|
144
|
+
}
|
145
|
+
|
146
|
+
// Reset decorations if the entire content was replaced.
|
147
|
+
let isReset = tr.state.doc.length === 0;
|
148
|
+
if (!isReset) {
|
149
|
+
tr.changes.iterChanges((fromA, toA) => {
|
150
|
+
if (fromA === 0 && toA === tr.startState.doc.length) {
|
151
|
+
isReset = true;
|
152
|
+
}
|
153
|
+
});
|
154
|
+
}
|
155
|
+
if (isReset) {
|
156
|
+
return Decoration.none;
|
157
|
+
}
|
158
|
+
|
159
|
+
// Add fade-in decorations for appended content at the end only.
|
160
|
+
const add: any[] = [];
|
161
|
+
tr.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
|
162
|
+
// Don't fade in initial content.
|
163
|
+
if (fromB === 0 && toB === inserted.length) {
|
164
|
+
return;
|
165
|
+
}
|
166
|
+
// At-the-end append.
|
167
|
+
if (toA === tr.startState.doc.length && inserted.length > 0) {
|
168
|
+
add.push(Decoration.mark({ class: 'cm-fade-in' }).range(fromB, toB));
|
169
|
+
}
|
170
|
+
});
|
171
|
+
|
172
|
+
return next.update({ add });
|
173
|
+
},
|
174
|
+
provide: (f) => EditorView.decorations.from(f),
|
175
|
+
});
|
176
|
+
|
177
|
+
// View plugin that tracks appended ranges and schedules their removal.
|
178
|
+
const timerPlugin = ViewPlugin.fromClass(
|
179
|
+
class {
|
180
|
+
// Map a simple key "from-to" to timer id.
|
181
|
+
_timers = new Map<string, any>();
|
182
|
+
|
183
|
+
constructor(private view: EditorView) {}
|
184
|
+
|
185
|
+
update(update: ViewUpdate) {
|
186
|
+
if (!update.docChanged) {
|
187
|
+
return;
|
188
|
+
}
|
189
|
+
|
190
|
+
update.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
|
191
|
+
// Only consider appends at the end.
|
192
|
+
if (toA !== update.startState.doc.length || inserted.length === 0) {
|
193
|
+
return;
|
194
|
+
}
|
195
|
+
|
196
|
+
const key = `${fromB}-${toB}`;
|
197
|
+
// Clear any prior timer for this exact range.
|
198
|
+
if (this._timers.has(key)) {
|
199
|
+
clearTimeout(this._timers.get(key));
|
200
|
+
}
|
201
|
+
|
202
|
+
const totalDelay = FADE_IN_DURATION + removalDelay;
|
203
|
+
const id = setTimeout(() => {
|
204
|
+
this.view.dispatch({ effects: removeDecoration.of({ from: fromB, to: toB }) });
|
205
|
+
this._timers.delete(key);
|
206
|
+
}, totalDelay);
|
207
|
+
|
208
|
+
this._timers.set(key, id);
|
209
|
+
});
|
210
|
+
}
|
211
|
+
|
212
|
+
destroy() {
|
213
|
+
for (const id of this._timers.values()) {
|
214
|
+
clearTimeout(id);
|
215
|
+
}
|
216
|
+
this._timers.clear();
|
217
|
+
}
|
218
|
+
},
|
219
|
+
);
|
220
|
+
|
221
|
+
return [
|
222
|
+
fadeField,
|
223
|
+
timerPlugin,
|
224
|
+
EditorView.theme({
|
225
|
+
'.cm-line > span': {
|
226
|
+
opacity: '0.8',
|
227
|
+
},
|
228
|
+
'.cm-fade-in': {
|
229
|
+
animation: 'fade-in 3s ease-out forwards',
|
230
|
+
},
|
231
|
+
'@keyframes fade-in': {
|
232
|
+
'0%': {
|
233
|
+
opacity: '0',
|
234
|
+
},
|
235
|
+
'80%': {
|
236
|
+
opacity: '1',
|
237
|
+
},
|
238
|
+
'100%': {
|
239
|
+
opacity: '0.8',
|
240
|
+
},
|
241
|
+
},
|
242
|
+
}),
|
243
|
+
];
|
244
|
+
};
|
@@ -0,0 +1,335 @@
|
|
1
|
+
//
|
2
|
+
// Copyright 2025 DXOS.org
|
3
|
+
//
|
4
|
+
|
5
|
+
import { syntaxTree } from '@codemirror/language';
|
6
|
+
import { type EditorState, type Extension, RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
|
7
|
+
import { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemirror/view';
|
8
|
+
import { type ComponentType, type FC } from 'react';
|
9
|
+
|
10
|
+
import { invariant } from '@dxos/invariant';
|
11
|
+
import { log } from '@dxos/log';
|
12
|
+
|
13
|
+
import { decorationSetToArray } from '../../util';
|
14
|
+
|
15
|
+
import { nodeToJson } from './xml-util';
|
16
|
+
|
17
|
+
export type StateDispatch<T> = T | ((state: T) => T);
|
18
|
+
|
19
|
+
/**
|
20
|
+
* Manages widget state.
|
21
|
+
*/
|
22
|
+
export interface XmlWidgetStateManager {
|
23
|
+
updateWidget<T>(id: string, props: StateDispatch<T>): void;
|
24
|
+
}
|
25
|
+
|
26
|
+
export type XmlEventHandler<TEvent = any> = (event: TEvent) => void;
|
27
|
+
|
28
|
+
/**
|
29
|
+
* Widget component.
|
30
|
+
*/
|
31
|
+
export type XmlWidgetProps<TContext = any, TProps = any> = TProps & {
|
32
|
+
_tag: string;
|
33
|
+
context: TContext;
|
34
|
+
onEvent?: XmlEventHandler;
|
35
|
+
};
|
36
|
+
|
37
|
+
/**
|
38
|
+
* Factory for creating widgets.
|
39
|
+
*/
|
40
|
+
export type XmlWidgetFactory = (props: XmlWidgetProps, onEvent?: XmlEventHandler) => WidgetType | null;
|
41
|
+
|
42
|
+
/**
|
43
|
+
* Widget registry definition.
|
44
|
+
*/
|
45
|
+
export type XmlWidgetDef = {
|
46
|
+
block?: boolean;
|
47
|
+
/** Native widget. */
|
48
|
+
factory?: XmlWidgetFactory;
|
49
|
+
/** React widget. */
|
50
|
+
Component?: FC<XmlWidgetProps>;
|
51
|
+
};
|
52
|
+
|
53
|
+
export type XmlWidgetRegistry = Record<string, XmlWidgetDef>;
|
54
|
+
|
55
|
+
/**
|
56
|
+
* Update context.
|
57
|
+
*/
|
58
|
+
export const xmlTagContextEffect = StateEffect.define<any>();
|
59
|
+
|
60
|
+
/**
|
61
|
+
* Update widget.
|
62
|
+
*/
|
63
|
+
export const xmlTagUpdateEffect = StateEffect.define<{ id: string; value: any }>();
|
64
|
+
|
65
|
+
/**
|
66
|
+
* Reset all state.
|
67
|
+
*/
|
68
|
+
export const xmlTagResetEffect = StateEffect.define();
|
69
|
+
|
70
|
+
type WidgetDecorationSet = {
|
71
|
+
from: number;
|
72
|
+
decorations: DecorationSet;
|
73
|
+
};
|
74
|
+
|
75
|
+
type XmlWidgetStateMap = Record<string, any>;
|
76
|
+
|
77
|
+
export type XmlWidgetState = {
|
78
|
+
id: string;
|
79
|
+
props: any;
|
80
|
+
root: HTMLElement;
|
81
|
+
Component: ComponentType<any>;
|
82
|
+
};
|
83
|
+
|
84
|
+
export interface XmlWidgetNotifier {
|
85
|
+
mounted(widget: XmlWidgetState): void;
|
86
|
+
unmounted(id: string): void;
|
87
|
+
}
|
88
|
+
|
89
|
+
export type XmlTagsOptions = {
|
90
|
+
registry?: XmlWidgetRegistry;
|
91
|
+
/**
|
92
|
+
* Called when a widget is mounted or unmounted.
|
93
|
+
*/
|
94
|
+
setWidgets?: (widgets: XmlWidgetState[]) => void;
|
95
|
+
};
|
96
|
+
|
97
|
+
/**
|
98
|
+
* Extension that adds thread-related functionality including XML tag decorations.
|
99
|
+
*/
|
100
|
+
export const xmlTags = (options: XmlTagsOptions = {}): Extension => {
|
101
|
+
//
|
102
|
+
// Context state.
|
103
|
+
//
|
104
|
+
const contextState = StateField.define<any>({
|
105
|
+
create: () => undefined,
|
106
|
+
update: (value, tr) => {
|
107
|
+
for (const effect of tr.effects) {
|
108
|
+
if (effect.is(xmlTagContextEffect)) {
|
109
|
+
return effect.value;
|
110
|
+
}
|
111
|
+
}
|
112
|
+
|
113
|
+
return value;
|
114
|
+
},
|
115
|
+
});
|
116
|
+
|
117
|
+
//
|
118
|
+
// Active widgets.
|
119
|
+
//
|
120
|
+
const widgets = new Map<string, XmlWidgetState>();
|
121
|
+
const notifier = {
|
122
|
+
mounted: (widget: XmlWidgetState) => {
|
123
|
+
widgets.set(widget.id, widget);
|
124
|
+
options.setWidgets?.([...widgets.values()]);
|
125
|
+
},
|
126
|
+
unmounted: (id: string) => {
|
127
|
+
widgets.delete(id);
|
128
|
+
options.setWidgets?.([...widgets.values()]);
|
129
|
+
},
|
130
|
+
} satisfies XmlWidgetNotifier;
|
131
|
+
|
132
|
+
//
|
133
|
+
// Widget decorations.
|
134
|
+
//
|
135
|
+
const decorationsState = StateField.define<WidgetDecorationSet>({
|
136
|
+
create: (state) => {
|
137
|
+
return buildDecorations(
|
138
|
+
state,
|
139
|
+
0,
|
140
|
+
state.doc.length,
|
141
|
+
state.field(contextState),
|
142
|
+
state.field(widgetState),
|
143
|
+
options,
|
144
|
+
notifier,
|
145
|
+
);
|
146
|
+
},
|
147
|
+
update: ({ from, decorations }, tr) => {
|
148
|
+
for (const effect of tr.effects) {
|
149
|
+
if (effect.is(xmlTagResetEffect)) {
|
150
|
+
return { from: 0, decorations: Decoration.none };
|
151
|
+
}
|
152
|
+
}
|
153
|
+
|
154
|
+
if (tr.docChanged) {
|
155
|
+
const { state } = tr;
|
156
|
+
|
157
|
+
// Flag if the transaction has modified the head of the document.
|
158
|
+
// (i.e., any changes that touch before the current `from` position).
|
159
|
+
const reset = tr.changes.touchesRange(0, from);
|
160
|
+
|
161
|
+
// Since append-only, rebuild decorations from after the last widget.
|
162
|
+
const result = buildDecorations(
|
163
|
+
state,
|
164
|
+
reset ? 0 : from,
|
165
|
+
state.doc.length,
|
166
|
+
state.field(contextState),
|
167
|
+
state.field(widgetState),
|
168
|
+
options,
|
169
|
+
notifier,
|
170
|
+
);
|
171
|
+
|
172
|
+
// Merge with existing decorations.
|
173
|
+
return {
|
174
|
+
from: result.from,
|
175
|
+
decorations: decorations.update({ add: decorationSetToArray(result.decorations) }),
|
176
|
+
};
|
177
|
+
}
|
178
|
+
|
179
|
+
// No document changes: avoid mapping decorations through an empty ChangeSet,
|
180
|
+
// which can throw when the decoration set was created for a different base length.
|
181
|
+
// Simply return the existing decorations unchanged.
|
182
|
+
return { from, decorations };
|
183
|
+
},
|
184
|
+
provide: (field) => [
|
185
|
+
EditorView.decorations.from(field, (v) => v.decorations),
|
186
|
+
EditorView.atomicRanges.of((view) => view.state.field(field).decorations || Decoration.none),
|
187
|
+
],
|
188
|
+
});
|
189
|
+
|
190
|
+
//
|
191
|
+
// Widget state management.
|
192
|
+
//
|
193
|
+
const widgetState = StateField.define<XmlWidgetStateMap>({
|
194
|
+
create: () => ({}),
|
195
|
+
update: (map, tr) => {
|
196
|
+
for (const effect of tr.effects) {
|
197
|
+
if (effect.is(xmlTagResetEffect)) {
|
198
|
+
return {};
|
199
|
+
}
|
200
|
+
|
201
|
+
if (effect.is(xmlTagUpdateEffect)) {
|
202
|
+
// Update accumulated widget props by id.
|
203
|
+
const { id, value } = effect.value;
|
204
|
+
const state = typeof value === 'function' ? value(map[id]) : value;
|
205
|
+
|
206
|
+
// Find and render widget.
|
207
|
+
const { decorations } = tr.state.field(decorationsState);
|
208
|
+
for (const range of decorationSetToArray(decorations)) {
|
209
|
+
const deco = range.value;
|
210
|
+
const widget = deco?.spec?.widget;
|
211
|
+
if (widget && widget instanceof PlaceholderWidget && widget.id === effect.value.id && widget.root) {
|
212
|
+
const props = { ...widget.props, ...state };
|
213
|
+
notifier.mounted({ id: widget.id, props, root: widget.root, Component: widget.Component });
|
214
|
+
}
|
215
|
+
}
|
216
|
+
|
217
|
+
return { ...map, [id]: state };
|
218
|
+
}
|
219
|
+
}
|
220
|
+
|
221
|
+
return map;
|
222
|
+
},
|
223
|
+
});
|
224
|
+
|
225
|
+
return [contextState, decorationsState, widgetState];
|
226
|
+
};
|
227
|
+
|
228
|
+
/**
|
229
|
+
* Creates widget decorations for XML tags in the document using the syntax tree.
|
230
|
+
*/
|
231
|
+
const buildDecorations = (
|
232
|
+
state: EditorState,
|
233
|
+
from: number,
|
234
|
+
to: number,
|
235
|
+
context: any,
|
236
|
+
widgetState: XmlWidgetStateMap,
|
237
|
+
options: XmlTagsOptions,
|
238
|
+
notifier: XmlWidgetNotifier,
|
239
|
+
): WidgetDecorationSet => {
|
240
|
+
const builder = new RangeSetBuilder<Decoration>();
|
241
|
+
const tree = syntaxTree(state);
|
242
|
+
if (!tree || (tree.type.name === 'Program' && tree.length === 0)) {
|
243
|
+
return { from, decorations: Decoration.none };
|
244
|
+
}
|
245
|
+
|
246
|
+
tree.iterate({
|
247
|
+
from,
|
248
|
+
to,
|
249
|
+
enter: (node) => {
|
250
|
+
switch (node.type.name) {
|
251
|
+
// XML Element.
|
252
|
+
case 'Element': {
|
253
|
+
try {
|
254
|
+
if (options.registry) {
|
255
|
+
const props = nodeToJson(state, node.node);
|
256
|
+
if (props) {
|
257
|
+
const def = options.registry[props._tag];
|
258
|
+
if (def) {
|
259
|
+
const { block, factory, Component } = def;
|
260
|
+
const state = props.id ? widgetState[props.id] : undefined;
|
261
|
+
const args = { context, ...props, ...state };
|
262
|
+
const widget: WidgetType | undefined = factory
|
263
|
+
? factory(args)
|
264
|
+
: Component
|
265
|
+
? props.id && new PlaceholderWidget(props.id, Component, args, notifier)
|
266
|
+
: undefined;
|
267
|
+
|
268
|
+
if (widget) {
|
269
|
+
from = node.node.to;
|
270
|
+
builder.add(
|
271
|
+
node.node.from,
|
272
|
+
node.node.to,
|
273
|
+
Decoration.replace({
|
274
|
+
widget,
|
275
|
+
block,
|
276
|
+
atomic: true,
|
277
|
+
inclusive: true,
|
278
|
+
}),
|
279
|
+
);
|
280
|
+
}
|
281
|
+
}
|
282
|
+
}
|
283
|
+
}
|
284
|
+
} catch (err) {
|
285
|
+
log.catch(err);
|
286
|
+
}
|
287
|
+
|
288
|
+
return false; // Don't descend into children.
|
289
|
+
}
|
290
|
+
}
|
291
|
+
},
|
292
|
+
});
|
293
|
+
|
294
|
+
return { from, decorations: builder.finish() };
|
295
|
+
};
|
296
|
+
|
297
|
+
/**
|
298
|
+
* Placeholder for React widgets.
|
299
|
+
*/
|
300
|
+
class PlaceholderWidget<TProps = {}> extends WidgetType {
|
301
|
+
private _root: HTMLElement | null = null;
|
302
|
+
|
303
|
+
constructor(
|
304
|
+
public readonly id: string,
|
305
|
+
public readonly Component: FC<TProps>,
|
306
|
+
public readonly props: TProps,
|
307
|
+
private readonly notifier: XmlWidgetNotifier,
|
308
|
+
) {
|
309
|
+
super();
|
310
|
+
invariant(id);
|
311
|
+
}
|
312
|
+
|
313
|
+
get root() {
|
314
|
+
return this._root;
|
315
|
+
}
|
316
|
+
|
317
|
+
override eq(other: WidgetType): boolean {
|
318
|
+
return other instanceof PlaceholderWidget && this.id === other.id;
|
319
|
+
}
|
320
|
+
|
321
|
+
override ignoreEvent() {
|
322
|
+
return true;
|
323
|
+
}
|
324
|
+
|
325
|
+
override toDOM(_view: EditorView): HTMLElement {
|
326
|
+
this._root = document.createElement('span');
|
327
|
+
this.notifier.mounted({ id: this.id, props: this.props, root: this._root, Component: this.Component });
|
328
|
+
return this._root;
|
329
|
+
}
|
330
|
+
|
331
|
+
override destroy(_dom: HTMLElement): void {
|
332
|
+
this.notifier.unmounted(this.id);
|
333
|
+
this._root = null;
|
334
|
+
}
|
335
|
+
}
|
@@ -0,0 +1,94 @@
|
|
1
|
+
//
|
2
|
+
// Copyright 2025 DXOS.org
|
3
|
+
//
|
4
|
+
|
5
|
+
import { type EditorState } from '@codemirror/state';
|
6
|
+
import { type SyntaxNode } from '@lezer/common';
|
7
|
+
|
8
|
+
import { invariant } from '@dxos/invariant';
|
9
|
+
|
10
|
+
export type Tag = Record<string, any> & {
|
11
|
+
_tag: string;
|
12
|
+
};
|
13
|
+
|
14
|
+
/**
|
15
|
+
* Parse XML Element.
|
16
|
+
*/
|
17
|
+
export const nodeToJson = (state: EditorState, node: SyntaxNode): Tag | undefined => {
|
18
|
+
invariant(node.type.name === 'Element', 'Node is not an Element');
|
19
|
+
|
20
|
+
// Find the opening tag.
|
21
|
+
const openTag = node.node.getChild('OpenTag') || node.node.getChild('SelfClosingTag');
|
22
|
+
if (openTag) {
|
23
|
+
// Extract tag name.
|
24
|
+
const tagName = openTag.getChild('TagName');
|
25
|
+
if (!tagName) {
|
26
|
+
return;
|
27
|
+
}
|
28
|
+
|
29
|
+
const tag: Tag = {
|
30
|
+
_tag: state.doc.sliceString(tagName.from, tagName.to),
|
31
|
+
};
|
32
|
+
|
33
|
+
// Extract attributes.
|
34
|
+
let attributeNode = openTag.getChild('Attribute');
|
35
|
+
while (attributeNode) {
|
36
|
+
const attrName = attributeNode.getChild('AttributeName');
|
37
|
+
const attrValue = attributeNode.getChild('AttributeValue');
|
38
|
+
if (attrName) {
|
39
|
+
const attr = state.doc.sliceString(attrName.from, attrName.to);
|
40
|
+
|
41
|
+
// Default for attributes without values.
|
42
|
+
let value: string | boolean = true;
|
43
|
+
|
44
|
+
if (attrValue) {
|
45
|
+
const rawValue = state.doc.sliceString(attrValue.from, attrValue.to);
|
46
|
+
// Remove quotes if present.
|
47
|
+
if (
|
48
|
+
(rawValue.startsWith('"') && rawValue.endsWith('"')) ||
|
49
|
+
(rawValue.startsWith("'") && rawValue.endsWith("'"))
|
50
|
+
) {
|
51
|
+
value = rawValue.slice(1, -1);
|
52
|
+
} else {
|
53
|
+
value = rawValue;
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
tag[attr] = value;
|
58
|
+
}
|
59
|
+
|
60
|
+
// Get next sibling attribute.
|
61
|
+
attributeNode = attributeNode.nextSibling;
|
62
|
+
}
|
63
|
+
|
64
|
+
// Extract children for non-self-closing tags.
|
65
|
+
if (node.type.name === 'Element' && openTag.type.name !== 'SelfClosingTag') {
|
66
|
+
const children: any[] = [];
|
67
|
+
let child = node.node.firstChild;
|
68
|
+
|
69
|
+
while (child) {
|
70
|
+
// Skip the opening and closing tags.
|
71
|
+
if (child.type.name !== 'OpenTag' && child.type.name !== 'CloseTag') {
|
72
|
+
if (child.type.name === 'Text') {
|
73
|
+
const text = state.doc.sliceString(child.from, child.to).trim();
|
74
|
+
if (text) {
|
75
|
+
children.push(text);
|
76
|
+
}
|
77
|
+
} else if (child.type.name === 'Element') {
|
78
|
+
const data = nodeToJson(state, child);
|
79
|
+
if (data) {
|
80
|
+
children.push(data);
|
81
|
+
}
|
82
|
+
}
|
83
|
+
}
|
84
|
+
child = child.nextSibling;
|
85
|
+
}
|
86
|
+
|
87
|
+
if (children.length > 0) {
|
88
|
+
tag.children = children;
|
89
|
+
}
|
90
|
+
}
|
91
|
+
|
92
|
+
return tag;
|
93
|
+
}
|
94
|
+
};
|
@@ -4,7 +4,7 @@
|
|
4
4
|
|
5
5
|
import { EditorState, type EditorStateConfig, type Text } from '@codemirror/state';
|
6
6
|
import { EditorView } from '@codemirror/view';
|
7
|
-
import {
|
7
|
+
import { type TabsterTypes, useFocusableGroup } from '@fluentui/react-tabster';
|
8
8
|
import {
|
9
9
|
type DependencyList,
|
10
10
|
type KeyboardEventHandler,
|
@@ -17,9 +17,9 @@ import {
|
|
17
17
|
} from 'react';
|
18
18
|
|
19
19
|
import { log } from '@dxos/log';
|
20
|
-
import { getProviderValue, isNotFalsy
|
20
|
+
import { type MaybeProvider, getProviderValue, isNotFalsy } from '@dxos/util';
|
21
21
|
|
22
|
-
import { type EditorSelection,
|
22
|
+
import { type EditorSelection, createEditorStateTransaction, documentId, editorInputMode } from '../extensions';
|
23
23
|
import { debugDispatcher } from '../util';
|
24
24
|
|
25
25
|
let instanceCount = 0;
|
@@ -96,18 +96,6 @@ export const useTextEditor = (
|
|
96
96
|
EditorView.exceptionSink.of((err) => {
|
97
97
|
log.catch(err);
|
98
98
|
}),
|
99
|
-
// TODO(burdon): Factor out debug inspector.
|
100
|
-
// ViewPlugin.fromClass(
|
101
|
-
// class {
|
102
|
-
// constructor(_view: EditorView) {
|
103
|
-
// log('construct', { id });
|
104
|
-
// }
|
105
|
-
//
|
106
|
-
// destroy() {
|
107
|
-
// log('destroy', { id });
|
108
|
-
// }
|
109
|
-
// },
|
110
|
-
// ),
|
111
99
|
].filter(isNotFalsy),
|
112
100
|
});
|
113
101
|
|