@dxos/react-ui-editor 0.3.11-main.68db74e → 0.3.11-main.69484a6
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 +1632 -1056
- 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 +13 -15
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +14 -6
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +1 -1
- package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/comments.stories.d.ts +1 -1
- package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/{yjs.stories.d.ts → hooks.stories.d.ts} +13 -2
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +25 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +23 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/index.d.ts +2 -0
- package/dist/types/src/components/Toolbar/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +1 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.spec.d.ts +2 -0
- package/dist/types/src/extensions/automerge/automerge.spec.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/automerge.test.d.ts +2 -0
- package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/defs.d.ts +9 -11
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/index.d.ts +0 -1
- package/dist/types/src/extensions/automerge/index.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/semaphore.d.ts +8 -12
- package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -2
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness.d.ts +2 -2
- package/dist/types/src/extensions/awareness.d.ts.map +1 -1
- package/dist/types/src/extensions/basic.d.ts +4 -5
- package/dist/types/src/extensions/basic.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +35 -18
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/cursor.d.ts +3 -3
- package/dist/types/src/extensions/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +1 -7
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts +3 -6
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/code.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/formatting.d.ts +13 -0
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/heading.d.ts +6 -0
- package/dist/types/src/extensions/markdown/heading.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/hr.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/image.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/index.d.ts +8 -0
- package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/table.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/tasklist.d.ts +3 -0
- package/dist/types/src/extensions/markdown/tasklist.d.ts.map +1 -0
- package/dist/types/src/extensions/mention.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner.d.ts +4 -0
- package/dist/types/src/extensions/outliner.d.ts.map +1 -0
- package/dist/types/src/hooks/awareness-provider.d.ts +7 -4
- package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -1
- package/dist/types/src/hooks/defs.d.ts +25 -0
- package/dist/types/src/hooks/defs.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +4 -2
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useActionHandler.d.ts +4 -0
- package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -0
- package/dist/types/src/hooks/useEditorView.d.ts +17 -0
- package/dist/types/src/hooks/useEditorView.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextEditor.d.ts +39 -0
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextModel.d.ts +2 -27
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/styles/markdown.d.ts +2 -2
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/util.d.ts +2 -0
- package/dist/types/src/util.d.ts.map +1 -1
- package/package.json +47 -37
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +73 -82
- package/src/components/TextEditor/TextEditor.tsx +112 -75
- package/src/components/TextEditor/automerge.stories.tsx +1 -2
- package/src/components/TextEditor/comments.stories.tsx +113 -71
- package/src/components/TextEditor/hooks.stories.tsx +106 -0
- package/src/components/Toolbar/Toolbar.stories.tsx +90 -0
- package/src/components/Toolbar/Toolbar.tsx +166 -0
- package/src/components/Toolbar/index.ts +5 -0
- package/src/components/index.ts +1 -0
- package/src/extensions/autocomplete.ts +6 -3
- package/src/extensions/automerge/automerge.spec.tsx +76 -0
- package/src/extensions/automerge/automerge.test.ts +13 -0
- package/src/extensions/automerge/automerge.ts +54 -45
- package/src/extensions/automerge/cursor.ts +3 -3
- package/src/extensions/automerge/defs.ts +13 -34
- package/src/extensions/automerge/index.ts +0 -1
- package/src/extensions/automerge/semaphore.ts +33 -32
- package/src/extensions/automerge/update-automerge.ts +14 -8
- package/src/extensions/automerge/update-codemirror.ts +5 -5
- package/src/extensions/awareness.ts +18 -17
- package/src/extensions/basic.ts +17 -10
- package/src/extensions/comments.ts +316 -165
- package/src/extensions/cursor.ts +7 -3
- package/src/extensions/index.ts +1 -7
- package/src/extensions/markdown/bundle.ts +23 -28
- package/src/extensions/markdown/code.ts +150 -0
- package/src/extensions/markdown/formatting.ts +188 -0
- package/src/extensions/markdown/heading.ts +59 -0
- package/src/extensions/markdown/highlight.ts +9 -15
- package/src/extensions/markdown/hr.ts +71 -0
- package/src/extensions/markdown/index.ts +8 -0
- package/src/extensions/{link.ts → markdown/link.ts} +8 -5
- package/src/extensions/{tasklist.ts → markdown/tasklist.ts} +48 -33
- package/src/extensions/mention.ts +9 -2
- package/src/extensions/outliner.ts +12 -0
- package/src/hooks/awareness-provider.ts +24 -20
- package/src/hooks/defs.ts +42 -0
- package/src/hooks/index.ts +5 -2
- package/src/hooks/useActionHandler.ts +62 -0
- package/src/hooks/useEditorView.ts +29 -0
- package/src/hooks/useTextEditor.ts +153 -0
- package/src/hooks/useTextModel.ts +49 -55
- package/src/styles/markdown.ts +9 -10
- package/src/themes/default.ts +4 -4
- package/src/util.ts +26 -3
- package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
- package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
- package/dist/types/src/extensions/code.d.ts.map +0 -1
- package/dist/types/src/extensions/hr.d.ts.map +0 -1
- package/dist/types/src/extensions/image.d.ts.map +0 -1
- package/dist/types/src/extensions/link.d.ts.map +0 -1
- package/dist/types/src/extensions/table.d.ts.map +0 -1
- package/dist/types/src/extensions/tasklist.d.ts +0 -7
- package/dist/types/src/extensions/tasklist.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
- package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/index.d.ts +0 -2
- package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
- package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
- package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge.d.ts +0 -3
- package/dist/types/src/hooks/automerge.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs.d.ts +0 -27
- package/dist/types/src/hooks/yjs.d.ts.map +0 -1
- package/dist/types/src/testing/index.d.ts +0 -3
- package/dist/types/src/testing/index.d.ts.map +0 -1
- package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
- package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
- package/dist/types/src/testing/proto/index.d.ts +0 -2
- package/dist/types/src/testing/proto/index.d.ts.map +0 -1
- package/dist/types/src/testing/replicator.d.ts +0 -45
- package/dist/types/src/testing/replicator.d.ts.map +0 -1
- package/src/components/TextEditor/codemirror.stories.ts +0 -115
- package/src/components/TextEditor/yjs.stories.tsx +0 -56
- package/src/extensions/code.ts +0 -106
- package/src/extensions/hr.ts +0 -73
- package/src/extensions/yjs/cursor.ts +0 -22
- package/src/extensions/yjs/index.ts +0 -5
- package/src/extensions/yjs/yjs.test.ts +0 -35
- package/src/extensions/yjs/yjs.ts +0 -16
- package/src/hooks/automerge.ts +0 -52
- package/src/hooks/yjs.ts +0 -156
- package/src/testing/index.ts +0 -6
- package/src/testing/proto/gen/schema.ts +0 -49
- package/src/testing/proto/index.ts +0 -5
- package/src/testing/proto/schema.proto +0 -15
- package/src/testing/replicator.ts +0 -184
- /package/dist/types/src/extensions/{code.d.ts → markdown/code.d.ts} +0 -0
- /package/dist/types/src/extensions/{hr.d.ts → markdown/hr.d.ts} +0 -0
- /package/dist/types/src/extensions/{image.d.ts → markdown/image.d.ts} +0 -0
- /package/dist/types/src/extensions/{link.d.ts → markdown/link.d.ts} +0 -0
- /package/dist/types/src/extensions/{table.d.ts → markdown/table.d.ts} +0 -0
- /package/src/extensions/{image.ts → markdown/image.ts} +0 -0
- /package/src/extensions/{table.ts → markdown/table.ts} +0 -0
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { EditorState, type Extension } from '@codemirror/state';
|
|
5
|
+
import { EditorState, type Extension, type StateEffect } from '@codemirror/state';
|
|
6
6
|
import { EditorView } from '@codemirror/view';
|
|
7
|
-
import { useFocusableGroup } from '@fluentui/react-tabster';
|
|
7
|
+
// import { useFocusableGroup } from '@fluentui/react-tabster';
|
|
8
8
|
import { vim } from '@replit/codemirror-vim';
|
|
9
9
|
import defaultsDeep from 'lodash.defaultsdeep';
|
|
10
10
|
import React, {
|
|
@@ -14,19 +14,22 @@ import React, {
|
|
|
14
14
|
useCallback,
|
|
15
15
|
useEffect,
|
|
16
16
|
useImperativeHandle,
|
|
17
|
-
useState,
|
|
18
17
|
useRef,
|
|
18
|
+
useState,
|
|
19
19
|
} from 'react';
|
|
20
20
|
|
|
21
|
-
import {
|
|
21
|
+
import { log } from '@dxos/log';
|
|
22
22
|
import { useThemeContext } from '@dxos/react-ui';
|
|
23
|
-
import {
|
|
23
|
+
import { attentionSurface, mx } from '@dxos/react-ui-theme';
|
|
24
|
+
import { isNotFalsy } from '@dxos/util';
|
|
24
25
|
|
|
25
|
-
import {
|
|
26
|
-
import { type
|
|
26
|
+
import { createBasicBundle, createMarkdownExtensions } from '../../extensions';
|
|
27
|
+
import { type EditorModel } from '../../hooks';
|
|
27
28
|
import { type ThemeStyles } from '../../styles';
|
|
28
29
|
import { defaultTheme, markdownTheme, textTheme } from '../../themes';
|
|
30
|
+
import { logChanges } from '../../util';
|
|
29
31
|
|
|
32
|
+
// TODO(burdon): Change to enum?
|
|
30
33
|
export const EditorModes = ['default', 'vim'] as const;
|
|
31
34
|
export type EditorMode = (typeof EditorModes)[number];
|
|
32
35
|
|
|
@@ -41,23 +44,28 @@ export type CursorInfo = {
|
|
|
41
44
|
|
|
42
45
|
export type TextEditorSlots = {
|
|
43
46
|
root?: Omit<ComponentProps<'div'>, 'ref'>;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
editor?: {
|
|
48
|
+
className?: string;
|
|
49
|
+
};
|
|
50
|
+
content?: {
|
|
51
|
+
className?: string;
|
|
52
|
+
};
|
|
47
53
|
};
|
|
48
54
|
|
|
49
55
|
// TODO(burdon): Spellcheck?
|
|
50
56
|
export type TextEditorProps = {
|
|
51
|
-
model: EditorModel;
|
|
52
|
-
focus?: boolean;
|
|
53
|
-
selection?: { anchor: number; head?: number };
|
|
57
|
+
model: EditorModel; // TODO(burdon): Optional (e.g., just provide content if readonly).
|
|
54
58
|
readonly?: boolean; // TODO(burdon): Move into model.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
autoFocus?: boolean;
|
|
60
|
+
lineWrapping?: boolean;
|
|
61
|
+
scrollTo?: StateEffect<any>; // TODO(burdon): Restore scroll position: scrollTo EditorView.scrollSnapshot().
|
|
62
|
+
selection?: { anchor: number; head?: number };
|
|
63
|
+
editorMode?: EditorMode; // TODO(burdon): Factor out.
|
|
58
64
|
placeholder?: string;
|
|
59
65
|
theme?: ThemeStyles;
|
|
60
66
|
slots?: TextEditorSlots;
|
|
67
|
+
extensions?: Extension[];
|
|
68
|
+
debug?: boolean;
|
|
61
69
|
};
|
|
62
70
|
|
|
63
71
|
/**
|
|
@@ -65,108 +73,134 @@ export type TextEditorProps = {
|
|
|
65
73
|
*/
|
|
66
74
|
export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
67
75
|
(
|
|
68
|
-
{
|
|
76
|
+
{
|
|
77
|
+
model,
|
|
78
|
+
readonly,
|
|
79
|
+
autoFocus,
|
|
80
|
+
scrollTo,
|
|
81
|
+
selection,
|
|
82
|
+
editorMode,
|
|
83
|
+
theme,
|
|
84
|
+
slots = defaultSlots,
|
|
85
|
+
extensions = [],
|
|
86
|
+
debug,
|
|
87
|
+
},
|
|
69
88
|
forwardedRef,
|
|
70
89
|
) => {
|
|
71
|
-
|
|
90
|
+
// TODO(burdon): Hook causes error even if properties are not spread into div.
|
|
91
|
+
// Uncaught TypeError: Cannot read properties of undefined (reading 'relatedTarget')
|
|
92
|
+
// Uses event.detail, which is deprecated (not event.details). At runtime the event has a property `details`.
|
|
93
|
+
// https://github.com/microsoft/tabster/blob/master/src/State/FocusedElement.ts#L348 (e.detail.relatedTarget)
|
|
94
|
+
// https://github.com/microsoft/keyborg/blob/49e49b2c3ba0a5f6cc518ac46825d7551def8109/src/FocusEvent.ts#L58
|
|
95
|
+
// const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
|
|
72
96
|
const { themeMode } = useThemeContext();
|
|
73
97
|
|
|
74
|
-
// The editor view ref should only be used as an escape hatch.
|
|
75
98
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
76
99
|
const [view, setView] = useState<EditorView | null>(null);
|
|
77
|
-
useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
|
|
78
|
-
|
|
79
|
-
// Focus.
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
if (view && focus) {
|
|
82
|
-
view.focus();
|
|
83
|
-
}
|
|
84
|
-
}, [view, focus]);
|
|
85
100
|
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (awareness && peer) {
|
|
90
|
-
awareness.setLocalStateField('user', {
|
|
91
|
-
name: peer.name ?? generateName(peer.id),
|
|
92
|
-
color: getColorForValue({ value: peer.id, type: 'color' }),
|
|
93
|
-
colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
}, [awareness, peer, themeMode]);
|
|
101
|
+
// The view ref can be used to focus the editor.
|
|
102
|
+
// NOTE: This does not cause the parent to re-render, so the ref is not available immediately.
|
|
103
|
+
useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
|
|
97
104
|
|
|
98
|
-
//
|
|
105
|
+
// Set focus.
|
|
99
106
|
useEffect(() => {
|
|
100
|
-
if (
|
|
101
|
-
view.
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
if (autoFocus && !view?.hasFocus) {
|
|
108
|
+
if (view?.state.selection.main?.from === 0) {
|
|
109
|
+
// Start at end of line.
|
|
110
|
+
const { to } = view.state.doc.lineAt(0);
|
|
111
|
+
view?.dispatch({ selection: { anchor: to } });
|
|
112
|
+
}
|
|
113
|
+
view?.focus();
|
|
104
114
|
}
|
|
105
|
-
}, [view,
|
|
115
|
+
}, [view, autoFocus]);
|
|
106
116
|
|
|
117
|
+
// Create editor state and view.
|
|
118
|
+
// The view is recreated if the model or extensions are changed.
|
|
107
119
|
useEffect(() => {
|
|
108
120
|
if (!model || !rootRef.current) {
|
|
109
121
|
return;
|
|
110
122
|
}
|
|
111
123
|
|
|
124
|
+
//
|
|
125
|
+
// EditorState
|
|
112
126
|
// https://codemirror.net/docs/ref/#state.EditorStateConfig
|
|
127
|
+
//
|
|
113
128
|
const state = EditorState.create({
|
|
114
129
|
doc: model.text(),
|
|
115
|
-
// TODO(burdon): Composer should store and set selection when switching documents.
|
|
116
130
|
selection,
|
|
117
131
|
extensions: [
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
132
|
+
// TODO(burdon): Doesn't catch errors in keymap functions.
|
|
133
|
+
EditorView.exceptionSink.of((err) => {
|
|
134
|
+
log.catch(err);
|
|
135
|
+
}),
|
|
122
136
|
|
|
123
137
|
// Theme.
|
|
124
|
-
// TODO(burdon): Make
|
|
138
|
+
// TODO(burdon): Make configurable.
|
|
125
139
|
EditorView.baseTheme(defaultTheme),
|
|
126
140
|
EditorView.theme(theme ?? {}),
|
|
127
141
|
EditorView.darkTheme.of(themeMode === 'dark'),
|
|
142
|
+
EditorView.editorAttributes.of({ class: slots.editor?.className ?? '' }),
|
|
143
|
+
EditorView.contentAttributes.of({ class: slots.content?.className ?? '' }),
|
|
144
|
+
|
|
145
|
+
// State.
|
|
146
|
+
EditorView.editable.of(!readonly),
|
|
147
|
+
EditorState.readOnly.of(!!readonly),
|
|
128
148
|
|
|
129
149
|
// Storage and replication.
|
|
150
|
+
// NOTE: This must come before user extensions.
|
|
130
151
|
model.extension,
|
|
131
152
|
|
|
153
|
+
// TODO(burdon): Factor out? (Requires special handling for Escape/Enter below).
|
|
154
|
+
editorMode === 'vim' && vim(),
|
|
155
|
+
|
|
132
156
|
// Custom.
|
|
133
157
|
...extensions,
|
|
134
|
-
].filter(
|
|
158
|
+
].filter(isNotFalsy),
|
|
135
159
|
});
|
|
136
160
|
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
161
|
+
//
|
|
162
|
+
// EditorView
|
|
163
|
+
// https://codemirror.net/docs/ref/#view.EditorViewConfig
|
|
164
|
+
//
|
|
141
165
|
const newView = new EditorView({
|
|
142
|
-
parent: rootRef.current,
|
|
143
166
|
state,
|
|
144
|
-
|
|
167
|
+
parent: rootRef.current,
|
|
168
|
+
scrollTo,
|
|
169
|
+
// NOTE: Uncomment to debug/monitor all transactions.
|
|
145
170
|
// https://codemirror.net/docs/ref/#view.EditorView.dispatch
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
171
|
+
dispatchTransactions: (trs, view) => {
|
|
172
|
+
if (debug) {
|
|
173
|
+
logChanges(trs);
|
|
174
|
+
}
|
|
175
|
+
view.update(trs);
|
|
176
|
+
},
|
|
149
177
|
});
|
|
150
178
|
|
|
179
|
+
view?.destroy();
|
|
151
180
|
setView(newView);
|
|
181
|
+
|
|
152
182
|
return () => {
|
|
153
183
|
newView?.destroy();
|
|
154
184
|
setView(null);
|
|
155
185
|
};
|
|
156
186
|
}, [rootRef, model, readonly, editorMode, themeMode]);
|
|
157
187
|
|
|
188
|
+
// Handles tab/focus.
|
|
189
|
+
// Pressing Escape focuses the outer div (to support tab navigation); pressing Enter refocuses the editor.
|
|
190
|
+
// TODO(burdon): Convert to keymap?
|
|
158
191
|
const handleKeyUp = useCallback(
|
|
159
192
|
(event: KeyboardEvent) => {
|
|
160
193
|
const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
|
|
161
194
|
switch (key) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
// }
|
|
195
|
+
case 'Enter': {
|
|
196
|
+
view?.focus();
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
167
199
|
|
|
168
200
|
case 'Escape': {
|
|
169
|
-
editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)
|
|
201
|
+
if (editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)) {
|
|
202
|
+
view?.focus();
|
|
203
|
+
}
|
|
170
204
|
break;
|
|
171
205
|
}
|
|
172
206
|
}
|
|
@@ -178,25 +212,25 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
178
212
|
<div
|
|
179
213
|
key={model.id}
|
|
180
214
|
role='none'
|
|
181
|
-
ref={rootRef}
|
|
182
215
|
tabIndex={0}
|
|
183
|
-
onKeyUp={handleKeyUp}
|
|
184
216
|
{...slots.root}
|
|
185
|
-
{...(editorMode !== 'vim' && tabsterDOMAttribute)}
|
|
217
|
+
// {...(editorMode !== 'vim' && tabsterDOMAttribute)}
|
|
218
|
+
onKeyUp={handleKeyUp}
|
|
219
|
+
ref={rootRef}
|
|
186
220
|
/>
|
|
187
221
|
);
|
|
188
222
|
},
|
|
189
223
|
);
|
|
190
224
|
|
|
191
225
|
export const TextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
192
|
-
({ readonly, placeholder,
|
|
226
|
+
({ readonly, placeholder, lineWrapping, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
|
|
193
227
|
const { themeMode } = useThemeContext();
|
|
194
228
|
const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
|
|
195
229
|
return (
|
|
196
230
|
<BaseTextEditor
|
|
197
231
|
ref={forwardedRef}
|
|
198
232
|
readonly={readonly}
|
|
199
|
-
extensions={[
|
|
233
|
+
extensions={[createBasicBundle({ themeMode, placeholder, lineWrapping }), ...extensions]}
|
|
200
234
|
theme={theme}
|
|
201
235
|
slots={updatedSlots}
|
|
202
236
|
{...props}
|
|
@@ -205,16 +239,15 @@ export const TextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
205
239
|
},
|
|
206
240
|
);
|
|
207
241
|
|
|
208
|
-
// TODO(burdon): Remove (Just provide bundle, slots).
|
|
209
242
|
export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
|
|
210
|
-
({ readonly, placeholder,
|
|
243
|
+
({ readonly, placeholder, theme = markdownTheme, slots, extensions = [], ...props }, forwardedRef) => {
|
|
211
244
|
const { themeMode } = useThemeContext();
|
|
212
245
|
const updatedSlots = defaultsDeep({}, slots, defaultMarkdownSlots);
|
|
213
246
|
return (
|
|
214
247
|
<BaseTextEditor
|
|
215
248
|
ref={forwardedRef}
|
|
216
249
|
readonly={readonly}
|
|
217
|
-
extensions={[
|
|
250
|
+
extensions={[createMarkdownExtensions({ themeMode, placeholder }), ...extensions]}
|
|
218
251
|
theme={theme}
|
|
219
252
|
slots={updatedSlots}
|
|
220
253
|
{...props}
|
|
@@ -225,7 +258,11 @@ export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
225
258
|
|
|
226
259
|
export const defaultSlots: TextEditorSlots = {
|
|
227
260
|
root: {
|
|
228
|
-
|
|
261
|
+
// TODO(burdon): Add focusRing by default/as property?
|
|
262
|
+
className: mx('flex flex-col grow overflow-y-auto', attentionSurface),
|
|
263
|
+
},
|
|
264
|
+
editor: {
|
|
265
|
+
className: 'h-full p-2',
|
|
229
266
|
},
|
|
230
267
|
};
|
|
231
268
|
|
|
@@ -13,7 +13,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
|
|
13
13
|
|
|
14
14
|
import { type Prop } from '@dxos/automerge/automerge';
|
|
15
15
|
import { Repo, type DocHandle } from '@dxos/automerge/automerge-repo';
|
|
16
|
-
import { Filter
|
|
16
|
+
import { Filter } from '@dxos/echo-schema';
|
|
17
17
|
import { type PublicKey } from '@dxos/keys';
|
|
18
18
|
import { Expando, TextObject, useSpace } from '@dxos/react-client/echo';
|
|
19
19
|
import { useIdentity } from '@dxos/react-client/halo';
|
|
@@ -130,7 +130,6 @@ const EchoStory = ({ id, spaceKey }: { id: number; spaceKey: PublicKey }) => {
|
|
|
130
130
|
|
|
131
131
|
export const WithEcho = {
|
|
132
132
|
render: () => {
|
|
133
|
-
setGlobalAutomergePreference(true);
|
|
134
133
|
return (
|
|
135
134
|
<ClientRepeater
|
|
136
135
|
count={2}
|
|
@@ -6,18 +6,19 @@ import '@dxosTheme';
|
|
|
6
6
|
|
|
7
7
|
import { type EditorView, keymap } from '@codemirror/view';
|
|
8
8
|
import { faker } from '@faker-js/faker';
|
|
9
|
-
import { Check } from '@phosphor-icons/react';
|
|
9
|
+
import { Check, Trash } from '@phosphor-icons/react';
|
|
10
10
|
import React, { type FC, useEffect, useMemo, useRef, useState } from 'react';
|
|
11
11
|
|
|
12
|
-
import { TextObject } from '@dxos/echo-schema';
|
|
12
|
+
import { getTextContent, TextObject } from '@dxos/echo-schema';
|
|
13
13
|
import { PublicKey } from '@dxos/keys';
|
|
14
|
+
import { log } from '@dxos/log';
|
|
14
15
|
import { Button, DensityProvider } from '@dxos/react-ui';
|
|
15
16
|
import { fixedInsetFlexLayout, mx } from '@dxos/react-ui-theme';
|
|
16
17
|
import { withTheme } from '@dxos/storybook-utils';
|
|
17
18
|
|
|
18
19
|
import { MarkdownEditor, TextEditor } from './TextEditor';
|
|
19
|
-
import { comments, type CommentsOptions,
|
|
20
|
-
import { type
|
|
20
|
+
import { comments, type CommentsOptions, focusComment, useComments } from '../../extensions';
|
|
21
|
+
import { type Comment, type Range, useTextModel } from '../../hooks';
|
|
21
22
|
|
|
22
23
|
faker.seed(101);
|
|
23
24
|
|
|
@@ -27,34 +28,41 @@ faker.seed(101);
|
|
|
27
28
|
|
|
28
29
|
const Editor: FC<{
|
|
29
30
|
item: { text: TextObject };
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
comments: Comment[];
|
|
32
|
+
selected?: string;
|
|
32
33
|
onCreateComment: CommentsOptions['onCreate'];
|
|
34
|
+
onDeleteComment: CommentsOptions['onDelete'];
|
|
35
|
+
onUpdateComment: CommentsOptions['onUpdate'];
|
|
33
36
|
onSelectComment: CommentsOptions['onSelect'];
|
|
34
|
-
}> = ({
|
|
37
|
+
}> = ({
|
|
38
|
+
item,
|
|
39
|
+
selected: selectedValue,
|
|
40
|
+
comments: commentRanges,
|
|
41
|
+
onCreateComment,
|
|
42
|
+
onDeleteComment,
|
|
43
|
+
onUpdateComment,
|
|
44
|
+
onSelectComment,
|
|
45
|
+
}) => {
|
|
35
46
|
const model = useTextModel({ text: item.text });
|
|
36
|
-
const
|
|
47
|
+
const view = useRef<EditorView>(null);
|
|
37
48
|
const [selected, setSelected] = useState<string>();
|
|
38
49
|
useEffect(() => {
|
|
39
|
-
if (!
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
const range = Cursor.getRangeFromCursor(editorRef.current!.state.facet(Cursor.converter), cursor);
|
|
44
|
-
if (range) {
|
|
45
|
-
// TODO(burdon): Scroll selection to center of screen?
|
|
46
|
-
editorRef.current?.dispatch({ selection: { anchor: range.from }, scrollIntoView: true });
|
|
47
|
-
}
|
|
50
|
+
if (!view.current?.hasFocus && selectedValue !== selected) {
|
|
51
|
+
setSelected(selectedValue);
|
|
52
|
+
if (selectedValue) {
|
|
53
|
+
focusComment(view.current!, selectedValue);
|
|
48
54
|
}
|
|
49
|
-
|
|
50
|
-
setSelected(commentSelected);
|
|
51
55
|
}
|
|
52
|
-
}, [selected, commentRanges,
|
|
56
|
+
}, [selected, commentRanges, selectedValue]);
|
|
57
|
+
|
|
58
|
+
useComments(view.current, commentRanges);
|
|
53
59
|
|
|
54
60
|
const extensions = useMemo(() => {
|
|
55
61
|
return [
|
|
56
62
|
comments({
|
|
57
63
|
onCreate: onCreateComment,
|
|
64
|
+
onDelete: onDeleteComment,
|
|
65
|
+
onUpdate: onUpdateComment,
|
|
58
66
|
onSelect: onSelectComment,
|
|
59
67
|
}),
|
|
60
68
|
];
|
|
@@ -67,7 +75,7 @@ const Editor: FC<{
|
|
|
67
75
|
// TODO(burdon): Highlight currently selected comment.
|
|
68
76
|
return (
|
|
69
77
|
<div className='flex grow overflow-y-scroll'>
|
|
70
|
-
<MarkdownEditor ref={
|
|
78
|
+
<MarkdownEditor ref={view} model={model} extensions={extensions} />
|
|
71
79
|
</div>
|
|
72
80
|
);
|
|
73
81
|
};
|
|
@@ -78,9 +86,9 @@ const Editor: FC<{
|
|
|
78
86
|
|
|
79
87
|
type CommentThread = {
|
|
80
88
|
id: string;
|
|
81
|
-
|
|
89
|
+
cursor?: string;
|
|
90
|
+
range?: Range;
|
|
82
91
|
yPos?: number;
|
|
83
|
-
selection?: Range;
|
|
84
92
|
messages: TextObject[];
|
|
85
93
|
};
|
|
86
94
|
|
|
@@ -90,23 +98,17 @@ const Thread: FC<{
|
|
|
90
98
|
onSelect: () => void;
|
|
91
99
|
onResolve: () => void;
|
|
92
100
|
}> = ({ thread, selected, onSelect, onResolve }) => {
|
|
101
|
+
const [focus, setFocus] = useState(false);
|
|
93
102
|
const [item, setItem] = useState({ text: new TextObject() });
|
|
94
103
|
const model = useTextModel({ text: item.text });
|
|
95
104
|
const editorRef = useRef<EditorView>(null);
|
|
96
105
|
|
|
97
|
-
// TODO(burdon): Hack to focus (view is recreated when text object changes).
|
|
98
|
-
const focus = () => {
|
|
99
|
-
setTimeout(() => {
|
|
100
|
-
editorRef.current?.focus();
|
|
101
|
-
}, 100);
|
|
102
|
-
};
|
|
103
|
-
|
|
104
106
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
105
107
|
useEffect(() => {
|
|
106
108
|
if (selected) {
|
|
107
109
|
containerRef.current?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
108
110
|
if (thread.messages.length === 0) {
|
|
109
|
-
|
|
111
|
+
setFocus(true);
|
|
110
112
|
}
|
|
111
113
|
}
|
|
112
114
|
}, [selected]);
|
|
@@ -116,7 +118,7 @@ const Thread: FC<{
|
|
|
116
118
|
if (text?.length) {
|
|
117
119
|
thread.messages.push(item.text);
|
|
118
120
|
setItem({ text: new TextObject() });
|
|
119
|
-
|
|
121
|
+
setFocus(true);
|
|
120
122
|
}
|
|
121
123
|
};
|
|
122
124
|
|
|
@@ -125,41 +127,49 @@ const Thread: FC<{
|
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
return (
|
|
128
|
-
<div
|
|
129
|
-
|
|
130
|
+
<div
|
|
131
|
+
className={mx(
|
|
132
|
+
'flex flex-col m-1 rounded shadow divide-y bg-white',
|
|
133
|
+
selected && 'ring',
|
|
134
|
+
!thread.cursor && 'opacity-50',
|
|
135
|
+
)}
|
|
136
|
+
>
|
|
137
|
+
<div className='flex p-2 gap-2 items-center text-xs font-mono text-neutral-500 font-thin'>
|
|
130
138
|
<span>id:{thread.id.slice(0, 4)}</span>
|
|
131
|
-
<span>from:{thread.
|
|
132
|
-
<span>to:{thread.
|
|
139
|
+
<span>from:{thread.range?.from}</span>
|
|
140
|
+
<span>to:{thread.range?.to}</span>
|
|
133
141
|
<span>y:{thread.yPos}</span>
|
|
142
|
+
<span className='grow' />
|
|
143
|
+
{!thread.cursor && <Trash />}
|
|
134
144
|
</div>
|
|
135
145
|
|
|
136
146
|
{thread.messages.map((message, i) => (
|
|
137
147
|
// TODO(burdon): Fix default editor padding so content doesn't jump on creating message.
|
|
138
148
|
<div key={i} className='p-2' onClick={() => onSelect()}>
|
|
139
|
-
{message
|
|
149
|
+
{getTextContent(message)}
|
|
140
150
|
</div>
|
|
141
151
|
))}
|
|
142
152
|
|
|
143
|
-
<div ref={containerRef} onClick={() => onSelect()}
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
<div ref={containerRef} className='flex' onClick={() => onSelect()}>
|
|
154
|
+
<TextEditor
|
|
155
|
+
ref={editorRef}
|
|
156
|
+
autoFocus={focus}
|
|
157
|
+
model={model}
|
|
158
|
+
placeholder={'Enter comment...'}
|
|
159
|
+
slots={{ root: { className: 'grow rounded-b' } }}
|
|
160
|
+
extensions={[
|
|
161
|
+
keymap.of([
|
|
162
|
+
{
|
|
163
|
+
key: 'Enter',
|
|
164
|
+
run: () => {
|
|
165
|
+
handleCreateMessage();
|
|
166
|
+
return true;
|
|
157
167
|
},
|
|
158
|
-
|
|
159
|
-
]
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
<Button variant='ghost' classNames='px-1
|
|
168
|
+
},
|
|
169
|
+
]),
|
|
170
|
+
]}
|
|
171
|
+
/>
|
|
172
|
+
<Button variant='ghost' classNames='px-1' title='Resolve' onClick={onResolve}>
|
|
163
173
|
<Check />
|
|
164
174
|
</Button>
|
|
165
175
|
</div>
|
|
@@ -183,7 +193,7 @@ const Sidebar: FC<{
|
|
|
183
193
|
|
|
184
194
|
return (
|
|
185
195
|
<DensityProvider density='fine'>
|
|
186
|
-
<div className='flex flex-col grow overflow-y-scroll py-4 gap-4
|
|
196
|
+
<div className='flex flex-col grow overflow-y-scroll py-4 gap-4'>
|
|
187
197
|
{sortedThreads.map((thread) => (
|
|
188
198
|
<Thread
|
|
189
199
|
key={thread.id}
|
|
@@ -210,44 +220,74 @@ type StoryProps = {
|
|
|
210
220
|
const Story = ({ text, autoCreate }: StoryProps) => {
|
|
211
221
|
const [item] = useState({ text: new TextObject(text) });
|
|
212
222
|
const [threads, setThreads] = useState<CommentThread[]>([]);
|
|
213
|
-
const commentRanges = useMemo(() => threads.map((thread) => thread.range), [threads]);
|
|
214
223
|
const [selected, setSelected] = useState<string>();
|
|
215
224
|
|
|
225
|
+
const comments = useMemo<Comment[]>(() => threads.map(({ id, cursor }) => ({ id, cursor })), [threads]);
|
|
226
|
+
|
|
216
227
|
// Filter by visibility.
|
|
217
228
|
const visibleThreads = useMemo(() => threads.filter((thread) => thread.yPos !== undefined), [threads]);
|
|
218
229
|
|
|
219
230
|
const handleCreateComment: CommentsOptions['onCreate'] = (cursor, location) => {
|
|
220
231
|
const id = PublicKey.random().toHex();
|
|
232
|
+
log.info('create', { id: id.slice(0, 4), cursor });
|
|
221
233
|
setThreads((threads) => [
|
|
222
234
|
...threads,
|
|
223
235
|
{
|
|
224
236
|
id,
|
|
225
|
-
|
|
237
|
+
cursor,
|
|
226
238
|
yPos: location ? Math.floor(location.top) : undefined,
|
|
227
239
|
messages: autoCreate
|
|
228
|
-
? faker.helpers.multiple(() => new TextObject(faker.lorem.sentence()), { count: { min:
|
|
240
|
+
? faker.helpers.multiple(() => new TextObject(faker.lorem.sentence()), { count: { min: 1, max: 3 } })
|
|
229
241
|
: [],
|
|
230
242
|
},
|
|
231
243
|
]);
|
|
232
|
-
setSelected(id);
|
|
244
|
+
setSelected(id); // TODO(burdon): Not required.
|
|
233
245
|
return id;
|
|
234
246
|
};
|
|
235
247
|
|
|
236
|
-
|
|
237
|
-
|
|
248
|
+
const handleDeleteComment: CommentsOptions['onDelete'] = (id) => {
|
|
249
|
+
log.info('delete', { id: id.slice(0, 4) });
|
|
238
250
|
setThreads((threads) =>
|
|
239
251
|
threads.map((thread) => {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
thread.yPos = range.location ? Math.floor(range.location.top) : undefined;
|
|
243
|
-
thread.selection = { from: range.from, to: range.to };
|
|
252
|
+
if (thread.id === id) {
|
|
253
|
+
thread.cursor = undefined;
|
|
244
254
|
}
|
|
245
255
|
|
|
246
256
|
return thread;
|
|
247
257
|
}),
|
|
248
258
|
);
|
|
249
259
|
|
|
250
|
-
setSelected(
|
|
260
|
+
setSelected((selected) => (selected === id ? undefined : selected));
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const handleUpdateComment: CommentsOptions['onUpdate'] = (id, cursor) => {
|
|
264
|
+
log.info('update', { id: id.slice(0, 4), cursor });
|
|
265
|
+
setThreads((threads) =>
|
|
266
|
+
threads.map((thread) => {
|
|
267
|
+
if (thread.id === id) {
|
|
268
|
+
thread.cursor = cursor;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return thread;
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const handleSelectComment: CommentsOptions['onSelect'] = ({ comments, selection: { current, closest } }) => {
|
|
277
|
+
log.info('select', { active: current?.slice(0, 4), closest: closest?.slice(0, 4) });
|
|
278
|
+
setThreads((threads) =>
|
|
279
|
+
threads.map((thread) => {
|
|
280
|
+
const comment = comments.find(({ comment }) => comment.id === thread.id);
|
|
281
|
+
if (comment) {
|
|
282
|
+
thread.yPos = comment.location ? Math.floor(comment.location.top) : undefined;
|
|
283
|
+
thread.range = { from: comment.range.from, to: comment.range.to };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return thread;
|
|
287
|
+
}),
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
setSelected(current ?? closest);
|
|
251
291
|
};
|
|
252
292
|
|
|
253
293
|
const handleSelectThread = (id: string) => {
|
|
@@ -265,9 +305,11 @@ const Story = ({ text, autoCreate }: StoryProps) => {
|
|
|
265
305
|
<div className='flex flex-col h-full w-[600px]'>
|
|
266
306
|
<Editor
|
|
267
307
|
item={item}
|
|
268
|
-
|
|
269
|
-
|
|
308
|
+
selected={selected}
|
|
309
|
+
comments={comments}
|
|
270
310
|
onCreateComment={handleCreateComment}
|
|
311
|
+
onDeleteComment={handleDeleteComment}
|
|
312
|
+
onUpdateComment={handleUpdateComment}
|
|
271
313
|
onSelectComment={handleSelectComment}
|
|
272
314
|
/>
|
|
273
315
|
</div>
|
|
@@ -307,7 +349,7 @@ export const Default = {
|
|
|
307
349
|
},
|
|
308
350
|
};
|
|
309
351
|
|
|
310
|
-
export const
|
|
352
|
+
export const Testing = {
|
|
311
353
|
args: {
|
|
312
354
|
text: document,
|
|
313
355
|
autoCreate: true,
|