@dxos/react-ui-editor 0.3.11-main.fbbdc2a → 0.3.11-main.fc97a54
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 +2904 -1394
- 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 +16 -14
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +20 -17
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +6 -6
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts +23 -0
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +29 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +25 -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/cursor.d.ts +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
- 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/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 +41 -20
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/{util → extensions}/cursor.d.ts +10 -2
- package/dist/types/src/extensions/cursor.d.ts.map +1 -0
- package/dist/types/src/extensions/index.d.ts +2 -8
- 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 +58 -0
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/formatting.test.d.ts +3 -0
- package/dist/types/src/extensions/markdown/formatting.test.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.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 +14 -32
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +3 -0
- package/dist/types/src/index.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/styles/tokens.d.ts +1 -0
- package/dist/types/src/styles/tokens.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/dist/types/src/util.d.ts +7 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/package.json +48 -37
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +85 -92
- package/src/components/TextEditor/TextEditor.stories.tsx +12 -46
- package/src/components/TextEditor/TextEditor.tsx +135 -95
- package/src/components/TextEditor/automerge.stories.tsx +1 -2
- package/src/components/TextEditor/hooks.stories.tsx +111 -0
- package/src/components/Toolbar/Toolbar.stories.tsx +102 -0
- package/src/components/Toolbar/Toolbar.tsx +264 -0
- package/src/components/Toolbar/index.ts +5 -0
- package/src/components/index.ts +1 -0
- package/src/extensions/autocomplete.ts +23 -19
- package/src/extensions/automerge/automerge.spec.tsx +76 -0
- package/src/extensions/automerge/automerge.test.ts +13 -0
- package/src/extensions/automerge/automerge.ts +55 -46
- package/src/extensions/automerge/cursor.ts +5 -4
- package/src/extensions/automerge/defs.ts +13 -34
- 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 +46 -49
- package/src/extensions/basic.ts +17 -10
- package/src/extensions/blast.ts +4 -1
- package/src/extensions/comments.ts +332 -267
- package/src/extensions/cursor.ts +50 -0
- package/src/extensions/index.ts +2 -8
- package/src/extensions/markdown/bundle.ts +23 -28
- package/src/extensions/markdown/code.ts +150 -0
- package/src/extensions/markdown/formatting.test.ts +482 -0
- package/src/extensions/markdown/formatting.ts +1237 -0
- package/src/extensions/markdown/heading.ts +59 -0
- package/src/extensions/markdown/highlight.ts +11 -26
- package/src/extensions/markdown/hr.ts +71 -0
- package/src/extensions/markdown/image.ts +87 -0
- package/src/extensions/markdown/index.ts +8 -0
- package/src/extensions/{link.ts → markdown/link.ts} +90 -83
- package/src/extensions/{table.ts → markdown/table.ts} +10 -4
- package/src/extensions/markdown/tasklist.ts +110 -0
- 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 +93 -0
- package/src/hooks/useEditorView.ts +29 -0
- package/src/hooks/useTextEditor.ts +153 -0
- package/src/hooks/useTextModel.ts +67 -60
- package/src/index.ts +3 -0
- package/src/styles/markdown.ts +9 -12
- package/src/styles/tokens.ts +3 -0
- package/src/themes/default.ts +59 -83
- package/src/util.ts +41 -0
- 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 +0 -12
- 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/mermaid.d.ts +0 -3
- package/dist/types/src/extensions/mermaid.d.ts.map +0 -1
- package/dist/types/src/extensions/table.d.ts.map +0 -1
- 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/dist/types/src/util/cursor.d.ts.map +0 -1
- package/dist/types/src/util/index.d.ts +0 -3
- package/dist/types/src/util/index.d.ts.map +0 -1
- package/dist/types/src/util/util.d.ts +0 -5
- package/dist/types/src/util/util.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 -99
- package/src/extensions/hr.ts +0 -74
- package/src/extensions/image.ts +0 -69
- package/src/extensions/mermaid.ts +0 -11
- package/src/extensions/tasklist.ts +0 -121
- package/src/extensions/yjs/cursor.ts +0 -21
- 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 -178
- 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/src/util/cursor.ts +0 -29
- package/src/util/index.ts +0 -6
- package/src/util/util.ts +0 -18
- /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/dist/types/src/extensions/{tasklist.d.ts → markdown/tasklist.d.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,18 +14,22 @@ import React, {
|
|
|
14
14
|
useCallback,
|
|
15
15
|
useEffect,
|
|
16
16
|
useImperativeHandle,
|
|
17
|
+
useRef,
|
|
17
18
|
useState,
|
|
18
19
|
} from 'react';
|
|
19
20
|
|
|
20
|
-
import {
|
|
21
|
+
import { log } from '@dxos/log';
|
|
21
22
|
import { useThemeContext } from '@dxos/react-ui';
|
|
22
|
-
import {
|
|
23
|
+
import { focusRing, mx } from '@dxos/react-ui-theme';
|
|
24
|
+
import { isNotFalsy } from '@dxos/util';
|
|
23
25
|
|
|
24
|
-
import {
|
|
25
|
-
import { type
|
|
26
|
+
import { createBasicBundle, createMarkdownExtensions } from '../../extensions';
|
|
27
|
+
import { type EditorModel } from '../../hooks';
|
|
26
28
|
import { type ThemeStyles } from '../../styles';
|
|
27
29
|
import { defaultTheme, markdownTheme, textTheme } from '../../themes';
|
|
30
|
+
import { logChanges } from '../../util';
|
|
28
31
|
|
|
32
|
+
// TODO(burdon): Change to enum?
|
|
29
33
|
export const EditorModes = ['default', 'vim'] as const;
|
|
30
34
|
export type EditorMode = (typeof EditorModes)[number];
|
|
31
35
|
|
|
@@ -34,133 +38,169 @@ export type CursorInfo = {
|
|
|
34
38
|
to: number;
|
|
35
39
|
line: number;
|
|
36
40
|
lines: number;
|
|
41
|
+
length: number;
|
|
37
42
|
after?: string;
|
|
38
43
|
};
|
|
39
44
|
|
|
40
|
-
export type TextEditorRef = {
|
|
41
|
-
root: HTMLDivElement | null;
|
|
42
|
-
state?: EditorState;
|
|
43
|
-
view?: EditorView;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
45
|
export type TextEditorSlots = {
|
|
47
46
|
root?: Omit<ComponentProps<'div'>, 'ref'>;
|
|
48
47
|
editor?: {
|
|
49
48
|
className?: string;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
theme?: ThemeStyles;
|
|
49
|
+
};
|
|
50
|
+
content?: {
|
|
51
|
+
className?: string;
|
|
54
52
|
};
|
|
55
53
|
};
|
|
56
54
|
|
|
55
|
+
// TODO(burdon): Spellcheck?
|
|
57
56
|
export type TextEditorProps = {
|
|
58
|
-
model: EditorModel;
|
|
57
|
+
model: EditorModel; // TODO(burdon): Optional (e.g., just provide content if readonly).
|
|
59
58
|
readonly?: boolean; // TODO(burdon): Move into model.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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.
|
|
64
|
+
placeholder?: string;
|
|
65
|
+
theme?: ThemeStyles;
|
|
63
66
|
slots?: TextEditorSlots;
|
|
67
|
+
extensions?: Extension[];
|
|
68
|
+
debug?: boolean;
|
|
64
69
|
};
|
|
65
70
|
|
|
66
71
|
/**
|
|
67
72
|
* Base text editor.
|
|
68
|
-
* NOTE: Rather than adding properties, try to create extensions that can be reused.
|
|
69
73
|
*/
|
|
70
|
-
export const BaseTextEditor = forwardRef<
|
|
71
|
-
(
|
|
74
|
+
export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
75
|
+
(
|
|
76
|
+
{
|
|
77
|
+
model,
|
|
78
|
+
readonly,
|
|
79
|
+
autoFocus,
|
|
80
|
+
scrollTo,
|
|
81
|
+
selection,
|
|
82
|
+
editorMode,
|
|
83
|
+
theme,
|
|
84
|
+
slots = defaultSlots,
|
|
85
|
+
extensions = [],
|
|
86
|
+
debug,
|
|
87
|
+
},
|
|
88
|
+
forwardedRef,
|
|
89
|
+
) => {
|
|
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
|
-
const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
|
|
74
|
-
const [root, setRoot] = useState<HTMLDivElement | null>(null);
|
|
75
|
-
const [{ state = undefined, view = undefined } = {}, setEditor] = useState<
|
|
76
|
-
{ state?: EditorState; view?: EditorView } | undefined
|
|
77
|
-
>();
|
|
78
|
-
useImperativeHandle(forwardedRef, () => ({ root, state, view }), [view, state, root]);
|
|
79
|
-
|
|
80
|
-
// TODO(burdon): Factor out as extension.
|
|
81
|
-
const { awareness, peer } = model;
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
if (awareness && peer) {
|
|
84
|
-
awareness.setLocalStateField('user', {
|
|
85
|
-
name: peer.name ?? generateName(peer.id),
|
|
86
|
-
color: getColorForValue({ value: peer.id, type: 'color' }),
|
|
87
|
-
colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
}, [awareness, peer, themeMode]);
|
|
91
97
|
|
|
92
|
-
|
|
98
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
99
|
+
const [view, setView] = useState<EditorView | null>(null);
|
|
100
|
+
|
|
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]);
|
|
104
|
+
|
|
105
|
+
// Set focus.
|
|
93
106
|
useEffect(() => {
|
|
94
|
-
if (
|
|
95
|
-
view.
|
|
96
|
-
|
|
97
|
-
|
|
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();
|
|
98
114
|
}
|
|
99
|
-
}, [view,
|
|
115
|
+
}, [view, autoFocus]);
|
|
100
116
|
|
|
117
|
+
// Create editor state and view.
|
|
118
|
+
// The view is recreated if the model or extensions are changed.
|
|
101
119
|
useEffect(() => {
|
|
102
|
-
if (!
|
|
120
|
+
if (!model || !rootRef.current) {
|
|
103
121
|
return;
|
|
104
122
|
}
|
|
105
123
|
|
|
106
|
-
//
|
|
124
|
+
//
|
|
125
|
+
// EditorState
|
|
107
126
|
// https://codemirror.net/docs/ref/#state.EditorStateConfig
|
|
127
|
+
//
|
|
108
128
|
const state = EditorState.create({
|
|
109
129
|
doc: model.text(),
|
|
130
|
+
selection,
|
|
110
131
|
extensions: [
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
132
|
+
// TODO(burdon): Doesn't catch errors in keymap functions.
|
|
133
|
+
EditorView.exceptionSink.of((err) => {
|
|
134
|
+
log.catch(err);
|
|
135
|
+
}),
|
|
115
136
|
|
|
116
137
|
// Theme.
|
|
117
|
-
// TODO(burdon): Make
|
|
138
|
+
// TODO(burdon): Make configurable.
|
|
118
139
|
EditorView.baseTheme(defaultTheme),
|
|
119
|
-
EditorView.theme(
|
|
140
|
+
EditorView.theme(theme ?? {}),
|
|
120
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),
|
|
121
148
|
|
|
122
149
|
// Storage and replication.
|
|
150
|
+
// NOTE: This must come before user extensions.
|
|
123
151
|
model.extension,
|
|
124
152
|
|
|
153
|
+
// TODO(burdon): Factor out? (Requires special handling for Escape/Enter below).
|
|
154
|
+
editorMode === 'vim' && vim(),
|
|
155
|
+
|
|
125
156
|
// Custom.
|
|
126
157
|
...extensions,
|
|
127
|
-
].filter(
|
|
158
|
+
].filter(isNotFalsy),
|
|
128
159
|
});
|
|
129
160
|
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
161
|
+
//
|
|
162
|
+
// EditorView
|
|
163
|
+
// https://codemirror.net/docs/ref/#view.EditorViewConfig
|
|
164
|
+
//
|
|
165
|
+
const newView = new EditorView({
|
|
135
166
|
state,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
167
|
+
parent: rootRef.current,
|
|
168
|
+
scrollTo,
|
|
169
|
+
// NOTE: Uncomment to debug/monitor all transactions.
|
|
170
|
+
// https://codemirror.net/docs/ref/#view.EditorView.dispatch
|
|
171
|
+
dispatchTransactions: (trs, view) => {
|
|
172
|
+
if (debug) {
|
|
173
|
+
logChanges(trs);
|
|
174
|
+
}
|
|
175
|
+
view.update(trs);
|
|
176
|
+
},
|
|
145
177
|
});
|
|
146
178
|
|
|
179
|
+
view?.destroy();
|
|
180
|
+
setView(newView);
|
|
181
|
+
|
|
147
182
|
return () => {
|
|
148
|
-
|
|
149
|
-
|
|
183
|
+
newView?.destroy();
|
|
184
|
+
setView(null);
|
|
150
185
|
};
|
|
151
|
-
}, [
|
|
186
|
+
}, [rootRef, model, readonly, editorMode, themeMode]);
|
|
152
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?
|
|
153
191
|
const handleKeyUp = useCallback(
|
|
154
192
|
(event: KeyboardEvent) => {
|
|
155
193
|
const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
|
|
156
194
|
switch (key) {
|
|
157
195
|
case 'Enter': {
|
|
158
|
-
view?.
|
|
196
|
+
view?.focus();
|
|
159
197
|
break;
|
|
160
198
|
}
|
|
161
199
|
|
|
162
200
|
case 'Escape': {
|
|
163
|
-
editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)
|
|
201
|
+
if (editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)) {
|
|
202
|
+
view?.focus();
|
|
203
|
+
}
|
|
164
204
|
break;
|
|
165
205
|
}
|
|
166
206
|
}
|
|
@@ -171,42 +211,45 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
171
211
|
return (
|
|
172
212
|
<div
|
|
173
213
|
key={model.id}
|
|
174
|
-
|
|
214
|
+
role='none'
|
|
175
215
|
tabIndex={0}
|
|
176
|
-
{...slots
|
|
177
|
-
{...(editorMode !== 'vim' && tabsterDOMAttribute)}
|
|
216
|
+
{...slots.root}
|
|
217
|
+
// {...(editorMode !== 'vim' && tabsterDOMAttribute)}
|
|
178
218
|
onKeyUp={handleKeyUp}
|
|
219
|
+
ref={rootRef}
|
|
179
220
|
/>
|
|
180
221
|
);
|
|
181
222
|
},
|
|
182
223
|
);
|
|
183
224
|
|
|
184
|
-
export const TextEditor = forwardRef<
|
|
185
|
-
({ readonly,
|
|
225
|
+
export const TextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
226
|
+
({ readonly, placeholder, lineWrapping, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
|
|
186
227
|
const { themeMode } = useThemeContext();
|
|
187
|
-
const
|
|
228
|
+
const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
|
|
188
229
|
return (
|
|
189
230
|
<BaseTextEditor
|
|
190
231
|
ref={forwardedRef}
|
|
191
232
|
readonly={readonly}
|
|
192
|
-
extensions={[
|
|
193
|
-
|
|
233
|
+
extensions={[createBasicBundle({ themeMode, placeholder, lineWrapping }), ...extensions]}
|
|
234
|
+
theme={theme}
|
|
235
|
+
slots={updatedSlots}
|
|
194
236
|
{...props}
|
|
195
237
|
/>
|
|
196
238
|
);
|
|
197
239
|
},
|
|
198
240
|
);
|
|
199
241
|
|
|
200
|
-
export const MarkdownEditor = forwardRef<
|
|
201
|
-
({ readonly,
|
|
242
|
+
export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
|
|
243
|
+
({ readonly, placeholder, theme = markdownTheme, slots, extensions = [], ...props }, forwardedRef) => {
|
|
202
244
|
const { themeMode } = useThemeContext();
|
|
203
|
-
const
|
|
245
|
+
const updatedSlots = defaultsDeep({}, slots, defaultMarkdownSlots);
|
|
204
246
|
return (
|
|
205
247
|
<BaseTextEditor
|
|
206
248
|
ref={forwardedRef}
|
|
207
249
|
readonly={readonly}
|
|
208
|
-
extensions={[
|
|
209
|
-
|
|
250
|
+
extensions={[createMarkdownExtensions({ themeMode, placeholder }), ...extensions]}
|
|
251
|
+
theme={theme}
|
|
252
|
+
slots={updatedSlots}
|
|
210
253
|
{...props}
|
|
211
254
|
/>
|
|
212
255
|
);
|
|
@@ -215,20 +258,17 @@ export const MarkdownEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
215
258
|
|
|
216
259
|
export const defaultSlots: TextEditorSlots = {
|
|
217
260
|
root: {
|
|
218
|
-
className: mx('
|
|
261
|
+
className: mx('grow', focusRing),
|
|
262
|
+
},
|
|
263
|
+
editor: {
|
|
264
|
+
className: 'bs-full',
|
|
219
265
|
},
|
|
220
266
|
};
|
|
221
267
|
|
|
222
268
|
export const defaultTextSlots: TextEditorSlots = {
|
|
223
269
|
...defaultSlots,
|
|
224
|
-
editor: {
|
|
225
|
-
theme: textTheme,
|
|
226
|
-
},
|
|
227
270
|
};
|
|
228
271
|
|
|
229
272
|
export const defaultMarkdownSlots: TextEditorSlots = {
|
|
230
273
|
...defaultSlots,
|
|
231
|
-
editor: {
|
|
232
|
-
theme: markdownTheme,
|
|
233
|
-
},
|
|
234
274
|
};
|
|
@@ -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}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import '@dxosTheme';
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
|
|
9
|
+
import { useThemeContext } from '@dxos/react-ui';
|
|
10
|
+
import { fixedInsetFlexLayout, groupSurface, mx } from '@dxos/react-ui-theme';
|
|
11
|
+
import { withTheme } from '@dxos/storybook-utils';
|
|
12
|
+
|
|
13
|
+
import { TextEditor } from './TextEditor';
|
|
14
|
+
import {
|
|
15
|
+
code,
|
|
16
|
+
createMarkdownExtensions,
|
|
17
|
+
formatting,
|
|
18
|
+
heading,
|
|
19
|
+
hr,
|
|
20
|
+
image,
|
|
21
|
+
link,
|
|
22
|
+
table,
|
|
23
|
+
tasklist,
|
|
24
|
+
useFormattingState,
|
|
25
|
+
} from '../../extensions';
|
|
26
|
+
import { createDataExtensions, createThemeExtensions, useActionHandler, useTextEditor } from '../../hooks';
|
|
27
|
+
import { markdownTheme } from '../../themes';
|
|
28
|
+
import { Toolbar } from '../Toolbar';
|
|
29
|
+
|
|
30
|
+
// TODO(burdon): Demo toolbar with hooks.
|
|
31
|
+
// TODO(burdon): Build components from hooks and adapters for model/extensions, etc.
|
|
32
|
+
// TODO(burdon): Remove BaseTextEditor.
|
|
33
|
+
// TODO(burdon): Move scrolling container layout/logic into TextEditor/MarkdownEditor components.
|
|
34
|
+
// TODO(burdon): Define keymap in composer framework format.
|
|
35
|
+
|
|
36
|
+
type StoryProps = {
|
|
37
|
+
autoFocus?: boolean;
|
|
38
|
+
placeholder?: string;
|
|
39
|
+
doc?: string;
|
|
40
|
+
readonly?: boolean;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
|
|
44
|
+
const { themeMode } = useThemeContext();
|
|
45
|
+
const [formattingState, trackFormatting] = useFormattingState();
|
|
46
|
+
const { parentRef, view } = useTextEditor({
|
|
47
|
+
autoFocus,
|
|
48
|
+
doc,
|
|
49
|
+
extensions: [
|
|
50
|
+
//
|
|
51
|
+
createDataExtensions({ readonly }),
|
|
52
|
+
createThemeExtensions({
|
|
53
|
+
themeMode,
|
|
54
|
+
theme: markdownTheme,
|
|
55
|
+
slots: {
|
|
56
|
+
// TODO(burdon): Document classes re base theme.
|
|
57
|
+
// Semantic tokens (e.g., replacement of input surface).
|
|
58
|
+
editor: {
|
|
59
|
+
className: 'h-full p-4 bg-white text-black dark:bg-black dark:text-white',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
// TODO(burdon): Move lineWrapping.
|
|
64
|
+
createMarkdownExtensions({ placeholder, lineWrapping: true }),
|
|
65
|
+
// TODO(burdon): Move into markdown bundle (with React callbacks).
|
|
66
|
+
code(),
|
|
67
|
+
formatting(),
|
|
68
|
+
heading(),
|
|
69
|
+
hr(),
|
|
70
|
+
image(),
|
|
71
|
+
link(),
|
|
72
|
+
table(),
|
|
73
|
+
tasklist(),
|
|
74
|
+
trackFormatting,
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const handleAction = useActionHandler(view);
|
|
79
|
+
|
|
80
|
+
// FIXME This doesn't update the state on view changes. Also not
|
|
81
|
+
// sure if view is even guaranteed to exist at this point.
|
|
82
|
+
return (
|
|
83
|
+
<div className={mx(fixedInsetFlexLayout, groupSurface)}>
|
|
84
|
+
<div className='flex h-full justify-center'>
|
|
85
|
+
<div className='flex flex-col h-full w-[800px]'>
|
|
86
|
+
<Toolbar.Root onAction={handleAction} state={formattingState}>
|
|
87
|
+
<Toolbar.Markdown />
|
|
88
|
+
</Toolbar.Root>
|
|
89
|
+
|
|
90
|
+
{/* TODO(burdon): Handle scrolling in component wrapper (like this). */}
|
|
91
|
+
<div role='none' className='h-full overflow-y-auto' ref={parentRef} />
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export default {
|
|
99
|
+
title: 'react-ui-editor/useTextEditor',
|
|
100
|
+
component: TextEditor,
|
|
101
|
+
decorators: [withTheme],
|
|
102
|
+
render: (args: StoryProps) => <Story {...args} />,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const Default = {
|
|
106
|
+
args: {
|
|
107
|
+
autoFocus: true,
|
|
108
|
+
placeholder: 'Text...',
|
|
109
|
+
doc: '# Demo\n\nThis is a document.\n\n',
|
|
110
|
+
},
|
|
111
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import '@dxosTheme';
|
|
6
|
+
|
|
7
|
+
import React, { type FC, useMemo, useState } from 'react';
|
|
8
|
+
|
|
9
|
+
import { TextObject } from '@dxos/echo-schema';
|
|
10
|
+
import { PublicKey } from '@dxos/keys';
|
|
11
|
+
import { faker } from '@dxos/random';
|
|
12
|
+
import { fixedInsetFlexLayout, groupSurface, mx } from '@dxos/react-ui-theme';
|
|
13
|
+
import { withTheme } from '@dxos/storybook-utils';
|
|
14
|
+
|
|
15
|
+
import { Toolbar } from './Toolbar';
|
|
16
|
+
import {
|
|
17
|
+
code,
|
|
18
|
+
comments,
|
|
19
|
+
formatting,
|
|
20
|
+
heading,
|
|
21
|
+
image,
|
|
22
|
+
table,
|
|
23
|
+
tasklist,
|
|
24
|
+
useComments,
|
|
25
|
+
useFormattingState,
|
|
26
|
+
} from '../../extensions';
|
|
27
|
+
import { type Comment, useActionHandler, useEditorView, useTextModel } from '../../hooks';
|
|
28
|
+
import { MarkdownEditor } from '../TextEditor';
|
|
29
|
+
|
|
30
|
+
faker.seed(101);
|
|
31
|
+
|
|
32
|
+
const Story: FC<{ content: string }> = ({ content }) => {
|
|
33
|
+
const [item] = useState({ text: new TextObject(content) });
|
|
34
|
+
const [_comments, setComments] = useState<Comment[]>([]);
|
|
35
|
+
const [editorRef, editorView] = useEditorView();
|
|
36
|
+
const model = useTextModel({ text: item.text });
|
|
37
|
+
const [formattingState, formattingObserver] = useFormattingState();
|
|
38
|
+
const extensions = useMemo(
|
|
39
|
+
() => [
|
|
40
|
+
code(),
|
|
41
|
+
comments({
|
|
42
|
+
onCreate: ({ cursor }) => {
|
|
43
|
+
const id = PublicKey.random().toHex();
|
|
44
|
+
setComments((comments) => [...comments, { id, cursor }]);
|
|
45
|
+
return id;
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
formatting(),
|
|
49
|
+
heading(),
|
|
50
|
+
image(),
|
|
51
|
+
table(),
|
|
52
|
+
tasklist(),
|
|
53
|
+
formattingObserver,
|
|
54
|
+
],
|
|
55
|
+
[],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
useComments(editorView, _comments);
|
|
59
|
+
const handleAction = useActionHandler(editorView);
|
|
60
|
+
|
|
61
|
+
if (!model) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className={mx(fixedInsetFlexLayout, groupSurface)}>
|
|
67
|
+
<div className='flex h-full justify-center'>
|
|
68
|
+
<div className='flex flex-col h-full w-[800px]'>
|
|
69
|
+
<Toolbar.Root onAction={handleAction} state={formattingState}>
|
|
70
|
+
<Toolbar.Markdown />
|
|
71
|
+
<Toolbar.Separator />
|
|
72
|
+
<Toolbar.Extended />
|
|
73
|
+
</Toolbar.Root>
|
|
74
|
+
<MarkdownEditor ref={editorRef} model={model} extensions={extensions} />
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export default {
|
|
82
|
+
title: 'react-ui-editor/Toolbar',
|
|
83
|
+
component: Toolbar,
|
|
84
|
+
render: (args: any) => <Story {...args} />,
|
|
85
|
+
decorators: [withTheme],
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const content = [
|
|
89
|
+
'Demo',
|
|
90
|
+
'',
|
|
91
|
+
'The editor supports **Markdown** styles.',
|
|
92
|
+
'',
|
|
93
|
+
faker.lorem.paragraph({ min: 5, max: 8 }),
|
|
94
|
+
'',
|
|
95
|
+
'',
|
|
96
|
+
].join('\n');
|
|
97
|
+
|
|
98
|
+
export const Default = {
|
|
99
|
+
args: {
|
|
100
|
+
content,
|
|
101
|
+
},
|
|
102
|
+
};
|