@dxos/react-ui-editor 0.8.4-main.406dc2a → 0.8.4-main.548089c
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/index.mjs +1379 -1139
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +1379 -1139
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/Editor/Editor.stories.d.ts +0 -3
- package/dist/types/src/components/Editor/Editor.stories.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts +17 -2
- package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/headings.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/util.d.ts +5 -19
- package/dist/types/src/components/EditorToolbar/util.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/sync.d.ts +1 -1
- package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/autoscroll.d.ts +14 -4
- package/dist/types/src/extensions/autoscroll.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness/awareness-provider.d.ts +1 -1
- package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -1
- package/dist/types/src/extensions/blocks.d.ts +2 -0
- package/dist/types/src/extensions/blocks.d.ts.map +1 -0
- package/dist/types/src/extensions/bookmarks.d.ts +12 -0
- package/dist/types/src/extensions/bookmarks.d.ts.map +1 -0
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts +4 -4
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/extensions/folding.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +4 -0
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/listener.d.ts +8 -6
- package/dist/types/src/extensions/listener.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts +1 -2
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/popover/PopoverMenuProvider.d.ts +1 -1
- package/dist/types/src/extensions/popover/PopoverMenuProvider.d.ts.map +1 -1
- package/dist/types/src/extensions/popover/popover.d.ts.map +1 -1
- package/dist/types/src/extensions/preview/preview.d.ts +6 -2
- package/dist/types/src/extensions/preview/preview.d.ts.map +1 -1
- package/dist/types/src/extensions/replacer.d.ts +21 -0
- package/dist/types/src/extensions/replacer.d.ts.map +1 -0
- package/dist/types/src/extensions/replacer.test.d.ts +2 -0
- package/dist/types/src/extensions/replacer.test.d.ts.map +1 -0
- package/dist/types/src/extensions/scrolling.d.ts +78 -0
- package/dist/types/src/extensions/scrolling.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/xml-tags.d.ts +41 -16
- package/dist/types/src/extensions/tags/xml-tags.d.ts.map +1 -1
- package/dist/types/src/stories/CommandDialog.stories.d.ts.map +1 -1
- package/dist/types/src/stories/EditorToolbar.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Popover.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Preview.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Tags.stories.d.ts.map +1 -1
- package/dist/types/src/stories/components/EditorStory.d.ts.map +1 -1
- package/dist/types/src/stories/components/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +41 -38
- package/src/components/Editor/Editor.stories.tsx +4 -7
- package/src/components/EditorToolbar/EditorToolbar.tsx +90 -90
- package/src/components/EditorToolbar/headings.ts +6 -4
- package/src/components/EditorToolbar/util.ts +4 -20
- package/src/extensions/autocomplete/autocomplete.ts +5 -5
- package/src/extensions/automerge/automerge.stories.tsx +1 -1
- package/src/extensions/automerge/automerge.ts +1 -1
- package/src/extensions/automerge/cursor.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 +74 -68
- package/src/extensions/awareness/awareness-provider.ts +2 -2
- package/src/extensions/blocks.ts +131 -0
- package/src/extensions/bookmarks.ts +75 -0
- package/src/extensions/comments.ts +2 -1
- package/src/extensions/factories.ts +6 -4
- package/src/extensions/folding.tsx +1 -2
- package/src/extensions/index.ts +4 -0
- package/src/extensions/listener.ts +14 -20
- package/src/extensions/markdown/bundle.ts +12 -2
- package/src/extensions/markdown/decorate.ts +8 -8
- package/src/extensions/markdown/formatting.ts +8 -8
- package/src/extensions/markdown/highlight.ts +1 -1
- package/src/extensions/markdown/image.ts +2 -2
- package/src/extensions/markdown/table.ts +6 -6
- package/src/extensions/popover/PopoverMenuProvider.tsx +2 -3
- package/src/extensions/popover/popover.ts +0 -4
- package/src/extensions/preview/preview.ts +14 -9
- package/src/extensions/replacer.test.ts +75 -0
- package/src/extensions/replacer.ts +93 -0
- package/src/extensions/scrolling.ts +189 -0
- package/src/extensions/selection.ts +1 -1
- package/src/extensions/tags/extended-markdown.test.ts +2 -1
- package/src/extensions/tags/xml-tags.ts +310 -203
- package/src/extensions/typewriter.ts +1 -1
- package/src/stories/CommandDialog.stories.tsx +9 -4
- package/src/stories/Comments.stories.tsx +1 -1
- package/src/stories/EditorToolbar.stories.tsx +4 -5
- package/src/stories/Popover.stories.tsx +4 -6
- package/src/stories/Preview.stories.tsx +15 -8
- package/src/stories/Tags.stories.tsx +19 -5
- package/src/stories/TextEditor.stories.tsx +2 -2
- package/src/stories/components/EditorStory.tsx +3 -3
- package/src/stories/components/util.tsx +39 -6
- package/src/styles/markdown.ts +1 -1
- package/src/styles/theme.ts +1 -1
|
@@ -3,24 +3,34 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { syntaxTree } from '@codemirror/language';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
StateEffect,
|
|
11
|
-
StateField,
|
|
12
|
-
Transaction,
|
|
13
|
-
} from '@codemirror/state';
|
|
14
|
-
import { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemirror/view';
|
|
6
|
+
import { Prec } from '@codemirror/state';
|
|
7
|
+
import { type EditorState, type Extension, RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
|
|
8
|
+
import { keymap } from '@codemirror/view';
|
|
9
|
+
import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate, WidgetType } from '@codemirror/view';
|
|
15
10
|
import { type ComponentType, type FC } from 'react';
|
|
16
11
|
|
|
17
12
|
import { invariant } from '@dxos/invariant';
|
|
18
13
|
import { log } from '@dxos/log';
|
|
19
14
|
|
|
15
|
+
import { type Range } from '../../types';
|
|
20
16
|
import { decorationSetToArray } from '../../util';
|
|
17
|
+
import { scrollToLineEffect } from '../scrolling';
|
|
21
18
|
|
|
22
19
|
import { nodeToJson } from './xml-util';
|
|
23
20
|
|
|
21
|
+
/**
|
|
22
|
+
* StateEffect for navigating to previous bookmark.
|
|
23
|
+
*/
|
|
24
|
+
export const navigatePreviousEffect = StateEffect.define<void>();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* StateEffect for navigating to next bookmark.
|
|
28
|
+
*/
|
|
29
|
+
export const navigateNextEffect = StateEffect.define<void>();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Dispatch function for updating state.
|
|
33
|
+
*/
|
|
24
34
|
export type StateDispatch<T> = T | ((state: T) => T);
|
|
25
35
|
|
|
26
36
|
/**
|
|
@@ -35,9 +45,11 @@ export type XmlEventHandler<TEvent = any> = (event: TEvent) => void;
|
|
|
35
45
|
/**
|
|
36
46
|
* Widget component.
|
|
37
47
|
*/
|
|
38
|
-
export type XmlWidgetProps<
|
|
48
|
+
export type XmlWidgetProps<TProps = any, TContext = any> = TProps & {
|
|
39
49
|
_tag: string;
|
|
40
|
-
context
|
|
50
|
+
context?: TContext;
|
|
51
|
+
range?: { from: number; to: number };
|
|
52
|
+
view?: EditorView;
|
|
41
53
|
onEvent?: XmlEventHandler;
|
|
42
54
|
};
|
|
43
55
|
|
|
@@ -50,10 +62,13 @@ export type XmlWidgetFactory = (props: XmlWidgetProps, onEvent?: XmlEventHandler
|
|
|
50
62
|
* Widget registry definition.
|
|
51
63
|
*/
|
|
52
64
|
export type XmlWidgetDef = {
|
|
65
|
+
/** Block widget. */
|
|
53
66
|
block?: boolean;
|
|
54
|
-
|
|
67
|
+
|
|
68
|
+
/** Native widget (rendered inline). */
|
|
55
69
|
factory?: XmlWidgetFactory;
|
|
56
|
-
|
|
70
|
+
|
|
71
|
+
/** React widget (rendered in portals outside of the editor). */
|
|
57
72
|
Component?: FC<XmlWidgetProps>;
|
|
58
73
|
};
|
|
59
74
|
|
|
@@ -70,14 +85,14 @@ export const getXmlTextChild = (children: any[]): string | null => {
|
|
|
70
85
|
export const xmlTagContextEffect = StateEffect.define<any>();
|
|
71
86
|
|
|
72
87
|
/**
|
|
73
|
-
*
|
|
88
|
+
* Reset all state.
|
|
74
89
|
*/
|
|
75
|
-
export const
|
|
90
|
+
export const xmlTagResetEffect = StateEffect.define();
|
|
76
91
|
|
|
77
92
|
/**
|
|
78
|
-
*
|
|
93
|
+
* Update widget.
|
|
79
94
|
*/
|
|
80
|
-
export const
|
|
95
|
+
export const xmlTagUpdateEffect = StateEffect.define<{ id: string; value: any }>();
|
|
81
96
|
|
|
82
97
|
type WidgetDecorationSet = {
|
|
83
98
|
from: number;
|
|
@@ -88,9 +103,9 @@ type XmlWidgetStateMap = Record<string, any>;
|
|
|
88
103
|
|
|
89
104
|
export type XmlWidgetState = {
|
|
90
105
|
id: string;
|
|
91
|
-
props: any;
|
|
92
106
|
root: HTMLElement;
|
|
93
|
-
|
|
107
|
+
props: any;
|
|
108
|
+
Component: ComponentType<XmlWidgetProps>;
|
|
94
109
|
};
|
|
95
110
|
|
|
96
111
|
export interface XmlWidgetNotifier {
|
|
@@ -98,145 +113,267 @@ export interface XmlWidgetNotifier {
|
|
|
98
113
|
unmounted(id: string): void;
|
|
99
114
|
}
|
|
100
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Context state.
|
|
118
|
+
*/
|
|
119
|
+
const widgetContextStateField = StateField.define<any>({
|
|
120
|
+
create: () => undefined,
|
|
121
|
+
update: (value, tr) => {
|
|
122
|
+
for (const effect of tr.effects) {
|
|
123
|
+
if (effect.is(xmlTagContextEffect)) {
|
|
124
|
+
return effect.value;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return value;
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Widget state management.
|
|
134
|
+
*/
|
|
135
|
+
const widgetStateMapStateField = StateField.define<XmlWidgetStateMap>({
|
|
136
|
+
create: () => ({}),
|
|
137
|
+
update: (map, tr) => {
|
|
138
|
+
for (const effect of tr.effects) {
|
|
139
|
+
if (effect.is(xmlTagResetEffect)) {
|
|
140
|
+
return {};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (effect.is(xmlTagUpdateEffect)) {
|
|
144
|
+
// Update accumulated widget props by id.
|
|
145
|
+
const { id, value } = effect.value;
|
|
146
|
+
log('widget updated', { id, value });
|
|
147
|
+
const state = typeof value === 'function' ? value(map[id]) : value;
|
|
148
|
+
return { ...map, [id]: state };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return map;
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
101
156
|
export type XmlTagsOptions = {
|
|
157
|
+
/** Tag registry. */
|
|
102
158
|
registry?: XmlWidgetRegistry;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
*/
|
|
159
|
+
|
|
160
|
+
/** Called when widgets are mounted or unmounted. */
|
|
106
161
|
setWidgets?: (widgets: XmlWidgetState[]) => void;
|
|
162
|
+
|
|
163
|
+
/** Tags to bookmark. */
|
|
164
|
+
bookmarks?: string[];
|
|
107
165
|
};
|
|
108
166
|
|
|
109
167
|
/**
|
|
110
|
-
*
|
|
168
|
+
* Implements custom XML tags via CodeMirror-native Widgets and portaled React components.
|
|
169
|
+
*
|
|
170
|
+
* Basic mechanism:
|
|
171
|
+
* - Decorations are created from XML tags that matched the provided Widget registry.
|
|
172
|
+
* - Native widgets are rendered inline.
|
|
173
|
+
* - React widgets are rendered in portals outside of the editor via the PlaceholderWidget.
|
|
174
|
+
* - Widget state can be update via effects.
|
|
175
|
+
* - NOTE: Widget state may be updated BEFORE the widget is mounted.
|
|
111
176
|
*/
|
|
112
|
-
export const xmlTags = (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return value;
|
|
126
|
-
},
|
|
127
|
-
});
|
|
177
|
+
export const xmlTags = ({ registry, setWidgets, bookmarks }: XmlTagsOptions): Extension => {
|
|
178
|
+
const notifier = createWidgetMap(setWidgets);
|
|
179
|
+
const widgetDecorationsField = createWidgetDecorationsField(registry, notifier);
|
|
180
|
+
return [
|
|
181
|
+
widgetContextStateField,
|
|
182
|
+
widgetStateMapStateField,
|
|
183
|
+
widgetDecorationsField,
|
|
184
|
+
createWidgetUpdatePlugin(widgetDecorationsField, notifier),
|
|
185
|
+
createNavigationEffectPlugin(widgetDecorationsField, bookmarks),
|
|
186
|
+
bookmarks?.length ? Prec.highest(keyHandlers) : [],
|
|
187
|
+
];
|
|
188
|
+
};
|
|
128
189
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
190
|
+
/**
|
|
191
|
+
* Manages the collection of widgets.
|
|
192
|
+
*/
|
|
193
|
+
const createWidgetMap = (setWidgets?: (widgets: XmlWidgetState[]) => void): XmlWidgetNotifier => {
|
|
132
194
|
const widgets = new Map<string, XmlWidgetState>();
|
|
195
|
+
|
|
196
|
+
// TODO(burdon): Batch updates?
|
|
133
197
|
const notifier = {
|
|
134
|
-
mounted: (
|
|
135
|
-
|
|
136
|
-
|
|
198
|
+
mounted: (state: XmlWidgetState) => {
|
|
199
|
+
log('widget mounted', { id: state.id, tag: state.props._tag });
|
|
200
|
+
widgets.set(state.id, state);
|
|
201
|
+
setWidgets?.([...widgets.values()]);
|
|
137
202
|
},
|
|
138
203
|
unmounted: (id: string) => {
|
|
204
|
+
const state = widgets.get(id);
|
|
205
|
+
log('widget unmounted', { id, tag: state?.props._tag });
|
|
139
206
|
widgets.delete(id);
|
|
140
|
-
|
|
207
|
+
setWidgets?.([...widgets.values()]);
|
|
141
208
|
},
|
|
142
209
|
} satisfies XmlWidgetNotifier;
|
|
143
210
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
notifier,
|
|
157
|
-
);
|
|
211
|
+
return notifier;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Navigation keys.
|
|
216
|
+
*/
|
|
217
|
+
const keyHandlers = keymap.of([
|
|
218
|
+
{
|
|
219
|
+
key: 'Mod-ArrowUp',
|
|
220
|
+
run: (view) => {
|
|
221
|
+
view.dispatch({ effects: navigatePreviousEffect.of() });
|
|
222
|
+
return true;
|
|
158
223
|
},
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
key: 'Mod-ArrowDown',
|
|
227
|
+
run: (view) => {
|
|
228
|
+
view.dispatch({ effects: navigateNextEffect.of() });
|
|
229
|
+
return true;
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
]);
|
|
165
233
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
234
|
+
/**
|
|
235
|
+
* Effect processing plugin for navigation.
|
|
236
|
+
* Handles navigation up/down effects.
|
|
237
|
+
*/
|
|
238
|
+
const createNavigationEffectPlugin = (
|
|
239
|
+
widgetDecorationsField: StateField<WidgetDecorationSet>,
|
|
240
|
+
bookmarks?: string[],
|
|
241
|
+
) => {
|
|
242
|
+
return EditorView.updateListener.of((update) => {
|
|
243
|
+
update.transactions.forEach((transaction) => {
|
|
244
|
+
for (const effect of transaction.effects) {
|
|
245
|
+
if (effect.is(navigatePreviousEffect)) {
|
|
246
|
+
const view = update.view;
|
|
247
|
+
const cursorPos = view.state.doc.lineAt(view.state.selection.main.head).from;
|
|
248
|
+
let widget: { from: number; to: number; tag: string } | null = null;
|
|
249
|
+
const { decorations } = view.state.field(widgetDecorationsField);
|
|
250
|
+
for (const range of decorationSetToArray(decorations)) {
|
|
251
|
+
if (range.from < cursorPos) {
|
|
252
|
+
const tag = range.value.spec.tag;
|
|
253
|
+
if (bookmarks?.includes(tag)) {
|
|
254
|
+
if (!widget || range.from > widget.from) {
|
|
255
|
+
widget = { from: range.from, to: range.to, tag };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
172
260
|
|
|
173
|
-
|
|
174
|
-
|
|
261
|
+
const line = view.state.doc.lineAt(widget?.from ?? 0);
|
|
262
|
+
view.dispatch({
|
|
263
|
+
selection: { anchor: line.from, head: line.from },
|
|
264
|
+
effects: scrollToLineEffect.of({ line: line.number, options: { offset: -16 } }),
|
|
265
|
+
});
|
|
175
266
|
|
|
176
|
-
|
|
177
|
-
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
178
269
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
270
|
+
if (effect.is(navigateNextEffect)) {
|
|
271
|
+
const view = update.view;
|
|
272
|
+
const cursorPos = view.state.doc.lineAt(view.state.selection.main.head).to;
|
|
273
|
+
let widget: { from: number; to: number; tag: string } | null = null;
|
|
274
|
+
const { decorations } = view.state.field(widgetDecorationsField);
|
|
275
|
+
for (const range of decorationSetToArray(decorations)) {
|
|
276
|
+
if (range.from > cursorPos) {
|
|
277
|
+
const tag = range.value.spec.tag;
|
|
278
|
+
if (bookmarks?.includes(tag)) {
|
|
279
|
+
if (!widget || range.from < widget.from) {
|
|
280
|
+
widget = { from: range.from, to: range.to, tag };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
182
284
|
}
|
|
183
285
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
286
|
+
if (widget) {
|
|
287
|
+
const line = view.state.doc.lineAt(widget?.from);
|
|
288
|
+
view.dispatch({
|
|
289
|
+
selection: { anchor: line.to, head: line.to },
|
|
290
|
+
effects: scrollToLineEffect.of({ line: line.number, options: { offset: -16 } }),
|
|
291
|
+
});
|
|
292
|
+
} else {
|
|
293
|
+
const line = view.state.doc.lineAt(view.state.doc.length);
|
|
294
|
+
view.dispatch({
|
|
295
|
+
selection: { anchor: line.to, head: line.to },
|
|
296
|
+
effects: scrollToLineEffect.of({ line: line.number, options: { position: 'end' } }),
|
|
297
|
+
});
|
|
187
298
|
}
|
|
188
299
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Handles effect that updates widget state.
|
|
309
|
+
*/
|
|
310
|
+
const createWidgetUpdatePlugin = (
|
|
311
|
+
widgetDecorationsField: StateField<WidgetDecorationSet>,
|
|
312
|
+
notifier: XmlWidgetNotifier,
|
|
313
|
+
) =>
|
|
314
|
+
ViewPlugin.fromClass(
|
|
315
|
+
class {
|
|
316
|
+
update(update: ViewUpdate) {
|
|
317
|
+
const widgetStateMap = update.state.field(widgetStateMapStateField);
|
|
318
|
+
const { decorations } = update.state.field(widgetDecorationsField);
|
|
319
|
+
|
|
320
|
+
// Check for widget update effects and re-render widgets.
|
|
321
|
+
for (const effect of update.transactions.flatMap((tr) => tr.effects)) {
|
|
322
|
+
if (effect.is(xmlTagUpdateEffect)) {
|
|
323
|
+
const widgetState = widgetStateMap[effect.value.id];
|
|
324
|
+
|
|
325
|
+
// Find and render widget.
|
|
326
|
+
for (const range of decorationSetToArray(decorations)) {
|
|
327
|
+
const deco = range.value;
|
|
328
|
+
const widget = deco?.spec?.widget;
|
|
329
|
+
|
|
330
|
+
// NOTE: If the widget has not yet been mounted, then the root will be null.
|
|
331
|
+
if (widget && widget instanceof PlaceholderWidget && widget.id === effect.value.id && widget.root) {
|
|
332
|
+
const props = { ...widget.props, ...widgetState };
|
|
333
|
+
notifier.mounted({ id: widget.id, props, root: widget.root, Component: widget.Component });
|
|
334
|
+
}
|
|
201
335
|
}
|
|
202
336
|
}
|
|
203
337
|
}
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
);
|
|
204
341
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
342
|
+
/**
|
|
343
|
+
* Builds and maintains decorations for XML widgets.
|
|
344
|
+
* Must be a StateField because block decorations cannot be provided via ViewPlugin.
|
|
345
|
+
*/
|
|
346
|
+
const createWidgetDecorationsField = (registry: XmlWidgetRegistry = {}, notifier: XmlWidgetNotifier) =>
|
|
347
|
+
StateField.define<WidgetDecorationSet>({
|
|
348
|
+
create: (state) => {
|
|
349
|
+
return buildDecorations(state, { from: 0, to: state.doc.length }, registry, notifier);
|
|
350
|
+
},
|
|
351
|
+
update: ({ from, decorations }, tr) => {
|
|
352
|
+
// Check for reset effect.
|
|
353
|
+
for (const effect of tr.effects) {
|
|
354
|
+
if (effect.is(xmlTagResetEffect)) {
|
|
355
|
+
return { from: 0, decorations: Decoration.none };
|
|
356
|
+
}
|
|
210
357
|
}
|
|
211
358
|
|
|
212
359
|
if (tr.docChanged) {
|
|
213
360
|
const { state } = tr;
|
|
214
|
-
|
|
215
361
|
// Flag if the transaction has modified the head of the document.
|
|
216
|
-
// (i.e., any changes that touch before the current `from` position).
|
|
217
362
|
const reset = tr.changes.touchesRange(0, from);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
state,
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
state.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
// Merge with existing decorations.
|
|
231
|
-
return {
|
|
232
|
-
from: result.from,
|
|
233
|
-
decorations: decorations.update({ add: decorationSetToArray(result.decorations) }),
|
|
234
|
-
};
|
|
363
|
+
if (reset) {
|
|
364
|
+
log('document reset', { from, to: state.doc.length });
|
|
365
|
+
// Full rebuild from start.
|
|
366
|
+
return buildDecorations(state, { from: 0, to: state.doc.length }, registry, notifier);
|
|
367
|
+
} else {
|
|
368
|
+
// Append-only: rebuild decorations from after the last widget and merge with existing decorations.
|
|
369
|
+
const result = buildDecorations(state, { from, to: state.doc.length }, registry, notifier);
|
|
370
|
+
return {
|
|
371
|
+
from: result.from,
|
|
372
|
+
decorations: decorations.update({ add: decorationSetToArray(result.decorations) }),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
235
375
|
}
|
|
236
376
|
|
|
237
|
-
// No document changes: avoid mapping decorations through an empty ChangeSet,
|
|
238
|
-
// which can throw when the decoration set was created for a different base length.
|
|
239
|
-
// Simply return the existing decorations unchanged.
|
|
240
377
|
return { from, decorations };
|
|
241
378
|
},
|
|
242
379
|
provide: (field) => [
|
|
@@ -245,97 +382,66 @@ export const xmlTags = (options: XmlTagsOptions = {}): Extension => {
|
|
|
245
382
|
],
|
|
246
383
|
});
|
|
247
384
|
|
|
248
|
-
//
|
|
249
|
-
// Widget state management.
|
|
250
|
-
//
|
|
251
|
-
const widgetState = StateField.define<XmlWidgetStateMap>({
|
|
252
|
-
create: () => ({}),
|
|
253
|
-
update: (map, tr) => {
|
|
254
|
-
for (const effect of tr.effects) {
|
|
255
|
-
if (effect.is(xmlTagResetEffect)) {
|
|
256
|
-
return {};
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (effect.is(xmlTagUpdateEffect)) {
|
|
260
|
-
// Update accumulated widget props by id.
|
|
261
|
-
const { id, value } = effect.value;
|
|
262
|
-
const state = typeof value === 'function' ? value(map[id]) : value;
|
|
263
|
-
|
|
264
|
-
// Find and render widget.
|
|
265
|
-
const { decorations } = tr.state.field(decorationsState);
|
|
266
|
-
for (const range of decorationSetToArray(decorations)) {
|
|
267
|
-
const deco = range.value;
|
|
268
|
-
const widget = deco?.spec?.widget;
|
|
269
|
-
if (widget && widget instanceof PlaceholderWidget && widget.id === effect.value.id && widget.root) {
|
|
270
|
-
const props = { ...widget.props, ...state };
|
|
271
|
-
notifier.mounted({ id: widget.id, props, root: widget.root, Component: widget.Component });
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return { ...map, [id]: state };
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return map;
|
|
280
|
-
},
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
return [contextState, decorationsState, widgetState];
|
|
284
|
-
};
|
|
285
|
-
|
|
286
385
|
/**
|
|
287
386
|
* Creates widget decorations for XML tags in the document using the syntax tree.
|
|
288
387
|
*/
|
|
289
388
|
const buildDecorations = (
|
|
290
389
|
state: EditorState,
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
context: any,
|
|
294
|
-
widgetState: XmlWidgetStateMap,
|
|
295
|
-
options: XmlTagsOptions,
|
|
390
|
+
range: Range,
|
|
391
|
+
registry: XmlWidgetRegistry,
|
|
296
392
|
notifier: XmlWidgetNotifier,
|
|
297
393
|
): WidgetDecorationSet => {
|
|
394
|
+
const context = state.field(widgetContextStateField, false);
|
|
395
|
+
const widgetStateMap = state.field(widgetStateMapStateField, false) ?? {};
|
|
396
|
+
|
|
298
397
|
const builder = new RangeSetBuilder<Decoration>();
|
|
299
398
|
const tree = syntaxTree(state);
|
|
300
399
|
if (!tree || (tree.type.name === 'Program' && tree.length === 0)) {
|
|
301
|
-
return { from, decorations: Decoration.none };
|
|
400
|
+
return { from: range.from, decorations: Decoration.none };
|
|
302
401
|
}
|
|
303
402
|
|
|
403
|
+
let last = range.from;
|
|
304
404
|
tree.iterate({
|
|
305
|
-
from,
|
|
306
|
-
to,
|
|
405
|
+
from: range.from,
|
|
406
|
+
to: range.to,
|
|
307
407
|
enter: (node) => {
|
|
308
408
|
switch (node.type.name) {
|
|
309
409
|
// XML Element.
|
|
310
410
|
case 'Element': {
|
|
311
411
|
try {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
412
|
+
const args = nodeToJson(state, node.node);
|
|
413
|
+
if (args) {
|
|
414
|
+
const def = registry[args._tag];
|
|
415
|
+
if (def) {
|
|
416
|
+
// NOTE: The widget state may already have been updated before the widget is mounted.
|
|
417
|
+
const { block, factory, Component } = def;
|
|
418
|
+
const widgetState = args.id ? widgetStateMap[args.id] : undefined;
|
|
419
|
+
const nodeRange = { from: node.node.from, to: node.node.to };
|
|
420
|
+
const props = { context, range: nodeRange, ...args, ...widgetState } satisfies XmlWidgetProps;
|
|
421
|
+
|
|
422
|
+
// Create widget.
|
|
423
|
+
const widget: WidgetType | undefined = factory
|
|
424
|
+
? factory(props)
|
|
425
|
+
: Component
|
|
426
|
+
? args.id && new PlaceholderWidget(args.id, Component, props, notifier)
|
|
427
|
+
: undefined;
|
|
428
|
+
|
|
429
|
+
// Add decoration.
|
|
430
|
+
if (widget) {
|
|
431
|
+
builder.add(
|
|
432
|
+
nodeRange.from,
|
|
433
|
+
nodeRange.to,
|
|
434
|
+
Decoration.replace({
|
|
435
|
+
widget,
|
|
436
|
+
block,
|
|
437
|
+
atomic: true,
|
|
438
|
+
inclusive: true,
|
|
439
|
+
tag: args._tag,
|
|
440
|
+
}),
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
// Track last widget (NOTE: range is inclusive).
|
|
444
|
+
last = nodeRange.to - 1;
|
|
339
445
|
}
|
|
340
446
|
}
|
|
341
447
|
}
|
|
@@ -343,19 +449,20 @@ const buildDecorations = (
|
|
|
343
449
|
log.catch(err);
|
|
344
450
|
}
|
|
345
451
|
|
|
346
|
-
|
|
452
|
+
// Don't descend into children.
|
|
453
|
+
return false;
|
|
347
454
|
}
|
|
348
455
|
}
|
|
349
456
|
},
|
|
350
457
|
});
|
|
351
458
|
|
|
352
|
-
return { from, decorations: builder.finish() };
|
|
459
|
+
return { from: last, decorations: builder.finish() };
|
|
353
460
|
};
|
|
354
461
|
|
|
355
462
|
/**
|
|
356
463
|
* Placeholder for React widgets.
|
|
357
464
|
*/
|
|
358
|
-
class PlaceholderWidget<TProps
|
|
465
|
+
class PlaceholderWidget<TProps extends XmlWidgetProps> extends WidgetType {
|
|
359
466
|
private _root: HTMLElement | null = null;
|
|
360
467
|
|
|
361
468
|
constructor(
|
|
@@ -368,25 +475,25 @@ class PlaceholderWidget<TProps = {}> extends WidgetType {
|
|
|
368
475
|
invariant(id);
|
|
369
476
|
}
|
|
370
477
|
|
|
371
|
-
get root() {
|
|
478
|
+
get root(): HTMLElement | null {
|
|
372
479
|
return this._root;
|
|
373
480
|
}
|
|
374
481
|
|
|
375
|
-
override eq(other:
|
|
376
|
-
return
|
|
482
|
+
override eq(other: this) {
|
|
483
|
+
return this.id === other.id;
|
|
377
484
|
}
|
|
378
485
|
|
|
379
486
|
override ignoreEvent() {
|
|
380
487
|
return true;
|
|
381
488
|
}
|
|
382
489
|
|
|
383
|
-
override toDOM(_view: EditorView)
|
|
490
|
+
override toDOM(_view: EditorView) {
|
|
384
491
|
this._root = document.createElement('span');
|
|
385
|
-
this.notifier.mounted({ id: this.id,
|
|
492
|
+
this.notifier.mounted({ id: this.id, root: this._root, props: this.props, Component: this.Component });
|
|
386
493
|
return this._root;
|
|
387
494
|
}
|
|
388
495
|
|
|
389
|
-
override destroy(_dom: HTMLElement)
|
|
496
|
+
override destroy(_dom: HTMLElement) {
|
|
390
497
|
this.notifier.unmounted(this.id);
|
|
391
498
|
this._root = null;
|
|
392
499
|
}
|
|
@@ -35,7 +35,7 @@ export const typewriter = ({ delay = 75, items = defaultItems }: DemoOptions = {
|
|
|
35
35
|
{
|
|
36
36
|
// Next prompt.
|
|
37
37
|
// TODO(burdon): Press 1-9 to select prompt?
|
|
38
|
-
key: "
|
|
38
|
+
key: "Shift-Meta-'",
|
|
39
39
|
run: (view) => {
|
|
40
40
|
clearTimeout(t);
|
|
41
41
|
// TODO(burdon): Add space if needed.
|