@dxos/react-ui-editor 0.6.6-main.e1a6e1f → 0.6.6-staging.41c080b
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 +203 -307
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +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/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts +5 -2
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness/awareness.d.ts +6 -6
- package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +1 -2
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/cursor.d.ts +1 -1
- package/dist/types/src/extensions/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/link.d.ts +1 -3
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
- package/dist/types/src/hooks/{useTextEditor.stories.d.ts → InputMode.stories.d.ts} +5 -9
- package/dist/types/src/hooks/InputMode.stories.d.ts.map +1 -0
- package/dist/types/src/{components/TextEditor → hooks}/TextEditor.stories.d.ts +4 -16
- package/dist/types/src/hooks/TextEditor.stories.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextEditor.d.ts +20 -3
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/package.json +25 -25
- package/src/components/Toolbar/Toolbar.stories.tsx +1 -1
- package/src/components/index.ts +0 -1
- package/src/extensions/automerge/automerge.stories.tsx +25 -18
- package/src/extensions/automerge/automerge.ts +2 -0
- package/src/extensions/automerge/cursor.ts +3 -4
- package/src/extensions/awareness/awareness-provider.ts +2 -0
- package/src/extensions/awareness/awareness.ts +34 -30
- package/src/extensions/comments.ts +6 -14
- package/src/extensions/cursor.ts +1 -1
- package/src/extensions/factories.ts +19 -13
- package/src/hooks/{useTextEditor.stories.tsx → InputMode.stories.tsx} +30 -35
- package/src/{components/TextEditor → hooks}/TextEditor.stories.tsx +22 -28
- package/src/hooks/useTextEditor.ts +75 -23
- package/src/themes/default.ts +20 -4
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +0 -34
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/index.d.ts +0 -2
- package/dist/types/src/components/TextEditor/index.d.ts.map +0 -1
- package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +0 -1
- package/src/components/TextEditor/TextEditor.tsx +0 -184
- package/src/components/TextEditor/index.ts +0 -5
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
import '@dxosTheme';
|
|
6
6
|
import { markdown } from '@codemirror/lang-markdown';
|
|
7
7
|
import { ArrowSquareOut, X } from '@phosphor-icons/react';
|
|
8
|
-
import {
|
|
8
|
+
import { effect, useSignal } from '@preact/signals-react';
|
|
9
9
|
import defaultsDeep from 'lodash.defaultsdeep';
|
|
10
|
-
import React, { type FC, type KeyboardEvent, StrictMode, useMemo,
|
|
10
|
+
import React, { type FC, type KeyboardEvent, StrictMode, useMemo, useState } from 'react';
|
|
11
11
|
import { createRoot } from 'react-dom/client';
|
|
12
12
|
|
|
13
13
|
import { TextType } from '@braneframe/types';
|
|
@@ -21,7 +21,7 @@ import { Button, DensityProvider, Input, ThemeProvider, useThemeContext } from '
|
|
|
21
21
|
import { baseSurface, defaultTx, getSize, mx, textBlockWidth } from '@dxos/react-ui-theme';
|
|
22
22
|
import { withTheme } from '@dxos/storybook-utils';
|
|
23
23
|
|
|
24
|
-
import {
|
|
24
|
+
import { useTextEditor, type UseTextEditorProps } from './useTextEditor';
|
|
25
25
|
import {
|
|
26
26
|
InputModeExtensions,
|
|
27
27
|
annotations,
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
comments,
|
|
33
33
|
createBasicExtensions,
|
|
34
34
|
createDataExtensions,
|
|
35
|
+
createExternalCommentSync,
|
|
35
36
|
createMarkdownExtensions,
|
|
36
37
|
createThemeExtensions,
|
|
37
38
|
decorateMarkdown,
|
|
@@ -45,14 +46,13 @@ import {
|
|
|
45
46
|
state,
|
|
46
47
|
table,
|
|
47
48
|
typewriter,
|
|
48
|
-
useComments,
|
|
49
49
|
type CommandAction,
|
|
50
50
|
type CommandOptions,
|
|
51
51
|
type Comment,
|
|
52
52
|
type CommentsOptions,
|
|
53
53
|
type SelectionState,
|
|
54
|
-
} from '
|
|
55
|
-
import translations from '
|
|
54
|
+
} from '../extensions';
|
|
55
|
+
import translations from '../translations';
|
|
56
56
|
|
|
57
57
|
faker.seed(101);
|
|
58
58
|
|
|
@@ -242,25 +242,19 @@ const renderLinkButton = (el: Element, url: string) => {
|
|
|
242
242
|
type StoryProps = {
|
|
243
243
|
id?: string;
|
|
244
244
|
text?: string;
|
|
245
|
-
comments?: Comment[];
|
|
246
245
|
readonly?: boolean;
|
|
247
246
|
placeholder?: string;
|
|
248
|
-
} & Pick<
|
|
247
|
+
} & Pick<UseTextEditorProps, 'selection' | 'extensions'>;
|
|
249
248
|
|
|
250
249
|
const Story = ({
|
|
251
250
|
id = 'editor-' + PublicKey.random().toHex().slice(0, 8),
|
|
252
251
|
text,
|
|
253
|
-
comments,
|
|
254
252
|
extensions: _extensions = [],
|
|
255
253
|
readonly,
|
|
256
254
|
placeholder = 'New document.',
|
|
257
|
-
|
|
255
|
+
selection,
|
|
258
256
|
}: StoryProps) => {
|
|
259
257
|
const [object] = useState(createEchoObject(create(TextType, { content: text ?? '' })));
|
|
260
|
-
|
|
261
|
-
const viewRef = useRef<EditorView>(null);
|
|
262
|
-
useComments(viewRef.current, id, comments);
|
|
263
|
-
|
|
264
258
|
const { themeMode } = useThemeContext();
|
|
265
259
|
const extensions = useMemo(
|
|
266
260
|
() => [
|
|
@@ -278,21 +272,16 @@ const Story = ({
|
|
|
278
272
|
[_extensions, object],
|
|
279
273
|
);
|
|
280
274
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
id={id}
|
|
285
|
-
ref={viewRef}
|
|
286
|
-
doc={text}
|
|
287
|
-
extensions={extensions}
|
|
288
|
-
className={mx(textBlockWidth, 'min-bs-dvh')}
|
|
289
|
-
/>
|
|
275
|
+
const { parentRef, focusAttributes } = useTextEditor(
|
|
276
|
+
() => ({ id, initialValue: text, extensions, selection }),
|
|
277
|
+
[extensions],
|
|
290
278
|
);
|
|
279
|
+
|
|
280
|
+
return <div role='none' ref={parentRef} className={mx(textBlockWidth, 'min-bs-dvh')} {...focusAttributes} />;
|
|
291
281
|
};
|
|
292
282
|
|
|
293
283
|
export default {
|
|
294
|
-
title: 'react-ui-editor/
|
|
295
|
-
component: TextEditor,
|
|
284
|
+
title: 'react-ui-editor/useTextEditor',
|
|
296
285
|
decorators: [withTheme],
|
|
297
286
|
render: Story,
|
|
298
287
|
parameters: { translations, layout: 'fullscreen' },
|
|
@@ -474,17 +463,22 @@ export const Command = {
|
|
|
474
463
|
|
|
475
464
|
export const Comments = {
|
|
476
465
|
render: () => {
|
|
477
|
-
const
|
|
466
|
+
const _comments = useSignal<Comment[]>([]);
|
|
478
467
|
return (
|
|
479
468
|
<Story
|
|
480
469
|
text={str('# Comments', '', text.paragraphs, text.footer)}
|
|
481
|
-
comments={_comments}
|
|
482
470
|
extensions={[
|
|
471
|
+
createExternalCommentSync(
|
|
472
|
+
'test',
|
|
473
|
+
(sink) => effect(() => sink()),
|
|
474
|
+
() => _comments.value,
|
|
475
|
+
),
|
|
483
476
|
comments({
|
|
477
|
+
id: 'test',
|
|
484
478
|
onHover: onCommentsHover,
|
|
485
479
|
onCreate: ({ cursor }) => {
|
|
486
480
|
const id = PublicKey.random().toHex();
|
|
487
|
-
|
|
481
|
+
_comments.value = [..._comments.value, { id, cursor }];
|
|
488
482
|
return id;
|
|
489
483
|
},
|
|
490
484
|
onSelect: (state) => {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { EditorState, type EditorStateConfig, type StateEffect } from '@codemirror/state';
|
|
6
6
|
import { EditorView } from '@codemirror/view';
|
|
7
7
|
import { useFocusableGroup } from '@fluentui/react-tabster';
|
|
8
8
|
import {
|
|
@@ -17,10 +17,10 @@ import {
|
|
|
17
17
|
} from 'react';
|
|
18
18
|
|
|
19
19
|
import { log } from '@dxos/log';
|
|
20
|
-
import {
|
|
20
|
+
import { useDefaultValue } from '@dxos/react-ui';
|
|
21
|
+
import { isNotFalsy, type MaybeFunction } from '@dxos/util';
|
|
21
22
|
|
|
22
|
-
import {
|
|
23
|
-
import { documentId } from '../extensions';
|
|
23
|
+
import { documentId, editorInputMode } from '../extensions';
|
|
24
24
|
import { logChanges } from '../util';
|
|
25
25
|
|
|
26
26
|
export type UseTextEditor = {
|
|
@@ -32,14 +32,50 @@ export type UseTextEditor = {
|
|
|
32
32
|
};
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
export type
|
|
35
|
+
export type CursorInfo = {
|
|
36
|
+
from: number;
|
|
37
|
+
to: number;
|
|
38
|
+
line: number;
|
|
39
|
+
lines: number;
|
|
40
|
+
length: number;
|
|
41
|
+
after?: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type UseTextEditorProps = Pick<EditorStateConfig, 'selection' | 'extensions'> & {
|
|
45
|
+
id?: string;
|
|
46
|
+
initialValue?: string;
|
|
47
|
+
className?: string;
|
|
48
|
+
autoFocus?: boolean;
|
|
49
|
+
scrollTo?: StateEffect<unknown>;
|
|
50
|
+
moveToEndOfLine?: boolean;
|
|
51
|
+
debug?: boolean;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let instanceCount = 0;
|
|
36
55
|
|
|
37
56
|
/**
|
|
38
57
|
* Hook for creating editor.
|
|
39
58
|
*/
|
|
40
|
-
export const useTextEditor = (
|
|
41
|
-
|
|
42
|
-
|
|
59
|
+
export const useTextEditor = (
|
|
60
|
+
props: MaybeFunction<UseTextEditorProps> = {},
|
|
61
|
+
deps: DependencyList = [],
|
|
62
|
+
): UseTextEditor => {
|
|
63
|
+
const {
|
|
64
|
+
id,
|
|
65
|
+
initialValue,
|
|
66
|
+
selection,
|
|
67
|
+
extensions,
|
|
68
|
+
autoFocus,
|
|
69
|
+
scrollTo: _scrollTo,
|
|
70
|
+
moveToEndOfLine,
|
|
71
|
+
debug,
|
|
72
|
+
} = useMemo<UseTextEditorProps>(() => {
|
|
73
|
+
return typeof props === 'function' ? props() : props;
|
|
74
|
+
}, deps ?? []);
|
|
75
|
+
|
|
76
|
+
// NOTE: Increments by 2 in strict mode.
|
|
77
|
+
const [instanceId] = useState(() => `text-editor-${++instanceCount}`);
|
|
78
|
+
const scrollTo = useDefaultValue(_scrollTo, EditorView.scrollIntoView(0, { yMargin: 0 }));
|
|
43
79
|
const onUpdate = useRef<() => void>();
|
|
44
80
|
const [view, setView] = useState<EditorView>();
|
|
45
81
|
const parentRef = useRef<HTMLDivElement>(null);
|
|
@@ -47,12 +83,20 @@ export const useTextEditor = (cb: () => UseTextEditorProps = () => ({}), deps: D
|
|
|
47
83
|
useEffect(() => {
|
|
48
84
|
let view: EditorView;
|
|
49
85
|
if (parentRef.current) {
|
|
50
|
-
log('create', { id });
|
|
86
|
+
log('create', { id, instanceId, doc: initialValue?.length ?? 0 });
|
|
87
|
+
|
|
88
|
+
let initialSelection = selection;
|
|
89
|
+
if (moveToEndOfLine && selection === undefined) {
|
|
90
|
+
const index = initialValue?.indexOf('\n');
|
|
91
|
+
const anchor = !index || index === -1 ? 0 : index;
|
|
92
|
+
initialSelection = { anchor };
|
|
93
|
+
}
|
|
51
94
|
|
|
52
95
|
// https://codemirror.net/docs/ref/#state.EditorStateConfig
|
|
53
96
|
// NOTE: Don't set selection here in case it is invalid (and crashes the state); dispatch below.
|
|
54
97
|
const state = EditorState.create({
|
|
55
|
-
doc,
|
|
98
|
+
doc: initialValue,
|
|
99
|
+
selection: initialSelection,
|
|
56
100
|
extensions: [
|
|
57
101
|
id && documentId.of(id),
|
|
58
102
|
// TODO(burdon): Doesn't catch errors in keymap functions.
|
|
@@ -70,6 +114,7 @@ export const useTextEditor = (cb: () => UseTextEditorProps = () => ({}), deps: D
|
|
|
70
114
|
view = new EditorView({
|
|
71
115
|
parent: parentRef.current,
|
|
72
116
|
scrollTo,
|
|
117
|
+
selection: initialSelection,
|
|
73
118
|
state,
|
|
74
119
|
// NOTE: Uncomment to debug/monitor all transactions.
|
|
75
120
|
// https://codemirror.net/docs/ref/#view.EditorView.dispatch
|
|
@@ -81,6 +126,12 @@ export const useTextEditor = (cb: () => UseTextEditorProps = () => ({}), deps: D
|
|
|
81
126
|
},
|
|
82
127
|
});
|
|
83
128
|
|
|
129
|
+
// Move to end of line after document loaded.
|
|
130
|
+
if (!initialValue && moveToEndOfLine) {
|
|
131
|
+
const { to } = view.state.doc.lineAt(0);
|
|
132
|
+
view.dispatch({ selection: { anchor: to } });
|
|
133
|
+
}
|
|
134
|
+
|
|
84
135
|
setView(view);
|
|
85
136
|
}
|
|
86
137
|
|
|
@@ -92,27 +143,29 @@ export const useTextEditor = (cb: () => UseTextEditorProps = () => ({}), deps: D
|
|
|
92
143
|
|
|
93
144
|
useEffect(() => {
|
|
94
145
|
if (view) {
|
|
95
|
-
//
|
|
96
|
-
if (
|
|
97
|
-
selection = EditorSelection.single(view.state.doc.line(1).to);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Set selection after first update (since content may rerender on focus).
|
|
101
|
-
// TODO(burdon): Make invisible until first render?
|
|
102
|
-
if (selection || scrollTo) {
|
|
146
|
+
// TODO(burdon): Set selection after first update (since content may rerender on focus)?
|
|
147
|
+
if (scrollTo) {
|
|
103
148
|
onUpdate.current = () => {
|
|
104
149
|
onUpdate.current = undefined;
|
|
105
|
-
view.dispatch({
|
|
150
|
+
view.dispatch({ effects: scrollTo && [scrollTo], scrollIntoView: !scrollTo });
|
|
106
151
|
};
|
|
107
152
|
}
|
|
108
153
|
|
|
109
|
-
|
|
110
|
-
|
|
154
|
+
// Remove tabster attribute (rely on custom keymap).
|
|
155
|
+
if (view.state.facet(editorInputMode).noTabster) {
|
|
156
|
+
parentRef.current?.removeAttribute('data-tabster');
|
|
111
157
|
}
|
|
112
158
|
}
|
|
113
|
-
}, [view,
|
|
159
|
+
}, [view, selection, scrollTo]);
|
|
160
|
+
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
if (view && autoFocus) {
|
|
163
|
+
view.focus();
|
|
164
|
+
}
|
|
165
|
+
}, [autoFocus, view]);
|
|
114
166
|
|
|
115
167
|
const focusableGroup = useFocusableGroup({ tabBehavior: 'limited' });
|
|
168
|
+
|
|
116
169
|
// Focus editor on Enter (e.g., when tabbing to this component).
|
|
117
170
|
const handleKeyUp = useCallback<KeyboardEventHandler<HTMLDivElement>>(
|
|
118
171
|
(event) => {
|
|
@@ -130,6 +183,5 @@ export const useTextEditor = (cb: () => UseTextEditorProps = () => ({}), deps: D
|
|
|
130
183
|
);
|
|
131
184
|
|
|
132
185
|
const focusAttributes = { tabIndex: 0 as const, ...focusableGroup, onKeyUp: handleKeyUp };
|
|
133
|
-
|
|
134
186
|
return { parentRef, view, focusAttributes };
|
|
135
187
|
};
|
package/src/themes/default.ts
CHANGED
|
@@ -152,19 +152,34 @@ export const defaultTheme: ThemeStyles = {
|
|
|
152
152
|
//
|
|
153
153
|
'.cm-tooltip': {
|
|
154
154
|
border: 'none',
|
|
155
|
-
|
|
155
|
+
},
|
|
156
|
+
'&light .cm-tooltip': {
|
|
157
|
+
background: `${get(tokens, 'extend.colors.neutral.100')} !important`,
|
|
158
|
+
},
|
|
159
|
+
'&dark .cm-tooltip': {
|
|
160
|
+
background: `${get(tokens, 'extend.colors.neutral.900')} !important`,
|
|
156
161
|
},
|
|
157
162
|
'.cm-tooltip-below': {},
|
|
158
163
|
|
|
159
164
|
//
|
|
160
165
|
// autocomplete
|
|
166
|
+
// https://github.com/codemirror/autocomplete/blob/main/src/completion.ts
|
|
161
167
|
//
|
|
162
168
|
'.cm-tooltip-autocomplete': {
|
|
163
169
|
marginTop: '4px',
|
|
164
170
|
marginLeft: '-3px',
|
|
165
171
|
},
|
|
166
|
-
'.cm-tooltip-autocomplete ul
|
|
167
|
-
|
|
172
|
+
'.cm-tooltip-autocomplete > ul': {
|
|
173
|
+
maxHeight: '20em !important',
|
|
174
|
+
},
|
|
175
|
+
'.cm-tooltip-autocomplete > ul > li': {},
|
|
176
|
+
'.cm-tooltip-autocomplete > ul > li[aria-selected]': {},
|
|
177
|
+
// TODO(burdon): Can we add a class prefix to avoid adding !important?
|
|
178
|
+
'.cm-tooltip.cm-tooltip-autocomplete > ul > completion-section': {
|
|
179
|
+
paddingLeft: '4px !important',
|
|
180
|
+
borderBottom: 'none !important',
|
|
181
|
+
color: get(tokens, 'extend.colors.primary.500'),
|
|
182
|
+
},
|
|
168
183
|
'.cm-completionIcon': {
|
|
169
184
|
display: 'none',
|
|
170
185
|
},
|
|
@@ -172,7 +187,8 @@ export const defaultTheme: ThemeStyles = {
|
|
|
172
187
|
fontFamily: get(tokens, 'fontFamily.body', []).join(','),
|
|
173
188
|
},
|
|
174
189
|
'.cm-completionMatchedText': {
|
|
175
|
-
textDecoration: 'none',
|
|
190
|
+
textDecoration: 'none !important',
|
|
191
|
+
opacity: 0.5,
|
|
176
192
|
},
|
|
177
193
|
|
|
178
194
|
//
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { type EditorStateConfig, type StateEffect } from '@codemirror/state';
|
|
2
|
-
import { EditorView } from '@codemirror/view';
|
|
3
|
-
import React from 'react';
|
|
4
|
-
export type CursorInfo = {
|
|
5
|
-
from: number;
|
|
6
|
-
to: number;
|
|
7
|
-
line: number;
|
|
8
|
-
lines: number;
|
|
9
|
-
length: number;
|
|
10
|
-
after?: string;
|
|
11
|
-
};
|
|
12
|
-
export type TextEditorProps = Pick<EditorStateConfig, 'doc' | 'selection' | 'extensions'> & {
|
|
13
|
-
id?: string;
|
|
14
|
-
className?: string;
|
|
15
|
-
autoFocus?: boolean;
|
|
16
|
-
scrollTo?: StateEffect<unknown>;
|
|
17
|
-
moveToEndOfLine?: boolean;
|
|
18
|
-
debug?: boolean;
|
|
19
|
-
dataTestId?: string;
|
|
20
|
-
};
|
|
21
|
-
/**
|
|
22
|
-
* Thin wrapper for text editor.
|
|
23
|
-
* Handles tabster and focus management.
|
|
24
|
-
*/
|
|
25
|
-
export declare const TextEditor: React.ForwardRefExoticComponent<Pick<EditorStateConfig, "doc" | "selection" | "extensions"> & {
|
|
26
|
-
id?: string | undefined;
|
|
27
|
-
className?: string | undefined;
|
|
28
|
-
autoFocus?: boolean | undefined;
|
|
29
|
-
scrollTo?: StateEffect<unknown> | undefined;
|
|
30
|
-
moveToEndOfLine?: boolean | undefined;
|
|
31
|
-
debug?: boolean | undefined;
|
|
32
|
-
dataTestId?: string | undefined;
|
|
33
|
-
} & React.RefAttributes<EditorView | null>>;
|
|
34
|
-
//# sourceMappingURL=TextEditor.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"TextEditor.d.ts","sourceRoot":"","sources":["../../../../../src/components/TextEditor/TextEditor.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAe,KAAK,iBAAiB,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC1F,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,KAQN,MAAM,OAAO,CAAC;AASf,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,iBAAiB,EAAE,KAAK,GAAG,WAAW,GAAG,YAAY,CAAC,GAAG;IAC1F,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAChC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAIF;;;GAGG;AAEH,eAAO,MAAM,UAAU;;;;;;;;2CAqItB,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"TextEditor.stories.d.ts","sourceRoot":"","sources":["../../../../../src/components/TextEditor/TextEditor.stories.tsx"],"names":[],"mappings":"AAIA,OAAO,YAAY,CAAC;AAGpB,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,KAA6E,MAAM,OAAO,CAAC;AAclG,OAAO,EAAc,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,EA0BL,KAAK,OAAO,EAGb,MAAM,kBAAkB,CAAC;AA4L1B,KAAK,UAAU,GAAG;IAChB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,IAAI,CAAC,eAAe,EAAE,WAAW,GAAG,YAAY,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CtD,wBAME;AAeF,eAAO,MAAM,OAAO;;CAEnB,CAAC;AAEF,eAAO,MAAM,QAAQ;;CAEpB,CAAC;AAEF,eAAO,MAAM,YAAY;;CAExB,CAAC;AASF,eAAO,MAAM,KAAK;;CAEjB,CAAC;AAIF,eAAO,MAAM,SAAS;;CAUrB,CAAC;AAEF,eAAO,MAAM,mBAAmB;;CAE/B,CAAC;AAEF,eAAO,MAAM,KAAK;;CAEjB,CAAC;AAEF,eAAO,MAAM,KAAK;;CAEjB,CAAC;AAEF,eAAO,MAAM,IAAI;;CAEhB,CAAC;AAEF,eAAO,MAAM,KAAK;;CAIjB,CAAC;AAEF,eAAO,MAAM,KAAK;;CAEjB,CAAC;AAEF,eAAO,MAAM,YAAY;;CAYxB,CAAC;AAEF,eAAO,MAAM,YAAY;;CAWxB,CAAC;AAEF,eAAO,MAAM,OAAO;;CAWnB,CAAC;AAmDF,eAAO,MAAM,OAAO;;CAOnB,CAAC;AAEF,eAAO,MAAM,QAAQ;;CAiCpB,CAAC;AAEF,eAAO,MAAM,GAAG;;CAOf,CAAC;AAEF,eAAO,MAAM,WAAW;;CAEvB,CAAC;AAEF,eAAO,MAAM,GAAG;;CAaf,CAAC;AAIF,eAAO,MAAM,QAAQ;;CAgBpB,CAAC;AAEF,eAAO,MAAM,UAAU;;CAOtB,CAAC;AAEF,eAAO,MAAM,KAAK;;CAqBjB,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/TextEditor/index.ts"],"names":[],"mappings":"AAIA,cAAc,cAAc,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useTextEditor.stories.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useTextEditor.stories.tsx"],"names":[],"mappings":";AAIA,OAAO,YAAY,CAAC;AAuBpB,KAAK,UAAU,GAAG;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;;;;mBA2Ee,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAH3B,wBASE;AAEF,eAAO,MAAM,OAAO;;CAKnB,CAAC;AAEF,eAAO,MAAM,KAAK;;CAQjB,CAAC;AAEF,eAAO,MAAM,QAAQ;;;;;;CAMpB,CAAC"}
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2023 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { EditorState, type EditorStateConfig, type StateEffect } from '@codemirror/state';
|
|
6
|
-
import { EditorView } from '@codemirror/view';
|
|
7
|
-
import { useFocusableGroup } from '@fluentui/react-tabster';
|
|
8
|
-
import React, {
|
|
9
|
-
type KeyboardEventHandler,
|
|
10
|
-
forwardRef,
|
|
11
|
-
useCallback,
|
|
12
|
-
useEffect,
|
|
13
|
-
useImperativeHandle,
|
|
14
|
-
useRef,
|
|
15
|
-
useState,
|
|
16
|
-
} from 'react';
|
|
17
|
-
|
|
18
|
-
import { log } from '@dxos/log';
|
|
19
|
-
import { useDefaultValue } from '@dxos/react-ui';
|
|
20
|
-
import { isNotFalsy } from '@dxos/util';
|
|
21
|
-
|
|
22
|
-
import { documentId, editorInputMode, focusEvent } from '../../extensions';
|
|
23
|
-
import { logChanges } from '../../util';
|
|
24
|
-
|
|
25
|
-
export type CursorInfo = {
|
|
26
|
-
from: number;
|
|
27
|
-
to: number;
|
|
28
|
-
line: number;
|
|
29
|
-
lines: number;
|
|
30
|
-
length: number;
|
|
31
|
-
after?: string;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export type TextEditorProps = Pick<EditorStateConfig, 'doc' | 'selection' | 'extensions'> & {
|
|
35
|
-
id?: string;
|
|
36
|
-
className?: string;
|
|
37
|
-
autoFocus?: boolean;
|
|
38
|
-
scrollTo?: StateEffect<unknown>;
|
|
39
|
-
moveToEndOfLine?: boolean;
|
|
40
|
-
debug?: boolean;
|
|
41
|
-
dataTestId?: string;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
let instanceCount = 0;
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Thin wrapper for text editor.
|
|
48
|
-
* Handles tabster and focus management.
|
|
49
|
-
*/
|
|
50
|
-
// TODO(burdon): Use useTextEditor internally.
|
|
51
|
-
export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
52
|
-
(
|
|
53
|
-
{
|
|
54
|
-
id,
|
|
55
|
-
// TODO(wittjosiah): Rename initialText?
|
|
56
|
-
doc,
|
|
57
|
-
selection,
|
|
58
|
-
extensions,
|
|
59
|
-
className,
|
|
60
|
-
autoFocus,
|
|
61
|
-
scrollTo: propsScrollTo,
|
|
62
|
-
moveToEndOfLine,
|
|
63
|
-
debug,
|
|
64
|
-
dataTestId,
|
|
65
|
-
},
|
|
66
|
-
forwardedRef,
|
|
67
|
-
) => {
|
|
68
|
-
// NOTE: Increments by 2 in strict mode.
|
|
69
|
-
const [instanceId] = useState(() => `text-editor-${++instanceCount}`);
|
|
70
|
-
const scrollTo = useDefaultValue(propsScrollTo, EditorView.scrollIntoView(0, { yMargin: 0 }));
|
|
71
|
-
|
|
72
|
-
// TODO(burdon): Make tabster optional.
|
|
73
|
-
const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
|
|
74
|
-
const rootRef = useRef<HTMLDivElement>(null);
|
|
75
|
-
const [view, setView] = useState<EditorView | null>(null);
|
|
76
|
-
|
|
77
|
-
// The view ref can be used to focus the editor.
|
|
78
|
-
// NOTE: Ref updates do not cause the parent to re-render; also the ref is not available immediately.
|
|
79
|
-
useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
|
|
80
|
-
|
|
81
|
-
// Set focus.
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
if (autoFocus) {
|
|
84
|
-
view?.focus();
|
|
85
|
-
}
|
|
86
|
-
}, [view, autoFocus]);
|
|
87
|
-
|
|
88
|
-
// Create editor state and view.
|
|
89
|
-
// The view is recreated if the model or extensions are changed.
|
|
90
|
-
useEffect(() => {
|
|
91
|
-
log('create', { id, instanceId });
|
|
92
|
-
|
|
93
|
-
//
|
|
94
|
-
// EditorState
|
|
95
|
-
// https://codemirror.net/docs/ref/#state.EditorStateConfig
|
|
96
|
-
// NOTE: Don't set selection here in case it is invalid (and crashes the state); dispatch below.
|
|
97
|
-
//
|
|
98
|
-
const state = EditorState.create({
|
|
99
|
-
doc,
|
|
100
|
-
extensions: [
|
|
101
|
-
id && documentId.of(id),
|
|
102
|
-
// TODO(burdon): NOTE: Doesn't catch errors in keymap functions.
|
|
103
|
-
EditorView.exceptionSink.of((err) => {
|
|
104
|
-
log.catch(err);
|
|
105
|
-
}),
|
|
106
|
-
|
|
107
|
-
// Focus.
|
|
108
|
-
EditorView.updateListener.of((update) => {
|
|
109
|
-
update.transactions.forEach((transaction) => {
|
|
110
|
-
if (transaction.isUserEvent(focusEvent)) {
|
|
111
|
-
rootRef.current?.focus();
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
}),
|
|
115
|
-
|
|
116
|
-
extensions,
|
|
117
|
-
].filter(isNotFalsy),
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
//
|
|
121
|
-
// EditorView
|
|
122
|
-
// https://codemirror.net/docs/ref/#view.EditorViewConfig
|
|
123
|
-
//
|
|
124
|
-
const view = new EditorView({
|
|
125
|
-
state,
|
|
126
|
-
parent: rootRef.current!,
|
|
127
|
-
scrollTo,
|
|
128
|
-
|
|
129
|
-
// NOTE: Uncomment to debug/monitor all transactions.
|
|
130
|
-
// https://codemirror.net/docs/ref/#view.EditorView.dispatch
|
|
131
|
-
dispatchTransactions: (trs, view) => {
|
|
132
|
-
if (debug) {
|
|
133
|
-
logChanges(trs);
|
|
134
|
-
}
|
|
135
|
-
view.update(trs);
|
|
136
|
-
},
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
// Position cursor at end of first line.
|
|
140
|
-
if (moveToEndOfLine && !(scrollTo || selection)) {
|
|
141
|
-
const { to } = view.state.doc.lineAt(0);
|
|
142
|
-
view.dispatch({ selection: { anchor: to } });
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Remove tabster attribute (rely on custom keymap).
|
|
146
|
-
if (state.facet(editorInputMode).noTabster) {
|
|
147
|
-
rootRef.current?.removeAttribute('data-tabster');
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
setView(view);
|
|
151
|
-
|
|
152
|
-
return () => {
|
|
153
|
-
log('destroy', { id, instanceId });
|
|
154
|
-
view?.destroy();
|
|
155
|
-
};
|
|
156
|
-
}, [id, selection, scrollTo, extensions]);
|
|
157
|
-
|
|
158
|
-
// Focus editor on Enter (e.g., when tabbing to this component).
|
|
159
|
-
const handleKeyUp = useCallback<KeyboardEventHandler<HTMLDivElement>>(
|
|
160
|
-
(event) => {
|
|
161
|
-
const { key } = event;
|
|
162
|
-
switch (key) {
|
|
163
|
-
case 'Enter': {
|
|
164
|
-
view?.focus();
|
|
165
|
-
break;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
},
|
|
169
|
-
[view],
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
return (
|
|
173
|
-
<div
|
|
174
|
-
role='none'
|
|
175
|
-
ref={rootRef}
|
|
176
|
-
tabIndex={0}
|
|
177
|
-
className={className}
|
|
178
|
-
data-testid={dataTestId}
|
|
179
|
-
{...tabsterDOMAttribute}
|
|
180
|
-
onKeyUp={handleKeyUp}
|
|
181
|
-
/>
|
|
182
|
-
);
|
|
183
|
-
},
|
|
184
|
-
);
|