@dxos/react-ui-editor 0.3.11-main.2c83f41 → 0.3.11-main.2cb95a4

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.
Files changed (107) hide show
  1. package/dist/lib/browser/index.mjs +779 -760
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +16 -13
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +20 -17
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +6 -6
  9. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  10. package/dist/types/src/components/TextEditor/comments.stories.d.ts +26 -0
  11. package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -0
  12. package/dist/types/src/components/TextEditor/yjs.stories.d.ts +1 -1
  13. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  14. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  15. package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
  16. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  17. package/dist/types/src/extensions/automerge/defs.d.ts +7 -9
  18. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  19. package/dist/types/src/extensions/automerge/index.d.ts +0 -1
  20. package/dist/types/src/extensions/automerge/index.d.ts.map +1 -1
  21. package/dist/types/src/extensions/automerge/semaphore.d.ts +7 -12
  22. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
  23. package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -2
  24. package/dist/types/src/extensions/awareness.d.ts +2 -0
  25. package/dist/types/src/extensions/awareness.d.ts.map +1 -1
  26. package/dist/types/src/extensions/basic.d.ts +4 -5
  27. package/dist/types/src/extensions/basic.d.ts.map +1 -1
  28. package/dist/types/src/extensions/code.d.ts.map +1 -1
  29. package/dist/types/src/extensions/comments.d.ts +31 -18
  30. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  31. package/dist/types/src/{util → extensions}/cursor.d.ts +10 -2
  32. package/dist/types/src/extensions/cursor.d.ts.map +1 -0
  33. package/dist/types/src/extensions/hr.d.ts.map +1 -1
  34. package/dist/types/src/extensions/index.d.ts +2 -0
  35. package/dist/types/src/extensions/index.d.ts.map +1 -1
  36. package/dist/types/src/extensions/markdown/bundle.d.ts +2 -5
  37. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  38. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  39. package/dist/types/src/extensions/mention.d.ts.map +1 -1
  40. package/dist/types/src/extensions/outliner.d.ts +4 -0
  41. package/dist/types/src/extensions/outliner.d.ts.map +1 -0
  42. package/dist/types/src/extensions/tasklist.d.ts.map +1 -1
  43. package/dist/types/src/extensions/yjs/cursor.d.ts +1 -1
  44. package/dist/types/src/extensions/yjs/cursor.d.ts.map +1 -1
  45. package/dist/types/src/hooks/automerge.d.ts +2 -1
  46. package/dist/types/src/hooks/automerge.d.ts.map +1 -1
  47. package/dist/types/src/hooks/defs.d.ts +28 -0
  48. package/dist/types/src/hooks/defs.d.ts.map +1 -0
  49. package/dist/types/src/hooks/index.d.ts +2 -1
  50. package/dist/types/src/hooks/index.d.ts.map +1 -1
  51. package/dist/types/src/hooks/useTextModel.d.ts +2 -32
  52. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  53. package/dist/types/src/hooks/yjs.d.ts +2 -1
  54. package/dist/types/src/hooks/yjs.d.ts.map +1 -1
  55. package/dist/types/src/index.d.ts +1 -0
  56. package/dist/types/src/index.d.ts.map +1 -1
  57. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  58. package/dist/types/src/themes/default.d.ts.map +1 -1
  59. package/dist/types/src/util.d.ts +7 -0
  60. package/dist/types/src/util.d.ts.map +1 -0
  61. package/package.json +32 -31
  62. package/src/components/TextEditor/MarkdownEditor.stories.tsx +77 -79
  63. package/src/components/TextEditor/TextEditor.stories.tsx +12 -46
  64. package/src/components/TextEditor/TextEditor.tsx +124 -92
  65. package/src/components/TextEditor/comments.stories.tsx +357 -0
  66. package/src/extensions/autocomplete.ts +6 -3
  67. package/src/extensions/automerge/automerge.ts +40 -46
  68. package/src/extensions/automerge/cursor.ts +2 -1
  69. package/src/extensions/automerge/defs.ts +10 -31
  70. package/src/extensions/automerge/index.ts +0 -1
  71. package/src/extensions/automerge/semaphore.ts +23 -20
  72. package/src/extensions/automerge/update-automerge.ts +2 -2
  73. package/src/extensions/awareness.ts +45 -34
  74. package/src/extensions/basic.ts +22 -17
  75. package/src/extensions/code.ts +24 -18
  76. package/src/extensions/comments.ts +242 -234
  77. package/src/extensions/cursor.ts +50 -0
  78. package/src/extensions/hr.ts +49 -50
  79. package/src/extensions/image.ts +2 -2
  80. package/src/extensions/index.ts +2 -0
  81. package/src/extensions/link.ts +7 -7
  82. package/src/extensions/markdown/bundle.ts +18 -24
  83. package/src/extensions/markdown/highlight.ts +0 -8
  84. package/src/extensions/mention.ts +9 -2
  85. package/src/extensions/outliner.ts +12 -0
  86. package/src/extensions/table.ts +2 -2
  87. package/src/extensions/tasklist.ts +78 -86
  88. package/src/extensions/yjs/cursor.ts +2 -1
  89. package/src/extensions/yjs/yjs.test.ts +1 -1
  90. package/src/extensions/yjs/yjs.ts +2 -2
  91. package/src/hooks/automerge.ts +2 -1
  92. package/src/hooks/defs.ts +47 -0
  93. package/src/hooks/index.ts +3 -1
  94. package/src/hooks/useTextModel.ts +3 -49
  95. package/src/hooks/yjs.ts +2 -23
  96. package/src/index.ts +1 -0
  97. package/src/styles/markdown.ts +0 -2
  98. package/src/themes/default.ts +17 -50
  99. package/src/util.ts +41 -0
  100. package/dist/types/src/util/cursor.d.ts.map +0 -1
  101. package/dist/types/src/util/index.d.ts +0 -3
  102. package/dist/types/src/util/index.d.ts.map +0 -1
  103. package/dist/types/src/util/util.d.ts +0 -5
  104. package/dist/types/src/util/util.d.ts.map +0 -1
  105. package/src/util/cursor.ts +0 -29
  106. package/src/util/index.ts +0 -6
  107. package/src/util/util.ts +0 -18
