@dxos/react-ui-editor 0.8.4-main.a4bbb77 → 0.8.4-main.ae835ea
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 +4577 -4832
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs.map +2 -2
- 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 +4577 -4832
- 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.map +2 -2
- package/dist/lib/node-esm/types/index.mjs +1 -1
- package/dist/types/src/components/Editor/Editor.d.ts +13 -4
- 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 +17 -2
- package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/headings.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/util.d.ts +5 -19
- package/dist/types/src/components/EditorToolbar/util.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +0 -1
- package/dist/types/src/components/index.d.ts.map +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/cursor.d.ts +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/sync.d.ts +3 -3
- package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/autoscroll.d.ts +2 -2
- package/dist/types/src/extensions/autoscroll.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness/awareness-provider.d.ts +1 -1
- package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts +9 -4
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +2 -3
- 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/listener.d.ts +8 -6
- package/dist/types/src/extensions/listener.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts +1 -2
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/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 +6 -3
- 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/hooks/useTextEditor.d.ts +2 -6
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
- package/dist/types/src/stories/CommandDialog.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Comments.stories.d.ts +2 -2
- package/dist/types/src/stories/EditorToolbar.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Experimental.stories.d.ts +2 -2
- package/dist/types/src/stories/Markdown.stories.d.ts +2 -2
- 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 -5
- package/dist/types/src/stories/Popover.stories.d.ts.map +1 -0
- package/dist/types/src/stories/Preview.stories.d.ts +2 -2
- package/dist/types/src/stories/Preview.stories.d.ts.map +1 -1
- package/dist/types/src/stories/TextEditor.stories.d.ts +2 -3
- package/dist/types/src/stories/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/stories/components/EditorStory.d.ts +3 -3
- 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.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/tsconfig.tsbuildinfo +1 -1
- package/package.json +41 -38
- package/src/components/Editor/Editor.stories.tsx +69 -0
- package/src/components/Editor/Editor.tsx +25 -17
- package/src/components/EditorToolbar/EditorToolbar.tsx +88 -87
- package/src/components/EditorToolbar/headings.ts +6 -4
- package/src/components/EditorToolbar/util.ts +2 -18
- package/src/components/index.ts +0 -1
- package/src/extensions/{autocomplete.ts → autocomplete/autocomplete.ts} +1 -0
- package/src/extensions/autocomplete/index.ts +8 -0
- package/src/extensions/autocomplete/match.ts +46 -0
- package/src/extensions/{command-menu → autocomplete}/placeholder.ts +21 -17
- package/src/extensions/{command-dialog → autocomplete}/typeahead.ts +6 -48
- package/src/extensions/automerge/automerge.ts +28 -9
- package/src/extensions/automerge/cursor.ts +1 -1
- package/src/extensions/automerge/sync.ts +8 -4
- package/src/extensions/automerge/update-automerge.ts +1 -1
- package/src/extensions/autoscroll.ts +3 -3
- package/src/extensions/awareness/awareness-provider.ts +2 -2
- package/src/extensions/factories.ts +18 -10
- package/src/extensions/hashtag.tsx +2 -2
- package/src/extensions/index.ts +2 -3
- package/src/extensions/json.ts +1 -1
- package/src/extensions/listener.ts +14 -20
- package/src/extensions/markdown/bundle.ts +14 -2
- package/src/extensions/markdown/formatting.ts +8 -8
- package/src/extensions/modes.ts +2 -2
- package/src/extensions/{floating-menu.ts → outliner/menu.ts} +7 -5
- package/src/extensions/outliner/outliner.ts +2 -2
- package/src/extensions/popover/PopoverMenuProvider.tsx +220 -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 +289 -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 +10 -7
- package/src/extensions/state.ts +7 -0
- package/src/hooks/useTextEditor.ts +21 -21
- package/src/stories/CommandDialog.stories.tsx +3 -14
- package/src/stories/EditorToolbar.stories.tsx +4 -5
- package/src/stories/Outliner.stories.tsx +16 -9
- package/src/stories/Popover.stories.tsx +163 -0
- package/src/stories/Preview.stories.tsx +15 -8
- package/src/stories/TextEditor.stories.tsx +3 -29
- package/src/stories/components/EditorStory.tsx +5 -3
- package/src/styles/theme.ts +2 -1
- package/src/testing/PreviewPopover.tsx +2 -0
- package/src/types/types.ts +1 -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/CommandMenu/CommandMenu.d.ts +0 -38
- package/dist/types/src/components/CommandMenu/CommandMenu.d.ts.map +0 -1
- package/dist/types/src/components/CommandMenu/index.d.ts +0 -2
- package/dist/types/src/components/CommandMenu/index.d.ts.map +0 -1
- package/dist/types/src/extensions/autocomplete.d.ts.map +0 -1
- package/dist/types/src/extensions/command-dialog/action.d.ts +0 -17
- package/dist/types/src/extensions/command-dialog/action.d.ts.map +0 -1
- package/dist/types/src/extensions/command-dialog/command-dialog.d.ts +0 -6
- package/dist/types/src/extensions/command-dialog/command-dialog.d.ts.map +0 -1
- package/dist/types/src/extensions/command-dialog/hint.d.ts +0 -19
- package/dist/types/src/extensions/command-dialog/hint.d.ts.map +0 -1
- package/dist/types/src/extensions/command-dialog/index.d.ts +0 -4
- package/dist/types/src/extensions/command-dialog/index.d.ts.map +0 -1
- package/dist/types/src/extensions/command-dialog/state.d.ts +0 -16
- package/dist/types/src/extensions/command-dialog/state.d.ts.map +0 -1
- package/dist/types/src/extensions/command-dialog/typeahead.d.ts +0 -22
- package/dist/types/src/extensions/command-dialog/typeahead.d.ts.map +0 -1
- package/dist/types/src/extensions/command-menu/command-menu.d.ts +0 -20
- package/dist/types/src/extensions/command-menu/command-menu.d.ts.map +0 -1
- package/dist/types/src/extensions/command-menu/index.d.ts +0 -3
- package/dist/types/src/extensions/command-menu/index.d.ts.map +0 -1
- package/dist/types/src/extensions/command-menu/placeholder.d.ts +0 -10
- package/dist/types/src/extensions/command-menu/placeholder.d.ts.map +0 -1
- package/dist/types/src/extensions/command-menu/useCommandMenu.d.ts +0 -24
- package/dist/types/src/extensions/command-menu/useCommandMenu.d.ts.map +0 -1
- package/dist/types/src/extensions/floating-menu.d.ts +0 -7
- package/dist/types/src/extensions/floating-menu.d.ts.map +0 -1
- package/dist/types/src/stories/CommandMenu.stories.d.ts.map +0 -1
- package/src/components/CommandMenu/CommandMenu.tsx +0 -348
- package/src/components/CommandMenu/index.ts +0 -5
- package/src/extensions/command-dialog/action.ts +0 -55
- package/src/extensions/command-dialog/command-dialog.ts +0 -34
- package/src/extensions/command-dialog/hint.ts +0 -103
- package/src/extensions/command-dialog/index.ts +0 -7
- package/src/extensions/command-dialog/state.ts +0 -90
- package/src/extensions/command-menu/command-menu.ts +0 -210
- package/src/extensions/command-menu/index.ts +0 -6
- package/src/extensions/command-menu/useCommandMenu.ts +0 -134
- package/src/stories/CommandMenu.stories.tsx +0 -158
- /package/dist/types/src/extensions/{autocomplete.d.ts → autocomplete/autocomplete.d.ts} +0 -0
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2024 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { StateField } from '@codemirror/state';
|
|
6
|
-
import { type EditorView, type Tooltip, type TooltipView, showTooltip } from '@codemirror/view';
|
|
7
|
-
|
|
8
|
-
import { type RenderCallback } from '../../types';
|
|
9
|
-
import { singleValueFacet } from '../../util';
|
|
10
|
-
|
|
11
|
-
import { type Action, closeEffect, openEffect } from './action';
|
|
12
|
-
import { type CommandOptions } from './command-dialog';
|
|
13
|
-
|
|
14
|
-
export const commandConfig = singleValueFacet<CommandOptions>();
|
|
15
|
-
|
|
16
|
-
export type PopupOptions = {
|
|
17
|
-
renderDialog: RenderCallback<{ onAction: (action?: Action) => void }>;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
type CommandState = {
|
|
21
|
-
tooltip?: Tooltip;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export const commandState = StateField.define<CommandState>({
|
|
25
|
-
create: () => ({}),
|
|
26
|
-
update: (state, tr) => {
|
|
27
|
-
for (const effect of tr.effects) {
|
|
28
|
-
if (effect.is(closeEffect)) {
|
|
29
|
-
return {};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const { renderDialog } = tr.state.facet(commandConfig);
|
|
33
|
-
if (effect.is(openEffect) && renderDialog) {
|
|
34
|
-
const { pos, fullWidth } = effect.value;
|
|
35
|
-
const tooltip: Tooltip = {
|
|
36
|
-
pos,
|
|
37
|
-
above: false,
|
|
38
|
-
arrow: false,
|
|
39
|
-
strictSide: true,
|
|
40
|
-
create: (view: EditorView) => {
|
|
41
|
-
const root = document.createElement('div');
|
|
42
|
-
const tooltipView: TooltipView = {
|
|
43
|
-
dom: root,
|
|
44
|
-
mount: (view: EditorView) => {
|
|
45
|
-
if (fullWidth) {
|
|
46
|
-
const parent = root.parentElement!;
|
|
47
|
-
const { paddingLeft, paddingRight } = window.getComputedStyle(parent);
|
|
48
|
-
const widthWithoutPadding = parent.clientWidth - parseFloat(paddingLeft) - parseFloat(paddingRight);
|
|
49
|
-
root.style.width = `${widthWithoutPadding}px`;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Render react component.
|
|
53
|
-
renderDialog(
|
|
54
|
-
root,
|
|
55
|
-
{
|
|
56
|
-
onAction: (action) => {
|
|
57
|
-
view.dispatch({ effects: closeEffect.of(null) });
|
|
58
|
-
switch (action?.type) {
|
|
59
|
-
case 'insert': {
|
|
60
|
-
// Insert into editor.
|
|
61
|
-
const text = action.text + '\n';
|
|
62
|
-
view.dispatch({
|
|
63
|
-
changes: { from: pos, insert: text },
|
|
64
|
-
selection: { anchor: pos + text.length },
|
|
65
|
-
});
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// NOTE: Truncates text if set focus immediately.
|
|
71
|
-
requestAnimationFrame(() => view.focus());
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
view,
|
|
75
|
-
);
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
return tooltipView;
|
|
80
|
-
},
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
return { tooltip };
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return state;
|
|
88
|
-
},
|
|
89
|
-
provide: (field) => [showTooltip.from(field, (value) => value.tooltip ?? null)],
|
|
90
|
-
});
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2024 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { type Extension, Prec, RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
|
|
6
|
-
import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate, keymap } from '@codemirror/view';
|
|
7
|
-
|
|
8
|
-
import { type Range } from '../../types';
|
|
9
|
-
|
|
10
|
-
import { type PlaceholderOptions, placeholder } from './placeholder';
|
|
11
|
-
|
|
12
|
-
export type CommandMenuOptions = {
|
|
13
|
-
trigger: string | string[];
|
|
14
|
-
placeholder?: Partial<PlaceholderOptions>;
|
|
15
|
-
|
|
16
|
-
// TODO(burdon): Replace with onKey?
|
|
17
|
-
onClose?: () => void;
|
|
18
|
-
onArrowDown?: () => void;
|
|
19
|
-
onArrowUp?: () => void;
|
|
20
|
-
onEnter?: () => void;
|
|
21
|
-
|
|
22
|
-
onTextChange?: (trigger: string, text: string) => void;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export const commandMenu = (options: CommandMenuOptions): Extension => {
|
|
26
|
-
const commandMenuPlugin = ViewPlugin.fromClass(
|
|
27
|
-
class {
|
|
28
|
-
decorations: DecorationSet = Decoration.none;
|
|
29
|
-
|
|
30
|
-
constructor(readonly view: EditorView) {}
|
|
31
|
-
|
|
32
|
-
// TODO(wittjosiah): The decorations are repainted on every update, this occasionally causes menu to flicker.
|
|
33
|
-
update(update: ViewUpdate) {
|
|
34
|
-
const builder = new RangeSetBuilder<Decoration>();
|
|
35
|
-
const selection = update.view.state.selection.main;
|
|
36
|
-
const { range: activeRange, trigger } = update.view.state.field(commandMenuState) ?? {};
|
|
37
|
-
|
|
38
|
-
// Check if we should show the widget - only if cursor is within the active command range.
|
|
39
|
-
const shouldShowWidget = activeRange && selection.head >= activeRange.from && selection.head <= activeRange.to;
|
|
40
|
-
if (shouldShowWidget) {
|
|
41
|
-
// Create mark decoration that wraps the entire line content in a dx-anchor.
|
|
42
|
-
builder.add(
|
|
43
|
-
activeRange.from,
|
|
44
|
-
activeRange.to,
|
|
45
|
-
Decoration.mark({
|
|
46
|
-
tagName: 'dx-anchor',
|
|
47
|
-
class: 'cm-floating-menu-trigger',
|
|
48
|
-
attributes: {
|
|
49
|
-
'data-visible-focus': 'false',
|
|
50
|
-
'data-auto-trigger': 'true',
|
|
51
|
-
'data-trigger': trigger!,
|
|
52
|
-
},
|
|
53
|
-
}),
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const activeRangeChanged = update.transactions.some((tr) =>
|
|
58
|
-
tr.effects.some((effect) => effect.is(commandRangeEffect)),
|
|
59
|
-
);
|
|
60
|
-
if (activeRange && activeRangeChanged && trigger) {
|
|
61
|
-
const content = update.view.state.sliceDoc(
|
|
62
|
-
activeRange.from + 1, // Skip the trigger character.
|
|
63
|
-
activeRange.to,
|
|
64
|
-
);
|
|
65
|
-
options.onTextChange?.(trigger, content);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
this.decorations = builder.finish();
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
decorations: (v) => v.decorations,
|
|
73
|
-
},
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
const triggers = Array.isArray(options.trigger) ? options.trigger : [options.trigger];
|
|
77
|
-
|
|
78
|
-
const commandKeymap = keymap.of([
|
|
79
|
-
...triggers.map((trigger) => ({
|
|
80
|
-
key: trigger,
|
|
81
|
-
run: (view: EditorView) => {
|
|
82
|
-
const selection = view.state.selection.main;
|
|
83
|
-
const line = view.state.doc.lineAt(selection.head);
|
|
84
|
-
// Check if we should trigger the command menu:
|
|
85
|
-
// 1. Empty lines or at the beginning of a line
|
|
86
|
-
// 2. When there's a preceding space
|
|
87
|
-
if (
|
|
88
|
-
line.text.trim() === '' ||
|
|
89
|
-
selection.head === line.from ||
|
|
90
|
-
(selection.head > line.from && line.text[selection.head - line.from - 1] === ' ')
|
|
91
|
-
) {
|
|
92
|
-
// Insert and select the trigger.
|
|
93
|
-
view.dispatch({
|
|
94
|
-
changes: { from: selection.head, insert: trigger },
|
|
95
|
-
selection: { anchor: selection.head + 1, head: selection.head + 1 },
|
|
96
|
-
effects: commandRangeEffect.of({ trigger, range: { from: selection.head, to: selection.head + 1 } }),
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return false;
|
|
103
|
-
},
|
|
104
|
-
})),
|
|
105
|
-
{
|
|
106
|
-
key: 'Enter',
|
|
107
|
-
run: (view) => {
|
|
108
|
-
const activeRange = view.state.field(commandMenuState)?.range;
|
|
109
|
-
if (activeRange) {
|
|
110
|
-
view.dispatch({ changes: { from: activeRange.from, to: activeRange.to, insert: '' } });
|
|
111
|
-
options.onEnter?.();
|
|
112
|
-
return true;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return false;
|
|
116
|
-
},
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
key: 'ArrowDown',
|
|
120
|
-
run: (view) => {
|
|
121
|
-
const activeRange = view.state.field(commandMenuState)?.range;
|
|
122
|
-
if (activeRange) {
|
|
123
|
-
options.onArrowDown?.();
|
|
124
|
-
return true;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return false;
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
key: 'ArrowUp',
|
|
132
|
-
run: (view) => {
|
|
133
|
-
const activeRange = view.state.field(commandMenuState)?.range;
|
|
134
|
-
if (activeRange) {
|
|
135
|
-
options.onArrowUp?.();
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return false;
|
|
140
|
-
},
|
|
141
|
-
},
|
|
142
|
-
]);
|
|
143
|
-
|
|
144
|
-
// Listen for selection and document changes to clean up the command menu.
|
|
145
|
-
const updateListener = EditorView.updateListener.of((update) => {
|
|
146
|
-
const { trigger, range: activeRange } = update.view.state.field(commandMenuState) ?? {};
|
|
147
|
-
if (!activeRange || !trigger) {
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const selection = update.view.state.selection.main;
|
|
152
|
-
const firstChar = update.view.state.doc.sliceString(activeRange.from, activeRange.from + 1);
|
|
153
|
-
const shouldRemove =
|
|
154
|
-
firstChar !== trigger || // Trigger deleted.
|
|
155
|
-
selection.head < activeRange.from || // Cursor moved before the range.
|
|
156
|
-
selection.head > activeRange.to + 1; // Cursor moved after the range (+1 to handle selection changing before doc).
|
|
157
|
-
|
|
158
|
-
const nextRange = shouldRemove
|
|
159
|
-
? null
|
|
160
|
-
: update.docChanged
|
|
161
|
-
? { from: activeRange.from, to: selection.head }
|
|
162
|
-
: activeRange;
|
|
163
|
-
if (nextRange !== activeRange) {
|
|
164
|
-
update.view.dispatch({ effects: commandRangeEffect.of(nextRange ? { trigger, range: nextRange } : null) });
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// TODO(burdon): Should delete if user presses escape? How else to insert the trigger character?
|
|
168
|
-
if (shouldRemove) {
|
|
169
|
-
options.onClose?.();
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
return [
|
|
174
|
-
Prec.highest(commandKeymap),
|
|
175
|
-
placeholder(
|
|
176
|
-
Object.assign(
|
|
177
|
-
{
|
|
178
|
-
content: `Press '${Array.isArray(options.trigger) ? options.trigger[0] : options.trigger}' for commands`,
|
|
179
|
-
},
|
|
180
|
-
options.placeholder,
|
|
181
|
-
),
|
|
182
|
-
),
|
|
183
|
-
updateListener,
|
|
184
|
-
commandMenuState,
|
|
185
|
-
commandMenuPlugin,
|
|
186
|
-
];
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
type CommandState = {
|
|
190
|
-
trigger: string;
|
|
191
|
-
range: Range;
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
// State effects for managing command menu state.
|
|
195
|
-
export const commandRangeEffect = StateEffect.define<CommandState | null>();
|
|
196
|
-
|
|
197
|
-
// State field to track the active command menu range.
|
|
198
|
-
const commandMenuState = StateField.define<CommandState | null>({
|
|
199
|
-
create: () => null,
|
|
200
|
-
update: (value, tr) => {
|
|
201
|
-
let newValue = value;
|
|
202
|
-
for (const effect of tr.effects) {
|
|
203
|
-
if (effect.is(commandRangeEffect)) {
|
|
204
|
-
newValue = effect.value;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return newValue;
|
|
209
|
-
},
|
|
210
|
-
});
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2024 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { type Extension } from '@codemirror/state';
|
|
6
|
-
import { type EditorView } from '@codemirror/view';
|
|
7
|
-
import { type RefObject, useCallback, useMemo, useRef, useState } from 'react';
|
|
8
|
-
|
|
9
|
-
import { type DxAnchorActivate } from '@dxos/react-ui';
|
|
10
|
-
import { type MaybePromise } from '@dxos/util';
|
|
11
|
-
|
|
12
|
-
import { type CommandMenuGroup, type CommandMenuItem, getItem, getNextItem, getPreviousItem } from '../../components';
|
|
13
|
-
|
|
14
|
-
import { commandMenu, commandRangeEffect } from './command-menu';
|
|
15
|
-
import { type PlaceholderOptions } from './placeholder';
|
|
16
|
-
|
|
17
|
-
export type UseCommandMenuOptions = {
|
|
18
|
-
// TODO(burdon): Extensions should not depend directly on the editor view.
|
|
19
|
-
viewRef: RefObject<EditorView | null>;
|
|
20
|
-
trigger: string | string[];
|
|
21
|
-
placeholder?: Partial<PlaceholderOptions>;
|
|
22
|
-
getMenu: (trigger: string, query?: string) => MaybePromise<CommandMenuGroup[]>;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export type UseCommandMenu = {
|
|
26
|
-
groupsRef: RefObject<CommandMenuGroup[]>;
|
|
27
|
-
commandMenu: Extension;
|
|
28
|
-
currentItem: string | undefined;
|
|
29
|
-
open: boolean;
|
|
30
|
-
onOpenChange: (open: boolean) => void;
|
|
31
|
-
onActivate: (event: DxAnchorActivate) => void;
|
|
32
|
-
onSelect: (item: CommandMenuItem) => void;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export const useCommandMenu = ({ viewRef, trigger, placeholder, getMenu }: UseCommandMenuOptions): UseCommandMenu => {
|
|
36
|
-
const currentRef = useRef<CommandMenuItem | null>(null);
|
|
37
|
-
const groupsRef = useRef<CommandMenuGroup[]>([]);
|
|
38
|
-
const [currentItem, setCurrentItem] = useState<string>();
|
|
39
|
-
const [open, setOpen] = useState(false);
|
|
40
|
-
const [_, refresh] = useState({});
|
|
41
|
-
|
|
42
|
-
const handleOpenChange = useCallback(
|
|
43
|
-
async (open: boolean, trigger?: string) => {
|
|
44
|
-
if (open && trigger) {
|
|
45
|
-
groupsRef.current = await getMenu(trigger);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
setOpen(open);
|
|
49
|
-
if (!open) {
|
|
50
|
-
setCurrentItem(undefined);
|
|
51
|
-
viewRef.current?.dispatch({ effects: [commandRangeEffect.of(null)] });
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
[getMenu],
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
const handleActivate = useCallback<UseCommandMenu['onActivate']>(
|
|
58
|
-
async (event) => {
|
|
59
|
-
const item = getItem(groupsRef.current, currentItem);
|
|
60
|
-
if (item) {
|
|
61
|
-
currentRef.current = item;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const triggerKey = event.trigger.getAttribute('data-trigger');
|
|
65
|
-
if (!open && triggerKey) {
|
|
66
|
-
await handleOpenChange(true, triggerKey);
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
[open, handleOpenChange],
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
// TODO(burdon): Move outside.
|
|
73
|
-
const handleSelect = useCallback<UseCommandMenu['onSelect']>((item) => {
|
|
74
|
-
if (!viewRef.current) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const selection = viewRef.current.state.selection.main;
|
|
79
|
-
void item.onSelect?.(viewRef.current, selection.head);
|
|
80
|
-
}, []);
|
|
81
|
-
|
|
82
|
-
const serializedTrigger = Array.isArray(trigger) ? trigger.join(',') : trigger;
|
|
83
|
-
|
|
84
|
-
const memoizedCommandMenu = useMemo<Extension>(() => {
|
|
85
|
-
return commandMenu({
|
|
86
|
-
trigger,
|
|
87
|
-
placeholder,
|
|
88
|
-
onClose: () => handleOpenChange(false),
|
|
89
|
-
onArrowDown: () => {
|
|
90
|
-
setCurrentItem((currentItem) => {
|
|
91
|
-
const next = getNextItem(groupsRef.current, currentItem);
|
|
92
|
-
currentRef.current = next;
|
|
93
|
-
return next.id;
|
|
94
|
-
});
|
|
95
|
-
},
|
|
96
|
-
onArrowUp: () => {
|
|
97
|
-
setCurrentItem((currentItem) => {
|
|
98
|
-
const previous = getPreviousItem(groupsRef.current, currentItem);
|
|
99
|
-
currentRef.current = previous;
|
|
100
|
-
return previous.id;
|
|
101
|
-
});
|
|
102
|
-
},
|
|
103
|
-
onEnter: () => {
|
|
104
|
-
if (currentRef.current) {
|
|
105
|
-
handleSelect(currentRef.current);
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
onTextChange: async (trigger, text) => {
|
|
109
|
-
if (/\W/.test(text)) {
|
|
110
|
-
return queueMicrotask(() => handleOpenChange(false));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
groupsRef.current = await getMenu(trigger, text);
|
|
114
|
-
const firstItem = groupsRef.current.filter((group) => group.items.length > 0)[0]?.items[0];
|
|
115
|
-
if (firstItem) {
|
|
116
|
-
setCurrentItem(firstItem.id);
|
|
117
|
-
currentRef.current = firstItem;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
refresh({});
|
|
121
|
-
},
|
|
122
|
-
});
|
|
123
|
-
}, [handleOpenChange, getMenu, serializedTrigger, placeholder]);
|
|
124
|
-
|
|
125
|
-
return {
|
|
126
|
-
groupsRef,
|
|
127
|
-
commandMenu: memoizedCommandMenu,
|
|
128
|
-
currentItem,
|
|
129
|
-
open,
|
|
130
|
-
onOpenChange: setOpen,
|
|
131
|
-
onActivate: handleActivate,
|
|
132
|
-
onSelect: handleSelect,
|
|
133
|
-
};
|
|
134
|
-
};
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2023 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { type EditorView } from '@codemirror/view';
|
|
6
|
-
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
7
|
-
import React, { useCallback, useRef } from 'react';
|
|
8
|
-
|
|
9
|
-
import { Obj, Query } from '@dxos/echo';
|
|
10
|
-
import { faker } from '@dxos/random';
|
|
11
|
-
import { useClientProvider, withClientProvider } from '@dxos/react-client/testing';
|
|
12
|
-
import { Domino } from '@dxos/react-ui';
|
|
13
|
-
import { withTheme } from '@dxos/react-ui/testing';
|
|
14
|
-
import { Testing, type ValueGenerator, createObjectFactory } from '@dxos/schema/testing';
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
type CommandMenuGroup,
|
|
18
|
-
type CommandMenuItem,
|
|
19
|
-
CommandMenuProvider,
|
|
20
|
-
coreSlashCommands,
|
|
21
|
-
filterItems,
|
|
22
|
-
insertAtCursor,
|
|
23
|
-
insertAtLineStart,
|
|
24
|
-
linkSlashCommands,
|
|
25
|
-
} from '../components';
|
|
26
|
-
import { type UseCommandMenuOptions, useCommandMenu } from '../extensions';
|
|
27
|
-
import { str } from '../testing';
|
|
28
|
-
|
|
29
|
-
import { EditorStory, names } from './components';
|
|
30
|
-
|
|
31
|
-
const generator: ValueGenerator = faker as any;
|
|
32
|
-
|
|
33
|
-
type StoryProps = Omit<UseCommandMenuOptions, 'viewRef'> & { text: string };
|
|
34
|
-
|
|
35
|
-
const DefaultStory = ({ text, ...options }: StoryProps) => {
|
|
36
|
-
const viewRef = useRef<EditorView>(null);
|
|
37
|
-
const { groupsRef, commandMenu, ...commandMenuProps } = useCommandMenu({ viewRef, ...options });
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
<CommandMenuProvider groups={groupsRef.current} {...commandMenuProps}>
|
|
41
|
-
<EditorStory ref={viewRef} text={text} placeholder={''} extensions={commandMenu} />
|
|
42
|
-
</CommandMenuProvider>
|
|
43
|
-
);
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const groups: CommandMenuGroup[] = [
|
|
47
|
-
coreSlashCommands,
|
|
48
|
-
linkSlashCommands,
|
|
49
|
-
{
|
|
50
|
-
id: 'custom',
|
|
51
|
-
label: 'Custom',
|
|
52
|
-
items: [
|
|
53
|
-
{
|
|
54
|
-
id: 'custom-1',
|
|
55
|
-
label: 'Log',
|
|
56
|
-
icon: 'ph--log--regular',
|
|
57
|
-
onSelect: console.log,
|
|
58
|
-
},
|
|
59
|
-
],
|
|
60
|
-
},
|
|
61
|
-
];
|
|
62
|
-
|
|
63
|
-
const meta = {
|
|
64
|
-
title: 'ui/react-ui-editor/CommandMenu',
|
|
65
|
-
render: DefaultStory,
|
|
66
|
-
decorators: [withTheme],
|
|
67
|
-
parameters: {
|
|
68
|
-
layout: 'fullscreen',
|
|
69
|
-
},
|
|
70
|
-
} satisfies Meta<typeof DefaultStory>;
|
|
71
|
-
|
|
72
|
-
export default meta;
|
|
73
|
-
|
|
74
|
-
type Story = StoryObj<typeof meta>;
|
|
75
|
-
|
|
76
|
-
// TODO(burdon): Not working.
|
|
77
|
-
export const Slash: Story = {
|
|
78
|
-
args: {
|
|
79
|
-
text: str('# Slash', '', names.join(' '), ''),
|
|
80
|
-
trigger: '/',
|
|
81
|
-
placeholder: {
|
|
82
|
-
content: () =>
|
|
83
|
-
Domino.of('div')
|
|
84
|
-
.children(
|
|
85
|
-
Domino.of('span').text('Press'),
|
|
86
|
-
Domino.of('span').text('/').classNames('border border-separator rounded-sm mx-1 px-1'),
|
|
87
|
-
Domino.of('span').text('for commands'),
|
|
88
|
-
)
|
|
89
|
-
.build(),
|
|
90
|
-
},
|
|
91
|
-
getMenu: (text) => {
|
|
92
|
-
return filterItems(groups, (item) =>
|
|
93
|
-
text ? (item.label as string).toLowerCase().includes(text.toLowerCase()) : true,
|
|
94
|
-
);
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
export const Link: Story = {
|
|
100
|
-
render: (args: StoryProps) => {
|
|
101
|
-
const { space } = useClientProvider();
|
|
102
|
-
const getMenu = useCallback(
|
|
103
|
-
async (trigger: string, query?: string): Promise<CommandMenuGroup[]> => {
|
|
104
|
-
if (trigger === '/') {
|
|
105
|
-
return filterItems(groups, (item) =>
|
|
106
|
-
query ? (item.label as string).toLowerCase().includes(query.toLowerCase()) : true,
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (!space) {
|
|
111
|
-
return [];
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const name = query?.startsWith('@') ? query.slice(1).toLowerCase() : (query?.toLowerCase() ?? '');
|
|
115
|
-
const result = await space?.db.query(Query.type(Testing.Contact)).run();
|
|
116
|
-
const items = result.objects
|
|
117
|
-
.filter((object) => object.name.toLowerCase().includes(name))
|
|
118
|
-
.map(
|
|
119
|
-
(object): CommandMenuItem => ({
|
|
120
|
-
id: object.id,
|
|
121
|
-
label: object.name,
|
|
122
|
-
icon: 'ph--user--regular',
|
|
123
|
-
onSelect: (view, head) => {
|
|
124
|
-
const link = `[${object.name}](${Obj.getDXN(object)})`;
|
|
125
|
-
if (query?.startsWith('@')) {
|
|
126
|
-
insertAtLineStart(view, head, `!${link}\n`);
|
|
127
|
-
} else {
|
|
128
|
-
insertAtCursor(view, head, `${link} `);
|
|
129
|
-
}
|
|
130
|
-
},
|
|
131
|
-
}),
|
|
132
|
-
);
|
|
133
|
-
return [{ id: 'echo', items }];
|
|
134
|
-
},
|
|
135
|
-
[space],
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
return <DefaultStory {...args} getMenu={getMenu} />;
|
|
139
|
-
},
|
|
140
|
-
decorators: [
|
|
141
|
-
withClientProvider({
|
|
142
|
-
createSpace: true,
|
|
143
|
-
onInitialized: async (client) => {
|
|
144
|
-
client.addTypes([Testing.Contact]);
|
|
145
|
-
},
|
|
146
|
-
onCreateSpace: async ({ space }) => {
|
|
147
|
-
const createObjects = createObjectFactory(space.db, generator);
|
|
148
|
-
await createObjects([{ type: Testing.Contact, count: 10 }]);
|
|
149
|
-
await space.db.flush({ indexes: true });
|
|
150
|
-
},
|
|
151
|
-
}),
|
|
152
|
-
],
|
|
153
|
-
args: {
|
|
154
|
-
text: str('# Link', '', names.join(' '), ''),
|
|
155
|
-
trigger: ['/', '@'],
|
|
156
|
-
getMenu: () => [],
|
|
157
|
-
},
|
|
158
|
-
};
|
|
File without changes
|