@dxos/react-ui-editor 0.4.7-main.733903e → 0.4.7-main.76c1dea
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 +997 -728
- 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 +1 -0
- 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 +5 -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/comments.d.ts.map +1 -1
- 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 +4 -0
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
- package/dist/types/src/extensions/mention.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} +2 -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 +31 -31
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +95 -8
- package/src/components/TextEditor/TextEditor.stories.tsx +0 -15
- package/src/components/TextEditor/TextEditor.tsx +13 -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/comments.ts +2 -1
- package/src/extensions/cursor-line-margin.ts +5 -2
- package/src/extensions/dnd.ts +17 -0
- package/src/extensions/index.ts +2 -1
- package/src/extensions/listener.ts +3 -3
- package/src/extensions/markdown/bundle.ts +4 -15
- package/src/extensions/markdown/decorate.ts +76 -32
- package/src/extensions/mention.ts +3 -1
- 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 +20 -18
- package/src/index.ts +2 -0
- package/src/styles/index.ts +0 -1
- package/src/themes/default.ts +1 -0
- package/src/util.ts +1 -1
- 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
|
@@ -6,22 +6,19 @@ import '@dxosTheme';
|
|
|
6
6
|
|
|
7
7
|
import { BroadcastChannelNetworkAdapter } from '@automerge/automerge-repo-network-broadcastchannel';
|
|
8
8
|
import '@preact/signals-react';
|
|
9
|
-
import { EditorView } from '@codemirror/view'; // Register react integration.
|
|
10
9
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
11
10
|
|
|
12
11
|
import { Repo } from '@dxos/automerge/automerge-repo';
|
|
13
|
-
import { Filter } from '@dxos/echo-schema';
|
|
12
|
+
import { createDocAccessor, Filter } from '@dxos/echo-schema';
|
|
14
13
|
import { type PublicKey } from '@dxos/keys';
|
|
15
14
|
import { Expando, TextObject, useSpace } from '@dxos/react-client/echo';
|
|
16
|
-
import { useIdentity } from '@dxos/react-client/halo';
|
|
17
15
|
import { ClientRepeater } from '@dxos/react-client/testing';
|
|
16
|
+
import { useThemeContext } from '@dxos/react-ui';
|
|
18
17
|
import { withTheme } from '@dxos/storybook-utils';
|
|
19
18
|
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import { useTextEditor, useTextModel } from '../../hooks';
|
|
24
|
-
import { defaultTheme } from '../../themes';
|
|
19
|
+
import { automerge } from './automerge';
|
|
20
|
+
import { DocAccessor } from './defs';
|
|
21
|
+
import { createBasicExtensions, createThemeExtensions, useTextEditor } from '../../hooks';
|
|
25
22
|
import translations from '../../translations';
|
|
26
23
|
|
|
27
24
|
const initialContent = 'Hello world!';
|
|
@@ -32,24 +29,24 @@ type TestObject = {
|
|
|
32
29
|
|
|
33
30
|
type EditorProps = {
|
|
34
31
|
source: DocAccessor;
|
|
32
|
+
autoFocus?: boolean;
|
|
35
33
|
};
|
|
36
34
|
|
|
37
|
-
const Editor = ({ source }: EditorProps) => {
|
|
35
|
+
const Editor = ({ source, autoFocus }: EditorProps) => {
|
|
36
|
+
const { themeMode } = useThemeContext();
|
|
38
37
|
const extensions = useMemo(
|
|
39
38
|
() => [
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
createBasicBundle({ placeholder: 'Type here...' }),
|
|
39
|
+
createBasicExtensions({ placeholder: 'Type here...' }),
|
|
40
|
+
createThemeExtensions({ themeMode, slots: { editor: { className: 'p-2 bg-white dark:bg-black' } } }),
|
|
43
41
|
automerge(source),
|
|
44
|
-
awareness(),
|
|
45
42
|
],
|
|
46
|
-
[source],
|
|
43
|
+
[themeMode, source],
|
|
47
44
|
);
|
|
48
45
|
|
|
49
46
|
const { parentRef } = useTextEditor({
|
|
50
|
-
autoFocus: true,
|
|
51
47
|
doc: DocAccessor.getValue(source),
|
|
52
48
|
extensions,
|
|
49
|
+
autoFocus,
|
|
53
50
|
});
|
|
54
51
|
|
|
55
52
|
return <div ref={parentRef} />;
|
|
@@ -61,12 +58,8 @@ const Story = () => {
|
|
|
61
58
|
|
|
62
59
|
useEffect(() => {
|
|
63
60
|
queueMicrotask(async () => {
|
|
64
|
-
const repo1 = new Repo({
|
|
65
|
-
|
|
66
|
-
});
|
|
67
|
-
const repo2 = new Repo({
|
|
68
|
-
network: [new BroadcastChannelNetworkAdapter()],
|
|
69
|
-
});
|
|
61
|
+
const repo1 = new Repo({ network: [new BroadcastChannelNetworkAdapter()] });
|
|
62
|
+
const repo2 = new Repo({ network: [new BroadcastChannelNetworkAdapter()] });
|
|
70
63
|
|
|
71
64
|
const object1 = repo1.create();
|
|
72
65
|
object1.change((doc: TestObject) => {
|
|
@@ -86,8 +79,8 @@ const Story = () => {
|
|
|
86
79
|
}
|
|
87
80
|
|
|
88
81
|
return (
|
|
89
|
-
<div role='none' className='grid grid-cols-2 bs-full is-full
|
|
90
|
-
<Editor source={object1} />
|
|
82
|
+
<div role='none' className='grid grid-cols-2 bs-full is-full divide-x divide-neutral-500'>
|
|
83
|
+
<Editor source={object1} autoFocus />
|
|
91
84
|
<Editor source={object2} />
|
|
92
85
|
</div>
|
|
93
86
|
);
|
|
@@ -96,36 +89,39 @@ const Story = () => {
|
|
|
96
89
|
export default {
|
|
97
90
|
title: 'react-ui-editor/Automerge',
|
|
98
91
|
component: Editor,
|
|
92
|
+
decorators: [withTheme],
|
|
99
93
|
render: () => <Story />,
|
|
100
94
|
parameters: { translations, layout: 'fullscreen' },
|
|
101
95
|
};
|
|
102
96
|
|
|
103
|
-
export const Default = {};
|
|
104
|
-
|
|
105
97
|
const EchoStory = ({ spaceKey }: { spaceKey: PublicKey }) => {
|
|
106
|
-
|
|
98
|
+
// TODO(burdon): Test identity.
|
|
99
|
+
// const identity = useIdentity();
|
|
107
100
|
const space = useSpace(spaceKey);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (!model) {
|
|
101
|
+
const source = useMemo<DocAccessor | undefined>(() => {
|
|
102
|
+
const { objects = [] } = space?.db.query(Filter.from({ type: 'test' })) ?? {};
|
|
103
|
+
if (objects.length) {
|
|
104
|
+
return createDocAccessor(objects[0].content);
|
|
105
|
+
}
|
|
106
|
+
}, [space]);
|
|
107
|
+
|
|
108
|
+
if (!source) {
|
|
117
109
|
return null;
|
|
118
110
|
}
|
|
119
111
|
|
|
120
|
-
return <
|
|
112
|
+
return <Editor source={source} />;
|
|
121
113
|
};
|
|
122
114
|
|
|
115
|
+
export const Default = {};
|
|
116
|
+
|
|
117
|
+
// TODO(burdon): Feed already added error.
|
|
123
118
|
export const WithEcho = {
|
|
124
119
|
decorators: [withTheme],
|
|
125
120
|
render: () => {
|
|
126
121
|
return (
|
|
127
122
|
<ClientRepeater
|
|
128
123
|
count={2}
|
|
124
|
+
component={EchoStory}
|
|
129
125
|
createSpace
|
|
130
126
|
onCreateSpace={async (space) => {
|
|
131
127
|
space.db.add(
|
|
@@ -135,7 +131,6 @@ export const WithEcho = {
|
|
|
135
131
|
}),
|
|
136
132
|
);
|
|
137
133
|
}}
|
|
138
|
-
component={EchoStory}
|
|
139
134
|
/>
|
|
140
135
|
);
|
|
141
136
|
},
|
|
@@ -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
|
+
];
|
|
@@ -9,6 +9,7 @@ import sortBy from 'lodash.sortby';
|
|
|
9
9
|
import { useEffect } from 'react';
|
|
10
10
|
|
|
11
11
|
import { debounce } from '@dxos/async';
|
|
12
|
+
import { log } from '@dxos/log';
|
|
12
13
|
import { nonNullable } from '@dxos/util';
|
|
13
14
|
|
|
14
15
|
import { Cursor } from './cursor';
|
|
@@ -128,7 +129,7 @@ const commentsDecorations = EditorView.decorations.compute([commentsState], (sta
|
|
|
128
129
|
?.flatMap((comment) => {
|
|
129
130
|
const range = comment.range;
|
|
130
131
|
if (!range || range.from === range.to) {
|
|
131
|
-
|
|
132
|
+
log.warn('Invalid range:', range);
|
|
132
133
|
return undefined;
|
|
133
134
|
}
|
|
134
135
|
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
import { type EditorState, type Extension, Facet, Transaction } from '@codemirror/state';
|
|
9
9
|
import { EditorView } from '@codemirror/view';
|
|
10
10
|
|
|
11
|
+
import { log } from '@dxos/log';
|
|
12
|
+
|
|
11
13
|
/**
|
|
12
14
|
* Number of lines above/below the cursor to keep visible in the viewport.
|
|
13
15
|
* Defaults to 3.
|
|
@@ -18,7 +20,8 @@ export const cursorLineMarginFacet = Facet.define<number, number>({
|
|
|
18
20
|
|
|
19
21
|
const annotation = 'cursorLineMargin';
|
|
20
22
|
const lineAtPos = (s: EditorState, pos: number) => s.doc.lineAt(pos).number;
|
|
21
|
-
|
|
23
|
+
|
|
24
|
+
// Seems to the best approximation of CM5's `cursorScrollMargin`
|
|
22
25
|
// https://discuss.codemirror.net/t/cursorscrollmargin-for-v6/7448
|
|
23
26
|
export const cursorLineMargin: Extension = EditorView.updateListener.of(
|
|
24
27
|
({ transactions, state, selectionSet, startState, view }) => {
|
|
@@ -60,7 +63,7 @@ export const cursorLineMargin: Extension = EditorView.updateListener.of(
|
|
|
60
63
|
|
|
61
64
|
// the scroll margins are overlapping
|
|
62
65
|
if (needsScrollTop && needsScrollBot) {
|
|
63
|
-
|
|
66
|
+
log.warn('is cursorScrollMargin too large for the editor size?');
|
|
64
67
|
return;
|
|
65
68
|
}
|
|
66
69
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type { Extension } from '@codemirror/state';
|
|
6
|
+
import { EditorView } from '@codemirror/view';
|
|
7
|
+
|
|
8
|
+
import { log } from '@dxos/log';
|
|
9
|
+
|
|
10
|
+
// TODO(burdon): DND experiment.
|
|
11
|
+
export const dnd = (): Extension => {
|
|
12
|
+
return EditorView.domEventHandlers({
|
|
13
|
+
drop: (event, view) => {
|
|
14
|
+
log.info('domEventHandlersDrop', { event });
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
};
|
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.
|
|
@@ -36,24 +28,21 @@ export type MarkdownBundleOptions = {
|
|
|
36
28
|
* https://codemirror.net/docs/community
|
|
37
29
|
* https://codemirror.net/docs/ref/#codemirror.basicSetup
|
|
38
30
|
*/
|
|
39
|
-
// TODO(burdon): Add Composer here: https://codemirror.net/docs/community
|
|
40
31
|
export const createMarkdownExtensions = ({
|
|
41
32
|
themeMode,
|
|
42
33
|
placeholder: _placeholder,
|
|
43
34
|
lineWrapping = true,
|
|
44
35
|
}: MarkdownBundleOptions = {}): Extension[] => {
|
|
45
36
|
return [
|
|
37
|
+
// TODO(burdon): Reconcile with createBasicExtensions.
|
|
46
38
|
lineWrapping && EditorView.lineWrapping,
|
|
47
39
|
EditorState.allowMultipleSelections.of(true),
|
|
48
40
|
EditorState.tabSize.of(2),
|
|
49
41
|
|
|
50
|
-
// https://github.com/codemirror/basic-setup
|
|
51
42
|
bracketMatching(),
|
|
52
43
|
closeBrackets(),
|
|
53
|
-
crosshairCursor(),
|
|
54
44
|
dropCursor(),
|
|
55
45
|
drawSelection(),
|
|
56
|
-
highlightActiveLine(),
|
|
57
46
|
history(),
|
|
58
47
|
indentOnInput(),
|
|
59
48
|
_placeholder && placeholder(_placeholder),
|