@@ -2,7 +2,7 @@
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
7
  import { useFocusableGroup } from '@fluentui/react-tabster';
8
8
  import { vim } from '@replit/codemirror-vim';
@@ -14,18 +14,21 @@ import React, {
14
14
  useCallback,
15
15
  useEffect,
16
16
  useImperativeHandle,
17
+ useRef,
17
18
  useState,
18
19
  } from 'react';
19
20
 
20
- import { generateName } from '@dxos/display-name';
21
+ import { log } from '@dxos/log';
21
22
  import { useThemeContext } from '@dxos/react-ui';
22
- import { getColorForValue, inputSurface, mx } from '@dxos/react-ui-theme';
23
+ import { inputSurface, mx } from '@dxos/react-ui-theme';
23
24
 
24
- import { basicBundle, markdownBundle, setCommentRange } from '../../extensions';
25
- import { type CommentRange, type EditorModel } from '../../hooks';
25
+ import { basicBundle, markdownBundle, useAwareness } from '../../extensions';
26
+ import { type EditorModel } from '../../hooks';
26
27
  import { type ThemeStyles } from '../../styles';
27
28
  import { defaultTheme, markdownTheme, textTheme } from '../../themes';
29
+ import { logChanges } from '../../util';
28
30
 
31
+ // TODO(burdon): Change to enum?
29
32
  export const EditorModes = ['default', 'vim'] as const;
30
33
  export type EditorMode = (typeof EditorModes)[number];
31
34
 
@@ -34,133 +37,160 @@ export type CursorInfo = {
34
37
  to: number;
35
38
  line: number;
36
39
  lines: number;
40
+ length: number;
37
41
  after?: string;
38
42
  };
39
43
 
40
- export type TextEditorRef = {
41
- root: HTMLDivElement | null;
42
- state?: EditorState;
43
- view?: EditorView;
44
- };
45
-
46
44
  export type TextEditorSlots = {
47
45
  root?: Omit<ComponentProps<'div'>, 'ref'>;
48
46
  editor?: {
49
47
  className?: string;
50
- placeholder?: string;
51
- spellCheck?: boolean;
52
- tabIndex?: number;
53
- theme?: ThemeStyles;
48
+ };
49
+ content?: {
50
+ className?: string;
54
51
  };
55
52
  };
56
53
 
54
+ // TODO(burdon): Spellcheck?
57
55
  export type TextEditorProps = {
58
56
  model: EditorModel;
59
57
  readonly?: boolean; // TODO(burdon): Move into model.
60
- comments?: CommentRange[]; // TODO(burdon): Move into extension.
61
- extensions?: Extension[];
62
- editorMode?: EditorMode;
58
+ autofocus?: boolean;
59
+ multiline?: boolean;
60
+ scrollTo?: StateEffect<any>; // TODO(burdon): Restore scroll position: scrollTo EditorView.scrollSnapshot().
61
+ selection?: { anchor: number; head?: number };
62
+ editorMode?: EditorMode; // TODO(burdon): Factor out.
63
+ placeholder?: string;
64
+ theme?: ThemeStyles;
63
65
  slots?: TextEditorSlots;
66
+ extensions?: Extension[];
67
+ debug?: boolean;
64
68
  };
65
69
 
