@dxos/react-ui-editor 0.4.7-main.33db07e → 0.4.7-main.3df814a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/browser/index.mjs +940 -740
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +9 -0
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +0 -9
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/index.d.ts +0 -2
- package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
- package/dist/types/src/{components/TextEditor → extensions/automerge}/automerge.stories.d.ts +4 -2
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -0
- package/dist/types/src/extensions/command/command.d.ts +10 -0
- package/dist/types/src/extensions/command/command.d.ts.map +1 -0
- package/dist/types/src/extensions/command/hint.d.ts +7 -0
- package/dist/types/src/extensions/command/hint.d.ts.map +1 -0
- package/dist/types/src/extensions/command/index.d.ts +2 -0
- package/dist/types/src/extensions/command/index.d.ts.map +1 -0
- package/dist/types/src/extensions/command/state.d.ts +18 -0
- package/dist/types/src/extensions/command/state.d.ts.map +1 -0
- package/dist/types/src/extensions/cursor-line-margin.d.ts.map +1 -1
- package/dist/types/src/extensions/dnd.d.ts +3 -0
- package/dist/types/src/extensions/dnd.d.ts.map +1 -0
- 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/listener.d.ts +1 -1
- package/dist/types/src/extensions/listener.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts +2 -2
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts +3 -0
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
- package/dist/types/src/extensions/util/dom.d.ts +15 -0
- package/dist/types/src/extensions/util/dom.d.ts.map +1 -0
- package/dist/types/src/hooks/defs.d.ts +5 -11
- package/dist/types/src/hooks/defs.d.ts.map +1 -1
- package/dist/types/src/hooks/useEditorView.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextEditor.d.ts +20 -5
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
- package/dist/types/src/{components/TextEditor/hooks.stories.d.ts → hooks/useTextEditor.stories.d.ts} +1 -3
- package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/index.d.ts +0 -1
- package/dist/types/src/styles/index.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/package.json +25 -25
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +95 -8
- package/src/components/TextEditor/TextEditor.stories.tsx +0 -15
- package/src/components/TextEditor/TextEditor.tsx +11 -6
- package/src/components/TextEditor/index.ts +0 -3
- package/src/components/Toolbar/Toolbar.stories.tsx +2 -1
- package/src/{components/TextEditor → extensions/automerge}/automerge.stories.tsx +32 -37
- package/src/extensions/awareness.ts +3 -3
- package/src/extensions/command/command.ts +35 -0
- package/src/extensions/command/hint.ts +76 -0
- package/src/extensions/command/index.ts +5 -0
- package/src/extensions/command/state.ts +106 -0
- package/src/extensions/cursor-line-margin.ts +2 -1
- package/src/extensions/dnd.ts +15 -0
- package/src/extensions/index.ts +2 -1
- package/src/extensions/listener.ts +3 -3
- package/src/extensions/markdown/bundle.ts +4 -14
- package/src/extensions/markdown/decorate.ts +19 -12
- package/src/extensions/util/dom.ts +36 -0
- package/src/hooks/defs.ts +6 -20
- package/src/hooks/useActionHandler.ts +11 -11
- package/src/hooks/useEditorView.ts +0 -1
- package/src/{components/TextEditor/hooks.stories.tsx → hooks/useTextEditor.stories.tsx} +8 -8
- package/src/hooks/useTextEditor.ts +69 -14
- package/src/hooks/useTextModel.ts +18 -18
- package/src/index.ts +2 -0
- package/src/styles/index.ts +0 -1
- package/src/themes/default.ts +1 -0
- package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +0 -1
- package/dist/types/src/extensions/basic.d.ts +0 -9
- package/dist/types/src/extensions/basic.d.ts.map +0 -1
- package/dist/types/src/styles/cursors.d.ts +0 -5
- package/dist/types/src/styles/cursors.d.ts.map +0 -1
- package/src/extensions/basic.ts +0 -41
- package/src/styles/cursors.ts +0 -21
|
@@ -164,7 +164,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
164
164
|
from: start,
|
|
165
165
|
to: startLine.from + startLine.length,
|
|
166
166
|
value: Decoration.mark({
|
|
167
|
-
attributes: { style: `background-color: ${
|
|
167
|
+
attributes: { style: `background-color: ${lightColor}` },
|
|
168
168
|
class: 'cm-collab-selection',
|
|
169
169
|
}),
|
|
170
170
|
});
|
|
@@ -174,7 +174,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
174
174
|
from: endLine.from,
|
|
175
175
|
to: end,
|
|
176
176
|
value: Decoration.mark({
|
|
177
|
-
attributes: { style: `background-color: ${
|
|
177
|
+
attributes: { style: `background-color: ${lightColor}` },
|
|
178
178
|
class: 'cm-collab-selection',
|
|
179
179
|
}),
|
|
180
180
|
});
|
|
@@ -185,7 +185,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
185
185
|
from: linePos,
|
|
186
186
|
to: linePos,
|
|
187
187
|
value: Decoration.line({
|
|
188
|
-
attributes: { style: `background-color: ${
|
|
188
|
+
attributes: { style: `background-color: ${lightColor}`, class: 'cm-collab-selectionLine' },
|
|
189
189
|
}),
|
|
190
190
|
});
|
|
191
191
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
6
|
+
import { EditorView, keymap } from '@codemirror/view';
|
|
7
|
+
|
|
8
|
+
import { hintViewPlugin } from './hint';
|
|
9
|
+
import { closeEffect, commandConfig, commandKeyBindings, commandState } from './state';
|
|
10
|
+
|
|
11
|
+
// TODO(burdon): Create knowledge base for CM notes and ideas.
|
|
12
|
+
// https://discuss.codemirror.net/t/inline-code-hints-like-vscode/5533/4
|
|
13
|
+
// https://github.com/saminzadeh/codemirror-extension-inline-suggestion
|
|
14
|
+
// https://github.com/ChromeDevTools/devtools-frontend/blob/main/front_end/ui/components/text_editor/config.ts#L370
|
|
15
|
+
|
|
16
|
+
export type CommandAction = {
|
|
17
|
+
insert?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type CommandOptions = {
|
|
21
|
+
onRender: (el: HTMLElement, cb: (action?: CommandAction) => void) => void;
|
|
22
|
+
onHint: () => string | undefined;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const command = (options: CommandOptions): Extension => {
|
|
26
|
+
return [
|
|
27
|
+
commandConfig.of(options),
|
|
28
|
+
commandState,
|
|
29
|
+
keymap.of(commandKeyBindings),
|
|
30
|
+
hintViewPlugin(options),
|
|
31
|
+
EditorView.focusChangeEffect.of((_, focusing) => {
|
|
32
|
+
return focusing ? closeEffect.of(null) : null;
|
|
33
|
+
}),
|
|
34
|
+
];
|
|
35
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { RangeSetBuilder } from '@codemirror/state';
|
|
6
|
+
import { Decoration, EditorView, ViewPlugin, type ViewUpdate, WidgetType } from '@codemirror/view';
|
|
7
|
+
|
|
8
|
+
import { type CommandOptions } from './command';
|
|
9
|
+
import { commandState } from './state';
|
|
10
|
+
import { clientRectsFor, flattenRect } from '../util/dom';
|
|
11
|
+
|
|
12
|
+
class CommandHint extends WidgetType {
|
|
13
|
+
constructor(readonly content: string | HTMLElement) {
|
|
14
|
+
super();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
toDOM() {
|
|
18
|
+
const wrap = document.createElement('span');
|
|
19
|
+
wrap.className = 'cm-placeholder';
|
|
20
|
+
wrap.style.pointerEvents = 'none';
|
|
21
|
+
wrap.appendChild(typeof this.content === 'string' ? document.createTextNode(this.content) : this.content);
|
|
22
|
+
if (typeof this.content === 'string') {
|
|
23
|
+
wrap.setAttribute('aria-label', 'placeholder ' + this.content);
|
|
24
|
+
} else {
|
|
25
|
+
wrap.setAttribute('aria-hidden', 'true');
|
|
26
|
+
}
|
|
27
|
+
return wrap;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
override coordsAt(dom: HTMLElement) {
|
|
31
|
+
const rects = dom.firstChild ? clientRectsFor(dom.firstChild) : [];
|
|
32
|
+
if (!rects.length) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const style = window.getComputedStyle(dom.parentNode as HTMLElement);
|
|
36
|
+
const rect = flattenRect(rects[0], style.direction !== 'rtl');
|
|
37
|
+
const lineHeight = parseInt(style.lineHeight);
|
|
38
|
+
if (rect.bottom - rect.top > lineHeight * 1.5) {
|
|
39
|
+
return { left: rect.left, right: rect.right, top: rect.top, bottom: rect.top + lineHeight };
|
|
40
|
+
}
|
|
41
|
+
return rect;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
override ignoreEvent() {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const hintViewPlugin = ({ onHint }: CommandOptions) =>
|
|
50
|
+
ViewPlugin.fromClass(
|
|
51
|
+
class {
|
|
52
|
+
deco = Decoration.none;
|
|
53
|
+
update(update: ViewUpdate) {
|
|
54
|
+
const builder = new RangeSetBuilder<Decoration>();
|
|
55
|
+
const cState = update.view.state.field(commandState, false);
|
|
56
|
+
if (!cState?.tooltip) {
|
|
57
|
+
const selection = update.view.state.selection.main;
|
|
58
|
+
const line = update.view.state.doc.lineAt(selection.from);
|
|
59
|
+
// Only show if blank line.
|
|
60
|
+
// TODO(burdon): Clashes with placeholder if pos === 0.
|
|
61
|
+
// TODO(burdon): Show after delay or if blank line above?
|
|
62
|
+
if (selection.from === selection.to && line.from === line.to) {
|
|
63
|
+
const hint = onHint();
|
|
64
|
+
if (hint) {
|
|
65
|
+
builder.add(selection.from, selection.to, Decoration.widget({ widget: new CommandHint(hint) }));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.deco = builder.finish();
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
provide: (plugin) => [EditorView.decorations.of((view) => view.plugin(plugin)?.deco ?? Decoration.none)],
|
|
75
|
+
},
|
|
76
|
+
);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Facet, StateEffect, StateField } from '@codemirror/state';
|
|
6
|
+
import {
|
|
7
|
+
type Command,
|
|
8
|
+
type EditorView,
|
|
9
|
+
type KeyBinding,
|
|
10
|
+
showTooltip,
|
|
11
|
+
type Tooltip,
|
|
12
|
+
type TooltipView,
|
|
13
|
+
} from '@codemirror/view';
|
|
14
|
+
|
|
15
|
+
import { type CommandOptions } from './command';
|
|
16
|
+
|
|
17
|
+
type CommandState = {
|
|
18
|
+
tooltip?: Tooltip | null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const commandConfig = Facet.define<CommandOptions, Required<CommandOptions>>({
|
|
22
|
+
combine: (providers) => providers[0],
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const commandState = StateField.define<CommandState>({
|
|
26
|
+
create: () => ({}),
|
|
27
|
+
update: (state, tr) => {
|
|
28
|
+
for (const effect of tr.effects) {
|
|
29
|
+
if (effect.is(closeEffect)) {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (effect.is(openEffect)) {
|
|
34
|
+
const options = tr.state.facet(commandConfig);
|
|
35
|
+
const { pos, fullWidth } = effect.value;
|
|
36
|
+
const tooltip: Tooltip = {
|
|
37
|
+
pos,
|
|
38
|
+
above: false,
|
|
39
|
+
arrow: false,
|
|
40
|
+
strictSide: true,
|
|
41
|
+
create: (view: EditorView) => {
|
|
42
|
+
const dom = document.createElement('div');
|
|
43
|
+
const tooltipView: TooltipView = {
|
|
44
|
+
dom,
|
|
45
|
+
mount: (view: EditorView) => {
|
|
46
|
+
if (fullWidth) {
|
|
47
|
+
const parent = dom.parentElement!;
|
|
48
|
+
const { paddingLeft, paddingRight } = window.getComputedStyle(parent);
|
|
49
|
+
const widthWithoutPadding = parent.clientWidth - parseFloat(paddingLeft) - parseFloat(paddingRight);
|
|
50
|
+
dom.style.width = `${widthWithoutPadding}px`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Render react component.
|
|
54
|
+
options.onRender(dom, (action) => {
|
|
55
|
+
view.dispatch({ effects: closeEffect.of(null) });
|
|
56
|
+
if (action?.insert?.length) {
|
|
57
|
+
view.dispatch({
|
|
58
|
+
changes: { from: pos, insert: action.insert },
|
|
59
|
+
selection: { anchor: pos + action.insert.length },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// TODO(burdon): Truncates text if set focus immediately.
|
|
63
|
+
setTimeout(() => view.focus());
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return tooltipView;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
return { tooltip };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return state;
|
|
76
|
+
},
|
|
77
|
+
provide: (f) => [showTooltip.from(f, (value) => value.tooltip ?? null)],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export const openEffect = StateEffect.define<{ pos: number; fullWidth?: boolean }>();
|
|
81
|
+
export const closeEffect = StateEffect.define<null>();
|
|
82
|
+
|
|
83
|
+
export const openCommand: Command = (view: EditorView) => {
|
|
84
|
+
if (view.state.field(commandState, false)) {
|
|
85
|
+
const selection = view.state.selection.main;
|
|
86
|
+
const line = view.state.doc.lineAt(selection.from);
|
|
87
|
+
if (line.from === selection.from && line.from === line.to) {
|
|
88
|
+
view.dispatch({ effects: openEffect.of({ pos: selection.anchor, fullWidth: true }) });
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const closeCommand: Command = (view: EditorView) => {
|
|
96
|
+
if (view.state.field(commandState, false)) {
|
|
97
|
+
view.dispatch({ effects: closeEffect.of(null) });
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const commandKeyBindings: readonly KeyBinding[] = [
|
|
104
|
+
{ key: '/', run: openCommand },
|
|
105
|
+
{ key: 'Escape', run: closeCommand },
|
|
106
|
+
];
|
|
@@ -18,7 +18,8 @@ export const cursorLineMarginFacet = Facet.define<number, number>({
|
|
|
18
18
|
|
|
19
19
|
const annotation = 'cursorLineMargin';
|
|
20
20
|
const lineAtPos = (s: EditorState, pos: number) => s.doc.lineAt(pos).number;
|
|
21
|
-
|
|
21
|
+
|
|
22
|
+
// Seems to the best approximation of CM5's `cursorScrollMargin`
|
|
22
23
|
// https://discuss.codemirror.net/t/cursorscrollmargin-for-v6/7448
|
|
23
24
|
export const cursorLineMargin: Extension = EditorView.updateListener.of(
|
|
24
25
|
({ transactions, state, selectionSet, startState, view }) => {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type { Extension } from '@codemirror/state';
|
|
6
|
+
import { EditorView } from '@codemirror/view';
|
|
7
|
+
|
|
8
|
+
// TODO(burdon): DND experiment.
|
|
9
|
+
export const dnd = (): Extension => {
|
|
10
|
+
return EditorView.domEventHandlers({
|
|
11
|
+
drop: (event, view) => {
|
|
12
|
+
console.log(event);
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
};
|
package/src/extensions/index.ts
CHANGED
|
@@ -6,11 +6,12 @@ export * from './annotations';
|
|
|
6
6
|
export * from './autocomplete';
|
|
7
7
|
export * from './automerge';
|
|
8
8
|
export * from './awareness';
|
|
9
|
-
export * from './basic';
|
|
10
9
|
export * from './blast';
|
|
10
|
+
export * from './command';
|
|
11
11
|
export * from './comments';
|
|
12
12
|
export * from './cursor';
|
|
13
13
|
export * from './cursor-line-margin';
|
|
14
|
+
export * from './dnd';
|
|
14
15
|
export * from './listener';
|
|
15
16
|
export * from './markdown';
|
|
16
17
|
export * from './mention';
|
|
@@ -6,7 +6,7 @@ import { type Extension } from '@codemirror/state';
|
|
|
6
6
|
import { EditorView } from '@codemirror/view';
|
|
7
7
|
|
|
8
8
|
export type ListenerOptions = {
|
|
9
|
-
onFocus?: (
|
|
9
|
+
onFocus?: (focusing: boolean) => void;
|
|
10
10
|
onChange?: (text: string) => void;
|
|
11
11
|
};
|
|
12
12
|
|
|
@@ -18,8 +18,8 @@ export const listener = ({ onFocus, onChange }: ListenerOptions): Extension => {
|
|
|
18
18
|
|
|
19
19
|
onFocus &&
|
|
20
20
|
extensions.push(
|
|
21
|
-
EditorView.focusChangeEffect.of((_,
|
|
22
|
-
onFocus(
|
|
21
|
+
EditorView.focusChangeEffect.of((_, focusing) => {
|
|
22
|
+
onFocus(focusing);
|
|
23
23
|
return null;
|
|
24
24
|
}),
|
|
25
25
|
);
|
|
@@ -10,17 +10,9 @@ import { languages } from '@codemirror/language-data';
|
|
|
10
10
|
import { searchKeymap } from '@codemirror/search';
|
|
11
11
|
import { EditorState, type Extension } from '@codemirror/state';
|
|
12
12
|
import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
|
|
13
|
-
import {
|
|
14
|
-
crosshairCursor,
|
|
15
|
-
drawSelection,
|
|
16
|
-
dropCursor,
|
|
17
|
-
EditorView,
|
|
18
|
-
highlightActiveLine,
|
|
19
|
-
keymap,
|
|
20
|
-
placeholder,
|
|
21
|
-
} from '@codemirror/view';
|
|
13
|
+
import { EditorView, drawSelection, dropCursor, keymap, placeholder } from '@codemirror/view';
|
|
22
14
|
|
|
23
|
-
import type
|
|
15
|
+
import { type ThemeMode } from '@dxos/react-ui';
|
|
24
16
|
import { isNotFalsy } from '@dxos/util';
|
|
25
17
|
|
|
26
18
|
import { markdownHighlightStyle, markdownTagsExtensions } from './highlight';
|
|
@@ -28,7 +20,7 @@ import { type TextEditorProps } from '../../components';
|
|
|
28
20
|
|
|
29
21
|
export type MarkdownBundleOptions = {
|
|
30
22
|
themeMode?: ThemeMode;
|
|
31
|
-
} & Pick<TextEditorProps, '
|
|
23
|
+
} & Pick<TextEditorProps, 'lineWrapping' | 'placeholder'>;
|
|
32
24
|
|
|
33
25
|
/**
|
|
34
26
|
* Markdown bundle.
|
|
@@ -42,17 +34,15 @@ export const createMarkdownExtensions = ({
|
|
|
42
34
|
lineWrapping = true,
|
|
43
35
|
}: MarkdownBundleOptions = {}): Extension[] => {
|
|
44
36
|
return [
|
|
37
|
+
// TODO(burdon): Reconcile with createBasicExtensions.
|
|
45
38
|
lineWrapping && EditorView.lineWrapping,
|
|
46
39
|
EditorState.allowMultipleSelections.of(true),
|
|
47
40
|
EditorState.tabSize.of(2),
|
|
48
41
|
|
|
49
|
-
// https://github.com/codemirror/basic-setup
|
|
50
42
|
bracketMatching(),
|
|
51
43
|
closeBrackets(),
|
|
52
|
-
crosshairCursor(),
|
|
53
44
|
dropCursor(),
|
|
54
45
|
drawSelection(),
|
|
55
|
-
highlightActiveLine(),
|
|
56
46
|
history(),
|
|
57
47
|
indentOnInput(),
|
|
58
48
|
_placeholder && placeholder(_placeholder),
|
|
@@ -186,6 +186,9 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
|
|
|
186
186
|
|
|
187
187
|
export interface DecorateOptions {
|
|
188
188
|
renderLinkButton?: (el: Element, url: string) => void;
|
|
189
|
+
/**
|
|
190
|
+
* Prevents triggering decorations as the cursor moves through the document.
|
|
191
|
+
*/
|
|
189
192
|
selectionChangeDelay?: number;
|
|
190
193
|
}
|
|
191
194
|
|
|
@@ -195,9 +198,9 @@ export const decorateMarkdown = (options: DecorateOptions = {}) => {
|
|
|
195
198
|
return [
|
|
196
199
|
ViewPlugin.fromClass(
|
|
197
200
|
class {
|
|
198
|
-
pendingUpdate = -1;
|
|
199
201
|
deco: DecorationSet;
|
|
200
202
|
atomicDeco: DecorationSet;
|
|
203
|
+
pendingUpdate?: NodeJS.Timeout;
|
|
201
204
|
|
|
202
205
|
constructor(view: EditorView) {
|
|
203
206
|
({ deco: this.deco, atomicDeco: this.atomicDeco } = buildDecorations(view, options, view.hasFocus));
|
|
@@ -208,31 +211,35 @@ export const decorateMarkdown = (options: DecorateOptions = {}) => {
|
|
|
208
211
|
update.docChanged ||
|
|
209
212
|
update.viewportChanged ||
|
|
210
213
|
update.focusChanged ||
|
|
211
|
-
update.transactions.some((tr) => tr.effects.some((e) => e.is(forceUpdate)))
|
|
214
|
+
update.transactions.some((tr) => tr.effects.some((e) => e.is(forceUpdate))) ||
|
|
215
|
+
(update.selectionSet && !options.selectionChangeDelay)
|
|
212
216
|
) {
|
|
213
217
|
({ deco: this.deco, atomicDeco: this.atomicDeco } = buildDecorations(
|
|
214
218
|
update.view,
|
|
215
219
|
options,
|
|
216
220
|
update.view.hasFocus,
|
|
217
221
|
));
|
|
222
|
+
|
|
218
223
|
this.clearUpdate();
|
|
219
224
|
} else if (update.selectionSet) {
|
|
220
225
|
this.scheduleUpdate(update.view);
|
|
221
226
|
}
|
|
222
227
|
}
|
|
223
228
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
window.clearTimeout(this.pendingUpdate);
|
|
227
|
-
this.pendingUpdate = -1;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
229
|
+
// TODO(burdon): BUG: If the cursor is at the end of a link at the end of a line,
|
|
230
|
+
// the cursor will float in space or be in the wrong position after the decoration is applied.
|
|
231
231
|
scheduleUpdate(view: EditorView) {
|
|
232
232
|
this.clearUpdate();
|
|
233
|
-
this.pendingUpdate =
|
|
233
|
+
this.pendingUpdate = setTimeout(() => {
|
|
234
234
|
view.dispatch({ effects: forceUpdate.of(null) });
|
|
235
|
-
}, options.selectionChangeDelay
|
|
235
|
+
}, options.selectionChangeDelay);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
clearUpdate() {
|
|
239
|
+
if (this.pendingUpdate) {
|
|
240
|
+
clearTimeout(this.pendingUpdate);
|
|
241
|
+
this.pendingUpdate = undefined;
|
|
242
|
+
}
|
|
236
243
|
}
|
|
237
244
|
|
|
238
245
|
destroy() {
|
|
@@ -242,8 +249,8 @@ export const decorateMarkdown = (options: DecorateOptions = {}) => {
|
|
|
242
249
|
{
|
|
243
250
|
provide: (plugin) => [
|
|
244
251
|
EditorView.atomicRanges.of((view) => view.plugin(plugin)?.atomicDeco ?? Decoration.none),
|
|
245
|
-
EditorView.decorations.of((view) => view.plugin(plugin)?.deco ?? Decoration.none),
|
|
246
252
|
EditorView.decorations.of((view) => view.plugin(plugin)?.atomicDeco ?? Decoration.none),
|
|
253
|
+
EditorView.decorations.of((view) => view.plugin(plugin)?.deco ?? Decoration.none),
|
|
247
254
|
],
|
|
248
255
|
},
|
|
249
256
|
),
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// Copied from @codemirror/view
|
|
6
|
+
|
|
7
|
+
export interface Rect {
|
|
8
|
+
readonly left: number;
|
|
9
|
+
readonly right: number;
|
|
10
|
+
readonly top: number;
|
|
11
|
+
readonly bottom: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const flattenRect = (rect: Rect, left: boolean) => {
|
|
15
|
+
const x = left ? rect.left : rect.right;
|
|
16
|
+
return { left: x, right: x, top: rect.top, bottom: rect.bottom };
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let scratchRange: Range | null;
|
|
20
|
+
|
|
21
|
+
export const textRange = (node: Text, from: number, to = from) => {
|
|
22
|
+
const range = scratchRange || (scratchRange = document.createRange());
|
|
23
|
+
range.setEnd(node, to);
|
|
24
|
+
range.setStart(node, from);
|
|
25
|
+
return range;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const clientRectsFor = (dom: Node) => {
|
|
29
|
+
if (dom.nodeType === 3) {
|
|
30
|
+
return textRange(dom as Text, 0, dom.nodeValue!.length).getClientRects();
|
|
31
|
+
} else if (dom.nodeType === 1) {
|
|
32
|
+
return (dom as HTMLElement).getClientRects();
|
|
33
|
+
} else {
|
|
34
|
+
return [] as any as DOMRectList;
|
|
35
|
+
}
|
|
36
|
+
};
|
package/src/hooks/defs.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type Extension
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
6
6
|
|
|
7
7
|
import { type DocAccessor } from '@dxos/echo-schema';
|
|
8
8
|
|
|
@@ -18,27 +18,13 @@ export type Comment = {
|
|
|
18
18
|
cursor?: string;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* @deprecated
|
|
23
|
+
*/
|
|
24
|
+
// TODO(burdon): Remove.
|
|
21
25
|
export type EditorModel = {
|
|
22
26
|
id: string;
|
|
23
27
|
text: () => string;
|
|
24
|
-
extension?: Extension;
|
|
25
|
-
|
|
26
|
-
// TODO(burdon): Remove.
|
|
27
28
|
content: string | DocAccessor;
|
|
28
|
-
|
|
29
|
-
// TODO(burdon): Remove.
|
|
30
|
-
peer?: {
|
|
31
|
-
id: string;
|
|
32
|
-
name?: string;
|
|
33
|
-
};
|
|
29
|
+
extension?: Extension;
|
|
34
30
|
};
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* State field makes the model available to other extensions.
|
|
38
|
-
* @deprecated
|
|
39
|
-
*/
|
|
40
|
-
// TODO(burdon): Remove.
|
|
41
|
-
export const modelState = StateField.define<EditorModel | undefined>({
|
|
42
|
-
create: () => undefined,
|
|
43
|
-
update: (model) => model,
|
|
44
|
-
});
|
|
@@ -6,23 +6,23 @@ import type { EditorView } from '@codemirror/view';
|
|
|
6
6
|
|
|
7
7
|
import type { ToolbarProps } from '../components';
|
|
8
8
|
import {
|
|
9
|
-
createComment,
|
|
10
|
-
insertTable,
|
|
11
|
-
setHeading,
|
|
12
|
-
setStyle,
|
|
13
|
-
toggleStyle,
|
|
14
|
-
addLink,
|
|
15
|
-
removeLink,
|
|
16
9
|
Inline,
|
|
17
10
|
List,
|
|
18
|
-
addList,
|
|
19
|
-
removeList,
|
|
20
|
-
toggleList,
|
|
21
11
|
addBlockquote,
|
|
22
|
-
removeBlockquote,
|
|
23
12
|
addCodeblock,
|
|
13
|
+
addLink,
|
|
14
|
+
addList,
|
|
15
|
+
createComment,
|
|
16
|
+
insertTable,
|
|
17
|
+
removeBlockquote,
|
|
24
18
|
removeCodeblock,
|
|
19
|
+
removeLink,
|
|
20
|
+
removeList,
|
|
21
|
+
setHeading,
|
|
22
|
+
setStyle,
|
|
25
23
|
toggleBlockquote,
|
|
24
|
+
toggleList,
|
|
25
|
+
toggleStyle,
|
|
26
26
|
} from '../extensions';
|
|
27
27
|
|
|
28
28
|
export const useActionHandler = (view?: EditorView | null): ToolbarProps['onAction'] => {
|
|
@@ -21,7 +21,6 @@ import { type MutableRefObject, useRef, useState } from 'react';
|
|
|
21
21
|
* };
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
|
-
// TODO(burdon): Replace with useTextEditor.
|
|
25
24
|
export const useEditorView = (id: string): [MutableRefObject<EditorView | null>, boolean] => {
|
|
26
25
|
const editorRef = useRef<EditorView | null>(null);
|
|
27
26
|
const [prev, setPrev] = useState<[EditorView | null, string]>([null, '']);
|
|
@@ -10,7 +10,9 @@ import { Toolbar as NaturalToolbar, Select, useThemeContext, Tooltip } from '@dx
|
|
|
10
10
|
import { attentionSurface, mx, textBlockWidth } from '@dxos/react-ui-theme';
|
|
11
11
|
import { withTheme } from '@dxos/storybook-utils';
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { useActionHandler } from './useActionHandler';
|
|
14
|
+
import { createBasicExtensions, createThemeExtensions, useTextEditor } from './useTextEditor';
|
|
15
|
+
import { Toolbar } from '../components';
|
|
14
16
|
import {
|
|
15
17
|
type EditorMode,
|
|
16
18
|
EditorModes,
|
|
@@ -20,11 +22,9 @@ import {
|
|
|
20
22
|
image,
|
|
21
23
|
table,
|
|
22
24
|
useFormattingState,
|
|
23
|
-
} from '
|
|
24
|
-
import {
|
|
25
|
-
import
|
|
26
|
-
import translations from '../../translations';
|
|
27
|
-
import { Toolbar } from '../Toolbar';
|
|
25
|
+
} from '../extensions';
|
|
26
|
+
import { markdownTheme } from '../themes';
|
|
27
|
+
import translations from '../translations';
|
|
28
28
|
|
|
29
29
|
// TODO(burdon): Demo toolbar with hooks.
|
|
30
30
|
// TODO(burdon): Build components from hooks and adapters for model/extensions, etc.
|
|
@@ -46,7 +46,7 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
|
|
|
46
46
|
const extensions = useMemo(
|
|
47
47
|
() => [
|
|
48
48
|
editorMode ? EditorModes[editorMode] : [],
|
|
49
|
-
|
|
49
|
+
createBasicExtensions({ readonly }),
|
|
50
50
|
createThemeExtensions({
|
|
51
51
|
themeMode,
|
|
52
52
|
theme: markdownTheme,
|
|
@@ -116,7 +116,7 @@ const EditorModeToolbar = ({
|
|
|
116
116
|
|
|
117
117
|
export default {
|
|
118
118
|
title: 'react-ui-editor/useTextEditor',
|
|
119
|
-
component: TextEditor,
|
|
119
|
+
// component: TextEditor,
|
|
120
120
|
decorators: [withTheme],
|
|
121
121
|
render: (args: StoryProps) => (
|
|
122
122
|
<Tooltip.Provider>
|