@dxos/react-ui-editor 0.8.4-main.dedc0f3 → 0.8.4-main.ead640a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/browser/{chunk-22UMM3QJ.mjs → chunk-HL3YF6WC.mjs} +2 -2
- package/dist/lib/browser/chunk-HL3YF6WC.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +5482 -5519
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +71 -1
- package/dist/lib/browser/testing/index.mjs.map +4 -4
- package/dist/lib/browser/types/index.mjs +1 -1
- package/dist/lib/node-esm/{chunk-YXYQPV6R.mjs → chunk-YJZGD3LY.mjs} +2 -2
- package/dist/lib/node-esm/chunk-YJZGD3LY.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +5482 -5519
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +71 -1
- package/dist/lib/node-esm/testing/index.mjs.map +4 -4
- package/dist/lib/node-esm/types/index.mjs +1 -1
- package/dist/types/src/components/Editor/Editor.d.ts +24 -9
- package/dist/types/src/components/Editor/Editor.d.ts.map +1 -1
- package/dist/types/src/components/Editor/Editor.stories.d.ts +27 -0
- package/dist/types/src/components/Editor/Editor.stories.d.ts.map +1 -0
- package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/util.d.ts +1 -1
- package/dist/types/src/components/index.d.ts +0 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/extensions/{autocomplete.d.ts → autocomplete/autocomplete.d.ts} +1 -1
- package/dist/types/src/extensions/autocomplete/autocomplete.d.ts.map +1 -0
- package/dist/types/src/extensions/autocomplete/index.d.ts +5 -0
- package/dist/types/src/extensions/autocomplete/index.d.ts.map +1 -0
- package/dist/types/src/extensions/autocomplete/match.d.ts +13 -0
- package/dist/types/src/extensions/autocomplete/match.d.ts.map +1 -0
- package/dist/types/src/extensions/autocomplete/placeholder.d.ts +20 -0
- package/dist/types/src/extensions/autocomplete/placeholder.d.ts.map +1 -0
- package/dist/types/src/extensions/autocomplete/typeahead.d.ts +10 -0
- package/dist/types/src/extensions/autocomplete/typeahead.d.ts.map +1 -0
- 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/automerge.stories.d.ts +1 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/sync.d.ts +2 -2
- package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
- package/dist/types/src/extensions/autoscroll.d.ts +2 -2
- package/dist/types/src/extensions/autoscroll.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts +7 -2
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/extensions/focus.d.ts.map +1 -1
- package/dist/types/src/extensions/folding.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +2 -1
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/json.d.ts +1 -1
- package/dist/types/src/extensions/json.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
- package/dist/types/src/extensions/modes.d.ts +1 -1
- package/dist/types/src/extensions/modes.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner/menu.d.ts +8 -0
- package/dist/types/src/extensions/outliner/menu.d.ts.map +1 -0
- package/dist/types/src/extensions/popover/PopoverMenuProvider.d.ts +36 -0
- package/dist/types/src/extensions/popover/PopoverMenuProvider.d.ts.map +1 -0
- package/dist/types/src/extensions/popover/index.d.ts +8 -0
- package/dist/types/src/extensions/popover/index.d.ts.map +1 -0
- package/dist/types/src/extensions/popover/menu-presets.d.ts +4 -0
- package/dist/types/src/extensions/popover/menu-presets.d.ts.map +1 -0
- package/dist/types/src/extensions/popover/menu.d.ts +24 -0
- package/dist/types/src/extensions/popover/menu.d.ts.map +1 -0
- package/dist/types/src/extensions/popover/modal.d.ts +7 -0
- package/dist/types/src/extensions/popover/modal.d.ts.map +1 -0
- package/dist/types/src/extensions/popover/popover.d.ts +47 -0
- package/dist/types/src/extensions/popover/popover.d.ts.map +1 -0
- package/dist/types/src/extensions/popover/usePopoverMenu.d.ts +34 -0
- package/dist/types/src/extensions/popover/usePopoverMenu.d.ts.map +1 -0
- package/dist/types/src/extensions/popover/util.d.ts +8 -0
- package/dist/types/src/extensions/popover/util.d.ts.map +1 -0
- package/dist/types/src/extensions/preview/preview.d.ts +0 -2
- package/dist/types/src/extensions/preview/preview.d.ts.map +1 -1
- package/dist/types/src/extensions/state.d.ts +2 -0
- package/dist/types/src/extensions/state.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/streamer.d.ts.map +1 -1
- package/dist/types/src/extensions/tags/xml-tags.d.ts +1 -0
- package/dist/types/src/extensions/tags/xml-tags.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextEditor.d.ts +4 -8
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
- package/dist/types/src/stories/{Command.stories.d.ts → CommandDialog.stories.d.ts} +2 -3
- package/dist/types/src/stories/CommandDialog.stories.d.ts.map +1 -0
- package/dist/types/src/stories/Comments.stories.d.ts +3 -4
- package/dist/types/src/stories/Comments.stories.d.ts.map +1 -1
- package/dist/types/src/stories/EditorToolbar.stories.d.ts +1 -2
- package/dist/types/src/stories/EditorToolbar.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Experimental.stories.d.ts +3 -4
- package/dist/types/src/stories/Experimental.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Markdown.stories.d.ts +3 -4
- package/dist/types/src/stories/Markdown.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Outliner.stories.d.ts +0 -1
- package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -1
- package/dist/types/src/stories/{CommandMenu.stories.d.ts → Popover.stories.d.ts} +6 -6
- package/dist/types/src/stories/Popover.stories.d.ts.map +1 -0
- package/dist/types/src/stories/Preview.stories.d.ts +3 -4
- package/dist/types/src/stories/Preview.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Tags.stories.d.ts +0 -1
- package/dist/types/src/stories/Tags.stories.d.ts.map +1 -1
- package/dist/types/src/stories/TextEditor.stories.d.ts +3 -5
- package/dist/types/src/stories/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/stories/components/EditorStory.d.ts +5 -5
- package/dist/types/src/stories/components/EditorStory.d.ts.map +1 -1
- package/dist/types/src/styles/theme.d.ts.map +1 -1
- package/dist/types/src/testing/PreviewPopover.d.ts +20 -0
- package/dist/types/src/testing/PreviewPopover.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +1 -0
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/types/types.d.ts +1 -1
- package/dist/types/src/types/types.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +0 -1
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +55 -52
- package/src/components/Editor/Editor.stories.tsx +69 -0
- package/src/components/Editor/Editor.tsx +57 -14
- package/src/components/EditorToolbar/EditorToolbar.tsx +1 -0
- package/src/components/index.ts +0 -1
- package/src/extensions/{autocomplete.ts → autocomplete/autocomplete.ts} +2 -1
- package/src/extensions/autocomplete/index.ts +8 -0
- package/src/extensions/autocomplete/match.ts +46 -0
- package/src/extensions/{command → autocomplete}/placeholder.ts +21 -17
- package/src/extensions/{command → autocomplete}/typeahead.ts +6 -48
- package/src/extensions/automerge/automerge.stories.tsx +8 -8
- package/src/extensions/automerge/automerge.ts +28 -9
- package/src/extensions/automerge/sync.ts +7 -3
- package/src/extensions/autoscroll.ts +43 -37
- package/src/extensions/factories.ts +41 -12
- package/src/extensions/focus.ts +5 -4
- package/src/extensions/folding.tsx +4 -6
- package/src/extensions/hashtag.tsx +2 -2
- package/src/extensions/index.ts +2 -1
- package/src/extensions/json.ts +1 -1
- package/src/extensions/markdown/bundle.ts +16 -4
- package/src/extensions/markdown/decorate.ts +1 -0
- package/src/extensions/markdown/link.ts +3 -0
- package/src/extensions/modes.ts +2 -2
- package/src/extensions/{command/floating-menu.ts → outliner/menu.ts} +15 -20
- package/src/extensions/outliner/outliner.ts +3 -3
- package/src/extensions/popover/PopoverMenuProvider.tsx +221 -0
- package/src/extensions/popover/index.ts +12 -0
- package/src/extensions/popover/menu-presets.ts +124 -0
- package/src/extensions/popover/menu.ts +67 -0
- package/src/extensions/popover/modal.ts +24 -0
- package/src/extensions/popover/popover.ts +291 -0
- package/src/extensions/popover/usePopoverMenu.ts +173 -0
- package/src/extensions/popover/util.ts +29 -0
- package/src/extensions/preview/index.ts +1 -1
- package/src/extensions/preview/preview.ts +0 -5
- package/src/extensions/selection.ts +2 -2
- package/src/extensions/state.ts +7 -0
- package/src/extensions/tags/streamer.ts +4 -5
- package/src/extensions/tags/xml-tags.ts +59 -1
- package/src/hooks/useTextEditor.ts +27 -39
- package/src/stories/{Command.stories.tsx → CommandDialog.stories.tsx} +10 -22
- package/src/stories/Comments.stories.tsx +5 -5
- package/src/stories/EditorToolbar.stories.tsx +6 -5
- package/src/stories/Experimental.stories.tsx +6 -6
- package/src/stories/Markdown.stories.tsx +5 -5
- package/src/stories/Outliner.stories.tsx +42 -26
- package/src/stories/Popover.stories.tsx +163 -0
- package/src/stories/Preview.stories.tsx +9 -9
- package/src/stories/Tags.stories.tsx +5 -5
- package/src/stories/TextEditor.stories.tsx +7 -32
- package/src/stories/components/EditorStory.tsx +7 -5
- package/src/styles/theme.ts +12 -10
- package/src/{components/Popover/RefDropdownMenu.tsx → testing/PreviewPopover.tsx} +20 -29
- package/src/testing/index.ts +1 -0
- package/src/types/types.ts +1 -1
- package/src/util/index.ts +0 -1
- package/dist/lib/browser/chunk-22UMM3QJ.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-YXYQPV6R.mjs.map +0 -7
- package/dist/types/src/components/Popover/CommandMenu.d.ts +0 -34
- package/dist/types/src/components/Popover/CommandMenu.d.ts.map +0 -1
- package/dist/types/src/components/Popover/RefDropdownMenu.d.ts +0 -14
- package/dist/types/src/components/Popover/RefDropdownMenu.d.ts.map +0 -1
- package/dist/types/src/components/Popover/RefPopover.d.ts +0 -37
- package/dist/types/src/components/Popover/RefPopover.d.ts.map +0 -1
- package/dist/types/src/components/Popover/index.d.ts +0 -4
- package/dist/types/src/components/Popover/index.d.ts.map +0 -1
- package/dist/types/src/extensions/autocomplete.d.ts.map +0 -1
- package/dist/types/src/extensions/command/action.d.ts +0 -17
- package/dist/types/src/extensions/command/action.d.ts.map +0 -1
- package/dist/types/src/extensions/command/command-menu.d.ts +0 -20
- package/dist/types/src/extensions/command/command-menu.d.ts.map +0 -1
- package/dist/types/src/extensions/command/command.d.ts +0 -6
- package/dist/types/src/extensions/command/command.d.ts.map +0 -1
- package/dist/types/src/extensions/command/floating-menu.d.ts +0 -7
- package/dist/types/src/extensions/command/floating-menu.d.ts.map +0 -1
- package/dist/types/src/extensions/command/hint.d.ts +0 -19
- package/dist/types/src/extensions/command/hint.d.ts.map +0 -1
- package/dist/types/src/extensions/command/index.d.ts +0 -7
- package/dist/types/src/extensions/command/index.d.ts.map +0 -1
- package/dist/types/src/extensions/command/placeholder.d.ts +0 -10
- package/dist/types/src/extensions/command/placeholder.d.ts.map +0 -1
- package/dist/types/src/extensions/command/state.d.ts +0 -16
- package/dist/types/src/extensions/command/state.d.ts.map +0 -1
- package/dist/types/src/extensions/command/typeahead.d.ts +0 -22
- package/dist/types/src/extensions/command/typeahead.d.ts.map +0 -1
- package/dist/types/src/extensions/command/useCommandMenu.d.ts +0 -26
- package/dist/types/src/extensions/command/useCommandMenu.d.ts.map +0 -1
- package/dist/types/src/stories/Command.stories.d.ts.map +0 -1
- package/dist/types/src/stories/CommandMenu.stories.d.ts.map +0 -1
- package/dist/types/src/util/domino.d.ts +0 -18
- package/dist/types/src/util/domino.d.ts.map +0 -1
- package/src/components/Popover/CommandMenu.tsx +0 -279
- package/src/components/Popover/RefPopover.tsx +0 -117
- package/src/components/Popover/index.ts +0 -7
- package/src/extensions/command/action.ts +0 -56
- package/src/extensions/command/command-menu.ts +0 -211
- package/src/extensions/command/command.ts +0 -34
- package/src/extensions/command/hint.ts +0 -103
- package/src/extensions/command/index.ts +0 -10
- package/src/extensions/command/state.ts +0 -90
- package/src/extensions/command/useCommandMenu.ts +0 -119
- package/src/stories/CommandMenu.stories.tsx +0 -160
- package/src/util/domino.ts +0 -51
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension, Prec, RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
|
|
6
|
+
import {
|
|
7
|
+
Decoration,
|
|
8
|
+
type DecorationSet,
|
|
9
|
+
EditorView,
|
|
10
|
+
type KeyBinding,
|
|
11
|
+
ViewPlugin,
|
|
12
|
+
type ViewUpdate,
|
|
13
|
+
keymap,
|
|
14
|
+
} from '@codemirror/view';
|
|
15
|
+
|
|
16
|
+
import { isNonNullable, isTruthy } from '@dxos/util';
|
|
17
|
+
|
|
18
|
+
import { type Range } from '../../types';
|
|
19
|
+
import { type PlaceholderOptions, placeholder } from '../autocomplete';
|
|
20
|
+
|
|
21
|
+
import { modalStateField } from './modal';
|
|
22
|
+
|
|
23
|
+
const DELIMITERS = [' ', ':'];
|
|
24
|
+
|
|
25
|
+
export type PopoverOptions = {
|
|
26
|
+
trigger?: string | string[];
|
|
27
|
+
triggerKey?: string;
|
|
28
|
+
placeholder?: Partial<PlaceholderOptions>;
|
|
29
|
+
delimiters?: string[];
|
|
30
|
+
|
|
31
|
+
// TODO(burdon): Auto.
|
|
32
|
+
// activateOnTyping?: boolean;
|
|
33
|
+
|
|
34
|
+
// Trigger update.
|
|
35
|
+
onTextChange?: (event: { view: EditorView; pos: number; text: string; trigger?: string }) => void;
|
|
36
|
+
onClose?: (event: { view: EditorView }) => void;
|
|
37
|
+
|
|
38
|
+
// Menu specific.
|
|
39
|
+
onEnter?: (event: { view: EditorView }) => void;
|
|
40
|
+
onArrowUp?: (event: { view: EditorView }) => void;
|
|
41
|
+
onArrowDown?: (event: { view: EditorView }) => void;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Creates a popover that appears when the trigger character is inserted.
|
|
46
|
+
* This can be used for context menus or autocompletion.
|
|
47
|
+
*/
|
|
48
|
+
export const popover = (options: PopoverOptions = {}): Extension => {
|
|
49
|
+
return [
|
|
50
|
+
Prec.highest(popoverKeymap(options)),
|
|
51
|
+
popoverStateField,
|
|
52
|
+
popoverTriggerListener(options),
|
|
53
|
+
popoverAnchorDecoration(options),
|
|
54
|
+
modalStateField,
|
|
55
|
+
options.trigger &&
|
|
56
|
+
placeholder({
|
|
57
|
+
// TODO(burdon): Translations.
|
|
58
|
+
content: `Press '${Array.isArray(options.trigger) ? options.trigger[0] : options.trigger}' for commands`,
|
|
59
|
+
...options.placeholder,
|
|
60
|
+
}),
|
|
61
|
+
].filter(isTruthy);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Listen for selection and document changes.
|
|
66
|
+
*/
|
|
67
|
+
const popoverTriggerListener = (options: PopoverOptions) =>
|
|
68
|
+
EditorView.updateListener.of(({ view, docChanged }) => {
|
|
69
|
+
const { range: activeRange, trigger } = view.state.field(popoverStateField) ?? {};
|
|
70
|
+
if (!activeRange) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const text = view.state.doc.sliceString(activeRange.from, activeRange.to);
|
|
75
|
+
const selection = view.state.selection.main;
|
|
76
|
+
const shouldClose =
|
|
77
|
+
// Trigger deleted.
|
|
78
|
+
(trigger ? trigger !== text[0] : false) ||
|
|
79
|
+
// Whitespace in text.
|
|
80
|
+
/\s/.test(trigger ? text.slice(1) : text) ||
|
|
81
|
+
// Cursor moved before the range.
|
|
82
|
+
selection.head < activeRange.from ||
|
|
83
|
+
// Cursor moved after the range (+1 to handle selection changing before doc).
|
|
84
|
+
selection.head > activeRange.to + 1;
|
|
85
|
+
|
|
86
|
+
const nextRange = shouldClose ? null : docChanged ? { from: activeRange.from, to: selection.head } : activeRange;
|
|
87
|
+
if (nextRange !== activeRange) {
|
|
88
|
+
view.dispatch({
|
|
89
|
+
effects: popoverRangeEffect.of(nextRange ? { range: nextRange, trigger } : null),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (shouldClose) {
|
|
94
|
+
options.onClose?.({ view });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Popover navigation.
|
|
100
|
+
*/
|
|
101
|
+
const popoverKeymap = (options: PopoverOptions) => {
|
|
102
|
+
const triggers = Array.isArray(options.trigger) ? options.trigger : [options.trigger];
|
|
103
|
+
return keymap.of(
|
|
104
|
+
[
|
|
105
|
+
// Prefix triggers.
|
|
106
|
+
...triggers.filter(isNonNullable).map((trigger) => ({
|
|
107
|
+
key: trigger,
|
|
108
|
+
run: (view: EditorView) => {
|
|
109
|
+
// Determine if we should trigger the popover:
|
|
110
|
+
// 1. Empty lines or at the beginning of a line
|
|
111
|
+
// 2. When there's a preceding space
|
|
112
|
+
const selection = view.state.selection.main;
|
|
113
|
+
const line = view.state.doc.lineAt(selection.head);
|
|
114
|
+
if (
|
|
115
|
+
line.text.trim() === '' ||
|
|
116
|
+
selection.head === line.from ||
|
|
117
|
+
(selection.head > line.from && line.text[selection.head - line.from - 1] === ' ')
|
|
118
|
+
) {
|
|
119
|
+
// Insert and select the trigger.
|
|
120
|
+
view.dispatch({
|
|
121
|
+
changes: { from: selection.head, insert: trigger },
|
|
122
|
+
selection: { anchor: selection.head + 1, head: selection.head + 1 },
|
|
123
|
+
effects: popoverRangeEffect.of({ trigger, range: { from: selection.head, to: selection.head + 1 } }),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return false;
|
|
130
|
+
},
|
|
131
|
+
})),
|
|
132
|
+
|
|
133
|
+
//
|
|
134
|
+
// Custom trigger.
|
|
135
|
+
//
|
|
136
|
+
options.triggerKey &&
|
|
137
|
+
({
|
|
138
|
+
key: options.triggerKey,
|
|
139
|
+
run: (view: EditorView) => {
|
|
140
|
+
const selection = view.state.selection.main;
|
|
141
|
+
const line = view.state.doc.lineAt(selection.head);
|
|
142
|
+
|
|
143
|
+
// Get last word.
|
|
144
|
+
let str = line.text.slice(0, selection.head - line.from);
|
|
145
|
+
const idx = getLastIndexOf(str, options.delimiters ?? DELIMITERS);
|
|
146
|
+
if (idx !== -1) {
|
|
147
|
+
str = str.slice(idx + 1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Create anchor even if zero length (append space).
|
|
151
|
+
const from = line.from + idx;
|
|
152
|
+
console.log('effect', from + 1, selection.head);
|
|
153
|
+
view.dispatch({
|
|
154
|
+
effects: popoverRangeEffect.of({ range: { from: from + 1, to: selection.head } }),
|
|
155
|
+
changes:
|
|
156
|
+
selection.head === view.state.doc.length
|
|
157
|
+
? { from: from + 1, to: selection.head, insert: ' ' }
|
|
158
|
+
: undefined,
|
|
159
|
+
});
|
|
160
|
+
return true;
|
|
161
|
+
},
|
|
162
|
+
} satisfies KeyBinding),
|
|
163
|
+
|
|
164
|
+
//
|
|
165
|
+
// Nav keys.
|
|
166
|
+
//
|
|
167
|
+
{
|
|
168
|
+
key: 'ArrowUp',
|
|
169
|
+
run: (view: EditorView) => {
|
|
170
|
+
const range = view.state.field(popoverStateField)?.range;
|
|
171
|
+
if (range) {
|
|
172
|
+
options.onArrowUp?.({ view });
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return false;
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
key: 'ArrowDown',
|
|
181
|
+
run: (view: EditorView) => {
|
|
182
|
+
const range = view.state.field(popoverStateField)?.range;
|
|
183
|
+
if (range) {
|
|
184
|
+
options.onArrowDown?.({ view });
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return false;
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
key: 'Enter',
|
|
193
|
+
run: (view: EditorView) => {
|
|
194
|
+
const range = view.state.field(popoverStateField)?.range;
|
|
195
|
+
if (range) {
|
|
196
|
+
view.dispatch({ changes: { from: range.from, to: range.to, insert: '' } });
|
|
197
|
+
options.onEnter?.({ view });
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return false;
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
].filter(isTruthy),
|
|
205
|
+
);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Creates a <dx-anchor> tag, which is used to anchor the Popver.
|
|
210
|
+
*/
|
|
211
|
+
const popoverAnchorDecoration = (options: PopoverOptions) => {
|
|
212
|
+
return ViewPlugin.fromClass(
|
|
213
|
+
class {
|
|
214
|
+
_decorations: DecorationSet = Decoration.none;
|
|
215
|
+
|
|
216
|
+
constructor(readonly view: EditorView) {}
|
|
217
|
+
|
|
218
|
+
// TODO(wittjosiah): The decorations are repainted on every update, this occasionally causes menu to flicker.
|
|
219
|
+
update({ view, transactions }: ViewUpdate) {
|
|
220
|
+
const builder = new RangeSetBuilder<Decoration>();
|
|
221
|
+
const { range, trigger } = view.state.field(popoverStateField) ?? {};
|
|
222
|
+
if (range) {
|
|
223
|
+
// Check if we should show the widget (only if cursor is within the active command range).
|
|
224
|
+
const selection = view.state.selection.main;
|
|
225
|
+
const showWidget = selection.head >= range.from && selection.head <= range.to;
|
|
226
|
+
console.log('update', showWidget, range.from, range.to + 1);
|
|
227
|
+
if (showWidget) {
|
|
228
|
+
builder.add(
|
|
229
|
+
range.from,
|
|
230
|
+
range.to + 1,
|
|
231
|
+
Decoration.mark({
|
|
232
|
+
tagName: 'dx-anchor',
|
|
233
|
+
class: 'cm-popover-trigger',
|
|
234
|
+
attributes: {
|
|
235
|
+
'data-visible-focus': 'false',
|
|
236
|
+
'data-auto-trigger': 'true',
|
|
237
|
+
'data-trigger': trigger ?? options.triggerKey ?? '',
|
|
238
|
+
},
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const rangeChanged = transactions.some((tr) => tr.effects.some((effect) => effect.is(popoverRangeEffect)));
|
|
244
|
+
if (rangeChanged) {
|
|
245
|
+
// NOTE: Content skips the trigger character.
|
|
246
|
+
const content = view.state.sliceDoc(range.from + (trigger ? trigger.length : 0), range.to);
|
|
247
|
+
options.onTextChange?.({ view, pos: selection.head, text: content, trigger });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this._decorations = builder.finish();
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
decorations: (v) => v._decorations,
|
|
256
|
+
},
|
|
257
|
+
);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
type PopoverState = {
|
|
261
|
+
/**
|
|
262
|
+
* Trigger prefix (in document).
|
|
263
|
+
*/
|
|
264
|
+
trigger?: string;
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Current document completion range.
|
|
268
|
+
*/
|
|
269
|
+
range: Range;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// State effects for managing popover state.
|
|
273
|
+
export const popoverRangeEffect = StateEffect.define<PopoverState | null>();
|
|
274
|
+
|
|
275
|
+
// State field to track the active popover trigger range.
|
|
276
|
+
export const popoverStateField = StateField.define<PopoverState | null>({
|
|
277
|
+
create: () => null,
|
|
278
|
+
update: (value, tr) => {
|
|
279
|
+
let newValue = value;
|
|
280
|
+
for (const effect of tr.effects) {
|
|
281
|
+
if (effect.is(popoverRangeEffect)) {
|
|
282
|
+
newValue = effect.value;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return newValue;
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const getLastIndexOf = (str: string, delimiters: string[]) =>
|
|
291
|
+
Math.max(...delimiters.map((delim) => str.lastIndexOf(delim)));
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
6
|
+
import { type EditorState } from '@codemirror/state';
|
|
7
|
+
import { type RefObject, useCallback, useMemo, useRef, useState } from 'react';
|
|
8
|
+
|
|
9
|
+
import { invariant } from '@dxos/invariant';
|
|
10
|
+
import { type MaybePromise } from '@dxos/util';
|
|
11
|
+
|
|
12
|
+
import { type PopoverMenuGroup, type PopoverMenuItem } from './menu';
|
|
13
|
+
import { filterMenuGroups, getMenuItem, getNextMenuItem, getPreviousMenuItem } from './menu';
|
|
14
|
+
import { modalStateEffect } from './modal';
|
|
15
|
+
import { type PopoverOptions, popover, popoverRangeEffect, popoverStateField } from './popover';
|
|
16
|
+
import { type PopoverMenuProviderProps } from './PopoverMenuProvider';
|
|
17
|
+
|
|
18
|
+
export type GetMenuContext = {
|
|
19
|
+
state: EditorState;
|
|
20
|
+
pos: number;
|
|
21
|
+
text: string;
|
|
22
|
+
trigger?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type UsePopoverMenuProps = {
|
|
26
|
+
filter?: boolean;
|
|
27
|
+
getMenu?: (context: GetMenuContext) => MaybePromise<PopoverMenuGroup[]>;
|
|
28
|
+
} & Pick<PopoverOptions, 'trigger' | 'triggerKey' | 'placeholder'>;
|
|
29
|
+
|
|
30
|
+
export type UsePopoverMenu = {
|
|
31
|
+
groupsRef: RefObject<PopoverMenuGroup[]>;
|
|
32
|
+
extension: Extension;
|
|
33
|
+
} & Pick<PopoverMenuProviderProps, 'currentItem' | 'open' | 'onOpenChange' | 'onActivate' | 'onSelect' | 'onCancel'>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* ```tsx
|
|
37
|
+
* const { groupsRef, extension, ...menuProps } = usePopoverMenu();
|
|
38
|
+
* const { parentRef, viewRef } = useTextEditor({ extensions: [extension] });
|
|
39
|
+
* return (
|
|
40
|
+
* <PopoverMenuProvider view={viewRef.current} groups={groupsRef.current} {...menuProps}>
|
|
41
|
+
* <div ref={parentRef} />
|
|
42
|
+
* </PopoverMenuProvider>
|
|
43
|
+
* );
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export const usePopoverMenu = ({
|
|
47
|
+
trigger,
|
|
48
|
+
triggerKey,
|
|
49
|
+
placeholder,
|
|
50
|
+
filter = true,
|
|
51
|
+
getMenu,
|
|
52
|
+
}: UsePopoverMenuProps): UsePopoverMenu => {
|
|
53
|
+
const groupsRef = useRef<PopoverMenuGroup[]>([]);
|
|
54
|
+
const currentRef = useRef<PopoverMenuItem | null>(null);
|
|
55
|
+
const [currentItem, setCurrentItem] = useState<string>();
|
|
56
|
+
const [open, setOpen] = useState(false);
|
|
57
|
+
const [_, refresh] = useState({});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get filtered options.
|
|
61
|
+
*/
|
|
62
|
+
const getMenuOptions = useCallback<NonNullable<UsePopoverMenuProps['getMenu']>>(
|
|
63
|
+
async ({ text, trigger, ...props }) => {
|
|
64
|
+
const groups = (await getMenu?.({ text, trigger, ...props })) ?? [];
|
|
65
|
+
return filter
|
|
66
|
+
? filterMenuGroups(groups, (item) =>
|
|
67
|
+
text ? (item.label as string).toLowerCase().startsWith(text.toLowerCase()) : true,
|
|
68
|
+
)
|
|
69
|
+
: groups;
|
|
70
|
+
},
|
|
71
|
+
[getMenu, filter],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const handleOpenChange = useCallback<NonNullable<UsePopoverMenu['onOpenChange']>>(
|
|
75
|
+
async ({ view, open }) => {
|
|
76
|
+
invariant(view);
|
|
77
|
+
setOpen(open);
|
|
78
|
+
if (!open) {
|
|
79
|
+
setCurrentItem(undefined);
|
|
80
|
+
view.dispatch({
|
|
81
|
+
effects: [popoverRangeEffect.of(null)],
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// TODO(burdon): Possible race condition.
|
|
86
|
+
// useTextEditor.handleKeyDown will get called after this handler completes.
|
|
87
|
+
requestAnimationFrame(() => {
|
|
88
|
+
view.dispatch({
|
|
89
|
+
effects: [modalStateEffect.of(open)],
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
[getMenuOptions],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const handleActivate = useCallback<NonNullable<UsePopoverMenu['onActivate']>>(
|
|
97
|
+
async ({ view, trigger }) => {
|
|
98
|
+
const item = getMenuItem(groupsRef.current, currentItem);
|
|
99
|
+
if (item) {
|
|
100
|
+
currentRef.current = item;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!open) {
|
|
104
|
+
handleOpenChange({ view, open: true, trigger });
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
[open, handleOpenChange],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const handleSelect = useCallback<NonNullable<UsePopoverMenu['onSelect']>>(({ view, item }) => {
|
|
111
|
+
void item.onSelect?.(view, view.state.selection.main.head);
|
|
112
|
+
}, []);
|
|
113
|
+
|
|
114
|
+
const handleCancel = useCallback<NonNullable<UsePopoverMenu['onCancel']>>(({ view }) => {
|
|
115
|
+
// Delete trigger.
|
|
116
|
+
const { range, trigger } = view.state.field(popoverStateField) ?? {};
|
|
117
|
+
if (range && trigger) {
|
|
118
|
+
view.dispatch({
|
|
119
|
+
changes: { ...range, insert: '' },
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}, []);
|
|
123
|
+
|
|
124
|
+
const serializedTrigger = Array.isArray(trigger) ? trigger.join(',') : trigger;
|
|
125
|
+
const extension = useMemo<Extension>(() => {
|
|
126
|
+
return popover({
|
|
127
|
+
trigger,
|
|
128
|
+
triggerKey,
|
|
129
|
+
placeholder,
|
|
130
|
+
onClose: ({ view }) => handleOpenChange({ view, open: false }),
|
|
131
|
+
onEnter: ({ view }) => {
|
|
132
|
+
if (currentRef.current) {
|
|
133
|
+
handleSelect({ view, item: currentRef.current });
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
onArrowUp: () => {
|
|
137
|
+
setCurrentItem((currentItem) => {
|
|
138
|
+
const previous = getPreviousMenuItem(groupsRef.current, currentItem);
|
|
139
|
+
currentRef.current = previous;
|
|
140
|
+
return previous.id;
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
onArrowDown: () => {
|
|
144
|
+
setCurrentItem((currentItem) => {
|
|
145
|
+
const next = getNextMenuItem(groupsRef.current, currentItem);
|
|
146
|
+
currentRef.current = next;
|
|
147
|
+
return next.id;
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
onTextChange: async ({ view, pos, text, trigger }) => {
|
|
151
|
+
groupsRef.current = (await getMenuOptions({ state: view.state, pos, text, trigger })) ?? [];
|
|
152
|
+
const firstItem = groupsRef.current.filter((group) => group.items.length > 0)[0]?.items[0];
|
|
153
|
+
if (firstItem) {
|
|
154
|
+
setCurrentItem(firstItem.id);
|
|
155
|
+
currentRef.current = firstItem;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
refresh({});
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}, [handleOpenChange, getMenuOptions, serializedTrigger, placeholder]);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
groupsRef,
|
|
165
|
+
extension,
|
|
166
|
+
currentItem,
|
|
167
|
+
open,
|
|
168
|
+
onOpenChange: handleOpenChange,
|
|
169
|
+
onActivate: handleActivate,
|
|
170
|
+
onSelect: handleSelect,
|
|
171
|
+
onCancel: handleCancel,
|
|
172
|
+
};
|
|
173
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type EditorView } from '@codemirror/view';
|
|
6
|
+
|
|
7
|
+
export const insertAtCursor = (view: EditorView, from: number, insert: string) => {
|
|
8
|
+
view.dispatch({
|
|
9
|
+
changes: { from, to: from, insert },
|
|
10
|
+
selection: { anchor: from + insert.length, head: from + insert.length },
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* If the cursor is at the start of a line, insert the text at the cursor.
|
|
16
|
+
* Otherwise, insert the text on a new line.
|
|
17
|
+
*/
|
|
18
|
+
export const insertAtLineStart = (view: EditorView, from: number, insert: string) => {
|
|
19
|
+
const line = view.state.doc.lineAt(from);
|
|
20
|
+
if (line.from === from) {
|
|
21
|
+
insertAtCursor(view, from, insert);
|
|
22
|
+
} else {
|
|
23
|
+
insert = '\n' + insert;
|
|
24
|
+
view.dispatch({
|
|
25
|
+
changes: { from: line.to, to: line.to, insert },
|
|
26
|
+
selection: { anchor: line.to + insert.length, head: line.to + insert.length },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
};
|
|
@@ -8,8 +8,6 @@ import { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemir
|
|
|
8
8
|
import { type SyntaxNode } from '@lezer/common';
|
|
9
9
|
|
|
10
10
|
export type PreviewLinkRef = {
|
|
11
|
-
/** @deprecated */
|
|
12
|
-
// TODO(burdon): Remove?
|
|
13
11
|
suggest?: boolean;
|
|
14
12
|
block?: boolean;
|
|
15
13
|
label: string;
|
|
@@ -22,9 +20,6 @@ export type PreviewLinkTarget = {
|
|
|
22
20
|
object?: any;
|
|
23
21
|
};
|
|
24
22
|
|
|
25
|
-
// TODO(wittjosiah): Remove.
|
|
26
|
-
export type PreviewLookup = (link: PreviewLinkRef) => Promise<PreviewLinkTarget | null | undefined>;
|
|
27
|
-
|
|
28
23
|
export type PreviewOptions = {
|
|
29
24
|
addBlockContainer?: (link: PreviewLinkRef, el: HTMLElement) => void;
|
|
30
25
|
removeBlockContainer?: (link: PreviewLinkRef) => void;
|
|
@@ -7,7 +7,7 @@ import { EditorView, keymap } from '@codemirror/view';
|
|
|
7
7
|
|
|
8
8
|
import { debounce } from '@dxos/async';
|
|
9
9
|
import { invariant } from '@dxos/invariant';
|
|
10
|
-
import {
|
|
10
|
+
import { isTruthy } from '@dxos/util';
|
|
11
11
|
|
|
12
12
|
import { singleValueFacet } from '../util';
|
|
13
13
|
|
|
@@ -96,5 +96,5 @@ export const selectionState = ({ getState, setState }: Partial<EditorStateStore>
|
|
|
96
96
|
},
|
|
97
97
|
},
|
|
98
98
|
]),
|
|
99
|
-
].filter(
|
|
99
|
+
].filter(isTruthy);
|
|
100
100
|
};
|
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
import { type Extension, StateEffect, StateField } from '@codemirror/state';
|
|
6
6
|
import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate, WidgetType } from '@codemirror/view';
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
import { Domino } from '../../util';
|
|
8
|
+
import { Domino } from '@dxos/react-ui';
|
|
9
|
+
import { isTruthy } from '@dxos/util';
|
|
11
10
|
|
|
12
11
|
const BLINK_RATE = 2_000;
|
|
13
12
|
|
|
@@ -24,7 +23,7 @@ export const streamer = (options: StreamerOptions = {}): Extension => {
|
|
|
24
23
|
return [
|
|
25
24
|
options.cursor && cursor(),
|
|
26
25
|
options.fadeIn && fadeIn(typeof options.fadeIn === 'object' ? options.fadeIn : {}),
|
|
27
|
-
].filter(
|
|
26
|
+
].filter(isTruthy);
|
|
28
27
|
};
|
|
29
28
|
|
|
30
29
|
/**
|
|
@@ -106,7 +105,7 @@ class CursorWidget extends WidgetType {
|
|
|
106
105
|
toDOM() {
|
|
107
106
|
return Domino.of('span')
|
|
108
107
|
.style({ opacity: '0.8' })
|
|
109
|
-
.
|
|
108
|
+
.children(Domino.of('span').text('\u258F').style({ animation: 'blink 2s infinite' }))
|
|
110
109
|
.build();
|
|
111
110
|
}
|
|
112
111
|
}
|
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { syntaxTree } from '@codemirror/language';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
type EditorState,
|
|
8
|
+
type Extension,
|
|
9
|
+
RangeSetBuilder,
|
|
10
|
+
StateEffect,
|
|
11
|
+
StateField,
|
|
12
|
+
Transaction,
|
|
13
|
+
} from '@codemirror/state';
|
|
7
14
|
import { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemirror/view';
|
|
8
15
|
import { type ComponentType, type FC } from 'react';
|
|
9
16
|
|
|
@@ -52,6 +59,11 @@ export type XmlWidgetDef = {
|
|
|
52
59
|
|
|
53
60
|
export type XmlWidgetRegistry = Record<string, XmlWidgetDef>;
|
|
54
61
|
|
|
62
|
+
export const getXmlTextChild = (children: any[]): string | null => {
|
|
63
|
+
const child = children?.[0];
|
|
64
|
+
return typeof child === 'string' ? child : null;
|
|
65
|
+
};
|
|
66
|
+
|
|
55
67
|
/**
|
|
56
68
|
* Update context.
|
|
57
69
|
*/
|
|
@@ -151,6 +163,52 @@ export const xmlTags = (options: XmlTagsOptions = {}): Extension => {
|
|
|
151
163
|
}
|
|
152
164
|
}
|
|
153
165
|
|
|
166
|
+
// Check if user pressed Backspace or Delete and remove adjacent decorations if present.
|
|
167
|
+
const userEvent = tr.annotation(Transaction.userEvent);
|
|
168
|
+
if (userEvent === 'delete.backward' || userEvent === 'delete.forward') {
|
|
169
|
+
const { state } = tr;
|
|
170
|
+
const decorationArray = decorationSetToArray(decorations);
|
|
171
|
+
const filteredDecorations = [];
|
|
172
|
+
|
|
173
|
+
// Get cursor position after the change.
|
|
174
|
+
const cursorPos = state.selection.main.head;
|
|
175
|
+
|
|
176
|
+
for (const range of decorationArray) {
|
|
177
|
+
let shouldKeep = true;
|
|
178
|
+
|
|
179
|
+
// For Backspace (delete.backward), check if decoration is immediately after cursor.
|
|
180
|
+
if (userEvent === 'delete.backward' && range.from === cursorPos) {
|
|
181
|
+
shouldKeep = false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// For Delete (delete.forward), check if decoration is immediately before cursor.
|
|
185
|
+
if (userEvent === 'delete.forward' && range.to === cursorPos) {
|
|
186
|
+
shouldKeep = false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (shouldKeep) {
|
|
190
|
+
// Map the decoration position through the transaction changes.
|
|
191
|
+
const mappedFrom = tr.changes.mapPos(range.from, -1);
|
|
192
|
+
const mappedTo = tr.changes.mapPos(range.to, 1);
|
|
193
|
+
|
|
194
|
+
// Only keep the decoration if mapping was successful and positions are valid.
|
|
195
|
+
if (mappedFrom >= 0 && mappedTo >= mappedFrom && mappedTo <= state.doc.length) {
|
|
196
|
+
filteredDecorations.push({
|
|
197
|
+
from: mappedFrom,
|
|
198
|
+
to: mappedTo,
|
|
199
|
+
value: range.value,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Return updated decorations with adjacent ones removed and positions mapped.
|
|
206
|
+
return {
|
|
207
|
+
from,
|
|
208
|
+
decorations: Decoration.set(filteredDecorations),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
154
212
|
if (tr.docChanged) {
|
|
155
213
|
const { state } = tr;
|
|
156
214
|
|