66
70
  /**
67
71
  * Base text editor.
68
- * NOTE: Rather than adding properties, try to create extensions that can be reused.
69
72
  */
70
- export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
71
- ({ model, readonly, comments, extensions = [], editorMode, slots = defaultSlots }, forwardedRef) => {
72
- const { themeMode } = useThemeContext();
73
+ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
74
+ (
75
+ {
76
+ model,
77
+ readonly,
78
+ autofocus,
79
+ scrollTo,
80
+ selection,
81
+ editorMode,
82
+ theme,
83
+ slots = defaultSlots,
84
+ extensions = [],
85
+ debug,
86
+ },
87
+ forwardedRef,
88
+ ) => {
73
89
  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]);
90
+ const { themeMode } = useThemeContext();
91
+
92
+ const rootRef = useRef<HTMLDivElement>(null);
93
+ const [view, setView] = useState<EditorView | null>(null);
94
+
95
+ // The view ref can be used to focus the editor.
96
+ // NOTE: This does not cause the parent to re-render, so the ref is not available immediately.
97
+ useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
91
98
 
92
- // TODO(burdon): Factor out as extension.
99
+ // Set focus.
93
100
  useEffect(() => {
94
- if (view && comments?.length) {
95
- view.dispatch({
96
- effects: setCommentRange.of({ model, comments }),
97
- });
101
+ if (autofocus) {
102
+ view?.focus();
98
103
  }
99
- }, [view, comments]);
104
+ }, [view, autofocus]);
100
105
 
106
+ // Monitor awareness.
107
+ useAwareness(model);
108
+
109
+ // Create editor state and view.
110
+ // The view is recreated if the model or extensions are changed.
101
111
  useEffect(() => {
102
- if (!root) {
112
+ if (!model || !rootRef.current) {
103
113
  return;
104
114
  }
105
115
 
106
- // TODO(burdon): Remember cursor position.
116
+ //
117
+ // EditorState
107
118
  // https://codemirror.net/docs/ref/#state.EditorStateConfig
119
+ //
108
120
  const state = EditorState.create({
109
121
  doc: model.text(),
122
+ selection,
110
123
  extensions: [
111
- readonly && EditorState.readOnly.of(readonly),
112
-
113
- // TODO(burdon): Factor out VIM mode? (manage via MarkdownPlugin).
114
- editorMode === 'vim' && vim(),
124
+ // TODO(burdon): Doesn't catch errors in keymap functions.
125
+ EditorView.exceptionSink.of((err) => {
126
+ log.catch(err);
127
+ }),
115
128
 
116
129
  // Theme.
117
- // TODO(burdon): Make theme configurable.
130
+ // TODO(burdon): Make configurable.
118
131
  EditorView.baseTheme(defaultTheme),
119
- EditorView.theme(slots?.editor?.theme ?? {}),
132
+ EditorView.theme(theme ?? {}),
120
133
  EditorView.darkTheme.of(themeMode === 'dark'),
134
+ EditorView.editorAttributes.of({ class: slots.editor?.className ?? '' }),
135
+ EditorView.contentAttributes.of({ class: slots.content?.className ?? '' }),
136
+
137
+ // State.
138
+ EditorState.readOnly.of(!!readonly),
121
139
 
122
140
  // Storage and replication.
141
+ // NOTE: This must come before user extensions.
123
142
  model.extension,
124
143
 
144
+ // TODO(burdon): Factor out (requires special handling for Escape/focus).
145
+ editorMode === 'vim' && vim(),
146
+
125
147
  // Custom.
126
148
  ...extensions,
127
149
  ].filter(Boolean) as Extension[],
128
150
  });
129
151
 
130
- // NOTE: This repaints the editor.
131
- // If the new state is derived from the old state, it will likely not be visible other than the cursor resetting.
132
- // Ideally this should not happen except when changing between text objects.
133
- view?.destroy();
134
- setEditor({
152
+ //
153
+ // EditorView
154
+ // https://codemirror.net/docs/ref/#view.EditorViewConfig
155
+ //
156
+ const newView = new EditorView({
135
157
  state,
136
- view: new EditorView({
137
- state,
138
- parent: root,
139
- // NOTE: Uncomment to spy on all transactions.
140
- // https://codemirror.net/docs/ref/#view.EditorView.dispatch
141
- // dispatch: (transaction, view) => {
142
- // view.update([transaction]);
143
- // },
144
- }),
158
+ parent: rootRef.current,
159
+ scrollTo,
160
+ // NOTE: Uncomment to debug/monitor all transactions.
161
+ // https://codemirror.net/docs/ref/#view.EditorView.dispatch
162
+ dispatchTransactions: (trs, view) => {
163
+ if (debug) {
164
+ logChanges(trs);
165
+ }
166
+ view.update(trs);
167
+ },
145
168
  });
146
169
 
170
+ view?.destroy();
171
+ setView(newView);
172
+
147
173
  return () => {
148
- view?.destroy();
149
- setEditor(undefined);
174
+ newView?.destroy();
175
+ setView(null);
150
176
  };
151
- }, [root, model, readonly, editorMode, themeMode]);
177
+ }, [rootRef, model, readonly, editorMode, themeMode]);
152
178
 
179
+ // Handles tab/focus.
180
+ // Pressing Escape focuses the outer div (to support tab navigation); pressing Enter refocuses the editor.
153
181
  const handleKeyUp = useCallback(
154
182
  (event: KeyboardEvent) => {
155
183
  const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
156
184
  switch (key) {
157
185
  case 'Enter': {
158
- view?.contentDOM.focus();
186
+ view?.focus();
159
187
  break;
160
188
  }
161
189
 
162
190
  case 'Escape': {
163
- editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey) && root?.focus();
191
+ if (editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)) {
192
+ rootRef.current?.focus();
193
+ }
164
194
  break;
165
195
  }
166
196
  }
@@ -171,42 +201,46 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
171
201
  return (
172
202
  <div
173
203
  key={model.id}
174
- ref={setRoot}
204
+ role='none'
175
205
  tabIndex={0}
176
- {...slots?.root}
177
- {...(editorMode !== 'vim' && tabsterDOMAttribute)}
178
206
  onKeyUp={handleKeyUp}
207
+ {...slots.root}
208
+ {...(editorMode !== 'vim' && tabsterDOMAttribute)}
209
+ ref={rootRef}
179
210
  />
180
211
  );
181
212
  },
