@dxos/react-ui-editor 0.10.0 → 0.11.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/dist/lib/index.mjs +1477 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/translations.mjs +32 -0
- package/dist/lib/translations.mjs.map +1 -0
- package/dist/types/src/components/Editor/Editor.d.ts +22 -17
- package/dist/types/src/components/Editor/Editor.d.ts.map +1 -1
- package/dist/types/src/components/Editor/EditorContext.d.ts +20 -0
- package/dist/types/src/components/Editor/EditorContext.d.ts.map +1 -0
- package/dist/types/src/components/Editor/EditorView.d.ts +1 -2
- package/dist/types/src/components/Editor/EditorView.d.ts.map +1 -1
- package/dist/types/src/components/Editor/index.d.ts +2 -0
- package/dist/types/src/components/Editor/index.d.ts.map +1 -1
- package/dist/types/src/components/EditorMenuProvider/EditorMenuProvider.d.ts +7 -1
- package/dist/types/src/components/EditorMenuProvider/EditorMenuProvider.d.ts.map +1 -1
- package/dist/types/src/components/EditorMenuProvider/menu.d.ts +4 -2
- package/dist/types/src/components/EditorMenuProvider/menu.d.ts.map +1 -1
- package/dist/types/src/components/EditorMenuProvider/useEditorMenu.d.ts +7 -2
- package/dist/types/src/components/EditorMenuProvider/useEditorMenu.d.ts.map +1 -1
- package/dist/types/src/components/EditorPreviewProvider/EditorPreviewContext.d.ts +18 -0
- package/dist/types/src/components/EditorPreviewProvider/EditorPreviewContext.d.ts.map +1 -0
- package/dist/types/src/components/EditorPreviewProvider/EditorPreviewProvider.d.ts +0 -6
- package/dist/types/src/components/EditorPreviewProvider/EditorPreviewProvider.d.ts.map +1 -1
- package/dist/types/src/components/EditorPreviewProvider/index.d.ts +1 -0
- package/dist/types/src/components/EditorPreviewProvider/index.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts +4 -1
- package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/index.d.ts +1 -0
- package/dist/types/src/components/EditorToolbar/index.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/lists.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/view-mode.d.ts +27 -2
- package/dist/types/src/components/EditorToolbar/view-mode.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +1 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +0 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/stories/Assistant.stories.d.ts.map +1 -0
- package/dist/types/src/stories/Automerge.stories.d.ts +16 -39
- package/dist/types/src/stories/Automerge.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Blocks.stories.d.ts +10 -0
- package/dist/types/src/stories/Blocks.stories.d.ts.map +1 -0
- package/dist/types/src/stories/Comments.stories.d.ts +53 -14
- package/dist/types/src/stories/Comments.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Diff.stories.d.ts +32 -0
- package/dist/types/src/stories/Diff.stories.d.ts.map +1 -0
- package/dist/types/src/stories/Experimental.stories.d.ts +1 -0
- package/dist/types/src/stories/Experimental.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Folding.stories.d.ts +18 -0
- package/dist/types/src/stories/Folding.stories.d.ts.map +1 -0
- package/dist/types/src/stories/Outliner.stories.d.ts +4 -4
- package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Popover.stories.d.ts +3 -3
- package/dist/types/src/stories/Popover.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Suggest.stories.d.ts +27 -0
- package/dist/types/src/stories/Suggest.stories.d.ts.map +1 -0
- package/dist/types/src/stories/TrackChanges.stories.d.ts +44 -0
- package/dist/types/src/stories/TrackChanges.stories.d.ts.map +1 -0
- package/dist/types/src/stories/Widgets.stories.d.ts +12 -0
- package/dist/types/src/stories/Widgets.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 +16 -0
- package/dist/types/src/stories/components/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +40 -83
- package/src/components/Editor/Editor.tsx +142 -35
- package/src/components/Editor/EditorContext.ts +34 -0
- package/src/components/Editor/EditorView.tsx +9 -6
- package/src/components/Editor/index.ts +3 -0
- package/src/components/EditorMenuProvider/EditorMenuProvider.tsx +98 -8
- package/src/components/EditorMenuProvider/menu.ts +4 -2
- package/src/components/EditorMenuProvider/useEditorMenu.ts +114 -29
- package/src/components/EditorPreviewProvider/EditorPreviewContext.ts +21 -0
- package/src/components/EditorPreviewProvider/EditorPreviewProvider.tsx +1 -10
- package/src/components/EditorPreviewProvider/index.ts +2 -0
- package/src/components/EditorToolbar/EditorToolbar.tsx +6 -3
- package/src/components/EditorToolbar/index.ts +1 -0
- package/src/components/EditorToolbar/lists.ts +2 -6
- package/src/components/EditorToolbar/view-mode.ts +41 -12
- package/src/components/index.ts +1 -0
- package/src/index.ts +0 -1
- package/src/{extensions → stories}/Assistant.stories.tsx +7 -2
- package/src/stories/Automerge.stories.tsx +99 -78
- package/src/stories/Blocks.stories.tsx +83 -0
- package/src/stories/Comments.stories.tsx +273 -74
- package/src/stories/Diff.stories.tsx +48 -0
- package/src/stories/EditorToolbar.stories.tsx +2 -2
- package/src/stories/Experimental.stories.tsx +13 -2
- package/src/stories/Folding.stories.tsx +120 -0
- package/src/stories/Markdown.stories.tsx +1 -1
- package/src/stories/Outliner.stories.tsx +18 -7
- package/src/stories/Popover.stories.tsx +27 -5
- package/src/stories/Suggest.stories.tsx +199 -0
- package/src/stories/TrackChanges.stories.tsx +226 -0
- package/src/stories/Widgets.stories.tsx +162 -10
- package/src/stories/components/EditorStory.tsx +15 -6
- package/src/stories/components/util.tsx +40 -2
- package/dist/lib/browser/index.mjs +0 -1676
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/translations.mjs +0 -39
- package/dist/lib/browser/translations.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -1678
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/translations.mjs +0 -41
- package/dist/lib/node-esm/translations.mjs.map +0 -7
- package/dist/types/src/extensions/Assistant.stories.d.ts.map +0 -1
- package/dist/types/src/extensions/assistant-extension.d.ts +0 -24
- package/dist/types/src/extensions/assistant-extension.d.ts.map +0 -1
- package/dist/types/src/extensions/index.d.ts +0 -2
- package/dist/types/src/extensions/index.d.ts.map +0 -1
- package/src/extensions/assistant-extension.tsx +0 -223
- package/src/extensions/index.ts +0 -5
- /package/dist/types/src/{extensions → stories}/Assistant.stories.d.ts +0 -0
|
@@ -0,0 +1,1477 @@
|
|
|
1
|
+
import { createContext } from "@radix-ui/react-context";
|
|
2
|
+
import { Atom } from "@effect-atom/atom";
|
|
3
|
+
import { Fragment, forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import { invariant } from "@dxos/invariant";
|
|
6
|
+
import { mx } from "@dxos/ui-theme";
|
|
7
|
+
import { getProviderValue, isNonNullable, isTruthy } from "@dxos/util";
|
|
8
|
+
import { Inline, List, addBlockquote, addCodeblock, addLink, createBasicExtensions, createEditorStateTransaction, createMarkdownExtensions, createThemeExtensions, debugDispatcher, decorateMarkdown, documentId, initialSync, insertAtCursor, insertAtLineStart, insertTable, modalStateEffect, modalStateField, placeholder, removeBlockquote, removeCodeblock, removeLink, setHeading, setStyle, toggleList } from "@dxos/ui-editor";
|
|
9
|
+
import { EditorState, Prec, RangeSetBuilder, StateEffect, StateField, Transaction } from "@codemirror/state";
|
|
10
|
+
import { Decoration, EditorView, ViewPlugin, keymap } from "@codemirror/view";
|
|
11
|
+
import { useControllableState } from "@radix-ui/react-use-controllable-state";
|
|
12
|
+
import { addEventListener } from "@dxos/async";
|
|
13
|
+
import { DX_ANCHOR_ACTIVATE, ElevationProvider, Icon, Input, Popover, ScrollArea, toLocalizedString, useDynamicRef, useThemeContext, useTranslation } from "@dxos/react-ui";
|
|
14
|
+
import { translationKey } from "#translations";
|
|
15
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
16
|
+
import { Menu, MenuBuilder, useMenuActions } from "@dxos/react-ui-menu";
|
|
17
|
+
import { openSearchPanel } from "@codemirror/search";
|
|
18
|
+
import { log } from "@dxos/log";
|
|
19
|
+
//#region src/components/Editor/controller.ts
|
|
20
|
+
var withHiddenView = (controller) => {
|
|
21
|
+
Object.defineProperty(controller, "view", { enumerable: false });
|
|
22
|
+
return controller;
|
|
23
|
+
};
|
|
24
|
+
var noopController = withHiddenView({
|
|
25
|
+
get view() {
|
|
26
|
+
return null;
|
|
27
|
+
},
|
|
28
|
+
getText: () => "",
|
|
29
|
+
setText: () => {},
|
|
30
|
+
focus: () => {}
|
|
31
|
+
});
|
|
32
|
+
var createEditorController = (view) => {
|
|
33
|
+
return withHiddenView({
|
|
34
|
+
get view() {
|
|
35
|
+
return view;
|
|
36
|
+
},
|
|
37
|
+
getText: () => {
|
|
38
|
+
return view?.state.doc.toString() ?? "";
|
|
39
|
+
},
|
|
40
|
+
setText: (text, focus) => {
|
|
41
|
+
view?.dispatch({
|
|
42
|
+
changes: {
|
|
43
|
+
from: 0,
|
|
44
|
+
to: view?.state.doc.length ?? 0,
|
|
45
|
+
insert: text
|
|
46
|
+
},
|
|
47
|
+
selection: {
|
|
48
|
+
anchor: text.length,
|
|
49
|
+
head: text.length
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
if (focus) view?.focus();
|
|
53
|
+
},
|
|
54
|
+
focus: () => view?.focus()
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/components/Editor/EditorContext.ts
|
|
59
|
+
var [EditorContextProvider, useEditorContext] = createContext("Editor");
|
|
60
|
+
/**
|
|
61
|
+
* Access the editor context. Must be used within `Editor.Root`.
|
|
62
|
+
*/
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/components/EditorMenuProvider/menu.ts
|
|
65
|
+
var getMenuItem = (groups, id) => {
|
|
66
|
+
return groups.flatMap((group) => group.items).find((item) => item.id === id);
|
|
67
|
+
};
|
|
68
|
+
/** Undefined when the menu is empty; at either end the current item is returned unchanged. */
|
|
69
|
+
var getNextMenuItem = (groups, id) => {
|
|
70
|
+
const items = groups.flatMap((group) => group.items);
|
|
71
|
+
const index = items.findIndex((item) => item.id === id);
|
|
72
|
+
return index < items.length - 1 ? items[index + 1] : items[index];
|
|
73
|
+
};
|
|
74
|
+
/** Undefined when the menu is empty; at either end the current item is returned unchanged. */
|
|
75
|
+
var getPreviousMenuItem = (groups, id) => {
|
|
76
|
+
const items = groups.flatMap((group) => group.items);
|
|
77
|
+
const index = items.findIndex((item) => item.id === id);
|
|
78
|
+
return index > 0 ? items[index - 1] : items[index];
|
|
79
|
+
};
|
|
80
|
+
var createMenuGroup = ({ id = "menu", label, filter, items }) => ({
|
|
81
|
+
id,
|
|
82
|
+
label,
|
|
83
|
+
items: items.filter((item) => !filter || item.toLowerCase().includes(filter.toLowerCase())).map((item, i) => ({
|
|
84
|
+
id: `${id}-${i}`,
|
|
85
|
+
label: item,
|
|
86
|
+
onSelect: ({ view, head }) => insertAtCursor(view, head, item)
|
|
87
|
+
}))
|
|
88
|
+
});
|
|
89
|
+
var filterMenuGroups = (groups, filter) => {
|
|
90
|
+
return groups.map((group) => ({
|
|
91
|
+
...group,
|
|
92
|
+
items: group.items.filter(filter)
|
|
93
|
+
}));
|
|
94
|
+
};
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/components/EditorMenuProvider/popover.ts
|
|
97
|
+
var DELIMITERS = [" ", ":"];
|
|
98
|
+
/**
|
|
99
|
+
* Creates a popover that appears when the trigger character is inserted.
|
|
100
|
+
* This can be used for context menus or autocompletion.
|
|
101
|
+
*/
|
|
102
|
+
var popover = (options = {}) => {
|
|
103
|
+
return [
|
|
104
|
+
Prec.highest(popoverKeymap(options)),
|
|
105
|
+
popoverStateField,
|
|
106
|
+
popoverTriggerListener(options),
|
|
107
|
+
options.activateOnTyping && popoverAutoActivate(options),
|
|
108
|
+
popoverAnchorDecoration(options),
|
|
109
|
+
modalStateField,
|
|
110
|
+
options.trigger && placeholder({
|
|
111
|
+
content: `Press '${Array.isArray(options.trigger) ? options.trigger[0] : options.trigger}' for commands`,
|
|
112
|
+
focusOnly: true,
|
|
113
|
+
...options.placeholder
|
|
114
|
+
})
|
|
115
|
+
].filter(isTruthy);
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Listen for selection and document changes.
|
|
119
|
+
*/
|
|
120
|
+
var popoverTriggerListener = (options) => EditorView.updateListener.of(({ view, docChanged }) => {
|
|
121
|
+
const { range: activeRange, trigger } = view.state.field(popoverStateField) ?? {};
|
|
122
|
+
if (!activeRange) return;
|
|
123
|
+
const text = view.state.doc.sliceString(activeRange.from, activeRange.to);
|
|
124
|
+
const selection = view.state.selection.main;
|
|
125
|
+
const shouldClose = (trigger ? trigger !== text[0] : false) || /\s/.test(trigger ? text.slice(1) : text) || selection.head < activeRange.from || selection.head > activeRange.to + 1;
|
|
126
|
+
const nextRange = shouldClose ? null : docChanged ? {
|
|
127
|
+
from: activeRange.from,
|
|
128
|
+
to: selection.head
|
|
129
|
+
} : activeRange;
|
|
130
|
+
if (nextRange !== activeRange) view.dispatch({ effects: popoverRangeEffect.of(nextRange ? {
|
|
131
|
+
range: nextRange,
|
|
132
|
+
trigger
|
|
133
|
+
} : null) });
|
|
134
|
+
if (shouldClose) options.onClose?.({ view });
|
|
135
|
+
});
|
|
136
|
+
/**
|
|
137
|
+
* Open the popover while typing a word, without requiring a trigger character. Once a range is
|
|
138
|
+
* active, {@link popoverTriggerListener} keeps it in sync and closes it (whitespace, cursor moves).
|
|
139
|
+
*/
|
|
140
|
+
var popoverAutoActivate = (options) => EditorView.updateListener.of(({ view, docChanged, transactions }) => {
|
|
141
|
+
if (!docChanged || !transactions.some((tr) => tr.isUserEvent("input")) || view.state.field(popoverStateField)) return;
|
|
142
|
+
const selection = view.state.selection.main;
|
|
143
|
+
if (!selection.empty) return;
|
|
144
|
+
const line = view.state.doc.lineAt(selection.head);
|
|
145
|
+
const before = line.text.slice(0, selection.head - line.from);
|
|
146
|
+
const idx = getLastIndexOf(before, options.delimiters ?? DELIMITERS);
|
|
147
|
+
if (before.slice(idx + 1).length === 0) return;
|
|
148
|
+
view.dispatch({ effects: popoverRangeEffect.of({ range: {
|
|
149
|
+
from: line.from + idx + 1,
|
|
150
|
+
to: selection.head
|
|
151
|
+
} }) });
|
|
152
|
+
});
|
|
153
|
+
/**
|
|
154
|
+
* Popover navigation.
|
|
155
|
+
*/
|
|
156
|
+
var popoverKeymap = (options) => {
|
|
157
|
+
const triggers = Array.isArray(options.trigger) ? options.trigger : [options.trigger];
|
|
158
|
+
return keymap.of([
|
|
159
|
+
...triggers.filter(isNonNullable).map((trigger) => ({
|
|
160
|
+
key: trigger,
|
|
161
|
+
run: (view) => {
|
|
162
|
+
const selection = view.state.selection.main;
|
|
163
|
+
const line = view.state.doc.lineAt(selection.head);
|
|
164
|
+
if (line.text.trim() === "" || selection.head === line.from || selection.head > line.from && line.text[selection.head - line.from - 1] === " ") {
|
|
165
|
+
view.dispatch({
|
|
166
|
+
changes: {
|
|
167
|
+
from: selection.head,
|
|
168
|
+
insert: trigger
|
|
169
|
+
},
|
|
170
|
+
selection: {
|
|
171
|
+
anchor: selection.head + 1,
|
|
172
|
+
head: selection.head + 1
|
|
173
|
+
},
|
|
174
|
+
effects: popoverRangeEffect.of({
|
|
175
|
+
trigger,
|
|
176
|
+
range: {
|
|
177
|
+
from: selection.head,
|
|
178
|
+
to: selection.head + 1
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
});
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
})),
|
|
187
|
+
options.triggerKey && {
|
|
188
|
+
key: options.triggerKey,
|
|
189
|
+
run: (view) => {
|
|
190
|
+
const selection = view.state.selection.main;
|
|
191
|
+
const line = view.state.doc.lineAt(selection.head);
|
|
192
|
+
let str = line.text.slice(0, selection.head - line.from);
|
|
193
|
+
const idx = getLastIndexOf(str, options.delimiters ?? DELIMITERS);
|
|
194
|
+
if (idx !== -1) str = str.slice(idx + 1);
|
|
195
|
+
const from = line.from + idx;
|
|
196
|
+
view.dispatch({
|
|
197
|
+
effects: popoverRangeEffect.of({ range: {
|
|
198
|
+
from: from + 1,
|
|
199
|
+
to: selection.head
|
|
200
|
+
} }),
|
|
201
|
+
changes: selection.head === view.state.doc.length ? {
|
|
202
|
+
from: from + 1,
|
|
203
|
+
to: selection.head,
|
|
204
|
+
insert: " "
|
|
205
|
+
} : void 0
|
|
206
|
+
});
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
key: "ArrowUp",
|
|
212
|
+
run: (view) => {
|
|
213
|
+
if (view.state.field(popoverStateField)?.range) {
|
|
214
|
+
options.onArrowUp?.({ view });
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
key: "ArrowDown",
|
|
222
|
+
run: (view) => {
|
|
223
|
+
if (view.state.field(popoverStateField)?.range) {
|
|
224
|
+
options.onArrowDown?.({ view });
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
key: "Enter",
|
|
232
|
+
run: (view) => {
|
|
233
|
+
if (view.state.field(popoverStateField)?.range) return options.onEnter?.({ view }) ?? false;
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
].filter(isTruthy));
|
|
238
|
+
};
|
|
239
|
+
/**
|
|
240
|
+
* Creates a <dx-anchor> tag, which is used to anchor the Popver.
|
|
241
|
+
*/
|
|
242
|
+
var popoverAnchorDecoration = (options) => {
|
|
243
|
+
return ViewPlugin.fromClass(class {
|
|
244
|
+
view;
|
|
245
|
+
_decorations = Decoration.none;
|
|
246
|
+
constructor(view) {
|
|
247
|
+
this.view = view;
|
|
248
|
+
}
|
|
249
|
+
update({ view, transactions }) {
|
|
250
|
+
const builder = new RangeSetBuilder();
|
|
251
|
+
const { range, trigger } = view.state.field(popoverStateField) ?? {};
|
|
252
|
+
if (range) {
|
|
253
|
+
const selection = view.state.selection.main;
|
|
254
|
+
if (selection.head >= range.from && selection.head <= range.to) builder.add(range.from, range.to + 1, Decoration.mark({
|
|
255
|
+
tagName: "dx-anchor",
|
|
256
|
+
class: "cm-popover-trigger",
|
|
257
|
+
attributes: {
|
|
258
|
+
"data-visible-focus": "false",
|
|
259
|
+
"data-auto-trigger": "true",
|
|
260
|
+
"data-trigger": trigger ?? options.triggerKey ?? ""
|
|
261
|
+
}
|
|
262
|
+
}));
|
|
263
|
+
if (transactions.some((tr) => tr.effects.some((effect) => effect.is(popoverRangeEffect)))) {
|
|
264
|
+
const content = view.state.sliceDoc(range.from + (trigger ? trigger.length : 0), range.to);
|
|
265
|
+
options.onTextChange?.({
|
|
266
|
+
view,
|
|
267
|
+
pos: selection.head,
|
|
268
|
+
text: content,
|
|
269
|
+
trigger
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
this._decorations = builder.finish();
|
|
274
|
+
}
|
|
275
|
+
}, { decorations: (v) => v._decorations });
|
|
276
|
+
};
|
|
277
|
+
var popoverRangeEffect = StateEffect.define();
|
|
278
|
+
var popoverStateField = StateField.define({
|
|
279
|
+
create: () => null,
|
|
280
|
+
update: (value, tr) => {
|
|
281
|
+
let newValue = value;
|
|
282
|
+
for (const effect of tr.effects) if (effect.is(popoverRangeEffect)) newValue = effect.value;
|
|
283
|
+
return newValue;
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
var getLastIndexOf = (str, delimiters) => Math.max(...delimiters.map((delim) => str.lastIndexOf(delim)));
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region src/components/EditorMenuProvider/menu-presets.ts
|
|
289
|
+
/**
|
|
290
|
+
* Re-open the popover in link-query ("@") mode at the cursor, seeding the same query shape the user
|
|
291
|
+
* would get by typing the trigger manually: a single "@" for an inline link, "@@" for a block embed.
|
|
292
|
+
* The consumer's `@` handler (e.g. plugin-markdown's link query) renders the object picker and, on
|
|
293
|
+
* selection, replaces this range with the corresponding markdown link.
|
|
294
|
+
*
|
|
295
|
+
* Deferred to the next frame: selecting a slash-menu item deletes the "/" range, which closes the
|
|
296
|
+
* popover; opening synchronously races that close (`onActivate` reads a stale open state) and the
|
|
297
|
+
* picker never appears.
|
|
298
|
+
*/
|
|
299
|
+
var openLinkQuery = (view, insert) => {
|
|
300
|
+
requestAnimationFrame(() => {
|
|
301
|
+
const { head } = view.state.selection.main;
|
|
302
|
+
const to = head + insert.length;
|
|
303
|
+
view.dispatch({
|
|
304
|
+
changes: {
|
|
305
|
+
from: head,
|
|
306
|
+
insert
|
|
307
|
+
},
|
|
308
|
+
selection: {
|
|
309
|
+
anchor: to,
|
|
310
|
+
head: to
|
|
311
|
+
},
|
|
312
|
+
effects: popoverRangeEffect.of({
|
|
313
|
+
trigger: "@",
|
|
314
|
+
range: {
|
|
315
|
+
from: head,
|
|
316
|
+
to
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
};
|
|
322
|
+
var formattingCommands = {
|
|
323
|
+
id: "markdown",
|
|
324
|
+
label: "Markdown",
|
|
325
|
+
items: [
|
|
326
|
+
{
|
|
327
|
+
id: "heading-1",
|
|
328
|
+
label: "Heading 1",
|
|
329
|
+
icon: "ph--text-h-one--regular",
|
|
330
|
+
onSelect: ({ view, head }) => insertAtLineStart(view, head, "# ")
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
id: "heading-2",
|
|
334
|
+
label: "Heading 2",
|
|
335
|
+
icon: "ph--text-h-two--regular",
|
|
336
|
+
onSelect: ({ view, head }) => insertAtLineStart(view, head, "## ")
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
id: "heading-3",
|
|
340
|
+
label: "Heading 3",
|
|
341
|
+
icon: "ph--text-h-three--regular",
|
|
342
|
+
onSelect: ({ view, head }) => insertAtLineStart(view, head, "### ")
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
id: "heading-4",
|
|
346
|
+
label: "Heading 4",
|
|
347
|
+
icon: "ph--text-h-four--regular",
|
|
348
|
+
onSelect: ({ view, head }) => insertAtLineStart(view, head, "#### ")
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
id: "heading-5",
|
|
352
|
+
label: "Heading 5",
|
|
353
|
+
icon: "ph--text-h-five--regular",
|
|
354
|
+
onSelect: ({ view, head }) => insertAtLineStart(view, head, "##### ")
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
id: "heading-6",
|
|
358
|
+
label: "Heading 6",
|
|
359
|
+
icon: "ph--text-h-six--regular",
|
|
360
|
+
onSelect: ({ view, head }) => insertAtLineStart(view, head, "###### ")
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
id: "bullet-list",
|
|
364
|
+
label: "Bullet List",
|
|
365
|
+
icon: "ph--list-bullets--regular",
|
|
366
|
+
onSelect: ({ view, head }) => insertAtLineStart(view, head, "- ")
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
id: "numbered-list",
|
|
370
|
+
label: "Numbered List",
|
|
371
|
+
icon: "ph--list-numbers--regular",
|
|
372
|
+
onSelect: ({ view, head }) => insertAtLineStart(view, head, "1. ")
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
id: "task-list",
|
|
376
|
+
label: "Task List",
|
|
377
|
+
icon: "ph--list-checks--regular",
|
|
378
|
+
onSelect: ({ view, head }) => insertAtLineStart(view, head, "- [ ] ")
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
id: "quote",
|
|
382
|
+
label: "Quote",
|
|
383
|
+
icon: "ph--quotes--regular",
|
|
384
|
+
onSelect: ({ view, head }) => insertAtLineStart(view, head, "> ")
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
id: "code-block",
|
|
388
|
+
label: "Code Block",
|
|
389
|
+
icon: "ph--code-block--regular",
|
|
390
|
+
onSelect: ({ view, head }) => insertAtLineStart(view, head, "```\n\n```")
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
id: "table",
|
|
394
|
+
label: "Table",
|
|
395
|
+
icon: "ph--table--regular",
|
|
396
|
+
onSelect: ({ view, head }) => insertAtLineStart(view, head, "| | | |\n|---|---|---|\n| | | |")
|
|
397
|
+
}
|
|
398
|
+
]
|
|
399
|
+
};
|
|
400
|
+
var linkSlashCommands = {
|
|
401
|
+
id: "link",
|
|
402
|
+
label: "Link",
|
|
403
|
+
items: [{
|
|
404
|
+
id: "inline-link",
|
|
405
|
+
label: "Inline link",
|
|
406
|
+
icon: "ph--link--regular",
|
|
407
|
+
onSelect: ({ view }) => openLinkQuery(view, "@")
|
|
408
|
+
}, {
|
|
409
|
+
id: "inline-object",
|
|
410
|
+
label: "Inline object",
|
|
411
|
+
icon: "ph--lego--regular",
|
|
412
|
+
onSelect: ({ view }) => openLinkQuery(view, "@@")
|
|
413
|
+
}]
|
|
414
|
+
};
|
|
415
|
+
//#endregion
|
|
416
|
+
//#region src/components/EditorMenuProvider/EditorMenuProvider.tsx
|
|
417
|
+
var __dxlog_file$3 = "/__w/dxos/dxos/packages/ui/react-ui-editor/src/components/EditorMenuProvider/EditorMenuProvider.tsx";
|
|
418
|
+
/**
|
|
419
|
+
* Implements the Popover and listens for the `dx-anchor-activate` event from the `popover` extension's decoration.
|
|
420
|
+
* NOTE: We don't use DropdownMenu because the command menu needs to manage focus explicitly.
|
|
421
|
+
* I.e., focus must remain in the editor while displaying the menu (for type-ahead).
|
|
422
|
+
*/
|
|
423
|
+
var EditorMenuProvider = ({ children, getView, groups, currentItem, open: openProp, defaultOpen, numItems = 8, search, query = "", searchPlaceholder, onOpenChange, onActivate, onSelect, onCancel, onQueryChange, onNavigate }) => {
|
|
424
|
+
const { t } = useTranslation(translationKey);
|
|
425
|
+
const triggerRef = useRef(null);
|
|
426
|
+
const getViewRef = useDynamicRef(getView);
|
|
427
|
+
const [open, setOpen] = useControllableState({
|
|
428
|
+
prop: openProp,
|
|
429
|
+
defaultProp: defaultOpen,
|
|
430
|
+
onChange: (open) => {
|
|
431
|
+
const view = getViewRef.current?.();
|
|
432
|
+
invariant(view, void 0, {
|
|
433
|
+
"~LogMeta": "~LogMeta",
|
|
434
|
+
F: __dxlog_file$3,
|
|
435
|
+
L: 90,
|
|
436
|
+
S: void 0,
|
|
437
|
+
A: ["view", ""]
|
|
438
|
+
});
|
|
439
|
+
onOpenChange?.({
|
|
440
|
+
view,
|
|
441
|
+
open
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
const [root, setRoot] = useState(null);
|
|
446
|
+
useEffect(() => {
|
|
447
|
+
if (!root) return;
|
|
448
|
+
return addEventListener(root, DX_ANCHOR_ACTIVATE, (event) => {
|
|
449
|
+
const { trigger, dxn } = event;
|
|
450
|
+
if (!dxn) {
|
|
451
|
+
triggerRef.current = trigger;
|
|
452
|
+
if (onActivate) {
|
|
453
|
+
const view = getViewRef.current?.();
|
|
454
|
+
if (view) onActivate({
|
|
455
|
+
view,
|
|
456
|
+
trigger: trigger.getAttribute("data-trigger") ?? void 0
|
|
457
|
+
});
|
|
458
|
+
} else requestAnimationFrame(() => setOpen(true));
|
|
459
|
+
}
|
|
460
|
+
}, {
|
|
461
|
+
capture: true,
|
|
462
|
+
passive: false
|
|
463
|
+
});
|
|
464
|
+
}, [root, onActivate]);
|
|
465
|
+
const handleSelect = useCallback((item) => {
|
|
466
|
+
const view = getViewRef.current?.();
|
|
467
|
+
invariant(view, void 0, {
|
|
468
|
+
"~LogMeta": "~LogMeta",
|
|
469
|
+
F: __dxlog_file$3,
|
|
470
|
+
L: 129,
|
|
471
|
+
S: void 0,
|
|
472
|
+
A: ["view", ""]
|
|
473
|
+
});
|
|
474
|
+
onSelect?.({
|
|
475
|
+
view,
|
|
476
|
+
item
|
|
477
|
+
});
|
|
478
|
+
setOpen(false);
|
|
479
|
+
view.focus();
|
|
480
|
+
}, [
|
|
481
|
+
getViewRef,
|
|
482
|
+
onSelect,
|
|
483
|
+
setOpen
|
|
484
|
+
]);
|
|
485
|
+
const menuGroups = groups?.filter((group) => group.items.length > 0) ?? [];
|
|
486
|
+
const searchInputRef = useRef(null);
|
|
487
|
+
useEffect(() => {
|
|
488
|
+
if (!search || !open) return;
|
|
489
|
+
let inner;
|
|
490
|
+
const outer = requestAnimationFrame(() => {
|
|
491
|
+
inner = requestAnimationFrame(() => searchInputRef.current?.focus());
|
|
492
|
+
});
|
|
493
|
+
return () => {
|
|
494
|
+
cancelAnimationFrame(outer);
|
|
495
|
+
cancelAnimationFrame(inner);
|
|
496
|
+
};
|
|
497
|
+
}, [search, open]);
|
|
498
|
+
const handleSearchKeyDown = useCallback((event) => {
|
|
499
|
+
switch (event.key) {
|
|
500
|
+
case "ArrowUp":
|
|
501
|
+
case "ArrowDown":
|
|
502
|
+
event.preventDefault();
|
|
503
|
+
onNavigate?.(event.key === "ArrowUp" ? "up" : "down");
|
|
504
|
+
break;
|
|
505
|
+
case "Enter": {
|
|
506
|
+
const item = getMenuItem(menuGroups, currentItem);
|
|
507
|
+
if (item) {
|
|
508
|
+
event.preventDefault();
|
|
509
|
+
handleSelect(item);
|
|
510
|
+
}
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}, [
|
|
515
|
+
menuGroups,
|
|
516
|
+
currentItem,
|
|
517
|
+
handleSelect,
|
|
518
|
+
onNavigate
|
|
519
|
+
]);
|
|
520
|
+
return /* @__PURE__ */ jsxs(Popover.Root, {
|
|
521
|
+
modal: false,
|
|
522
|
+
open,
|
|
523
|
+
onOpenChange: setOpen,
|
|
524
|
+
children: [
|
|
525
|
+
/* @__PURE__ */ jsx(Popover.VirtualTrigger, { virtualRef: triggerRef }),
|
|
526
|
+
/* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsxs(Popover.Content, {
|
|
527
|
+
align: "start",
|
|
528
|
+
classNames: ["flex flex-col", !search && !menuGroups.length && "hidden"],
|
|
529
|
+
style: { maxBlockSize: 36 * numItems + 10 + (search ? 36 : 0) },
|
|
530
|
+
onEscapeKeyDown: () => {
|
|
531
|
+
const currentView = getViewRef.current?.();
|
|
532
|
+
if (currentView) onCancel?.({ view: currentView });
|
|
533
|
+
},
|
|
534
|
+
onOpenAutoFocus: search ? void 0 : (event) => event.preventDefault(),
|
|
535
|
+
children: [
|
|
536
|
+
search && /* @__PURE__ */ jsx(Input.Root, { children: /* @__PURE__ */ jsx(Input.TextInput, {
|
|
537
|
+
ref: searchInputRef,
|
|
538
|
+
density: "sm",
|
|
539
|
+
variant: "subdued",
|
|
540
|
+
classNames: "shrink-0 mbe-1",
|
|
541
|
+
value: query,
|
|
542
|
+
placeholder: searchPlaceholder,
|
|
543
|
+
"aria-label": searchPlaceholder ?? t("search.label"),
|
|
544
|
+
onChange: (event) => onQueryChange?.(event.target.value),
|
|
545
|
+
onKeyDown: handleSearchKeyDown
|
|
546
|
+
}) }),
|
|
547
|
+
/* @__PURE__ */ jsx(Popover.Viewport, {
|
|
548
|
+
asChild: true,
|
|
549
|
+
classNames: "dx-container",
|
|
550
|
+
children: /* @__PURE__ */ jsx(ScrollArea.Root, {
|
|
551
|
+
thin: true,
|
|
552
|
+
children: /* @__PURE__ */ jsx(ScrollArea.Viewport, { children: /* @__PURE__ */ jsx(Menu$1, {
|
|
553
|
+
groups: menuGroups,
|
|
554
|
+
currentItem,
|
|
555
|
+
onSelect: handleSelect
|
|
556
|
+
}) })
|
|
557
|
+
})
|
|
558
|
+
}),
|
|
559
|
+
/* @__PURE__ */ jsx(Popover.Arrow, {})
|
|
560
|
+
]
|
|
561
|
+
}) }),
|
|
562
|
+
/* @__PURE__ */ jsx("div", {
|
|
563
|
+
className: "contents",
|
|
564
|
+
ref: setRoot,
|
|
565
|
+
children
|
|
566
|
+
})
|
|
567
|
+
]
|
|
568
|
+
});
|
|
569
|
+
};
|
|
570
|
+
var Menu$1 = ({ groups, currentItem, onSelect }) => {
|
|
571
|
+
const { tx } = useThemeContext();
|
|
572
|
+
return /* @__PURE__ */ jsx("ul", { children: groups.map((group, index) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MenuGroup, {
|
|
573
|
+
group,
|
|
574
|
+
currentItem,
|
|
575
|
+
onSelect
|
|
576
|
+
}), index < groups.length - 1 && /* @__PURE__ */ jsx("div", { className: tx("menu.separator", {}) })] }, group.id)) });
|
|
577
|
+
};
|
|
578
|
+
var MenuGroup = ({ group, currentItem, onSelect }) => {
|
|
579
|
+
const { tx } = useThemeContext();
|
|
580
|
+
const { t } = useTranslation();
|
|
581
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [group.label && /* @__PURE__ */ jsx("div", {
|
|
582
|
+
className: tx("menu.groupLabel", {}),
|
|
583
|
+
children: /* @__PURE__ */ jsx("span", { children: toLocalizedString(group.label, t) })
|
|
584
|
+
}), group.items.map((item) => /* @__PURE__ */ jsx(MenuItem, {
|
|
585
|
+
item,
|
|
586
|
+
current: currentItem === item.id,
|
|
587
|
+
onSelect
|
|
588
|
+
}, item.id))] });
|
|
589
|
+
};
|
|
590
|
+
var MenuItem = ({ item, current, onSelect }) => {
|
|
591
|
+
const { tx } = useThemeContext();
|
|
592
|
+
const { t } = useTranslation();
|
|
593
|
+
const listRef = useRef(null);
|
|
594
|
+
useEffect(() => {
|
|
595
|
+
if (current && listRef.current) listRef.current.scrollIntoView({
|
|
596
|
+
behavior: "instant",
|
|
597
|
+
block: "nearest"
|
|
598
|
+
});
|
|
599
|
+
}, [current]);
|
|
600
|
+
const handleSelect = useCallback(() => onSelect?.(item), [item, onSelect]);
|
|
601
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
602
|
+
ref: listRef,
|
|
603
|
+
className: tx("menu.item", {}, [current && "bg-hover-surface"]),
|
|
604
|
+
onClick: handleSelect,
|
|
605
|
+
children: [item.icon && /* @__PURE__ */ jsx(Icon, { icon: item.icon }), /* @__PURE__ */ jsx("span", {
|
|
606
|
+
className: "grow truncate",
|
|
607
|
+
children: toLocalizedString(item.label, t)
|
|
608
|
+
})]
|
|
609
|
+
});
|
|
610
|
+
};
|
|
611
|
+
//#endregion
|
|
612
|
+
//#region src/components/EditorMenuProvider/useEditorMenu.ts
|
|
613
|
+
var __dxlog_file$2 = "/__w/dxos/dxos/packages/ui/react-ui-editor/src/components/EditorMenuProvider/useEditorMenu.ts";
|
|
614
|
+
/**
|
|
615
|
+
* ```tsx
|
|
616
|
+
* const { groupsRef, extension, ...menuProps } = useEditorMenu();
|
|
617
|
+
* const { parentRef, viewRef } = useTextEditor({ extensions: [extension] });
|
|
618
|
+
* return (
|
|
619
|
+
* <EditorMenuProvider getView={() => viewRef.current} groups={groupsRef.current} {...menuProps}>
|
|
620
|
+
* <div ref={parentRef} />
|
|
621
|
+
* </EditorMenuProvider>
|
|
622
|
+
* );
|
|
623
|
+
* ```
|
|
624
|
+
*/
|
|
625
|
+
var useEditorMenu = ({ trigger, triggerKey, placeholder, activateOnTyping, filter = true, searchTriggers, getMenu }) => {
|
|
626
|
+
const groupsRef = useRef([]);
|
|
627
|
+
const currentRef = useRef(null);
|
|
628
|
+
const [currentItem, setCurrentItem] = useState();
|
|
629
|
+
const [open, setOpen] = useState(false);
|
|
630
|
+
const [search, setSearch] = useState(false);
|
|
631
|
+
const [query, setQuery] = useState("");
|
|
632
|
+
const [_, refresh] = useState({});
|
|
633
|
+
const contextRef = useRef(null);
|
|
634
|
+
const requestRef = useRef(0);
|
|
635
|
+
/**
|
|
636
|
+
* Get filtered options.
|
|
637
|
+
*/
|
|
638
|
+
const getMenuOptions = useCallback(async ({ text, trigger, ...props }) => {
|
|
639
|
+
const groups = await getMenu?.({
|
|
640
|
+
text,
|
|
641
|
+
trigger,
|
|
642
|
+
...props
|
|
643
|
+
}) ?? [];
|
|
644
|
+
return filter && trigger !== "@" ? filterMenuGroups(groups, (item) => text ? item.label.toLowerCase().startsWith(text.toLowerCase()) : true) : groups;
|
|
645
|
+
}, [getMenu, filter]);
|
|
646
|
+
/**
|
|
647
|
+
* Recompute the menu and preselect the first item.
|
|
648
|
+
*/
|
|
649
|
+
const updateGroups = useCallback(async ({ view, pos, text, trigger }) => {
|
|
650
|
+
const request = ++requestRef.current;
|
|
651
|
+
const groups = await getMenuOptions({
|
|
652
|
+
state: view.state,
|
|
653
|
+
pos,
|
|
654
|
+
text,
|
|
655
|
+
trigger
|
|
656
|
+
}) ?? [];
|
|
657
|
+
if (request !== requestRef.current) return;
|
|
658
|
+
groupsRef.current = groups;
|
|
659
|
+
const firstItem = groupsRef.current.filter((group) => group.items.length > 0)[0]?.items[0];
|
|
660
|
+
if (firstItem) {
|
|
661
|
+
setCurrentItem(firstItem.id);
|
|
662
|
+
currentRef.current = firstItem;
|
|
663
|
+
} else {
|
|
664
|
+
setCurrentItem(void 0);
|
|
665
|
+
currentRef.current = null;
|
|
666
|
+
}
|
|
667
|
+
refresh({});
|
|
668
|
+
}, [getMenuOptions]);
|
|
669
|
+
const handleOpenChange = useCallback(async ({ view, open }) => {
|
|
670
|
+
invariant(view, void 0, {
|
|
671
|
+
"~LogMeta": "~LogMeta",
|
|
672
|
+
F: __dxlog_file$2,
|
|
673
|
+
L: 136,
|
|
674
|
+
S: void 0,
|
|
675
|
+
A: ["view", ""]
|
|
676
|
+
});
|
|
677
|
+
setOpen(open);
|
|
678
|
+
if (!open) {
|
|
679
|
+
setCurrentItem(void 0);
|
|
680
|
+
setSearch(false);
|
|
681
|
+
setQuery("");
|
|
682
|
+
contextRef.current = null;
|
|
683
|
+
requestRef.current++;
|
|
684
|
+
groupsRef.current = [];
|
|
685
|
+
currentRef.current = null;
|
|
686
|
+
view.dispatch({ effects: [popoverRangeEffect.of(null)] });
|
|
687
|
+
}
|
|
688
|
+
requestAnimationFrame(() => {
|
|
689
|
+
view.dispatch({ effects: [modalStateEffect.of(open)] });
|
|
690
|
+
});
|
|
691
|
+
}, [getMenuOptions]);
|
|
692
|
+
const handleActivate = useCallback(async ({ view, trigger }) => {
|
|
693
|
+
const item = getMenuItem(groupsRef.current, currentItem);
|
|
694
|
+
if (item) currentRef.current = item;
|
|
695
|
+
if (!open) handleOpenChange({
|
|
696
|
+
view,
|
|
697
|
+
open: true,
|
|
698
|
+
trigger
|
|
699
|
+
});
|
|
700
|
+
}, [open, handleOpenChange]);
|
|
701
|
+
const handleSelect = useCallback(({ view, item }) => {
|
|
702
|
+
const { range } = view.state.field(popoverStateField) ?? {};
|
|
703
|
+
if (range) view.dispatch({ changes: {
|
|
704
|
+
from: range.from,
|
|
705
|
+
to: range.to,
|
|
706
|
+
insert: ""
|
|
707
|
+
} });
|
|
708
|
+
item.onSelect?.({
|
|
709
|
+
view,
|
|
710
|
+
head: view.state.selection.main.head
|
|
711
|
+
});
|
|
712
|
+
view.focus();
|
|
713
|
+
}, []);
|
|
714
|
+
const handleCancel = useCallback(({ view }) => {
|
|
715
|
+
const { range, trigger } = view.state.field(popoverStateField) ?? {};
|
|
716
|
+
if (range && trigger) view.dispatch({ changes: {
|
|
717
|
+
...range,
|
|
718
|
+
insert: ""
|
|
719
|
+
} });
|
|
720
|
+
}, []);
|
|
721
|
+
const handleQueryChange = useCallback((query) => {
|
|
722
|
+
setQuery(query);
|
|
723
|
+
const context = contextRef.current;
|
|
724
|
+
if (context) updateGroups({
|
|
725
|
+
...context,
|
|
726
|
+
text: context.text + query
|
|
727
|
+
});
|
|
728
|
+
}, [updateGroups]);
|
|
729
|
+
const handleNavigate = useCallback((direction) => {
|
|
730
|
+
setCurrentItem((currentItem) => {
|
|
731
|
+
const next = direction === "up" ? getPreviousMenuItem(groupsRef.current, currentItem) : getNextMenuItem(groupsRef.current, currentItem);
|
|
732
|
+
if (!next) return currentItem;
|
|
733
|
+
currentRef.current = next;
|
|
734
|
+
return next.id;
|
|
735
|
+
});
|
|
736
|
+
}, []);
|
|
737
|
+
const serializedTrigger = Array.isArray(trigger) ? trigger.join(",") : trigger;
|
|
738
|
+
const serializedSearchTriggers = searchTriggers?.join(",");
|
|
739
|
+
return {
|
|
740
|
+
groupsRef,
|
|
741
|
+
extension: useMemo(() => {
|
|
742
|
+
return popover({
|
|
743
|
+
trigger,
|
|
744
|
+
triggerKey,
|
|
745
|
+
placeholder,
|
|
746
|
+
activateOnTyping,
|
|
747
|
+
onClose: ({ view }) => handleOpenChange({
|
|
748
|
+
view,
|
|
749
|
+
open: false
|
|
750
|
+
}),
|
|
751
|
+
onEnter: ({ view }) => {
|
|
752
|
+
if (currentRef.current) {
|
|
753
|
+
handleSelect({
|
|
754
|
+
view,
|
|
755
|
+
item: currentRef.current
|
|
756
|
+
});
|
|
757
|
+
return true;
|
|
758
|
+
}
|
|
759
|
+
return false;
|
|
760
|
+
},
|
|
761
|
+
onArrowUp: () => handleNavigate("up"),
|
|
762
|
+
onArrowDown: () => handleNavigate("down"),
|
|
763
|
+
onTextChange: async ({ view, pos, text, trigger }) => {
|
|
764
|
+
contextRef.current = {
|
|
765
|
+
view,
|
|
766
|
+
pos,
|
|
767
|
+
text,
|
|
768
|
+
trigger
|
|
769
|
+
};
|
|
770
|
+
setSearch(!!trigger && !!searchTriggers?.includes(trigger));
|
|
771
|
+
setQuery("");
|
|
772
|
+
await updateGroups({
|
|
773
|
+
view,
|
|
774
|
+
pos,
|
|
775
|
+
text,
|
|
776
|
+
trigger
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
}, [
|
|
781
|
+
handleOpenChange,
|
|
782
|
+
handleNavigate,
|
|
783
|
+
updateGroups,
|
|
784
|
+
serializedTrigger,
|
|
785
|
+
serializedSearchTriggers,
|
|
786
|
+
placeholder,
|
|
787
|
+
activateOnTyping
|
|
788
|
+
]),
|
|
789
|
+
currentItem,
|
|
790
|
+
open,
|
|
791
|
+
search,
|
|
792
|
+
query,
|
|
793
|
+
onOpenChange: handleOpenChange,
|
|
794
|
+
onActivate: handleActivate,
|
|
795
|
+
onSelect: handleSelect,
|
|
796
|
+
onCancel: handleCancel,
|
|
797
|
+
onQueryChange: handleQueryChange,
|
|
798
|
+
onNavigate: handleNavigate
|
|
799
|
+
};
|
|
800
|
+
};
|
|
801
|
+
//#endregion
|
|
802
|
+
//#region src/components/EditorToolbar/blocks.ts
|
|
803
|
+
var blockTypes = {
|
|
804
|
+
blockquote: "ph--quotes--regular",
|
|
805
|
+
codeblock: "ph--code-block--regular",
|
|
806
|
+
table: "ph--table--regular"
|
|
807
|
+
};
|
|
808
|
+
/** Add block actions to the builder. */
|
|
809
|
+
var addBlocks = (state, getView) => (builder) => {
|
|
810
|
+
const value = state?.blockQuote ? "blockquote" : state.blockType ?? "";
|
|
811
|
+
builder.group("block", {
|
|
812
|
+
label: ["block.label", { ns: translationKey }],
|
|
813
|
+
iconOnly: true,
|
|
814
|
+
variant: "toggleGroup",
|
|
815
|
+
selectCardinality: "single",
|
|
816
|
+
value
|
|
817
|
+
}, (group) => {
|
|
818
|
+
for (const [type, icon] of Object.entries(blockTypes)) {
|
|
819
|
+
const checked = type === value;
|
|
820
|
+
group.action(type, {
|
|
821
|
+
label: [`block.${type}.label`, { ns: translationKey }],
|
|
822
|
+
checked,
|
|
823
|
+
...type === "table" && { disabled: !!state.blankLine },
|
|
824
|
+
icon
|
|
825
|
+
}, () => {
|
|
826
|
+
const view = getView();
|
|
827
|
+
if (!view) return;
|
|
828
|
+
switch (type) {
|
|
829
|
+
case "blockquote":
|
|
830
|
+
checked ? removeBlockquote(view) : addBlockquote(view);
|
|
831
|
+
break;
|
|
832
|
+
case "codeblock":
|
|
833
|
+
checked ? removeCodeblock(view) : addCodeblock(view);
|
|
834
|
+
break;
|
|
835
|
+
case "table":
|
|
836
|
+
insertTable(view);
|
|
837
|
+
break;
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
};
|
|
843
|
+
//#endregion
|
|
844
|
+
//#region src/components/EditorToolbar/formatting.ts
|
|
845
|
+
var formats = {
|
|
846
|
+
strong: "ph--text-b--regular",
|
|
847
|
+
emphasis: "ph--text-italic--regular",
|
|
848
|
+
strikethrough: "ph--text-strikethrough--regular",
|
|
849
|
+
code: "ph--code--regular",
|
|
850
|
+
link: "ph--link--regular"
|
|
851
|
+
};
|
|
852
|
+
/** Add formatting actions to the builder. */
|
|
853
|
+
var addFormatting = (state, getView) => (builder) => {
|
|
854
|
+
const formatting = state;
|
|
855
|
+
builder.group("formatting", {
|
|
856
|
+
label: ["formatting.label", { ns: translationKey }],
|
|
857
|
+
iconOnly: true,
|
|
858
|
+
variant: "toggleGroup",
|
|
859
|
+
selectCardinality: "multiple",
|
|
860
|
+
value: Object.keys(formats).filter((key) => !!formatting[key])
|
|
861
|
+
}, (group) => {
|
|
862
|
+
for (const [type, icon] of Object.entries(formats)) {
|
|
863
|
+
const checked = !!formatting[type];
|
|
864
|
+
group.action(type, {
|
|
865
|
+
label: [`formatting.${type}.label`, { ns: translationKey }],
|
|
866
|
+
checked,
|
|
867
|
+
icon
|
|
868
|
+
}, () => {
|
|
869
|
+
const view = getView();
|
|
870
|
+
if (!view) return;
|
|
871
|
+
if (type === "link") {
|
|
872
|
+
checked ? removeLink(view) : addLink()(view);
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
setStyle(type === "strong" ? Inline.Strong : type === "emphasis" ? Inline.Emphasis : type === "strikethrough" ? Inline.Strikethrough : Inline.Code, !checked)(view);
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
};
|
|
880
|
+
//#endregion
|
|
881
|
+
//#region src/components/EditorToolbar/headings.ts
|
|
882
|
+
var headingIcons = {
|
|
883
|
+
0: "ph--paragraph--regular",
|
|
884
|
+
1: "ph--text-h-one--regular",
|
|
885
|
+
2: "ph--text-h-two--regular",
|
|
886
|
+
3: "ph--text-h-three--regular",
|
|
887
|
+
4: "ph--text-h-four--regular",
|
|
888
|
+
5: "ph--text-h-five--regular",
|
|
889
|
+
6: "ph--text-h-six--regular"
|
|
890
|
+
};
|
|
891
|
+
var computeHeadingValue = (state) => {
|
|
892
|
+
const blockType = state ? state.blockType : "paragraph";
|
|
893
|
+
const heading = blockType && /heading(\d)/.exec(blockType);
|
|
894
|
+
return heading ? heading[1] : blockType === "paragraph" || !blockType ? "0" : "";
|
|
895
|
+
};
|
|
896
|
+
/** Add heading actions to the builder. */
|
|
897
|
+
var addHeadings = (state, getView) => (builder) => {
|
|
898
|
+
const headingValue = computeHeadingValue(state);
|
|
899
|
+
builder.group("heading", {
|
|
900
|
+
label: ["heading.label", { ns: translationKey }],
|
|
901
|
+
icon: "ph--text-h--regular",
|
|
902
|
+
iconOnly: true,
|
|
903
|
+
variant: "dropdownMenu",
|
|
904
|
+
applyActive: true,
|
|
905
|
+
selectCardinality: "single",
|
|
906
|
+
value: headingValue
|
|
907
|
+
}, (group) => {
|
|
908
|
+
for (const [levelStr, icon] of Object.entries(headingIcons)) {
|
|
909
|
+
const level = parseInt(levelStr);
|
|
910
|
+
group.action(`heading--${levelStr}`, {
|
|
911
|
+
label: ["heading-level.label", {
|
|
912
|
+
count: level,
|
|
913
|
+
ns: translationKey
|
|
914
|
+
}],
|
|
915
|
+
icon,
|
|
916
|
+
checked: levelStr === headingValue
|
|
917
|
+
}, () => setHeading(level)(getView()));
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
};
|
|
921
|
+
//#endregion
|
|
922
|
+
//#region src/components/EditorToolbar/image.ts
|
|
923
|
+
/** Add image upload action to the builder. */
|
|
924
|
+
var addImageUpload = (onImageUpload) => (builder) => {
|
|
925
|
+
builder.action("image", {
|
|
926
|
+
label: ["image.label", { ns: translationKey }],
|
|
927
|
+
testId: "editor.toolbar.image",
|
|
928
|
+
icon: "ph--image-square--regular"
|
|
929
|
+
}, onImageUpload);
|
|
930
|
+
};
|
|
931
|
+
//#endregion
|
|
932
|
+
//#region src/components/EditorToolbar/lists.ts
|
|
933
|
+
var listStyles = {
|
|
934
|
+
bullet: "ph--list-bullets--regular",
|
|
935
|
+
ordered: "ph--list-numbers--regular",
|
|
936
|
+
task: "ph--list-checks--regular"
|
|
937
|
+
};
|
|
938
|
+
/** Add list actions to the builder. */
|
|
939
|
+
var addLists = (state, getView) => (builder) => {
|
|
940
|
+
const value = state.listStyle ?? "";
|
|
941
|
+
builder.group("list", {
|
|
942
|
+
label: ["list.label", { ns: translationKey }],
|
|
943
|
+
iconOnly: true,
|
|
944
|
+
variant: "toggleGroup",
|
|
945
|
+
selectCardinality: "single",
|
|
946
|
+
value
|
|
947
|
+
}, (group) => {
|
|
948
|
+
for (const [listStyle, icon] of Object.entries(listStyles)) {
|
|
949
|
+
const checked = value === listStyle;
|
|
950
|
+
group.action(`list-${listStyle}`, {
|
|
951
|
+
label: [`list.${listStyle}.label`, { ns: translationKey }],
|
|
952
|
+
checked,
|
|
953
|
+
icon
|
|
954
|
+
}, () => {
|
|
955
|
+
const view = getView();
|
|
956
|
+
if (!view) return;
|
|
957
|
+
toggleList(listStyle === "ordered" ? List.Ordered : listStyle === "bullet" ? List.Bullet : List.Task)(view);
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
};
|
|
962
|
+
//#endregion
|
|
963
|
+
//#region src/components/EditorToolbar/search.ts
|
|
964
|
+
/** Add search action to the builder. */
|
|
965
|
+
var addSearch = (getView) => (builder) => {
|
|
966
|
+
builder.action("search", {
|
|
967
|
+
label: ["search.label", { ns: translationKey }],
|
|
968
|
+
testId: "editor.toolbar.search",
|
|
969
|
+
icon: "ph--magnifying-glass--regular"
|
|
970
|
+
}, () => openSearchPanel(getView()));
|
|
971
|
+
};
|
|
972
|
+
//#endregion
|
|
973
|
+
//#region src/components/EditorToolbar/view-mode.ts
|
|
974
|
+
/**
|
|
975
|
+
* The built-in view modes: preview (rendered), source (raw edit), readonly (non-editable source). Typed
|
|
976
|
+
* so each `id` is an {@link EditorViewMode}, letting callers pass `item.id` to `onViewModeChange` without a cast.
|
|
977
|
+
*/
|
|
978
|
+
var defaultViewModeItems = [
|
|
979
|
+
{
|
|
980
|
+
id: "preview",
|
|
981
|
+
icon: "ph--eye--regular"
|
|
982
|
+
},
|
|
983
|
+
{
|
|
984
|
+
id: "source",
|
|
985
|
+
icon: "ph--pencil-simple--regular"
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
id: "readonly",
|
|
989
|
+
icon: "ph--pencil-slash--regular"
|
|
990
|
+
}
|
|
991
|
+
];
|
|
992
|
+
/**
|
|
993
|
+
* Add view mode actions to the builder. `items` defaults to the three built-in editor view modes;
|
|
994
|
+
* callers pass an extended list to surface contributed modes (see {@link ViewModeItem}) in the same
|
|
995
|
+
* single-select dropdown. The trigger tracks the checked item (via the group's `applyActive`).
|
|
996
|
+
*/
|
|
997
|
+
var addViewMode = (state, onViewModeChange, items = defaultViewModeItems) => (builder) => {
|
|
998
|
+
const value = state.viewMode ?? "source";
|
|
999
|
+
builder.group("viewMode", {
|
|
1000
|
+
label: ["view-mode.label", { ns: translationKey }],
|
|
1001
|
+
icon: "ph--eye--regular",
|
|
1002
|
+
iconOnly: true,
|
|
1003
|
+
variant: "dropdownMenu",
|
|
1004
|
+
applyActive: true,
|
|
1005
|
+
selectCardinality: "single",
|
|
1006
|
+
value
|
|
1007
|
+
}, (group) => {
|
|
1008
|
+
for (const item of items) {
|
|
1009
|
+
const checked = item.checked ?? item.id === value;
|
|
1010
|
+
group.action(`view-mode--${item.id}`, {
|
|
1011
|
+
label: item.label ?? [`view-mode.${item.id}.label`, { ns: translationKey }],
|
|
1012
|
+
checked,
|
|
1013
|
+
icon: item.icon
|
|
1014
|
+
}, () => item.onSelect ? item.onSelect() : onViewModeChange(item.id));
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
};
|
|
1018
|
+
//#endregion
|
|
1019
|
+
//#region src/components/EditorToolbar/EditorToolbar.tsx
|
|
1020
|
+
var SECTION_ROLE = "org.dxos.role.section";
|
|
1021
|
+
var EditorToolbar$1 = memo(({ classNames, role, attendableId, onAction, ...props }) => {
|
|
1022
|
+
const menuActions = useMarkdownMenuActions(props);
|
|
1023
|
+
return /* @__PURE__ */ jsx(ElevationProvider, {
|
|
1024
|
+
elevation: role === SECTION_ROLE ? "positioned" : "base",
|
|
1025
|
+
children: /* @__PURE__ */ jsx(Menu.Root, {
|
|
1026
|
+
...menuActions,
|
|
1027
|
+
attendableId,
|
|
1028
|
+
onAction,
|
|
1029
|
+
children: /* @__PURE__ */ jsx(Menu.Toolbar, { classNames })
|
|
1030
|
+
})
|
|
1031
|
+
});
|
|
1032
|
+
});
|
|
1033
|
+
var useMarkdownMenuActions = ({ state, getView, customActions, ...features }) => {
|
|
1034
|
+
return useMenuActions(useMemo(() => createMarkdownActions({
|
|
1035
|
+
state,
|
|
1036
|
+
getView,
|
|
1037
|
+
customActions,
|
|
1038
|
+
...features
|
|
1039
|
+
}), [
|
|
1040
|
+
state,
|
|
1041
|
+
getView,
|
|
1042
|
+
customActions,
|
|
1043
|
+
features?.showHeadings,
|
|
1044
|
+
features?.showFormatting,
|
|
1045
|
+
features?.showLists,
|
|
1046
|
+
features?.showBlocks,
|
|
1047
|
+
features?.showSearch,
|
|
1048
|
+
features?.onImageUpload,
|
|
1049
|
+
features?.onViewModeChange,
|
|
1050
|
+
features?.viewModes
|
|
1051
|
+
]));
|
|
1052
|
+
};
|
|
1053
|
+
var createMarkdownActions = ({ state, getView, customActions, ...features }) => {
|
|
1054
|
+
return Atom.make((get) => {
|
|
1055
|
+
const stateSnapshot = get(state);
|
|
1056
|
+
return MenuBuilder.make().subgraph(features?.showHeadings !== false && addHeadings(stateSnapshot, getView)).subgraph(features?.showFormatting !== false && addFormatting(stateSnapshot, getView)).subgraph(features?.showLists !== false && addLists(stateSnapshot, getView)).subgraph(features?.showBlocks !== false && addBlocks(stateSnapshot, getView)).subgraph(features?.onImageUpload && addImageUpload(features.onImageUpload)).separator("gap").subgraph(customActions && get(customActions)).subgraph(features?.showSearch !== false && addSearch(getView)).subgraph(features?.onViewModeChange && addViewMode(stateSnapshot, features.onViewModeChange, features.viewModes)).build();
|
|
1057
|
+
});
|
|
1058
|
+
};
|
|
1059
|
+
//#endregion
|
|
1060
|
+
//#region src/hooks/useBasicMarkdownExtensions.ts
|
|
1061
|
+
/**
|
|
1062
|
+
* Returns the standard CodeMirror extension stack for an inline markdown editor:
|
|
1063
|
+
* basic editor behaviors, themed syntax highlighting (theme mode read from {@link useThemeContext}),
|
|
1064
|
+
* markdown parsing, and visual decorations.
|
|
1065
|
+
*
|
|
1066
|
+
* Used by surfaces such as `AgentProperties` and `MagazineProperties` to render the
|
|
1067
|
+
* `instructions` field of an object as a small editable markdown block.
|
|
1068
|
+
*
|
|
1069
|
+
* The memoization depends on primitive option values plus the `extensions` array reference,
|
|
1070
|
+
* so callers passing fresh option literals each render still hit the cache.
|
|
1071
|
+
*/
|
|
1072
|
+
var useBasicMarkdownExtensions = ({ placeholder, decorate = true, readonly = false, extensions } = {}) => {
|
|
1073
|
+
const { themeMode } = useThemeContext();
|
|
1074
|
+
return useMemo(() => [
|
|
1075
|
+
createBasicExtensions({
|
|
1076
|
+
placeholder,
|
|
1077
|
+
readOnly: readonly
|
|
1078
|
+
}),
|
|
1079
|
+
createThemeExtensions({
|
|
1080
|
+
syntaxHighlighting: true,
|
|
1081
|
+
themeMode
|
|
1082
|
+
}),
|
|
1083
|
+
createMarkdownExtensions(),
|
|
1084
|
+
...decorate ? [decorateMarkdown()] : [],
|
|
1085
|
+
...extensions ?? []
|
|
1086
|
+
], [
|
|
1087
|
+
placeholder,
|
|
1088
|
+
themeMode,
|
|
1089
|
+
decorate,
|
|
1090
|
+
readonly,
|
|
1091
|
+
extensions
|
|
1092
|
+
]);
|
|
1093
|
+
};
|
|
1094
|
+
//#endregion
|
|
1095
|
+
//#region src/hooks/useTextEditor.ts
|
|
1096
|
+
var __dxlog_file$1 = "/__w/dxos/dxos/packages/ui/react-ui-editor/src/hooks/useTextEditor.ts";
|
|
1097
|
+
var instanceCount = 0;
|
|
1098
|
+
/**
|
|
1099
|
+
* Creates codemirror text editor.
|
|
1100
|
+
*/
|
|
1101
|
+
var useTextEditor = (props = {}, deps = []) => {
|
|
1102
|
+
const { id, doc, initialValue, extensions, autoFocus, scrollTo, selection, selectionEnd, debug } = useMemo(() => getProviderValue(props), deps ?? []);
|
|
1103
|
+
const [instanceId] = useState(() => `text-editor-${++instanceCount}`);
|
|
1104
|
+
const [view, setView] = useState(null);
|
|
1105
|
+
const parentRef = useRef(null);
|
|
1106
|
+
useEffect(() => {
|
|
1107
|
+
let view = null;
|
|
1108
|
+
if (parentRef.current) {
|
|
1109
|
+
log("create", {
|
|
1110
|
+
id,
|
|
1111
|
+
instanceId,
|
|
1112
|
+
doc: initialValue?.length ?? 0
|
|
1113
|
+
}, {
|
|
1114
|
+
"~LogMeta": "~LogMeta",
|
|
1115
|
+
F: __dxlog_file$1,
|
|
1116
|
+
L: 75,
|
|
1117
|
+
S: void 0
|
|
1118
|
+
});
|
|
1119
|
+
let initialSelection;
|
|
1120
|
+
if (selection?.anchor && initialValue?.length) {
|
|
1121
|
+
if (selection.anchor <= initialValue.length && (selection?.head ?? 0) <= initialValue.length) initialSelection = selection;
|
|
1122
|
+
} else if (selectionEnd && selection === void 0) {
|
|
1123
|
+
const index = initialValue?.indexOf("\n");
|
|
1124
|
+
initialSelection = { anchor: !index || index === -1 ? 0 : index };
|
|
1125
|
+
}
|
|
1126
|
+
const state = EditorState.create({
|
|
1127
|
+
doc: doc ?? initialValue,
|
|
1128
|
+
extensions: [
|
|
1129
|
+
id && documentId.of(id),
|
|
1130
|
+
extensions,
|
|
1131
|
+
EditorView.exceptionSink.of((err) => {
|
|
1132
|
+
log.catch(err, void 0, {
|
|
1133
|
+
"~LogMeta": "~LogMeta",
|
|
1134
|
+
F: __dxlog_file$1,
|
|
1135
|
+
L: 97,
|
|
1136
|
+
S: void 0
|
|
1137
|
+
});
|
|
1138
|
+
})
|
|
1139
|
+
].filter(isTruthy)
|
|
1140
|
+
});
|
|
1141
|
+
view = new EditorView({
|
|
1142
|
+
parent: parentRef.current,
|
|
1143
|
+
state,
|
|
1144
|
+
scrollTo: scrollTo ? EditorView.scrollIntoView(scrollTo, { yMargin: 96 }) : void 0,
|
|
1145
|
+
dispatchTransactions: debug ? debugDispatcher : void 0
|
|
1146
|
+
});
|
|
1147
|
+
if (selectionEnd && !initialSelection) {
|
|
1148
|
+
const { to } = view.state.doc.lineAt(0);
|
|
1149
|
+
if (to) view.dispatch({ selection: { anchor: to } });
|
|
1150
|
+
}
|
|
1151
|
+
setView(view);
|
|
1152
|
+
}
|
|
1153
|
+
return () => {
|
|
1154
|
+
log("destroy", { id }, {
|
|
1155
|
+
"~LogMeta": "~LogMeta",
|
|
1156
|
+
F: __dxlog_file$1,
|
|
1157
|
+
L: 122,
|
|
1158
|
+
S: void 0
|
|
1159
|
+
});
|
|
1160
|
+
view?.destroy();
|
|
1161
|
+
};
|
|
1162
|
+
}, deps);
|
|
1163
|
+
useEffect(() => {
|
|
1164
|
+
if (view) {
|
|
1165
|
+
if (scrollTo || selection) {
|
|
1166
|
+
if (selection && selection.anchor > view.state.doc.length) {
|
|
1167
|
+
log.warn("invalid selection", {
|
|
1168
|
+
length: view.state.doc.length,
|
|
1169
|
+
scrollTo,
|
|
1170
|
+
selection
|
|
1171
|
+
}, {
|
|
1172
|
+
"~LogMeta": "~LogMeta",
|
|
1173
|
+
F: __dxlog_file$1,
|
|
1174
|
+
L: 131,
|
|
1175
|
+
S: void 0
|
|
1176
|
+
});
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
view.dispatch(createEditorStateTransaction({
|
|
1180
|
+
scrollTo,
|
|
1181
|
+
selection
|
|
1182
|
+
}));
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}, [
|
|
1186
|
+
view,
|
|
1187
|
+
scrollTo,
|
|
1188
|
+
selection
|
|
1189
|
+
]);
|
|
1190
|
+
useEffect(() => {
|
|
1191
|
+
if (view && autoFocus) view.focus();
|
|
1192
|
+
}, [autoFocus, view]);
|
|
1193
|
+
return {
|
|
1194
|
+
parentRef,
|
|
1195
|
+
view,
|
|
1196
|
+
focusAttributes: {
|
|
1197
|
+
tabIndex: 0,
|
|
1198
|
+
onKeyDown: useCallback((event) => {
|
|
1199
|
+
const { key, target, currentTarget } = event;
|
|
1200
|
+
switch (key) {
|
|
1201
|
+
case "Escape":
|
|
1202
|
+
if (view?.state.field(modalStateField, false)) return;
|
|
1203
|
+
(view?.contentDOM.closest("[tabindex=\"0\"]"))?.focus();
|
|
1204
|
+
break;
|
|
1205
|
+
case "Enter":
|
|
1206
|
+
event.preventDefault();
|
|
1207
|
+
if (target === currentTarget) view?.focus();
|
|
1208
|
+
break;
|
|
1209
|
+
}
|
|
1210
|
+
}, [view])
|
|
1211
|
+
}
|
|
1212
|
+
};
|
|
1213
|
+
};
|
|
1214
|
+
//#endregion
|
|
1215
|
+
//#region src/components/Editor/EditorView.tsx
|
|
1216
|
+
/**
|
|
1217
|
+
* Minimal text editor.
|
|
1218
|
+
* NOTE: This shouold not be used with the automerge extension.
|
|
1219
|
+
*/
|
|
1220
|
+
var EditorView$2 = forwardRef(({ classNames, id, extensions, selectionEnd, focusable = true, value, onChange, ...props }, forwardedRef) => {
|
|
1221
|
+
const onChangeRef = useRef(onChange);
|
|
1222
|
+
onChangeRef.current = onChange;
|
|
1223
|
+
const { parentRef, focusAttributes, view } = useTextEditor(() => ({
|
|
1224
|
+
id,
|
|
1225
|
+
initialValue: value,
|
|
1226
|
+
selectionEnd,
|
|
1227
|
+
extensions: [extensions ?? [], EditorView.updateListener.of(({ view, docChanged, transactions }) => {
|
|
1228
|
+
if (!transactions.some((tr) => tr.annotation(Transaction.userEvent) === initialSync.value) && docChanged) onChangeRef.current?.(view.state.doc.toString());
|
|
1229
|
+
})],
|
|
1230
|
+
...props
|
|
1231
|
+
}), [
|
|
1232
|
+
id,
|
|
1233
|
+
extensions,
|
|
1234
|
+
selectionEnd
|
|
1235
|
+
]);
|
|
1236
|
+
useImperativeHandle(forwardedRef, () => {
|
|
1237
|
+
return createEditorController(view);
|
|
1238
|
+
}, [id, view]);
|
|
1239
|
+
useEffect(() => {
|
|
1240
|
+
if (!view || value === void 0) return;
|
|
1241
|
+
const next = value;
|
|
1242
|
+
if (view.state.doc.toString() === next) return;
|
|
1243
|
+
requestAnimationFrame(() => {
|
|
1244
|
+
if (view.state.doc.toString() === next) return;
|
|
1245
|
+
view.dispatch({
|
|
1246
|
+
annotations: initialSync,
|
|
1247
|
+
changes: [{
|
|
1248
|
+
from: 0,
|
|
1249
|
+
to: view.state.doc.length,
|
|
1250
|
+
insert: next
|
|
1251
|
+
}],
|
|
1252
|
+
selection: selectionEnd ? { anchor: next.length } : void 0
|
|
1253
|
+
});
|
|
1254
|
+
if (selectionEnd) view.focus();
|
|
1255
|
+
});
|
|
1256
|
+
}, [
|
|
1257
|
+
view,
|
|
1258
|
+
value,
|
|
1259
|
+
selectionEnd
|
|
1260
|
+
]);
|
|
1261
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1262
|
+
className: mx("w-full outline-hidden", classNames),
|
|
1263
|
+
...focusable ? focusAttributes : {},
|
|
1264
|
+
ref: parentRef
|
|
1265
|
+
});
|
|
1266
|
+
});
|
|
1267
|
+
//#endregion
|
|
1268
|
+
//#region src/components/Editor/Editor.tsx
|
|
1269
|
+
var __dxlog_file = "/__w/dxos/dxos/packages/ui/react-ui-editor/src/components/Editor/Editor.tsx";
|
|
1270
|
+
/**
|
|
1271
|
+
* Root component for the Editor compound component.
|
|
1272
|
+
* Provides context for all child components and manages the editor controller state.
|
|
1273
|
+
*/
|
|
1274
|
+
var EditorRoot = forwardRef(({ children, extensions: extensionsProp, widgets, viewMode, numItems, searchPlaceholder, ...props }, forwardedRef) => {
|
|
1275
|
+
const state = useMemo(() => Atom.make({ viewMode }), [viewMode]);
|
|
1276
|
+
const { groupsRef, extension, ...menuProps } = useEditorMenu(props);
|
|
1277
|
+
const extensions = useMemo(() => [extension, extensionsProp].filter(isNonNullable).flat(), [extension, extensionsProp]);
|
|
1278
|
+
const [controller, setController] = useState(noopController);
|
|
1279
|
+
useImperativeHandle(forwardedRef, () => controller, [controller]);
|
|
1280
|
+
return /* @__PURE__ */ jsx(EditorContextProvider, {
|
|
1281
|
+
controller,
|
|
1282
|
+
setController,
|
|
1283
|
+
extensions,
|
|
1284
|
+
widgets,
|
|
1285
|
+
state,
|
|
1286
|
+
children: /* @__PURE__ */ jsx(EditorMenuProvider, {
|
|
1287
|
+
getView: useCallback(() => controller?.view ?? null, [controller]),
|
|
1288
|
+
groups: groupsRef.current,
|
|
1289
|
+
numItems,
|
|
1290
|
+
searchPlaceholder,
|
|
1291
|
+
...menuProps,
|
|
1292
|
+
children
|
|
1293
|
+
})
|
|
1294
|
+
});
|
|
1295
|
+
});
|
|
1296
|
+
EditorRoot.displayName = "Editor.Root";
|
|
1297
|
+
var EDITOR_CONTENT_NAME = "Editor.Content";
|
|
1298
|
+
/**
|
|
1299
|
+
* Content component that wraps the toolbar and editor view area.
|
|
1300
|
+
*/
|
|
1301
|
+
var EditorContent = ({ classNames, children }) => {
|
|
1302
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1303
|
+
className: mx("grid grid-rows-[min-content_1fr] h-full overflow-hidden", classNames),
|
|
1304
|
+
children
|
|
1305
|
+
});
|
|
1306
|
+
};
|
|
1307
|
+
EditorContent.displayName = EDITOR_CONTENT_NAME;
|
|
1308
|
+
var EDITOR_VIEW_NAME = "Editor.View";
|
|
1309
|
+
/**
|
|
1310
|
+
* View component that renders the actual CodeMirror editor.
|
|
1311
|
+
* Automatically registers the editor controller with the context.
|
|
1312
|
+
*/
|
|
1313
|
+
var EditorView$1 = ({ extensions: providedExtensions, ...props }) => {
|
|
1314
|
+
const { extensions: additionalExtensions = [], setController } = useEditorContext(EDITOR_VIEW_NAME);
|
|
1315
|
+
const extensions = useMemo(() => [additionalExtensions, providedExtensions].filter(isNonNullable).flat(), [providedExtensions, additionalExtensions]);
|
|
1316
|
+
return /* @__PURE__ */ jsx(EditorView$2, {
|
|
1317
|
+
...props,
|
|
1318
|
+
extensions,
|
|
1319
|
+
ref: setController
|
|
1320
|
+
});
|
|
1321
|
+
};
|
|
1322
|
+
EditorView$1.displayName = EDITOR_VIEW_NAME;
|
|
1323
|
+
var EDITOR_TOOLBAR_NAME = "Editor.Toolbar";
|
|
1324
|
+
/**
|
|
1325
|
+
* Toolbar component that provides editor formatting and control actions.
|
|
1326
|
+
* Automatically connects to the editor view through context.
|
|
1327
|
+
*/
|
|
1328
|
+
var EditorToolbar = (props) => {
|
|
1329
|
+
const { controller, state } = useEditorContext(EDITOR_TOOLBAR_NAME);
|
|
1330
|
+
const getView = useCallback(() => {
|
|
1331
|
+
invariant(controller?.view, void 0, {
|
|
1332
|
+
"~LogMeta": "~LogMeta",
|
|
1333
|
+
F: __dxlog_file,
|
|
1334
|
+
L: 155,
|
|
1335
|
+
S: void 0,
|
|
1336
|
+
A: ["controller?.view", ""]
|
|
1337
|
+
});
|
|
1338
|
+
return controller?.view;
|
|
1339
|
+
}, [controller]);
|
|
1340
|
+
return /* @__PURE__ */ jsx(EditorToolbar$1, {
|
|
1341
|
+
...props,
|
|
1342
|
+
getView,
|
|
1343
|
+
state
|
|
1344
|
+
});
|
|
1345
|
+
};
|
|
1346
|
+
EditorToolbar.displayName = EDITOR_TOOLBAR_NAME;
|
|
1347
|
+
var EDITOR_BLOCKS_NAME = "Editor.Blocks";
|
|
1348
|
+
/**
|
|
1349
|
+
* Renders the xmlTags widget portals (embedded blocks) contributed via the editor's `setWidgets`
|
|
1350
|
+
* callback. Place inside `Editor.Root`; the widgets come from context (pass them to `Editor.Root`).
|
|
1351
|
+
*/
|
|
1352
|
+
var EditorBlocks = () => {
|
|
1353
|
+
const { widgets = [] } = useEditorContext(EDITOR_BLOCKS_NAME);
|
|
1354
|
+
return /* @__PURE__ */ jsx(Fragment$1, { children: widgets.map(({ id, root, Component, props }) => /* @__PURE__ */ jsx("div", { children: createPortal(/* @__PURE__ */ jsx(Component, { ...props }), root) }, id)) });
|
|
1355
|
+
};
|
|
1356
|
+
EditorBlocks.displayName = EDITOR_BLOCKS_NAME;
|
|
1357
|
+
var EDITOR_DIAGNOSTICS_NAME = "Editor.Diagnostics";
|
|
1358
|
+
/** Rough leaf count of a (recursively nested) CodeMirror extension tree. */
|
|
1359
|
+
var countExtensions = (extension) => extension == null ? 0 : Array.isArray(extension) ? extension.reduce((sum, child) => sum + countExtensions(child), 0) : 1;
|
|
1360
|
+
/**
|
|
1361
|
+
* Developer panel showing live editor state read from the CodeMirror `EditorState` (document size,
|
|
1362
|
+
* selection, viewport, configured-extension count). A debugging aid — gate it behind a dev/debug
|
|
1363
|
+
* setting; unrelated to `@codemirror/lint` diagnostics. Place inside `Editor.Root`, below the content.
|
|
1364
|
+
*/
|
|
1365
|
+
var EditorDiagnostics = ({ classNames }) => {
|
|
1366
|
+
const { controller, extensions } = useEditorContext(EDITOR_DIAGNOSTICS_NAME);
|
|
1367
|
+
const view = controller?.view ?? null;
|
|
1368
|
+
const [, setRevision] = useState(0);
|
|
1369
|
+
const lastState = useRef(null);
|
|
1370
|
+
useEffect(() => {
|
|
1371
|
+
if (!view) return;
|
|
1372
|
+
let frame = 0;
|
|
1373
|
+
const tick = () => {
|
|
1374
|
+
if (view.state !== lastState.current) {
|
|
1375
|
+
lastState.current = view.state;
|
|
1376
|
+
setRevision((revision) => revision + 1);
|
|
1377
|
+
}
|
|
1378
|
+
frame = requestAnimationFrame(tick);
|
|
1379
|
+
};
|
|
1380
|
+
frame = requestAnimationFrame(tick);
|
|
1381
|
+
return () => cancelAnimationFrame(frame);
|
|
1382
|
+
}, [view]);
|
|
1383
|
+
if (!view) return null;
|
|
1384
|
+
const { state } = view;
|
|
1385
|
+
const { doc, selection } = state;
|
|
1386
|
+
const { main } = selection;
|
|
1387
|
+
const head = doc.lineAt(main.head);
|
|
1388
|
+
const extensionCount = countExtensions(extensions);
|
|
1389
|
+
const item = (label, value) => /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsxs("span", {
|
|
1390
|
+
className: "text-subdued",
|
|
1391
|
+
children: [label, " "]
|
|
1392
|
+
}), value] });
|
|
1393
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1394
|
+
className: mx("flex flex-wrap gap-x-4 gap-y-0.5 border-bs border-separator px-2 py-1 text-xs font-mono text-description", classNames),
|
|
1395
|
+
"data-testid": "editor.diagnostics",
|
|
1396
|
+
children: [
|
|
1397
|
+
item("chars", doc.length),
|
|
1398
|
+
item("lines", doc.lines),
|
|
1399
|
+
item("cursor", main.empty ? `${head.number}:${main.head - head.from + 1}` : `${main.from}–${main.to} (${main.to - main.from})`),
|
|
1400
|
+
selection.ranges.length > 1 && item("ranges", selection.ranges.length),
|
|
1401
|
+
item("viewport", `${view.viewport.from}–${view.viewport.to}`),
|
|
1402
|
+
item("mode", state.readOnly ? "read-only" : "editable"),
|
|
1403
|
+
item("extensions", extensionCount)
|
|
1404
|
+
]
|
|
1405
|
+
});
|
|
1406
|
+
};
|
|
1407
|
+
EditorDiagnostics.displayName = EDITOR_DIAGNOSTICS_NAME;
|
|
1408
|
+
var Editor = {
|
|
1409
|
+
Root: EditorRoot,
|
|
1410
|
+
Toolbar: EditorToolbar,
|
|
1411
|
+
Content: EditorContent,
|
|
1412
|
+
View: EditorView$1,
|
|
1413
|
+
Blocks: EditorBlocks,
|
|
1414
|
+
Diagnostics: EditorDiagnostics
|
|
1415
|
+
};
|
|
1416
|
+
//#endregion
|
|
1417
|
+
//#region src/components/EditorPreviewProvider/EditorPreviewContext.ts
|
|
1418
|
+
var [EditorPreviewContextProvider, useEditorPreview] = createContext("PreviewPopover", {});
|
|
1419
|
+
//#endregion
|
|
1420
|
+
//#region src/components/EditorPreviewProvider/EditorPreviewProvider.tsx
|
|
1421
|
+
/**
|
|
1422
|
+
* Standalone anchor-preview popover for use outside Composer (storybook, tests).
|
|
1423
|
+
* In Composer, PreviewPlugin listens on window for DX_ANCHOR_ACTIVATE events and
|
|
1424
|
+
* dispatches LayoutOperation.UpdatePopover instead; this provider is not needed there.
|
|
1425
|
+
*/
|
|
1426
|
+
var EditorPreviewProvider = ({ children, onLookup }) => {
|
|
1427
|
+
const triggerRef = useRef(null);
|
|
1428
|
+
const [value, setValue] = useState({});
|
|
1429
|
+
const [open, setOpen] = useState(false);
|
|
1430
|
+
const handleActivate = useCallback((event) => {
|
|
1431
|
+
const { dxn, label, trigger } = event;
|
|
1432
|
+
setValue((value) => ({
|
|
1433
|
+
...value,
|
|
1434
|
+
link: {
|
|
1435
|
+
label,
|
|
1436
|
+
dxn
|
|
1437
|
+
},
|
|
1438
|
+
pending: true
|
|
1439
|
+
}));
|
|
1440
|
+
triggerRef.current = trigger;
|
|
1441
|
+
queueMicrotask(() => setOpen(true));
|
|
1442
|
+
onLookup?.({
|
|
1443
|
+
label,
|
|
1444
|
+
dxn
|
|
1445
|
+
}).then((target) => setValue((value) => ({
|
|
1446
|
+
...value,
|
|
1447
|
+
target: target ?? void 0,
|
|
1448
|
+
pending: false
|
|
1449
|
+
})));
|
|
1450
|
+
}, [onLookup]);
|
|
1451
|
+
const [root, setRoot] = useState(null);
|
|
1452
|
+
useEffect(() => {
|
|
1453
|
+
if (!root || !handleActivate) return;
|
|
1454
|
+
return addEventListener(root, DX_ANCHOR_ACTIVATE, handleActivate, {
|
|
1455
|
+
capture: true,
|
|
1456
|
+
passive: false
|
|
1457
|
+
});
|
|
1458
|
+
}, [root, handleActivate]);
|
|
1459
|
+
return /* @__PURE__ */ jsx(EditorPreviewContextProvider, {
|
|
1460
|
+
pending: value.pending,
|
|
1461
|
+
link: value.link,
|
|
1462
|
+
target: value.target,
|
|
1463
|
+
children: /* @__PURE__ */ jsxs(Popover.Root, {
|
|
1464
|
+
open,
|
|
1465
|
+
onOpenChange: setOpen,
|
|
1466
|
+
children: [/* @__PURE__ */ jsx(Popover.VirtualTrigger, { virtualRef: triggerRef }), /* @__PURE__ */ jsx("div", {
|
|
1467
|
+
className: "contents",
|
|
1468
|
+
ref: setRoot,
|
|
1469
|
+
children
|
|
1470
|
+
})]
|
|
1471
|
+
})
|
|
1472
|
+
});
|
|
1473
|
+
};
|
|
1474
|
+
//#endregion
|
|
1475
|
+
export { Editor, EditorMenuProvider, EditorPreviewProvider, addViewMode, createEditorController, createMenuGroup, defaultViewModeItems, filterMenuGroups, formattingCommands, getMenuItem, getNextMenuItem, getPreviousMenuItem, linkSlashCommands, noopController, popover, popoverRangeEffect, popoverStateField, useBasicMarkdownExtensions, useEditorContext, useEditorMenu, useEditorPreview, useTextEditor };
|
|
1476
|
+
|
|
1477
|
+
//# sourceMappingURL=index.mjs.map
|