182
213
  );
183
214
 
184
- export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
185
- ({ readonly, extensions = [], slots: _slots, ...props }, forwardedRef) => {
215
+ // TODO(burdon): Single-line/scroll.
216
+ export const TextEditor = forwardRef<EditorView, TextEditorProps>(
217
+ ({ readonly, placeholder, multiline, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
186
218
  const { themeMode } = useThemeContext();
187
- const slots = defaultsDeep({}, _slots, defaultTextSlots);
219
+ const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
188
220
  return (
189
221
  <BaseTextEditor
190
222
  ref={forwardedRef}
191
223
  readonly={readonly}
192
- extensions={[basicBundle({ readonly, themeMode, placeholder: slots?.editor?.placeholder }), ...extensions]}
193
- slots={slots}
224
+ extensions={[basicBundle({ themeMode, placeholder, multiline }), ...extensions]}
225
+ theme={theme}
226
+ slots={updatedSlots}
194
227
  {...props}
195
228
  />
196
229
  );
197
230
  },
198
231
  );
199
232
 
200
- export const MarkdownEditor = forwardRef<TextEditorRef, TextEditorProps>(
201
- ({ readonly, extensions = [], slots: _slots, ...props }, forwardedRef) => {
233
+ export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
234
+ ({ readonly, placeholder, theme = markdownTheme, slots, extensions = [], ...props }, forwardedRef) => {
202
235
  const { themeMode } = useThemeContext();
203
- const slots = defaultsDeep({}, _slots, defaultMarkdownSlots);
236
+ const updatedSlots = defaultsDeep({}, slots, defaultMarkdownSlots);
204
237
  return (
205
238
  <BaseTextEditor
206
239
  ref={forwardedRef}
207
240
  readonly={readonly}
208
- extensions={[markdownBundle({ readonly, themeMode, placeholder: slots?.editor?.placeholder }), ...extensions]}
209
- slots={slots}
241
+ extensions={[markdownBundle({ themeMode, readonly, placeholder }), ...extensions]}
242
+ theme={theme}
243
+ slots={updatedSlots}
210
244
  {...props}
211
245
  />
212
246
  );
@@ -215,20 +249,18 @@ export const MarkdownEditor = forwardRef<TextEditorRef, TextEditorProps>(
215
249
 
216
250
  export const defaultSlots: TextEditorSlots = {
217
251
  root: {
218
- className: mx('p-2', inputSurface),
252
+ // TODO(burdon): Add focusRing by default/as property?
253
+ className: mx('flex flex-col grow overflow-y-auto', inputSurface),
254
+ },
255
+ editor: {
256
+ className: 'h-full p-2',
219
257
  },
220
258
  };
221
259
 
222
260
  export const defaultTextSlots: TextEditorSlots = {
223
261
  ...defaultSlots,
224
- editor: {
225
- theme: textTheme,
226
- },
227
262
  };
228
263
 
229
264
  export const defaultMarkdownSlots: TextEditorSlots = {
230
265
  ...defaultSlots,
231
- editor: {
232
- theme: markdownTheme,
233
- },
234
266
  };