@dxos/react-ui-editor 0.3.11-main.cd64fe0 → 0.3.11-main.cf49a01

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 (140) hide show
  1. package/dist/lib/browser/index.mjs +1016 -1024
  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 +12 -16
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +13 -5
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +1 -1
  9. package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -1
  10. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  11. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  12. package/dist/types/src/extensions/automerge/defs.d.ts +6 -8
  13. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  14. package/dist/types/src/extensions/automerge/semaphore.d.ts +6 -6
  15. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
  16. package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -2
  17. package/dist/types/src/extensions/awareness.d.ts +1 -1
  18. package/dist/types/src/extensions/awareness.d.ts.map +1 -1
  19. package/dist/types/src/extensions/basic.d.ts +4 -5
  20. package/dist/types/src/extensions/basic.d.ts.map +1 -1
  21. package/dist/types/src/extensions/comments.d.ts +18 -14
  22. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  23. package/dist/types/src/extensions/cursor.d.ts +3 -3
  24. package/dist/types/src/extensions/cursor.d.ts.map +1 -1
  25. package/dist/types/src/extensions/index.d.ts +0 -7
  26. package/dist/types/src/extensions/index.d.ts.map +1 -1
  27. package/dist/types/src/extensions/markdown/bundle.d.ts +3 -6
  28. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  29. package/dist/types/src/extensions/markdown/code.d.ts.map +1 -0
  30. package/dist/types/src/extensions/markdown/heading.d.ts +6 -0
  31. package/dist/types/src/extensions/markdown/heading.d.ts.map +1 -0
  32. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  33. package/dist/types/src/extensions/markdown/hr.d.ts.map +1 -0
  34. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -0
  35. package/dist/types/src/extensions/markdown/index.d.ts +7 -0
  36. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  37. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -0
  38. package/dist/types/src/extensions/markdown/table.d.ts.map +1 -0
  39. package/dist/types/src/extensions/markdown/tasklist.d.ts +3 -0
  40. package/dist/types/src/extensions/markdown/tasklist.d.ts.map +1 -0
  41. package/dist/types/src/extensions/mention.d.ts.map +1 -1
  42. package/dist/types/src/hooks/awareness-provider.d.ts +7 -4
  43. package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -1
  44. package/dist/types/src/hooks/defs.d.ts +25 -0
  45. package/dist/types/src/hooks/defs.d.ts.map +1 -0
  46. package/dist/types/src/hooks/index.d.ts +2 -2
  47. package/dist/types/src/hooks/index.d.ts.map +1 -1
  48. package/dist/types/src/hooks/useTextEditor.d.ts +17 -0
  49. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
  50. package/dist/types/src/hooks/useTextModel.d.ts +2 -27
  51. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  52. package/dist/types/src/styles/markdown.d.ts +2 -2
  53. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  54. package/dist/types/src/themes/default.d.ts.map +1 -1
  55. package/dist/types/src/util.d.ts +2 -0
  56. package/dist/types/src/util.d.ts.map +1 -1
  57. package/package.json +32 -35
  58. package/src/components/TextEditor/MarkdownEditor.stories.tsx +60 -65
  59. package/src/components/TextEditor/TextEditor.tsx +91 -66
  60. package/src/components/TextEditor/comments.stories.tsx +45 -43
  61. package/src/extensions/autocomplete.ts +6 -3
  62. package/src/extensions/automerge/automerge.ts +35 -39
  63. package/src/extensions/automerge/defs.ts +8 -33
  64. package/src/extensions/automerge/semaphore.ts +17 -12
  65. package/src/extensions/automerge/update-automerge.ts +2 -2
  66. package/src/extensions/awareness.ts +14 -14
  67. package/src/extensions/basic.ts +22 -17
  68. package/src/extensions/comments.ts +161 -141
  69. package/src/extensions/cursor.ts +7 -3
  70. package/src/extensions/index.ts +0 -7
  71. package/src/extensions/markdown/bundle.ts +20 -30
  72. package/src/extensions/markdown/code.ts +127 -0
  73. package/src/extensions/markdown/heading.ts +59 -0
  74. package/src/extensions/markdown/highlight.ts +9 -15
  75. package/src/extensions/markdown/hr.ts +71 -0
  76. package/src/extensions/markdown/index.ts +7 -0
  77. package/src/extensions/{link.ts → markdown/link.ts} +1 -2
  78. package/src/extensions/{tasklist.ts → markdown/tasklist.ts} +48 -34
  79. package/src/extensions/mention.ts +9 -2
  80. package/src/hooks/awareness-provider.ts +24 -20
  81. package/src/hooks/defs.ts +42 -0
  82. package/src/hooks/index.ts +2 -2
  83. package/src/hooks/useTextEditor.ts +29 -0
  84. package/src/hooks/useTextModel.ts +47 -56
  85. package/src/styles/markdown.ts +9 -10
  86. package/src/themes/default.ts +0 -2
  87. package/src/util.ts +26 -3
  88. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
  89. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
  90. package/dist/types/src/components/TextEditor/yjs.stories.d.ts +0 -12
  91. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
  92. package/dist/types/src/extensions/code.d.ts.map +0 -1
  93. package/dist/types/src/extensions/hr.d.ts.map +0 -1
  94. package/dist/types/src/extensions/image.d.ts.map +0 -1
  95. package/dist/types/src/extensions/link.d.ts.map +0 -1
  96. package/dist/types/src/extensions/table.d.ts.map +0 -1
  97. package/dist/types/src/extensions/tasklist.d.ts +0 -7
  98. package/dist/types/src/extensions/tasklist.d.ts.map +0 -1
  99. package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
  100. package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
  101. package/dist/types/src/extensions/yjs/index.d.ts +0 -2
  102. package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
  103. package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
  104. package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
  105. package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
  106. package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
  107. package/dist/types/src/hooks/automerge.d.ts +0 -3
  108. package/dist/types/src/hooks/automerge.d.ts.map +0 -1
  109. package/dist/types/src/hooks/yjs.d.ts +0 -27
  110. package/dist/types/src/hooks/yjs.d.ts.map +0 -1
  111. package/dist/types/src/testing/index.d.ts +0 -3
  112. package/dist/types/src/testing/index.d.ts.map +0 -1
  113. package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
  114. package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
  115. package/dist/types/src/testing/proto/index.d.ts +0 -2
  116. package/dist/types/src/testing/proto/index.d.ts.map +0 -1
  117. package/dist/types/src/testing/replicator.d.ts +0 -45
  118. package/dist/types/src/testing/replicator.d.ts.map +0 -1
  119. package/src/components/TextEditor/codemirror.stories.ts +0 -115
  120. package/src/components/TextEditor/yjs.stories.tsx +0 -56
  121. package/src/extensions/code.ts +0 -104
  122. package/src/extensions/hr.ts +0 -72
  123. package/src/extensions/yjs/cursor.ts +0 -22
  124. package/src/extensions/yjs/index.ts +0 -5
  125. package/src/extensions/yjs/yjs.test.ts +0 -35
  126. package/src/extensions/yjs/yjs.ts +0 -16
  127. package/src/hooks/automerge.ts +0 -52
  128. package/src/hooks/yjs.ts +0 -156
  129. package/src/testing/index.ts +0 -6
  130. package/src/testing/proto/gen/schema.ts +0 -49
  131. package/src/testing/proto/index.ts +0 -5
  132. package/src/testing/proto/schema.proto +0 -15
  133. package/src/testing/replicator.ts +0 -184
  134. /package/dist/types/src/extensions/{code.d.ts → markdown/code.d.ts} +0 -0
  135. /package/dist/types/src/extensions/{hr.d.ts → markdown/hr.d.ts} +0 -0
  136. /package/dist/types/src/extensions/{image.d.ts → markdown/image.d.ts} +0 -0
  137. /package/dist/types/src/extensions/{link.d.ts → markdown/link.d.ts} +0 -0
  138. /package/dist/types/src/extensions/{table.d.ts → markdown/table.d.ts} +0 -0
  139. /package/src/extensions/{image.ts → markdown/image.ts} +0 -0
  140. /package/src/extensions/{table.ts → markdown/table.ts} +0 -0
@@ -4,11 +4,11 @@
4
4
 
5
5
  import '@dxosTheme';
6
6
 
7
- import { EditorView } from '@codemirror/view';
7
+ import { type EditorView } from '@codemirror/view';
8
8
  import { faker } from '@faker-js/faker';
9
9
  import { ArrowSquareOut } from '@phosphor-icons/react';
10
10
  import defaultsDeep from 'lodash.defaultsdeep';
11
- import React, { type FC, StrictMode, useState } from 'react';
11
+ import React, { type FC, StrictMode, useRef, useState } from 'react';
12
12
  import { createRoot } from 'react-dom/client';
13
13
 
14
14
  import { TextObject } from '@dxos/echo-schema';
@@ -33,9 +33,10 @@ import {
33
33
  typewriter,
34
34
  type CommentsOptions,
35
35
  type LinkOptions,
36
- outliner,
36
+ useComments,
37
+ heading,
37
38
  } from '../../extensions';
38
- import { type CommentRange, useTextModel } from '../../hooks';
39
+ import { type Comment, useTextModel } from '../../hooks';
39
40
 
40
41
  // Extensions:
41
42
  // TODO(burdon): Table of contents.
@@ -55,8 +56,8 @@ const text = {
55
56
  '- [x] decorator',
56
57
  '- [ ] checkbox',
57
58
  ' - [ ] state',
58
- ' - [ ] indent',
59
- ' - [x] style',
59
+ ' - [ ] indent',
60
+ ' - [x] style',
60
61
  '',
61
62
  ),
62
63
 
@@ -82,7 +83,7 @@ const text = {
82
83
  '## Code',
83
84
  '',
84
85
  '```',
85
- 'const x = 100;',
86
+ '$ ls -las',
86
87
  '```',
87
88
  '',
88
89
  '```tsx',
@@ -162,6 +163,8 @@ const links = [
162
163
  { label: 'StackEdit', apply: '[StackEdit](https://stackedit.io/app)' },
163
164
  ];
164
165
 
166
+ const names = ['adam', 'alice', 'alison', 'bob', 'carol', 'charlie', 'sayuri', 'shoko'];
167
+
165
168
  const hover =
166
169
  'rounded-sm text-base text-primary-600 hover:text-primary-500 dark:text-primary-300 hover:dark:text-primary-200';
167
170
 
@@ -210,12 +213,15 @@ const onRenderLink: LinkOptions['onRender'] = (el, url) => {
210
213
 
211
214
  type StoryProps = {
212
215
  text?: string;
216
+ comments?: Comment[];
213
217
  automerge?: boolean;
214
- } & Pick<TextEditorProps, 'comments' | 'readonly' | 'extensions' | 'slots'>;
218
+ } & Pick<TextEditorProps, 'readonly' | 'placeholder' | 'slots' | 'extensions'>;
215
219
 
216
- const Story = ({ text, automerge, ...props }: StoryProps) => {
220
+ const Story = ({ text, comments, automerge, placeholder = 'New document.', ...props }: StoryProps) => {
217
221
  const [item] = useState({ text: new TextObject(text, undefined, undefined, { automerge }) });
222
+ const view = useRef<EditorView>(null);
218
223
  const model = useTextModel({ text: item.text });
224
+ useComments(view.current, comments);
219
225
  if (!model) {
220
226
  return null;
221
227
  }
@@ -224,7 +230,7 @@ const Story = ({ text, automerge, ...props }: StoryProps) => {
224
230
  <div className={mx(fixedInsetFlexLayout, groupSurface)}>
225
231
  <div className='flex h-full justify-center'>
226
232
  <div className='flex flex-col h-full w-[800px]'>
227
- <MarkdownEditor model={model} {...props} />
233
+ <MarkdownEditor ref={view} model={model} placeholder={placeholder} {...props} />
228
234
  </div>
229
235
  </div>
230
236
  </div>
@@ -238,48 +244,42 @@ export default {
238
244
  render: Story,
239
245
  };
240
246
 
241
- const extensions = [
247
+ const defaults = [
242
248
  autocomplete({
243
249
  onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
244
250
  }),
245
251
  code(),
252
+ heading(),
246
253
  hr(),
247
254
  image(),
248
255
  link({ onRender: onRenderLink, onHover: onHoverLinkTooltip }),
256
+ // mention({
257
+ // onSearch: (text) => names.filter((name) => name.toLowerCase().startsWith(text.toLowerCase())),
258
+ // }),
249
259
  table(),
250
260
  tasklist(),
251
261
  ];
252
262
 
253
263
  export const Default = {
254
- render: () => <Story text={document} extensions={extensions} />,
264
+ render: () => <Story text={document} extensions={defaults} />,
255
265
  };
256
266
 
257
267
  export const Readonly = {
258
- render: () => <Story text={document} extensions={extensions} readonly />,
268
+ render: () => <Story text={document} extensions={defaults} readonly />,
259
269
  };
260
270
 
261
- const large = faker.helpers.multiple(() => faker.lorem.paragraph({ min: 8, max: 16 }), { count: 20 }).join('\n\n');
262
-
263
- export const Large = {
264
- render: () => <Story text={str('# Large Document', '', large)} extensions={[]} />,
271
+ export const NoExtensions = {
272
+ render: () => <Story text={document} />,
265
273
  };
266
274
 
275
+ const large = faker.helpers.multiple(() => faker.lorem.paragraph({ min: 8, max: 16 }), { count: 20 }).join('\n\n');
276
+
267
277
  export const Empty = {
268
278
  render: () => <Story />,
269
279
  };
270
280
 
271
- export const NoExtensions = {
272
- render: () => <Story text={document} />,
273
- };
274
-
275
- // TODO(burdon): Cursor bug if no newline after BR (cursor down just loops back to start of each paragraph).
276
- export const HorizontalRule = {
277
- render: () => (
278
- <Story
279
- text={str('# Horizontal Rule', '', text.paragraphs, '---', '', text.paragraphs, '---', '', text.paragraphs)}
280
- extensions={[hr()]}
281
- />
282
- ),
281
+ export const Scrolling = {
282
+ render: () => <Story text={str('# Large Document', '', large)} extensions={[]} />,
283
283
  };
284
284
 
285
285
  export const Links = {
@@ -292,34 +292,36 @@ export const Links = {
292
292
  };
293
293
 
294
294
  export const Code = {
295
- render: () => <Story text={str(text.code, text.footer)} extensions={[code()]} readonly />,
296
- };
297
-
298
- export const Table = {
299
- render: () => <Story text={str(text.table, text.footer)} extensions={[table()]} />,
295
+ render: () => <Story text={str(text.code, text.footer)} extensions={[code()]} />,
300
296
  };
301
297
 
302
298
  export const Image = {
303
- render: () => <Story text={str(text.image, text.footer)} readonly extensions={[image()]} />,
304
- };
305
-
306
- export const Tasklist = {
307
- render: () => <Story text={str(text.tasks, '', text.list, text.footer)} extensions={[tasklist()]} />,
299
+ render: () => <Story text={str(text.image, text.footer)} extensions={[image()]} />,
308
300
  };
309
301
 
310
- export const Outliner = {
302
+ export const Lists = {
311
303
  render: () => (
312
- <Story
313
- text={str('# Outliner', '', 'Block', ': this is a block', ': with multiple lines', text.footer)}
314
- extensions={[outliner()]}
315
- />
304
+ <Story text={str(text.tasks, '', text.list, '', text.numbered, text.footer)} extensions={[tasklist()]} />
316
305
  ),
317
306
  };
318
307
 
308
+ export const Table = {
309
+ render: () => <Story text={str(text.table, text.footer)} extensions={[table()]} />,
310
+ };
311
+
312
+ // export const Outliner = {
313
+ // render: () => (
314
+ // <Story
315
+ // text={str('# Outliner', '', 'Block', ': this is a block', ': with multiple lines', text.footer)}
316
+ // extensions={[outliner()]}
317
+ // />
318
+ // ),
319
+ // };
320
+
319
321
  export const Autocomplete = {
320
322
  render: () => (
321
323
  <Story
322
- text={str('# Autocomplete', '', 'Press CTRL-SPACE', text.footer)}
324
+ text={str('# Autocomplete', '', 'Press Ctrl-Space...', text.footer)}
323
325
  extensions={[
324
326
  link({ onRender: onRenderLink }),
325
327
  autocomplete({
@@ -330,8 +332,6 @@ export const Autocomplete = {
330
332
  ),
331
333
  };
332
334
 
333
- const names = ['adam', 'alice', 'alison', 'bob', 'carol', 'charlie', 'sayuri', 'shoko'];
334
-
335
335
  export const Mention = {
336
336
  render: () => (
337
337
  <Story
@@ -347,7 +347,7 @@ export const Mention = {
347
347
 
348
348
  export const Comments = {
349
349
  render: () => {
350
- const [commentRanges, setCommentRanges] = useState<CommentRange[]>([]);
350
+ const [commentRanges, setCommentRanges] = useState<Comment[]>([]);
351
351
 
352
352
  return (
353
353
  <Story
@@ -367,9 +367,9 @@ export const Comments = {
367
367
  console.log(
368
368
  'update',
369
369
  JSON.stringify({
370
- active: state.active?.slice(0, 8),
371
- closest: state.closest?.slice(0, 8),
372
- ranges: state.ranges.length,
370
+ comments: state.comments.length,
371
+ active: state.selection.current?.slice(0, 8),
372
+ closest: state.selection.closest?.slice(0, 8),
373
373
  }),
374
374
  );
375
375
  }
@@ -381,6 +381,15 @@ export const Comments = {
381
381
  },
382
382
  };
383
383
 
384
+ export const HorizontalRule = {
385
+ render: () => (
386
+ <Story
387
+ text={str('# Horizontal Rule', '', text.paragraphs, '---', text.paragraphs, '---', text.paragraphs)}
388
+ extensions={[hr()]}
389
+ />
390
+ ),
391
+ };
392
+
384
393
  const typewriterItems = localStorage.getItem('dxos.org/plugin/markdown/typewriter')?.split(',');
385
394
 
386
395
  export const Typewriter = {
@@ -414,17 +423,3 @@ export const Blast = {
414
423
  />
415
424
  ),
416
425
  };
417
-
418
- export const Diagnostics = {
419
- render: () => (
420
- <Story
421
- text={document}
422
- extensions={[
423
- // Cursor moved.
424
- EditorView.updateListener.of((update) => {
425
- console.log('update', update.view.state.selection.main.head);
426
- }),
427
- ]}
428
- />
429
- ),
430
- };
@@ -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,19 +14,21 @@ 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 { generateName } from '@dxos/display-name';
21
+ import { log } from '@dxos/log';
22
22
  import { useThemeContext } from '@dxos/react-ui';
23
- import { getColorForValue, inputSurface, mx } from '@dxos/react-ui-theme';
23
+ import { attentionSurface, mx } from '@dxos/react-ui-theme';
24
24
 
25
- import { basicBundle, markdownBundle, setCommentRange } from '../../extensions';
26
- import { type CommentRange, type EditorModel } from '../../hooks';
25
+ import { basicBundle, markdownBundle } from '../../extensions';
26
+ import { type EditorModel } from '../../hooks';
27
27
  import { type ThemeStyles } from '../../styles';
28
28
  import { defaultTheme, markdownTheme, textTheme } from '../../themes';
29
+ import { logChanges } from '../../util';
29
30
 
31
+ // TODO(burdon): Change to enum?
30
32
  export const EditorModes = ['default', 'vim'] as const;
31
33
  export type EditorMode = (typeof EditorModes)[number];
32
34
 
@@ -41,23 +43,28 @@ export type CursorInfo = {
41
43
 
42
44
  export type TextEditorSlots = {
43
45
  root?: Omit<ComponentProps<'div'>, 'ref'>;
44
- // editor?: {
45
- // className?: string;
46
- // };
46
+ editor?: {
47
+ className?: string;
48
+ };
49
+ content?: {
50
+ className?: string;
51
+ };
47
52
  };
48
53
 
49
54
  // TODO(burdon): Spellcheck?
50
55
  export type TextEditorProps = {
51
- model: EditorModel;
52
- autofocus?: boolean;
56
+ model: EditorModel; // TODO(burdon): Optional (e.g., just provide content if readonly).
53
57
  readonly?: boolean; // TODO(burdon): Move into model.
58
+ autoFocus?: boolean;
59
+ lineWrapping?: boolean;
60
+ scrollTo?: StateEffect<any>; // TODO(burdon): Restore scroll position: scrollTo EditorView.scrollSnapshot().
54
61
  selection?: { anchor: number; head?: number };
55
- comments?: CommentRange[]; // TODO(burdon): Move into extension?
56
- extensions?: Extension[];
57
- editorMode?: EditorMode;
62
+ editorMode?: EditorMode; // TODO(burdon): Factor out.
58
63
  placeholder?: string;
59
64
  theme?: ThemeStyles;
60
65
  slots?: TextEditorSlots;
66
+ extensions?: Extension[];
67
+ debug?: boolean;
61
68
  };
62
69
 
63
70
  /**
@@ -65,108 +72,122 @@ export type TextEditorProps = {
65
72
  */
66
73
  export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
67
74
  (
68
- { model, autofocus, readonly, selection, comments, extensions = [], editorMode, theme, slots = defaultSlots },
75
+ {
76
+ model,
77
+ readonly,
78
+ autoFocus,
79
+ scrollTo,
80
+ selection,
81
+ editorMode,
82
+ theme,
83
+ slots = defaultSlots,
84
+ extensions = [],
85
+ debug,
86
+ },
69
87
  forwardedRef,
70
88
  ) => {
71
89
  const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
72
90
  const { themeMode } = useThemeContext();
73
91
 
74
- // The editor view ref should only be used as an escape hatch.
75
92
  const rootRef = useRef<HTMLDivElement>(null);
76
93
  const [view, setView] = useState<EditorView | null>(null);
94
+
95
+ // The view ref can be used to focus the editor.
77
96
  // NOTE: This does not cause the parent to re-render, so the ref is not available immediately.
78
97
  useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
79
98
 
99
+ // Set focus.
80
100
  useEffect(() => {
81
- if (autofocus) {
101
+ if (autoFocus) {
82
102
  view?.focus();
83
103
  }
84
- }, [autofocus, view]);
85
-
86
- // TODO(burdon): Factor out as extension/hook.
87
- const { awareness, peer } = model;
88
- useEffect(() => {
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]);
97
-
98
- // TODO(burdon): Factor out as extension/hook.
99
- useEffect(() => {
100
- if (view && comments !== undefined) {
101
- view.dispatch({
102
- effects: setCommentRange.of({ model, comments }),
103
- });
104
- }
105
- }, [view, comments]);
104
+ }, [view, autoFocus]);
106
105
 
106
+ // Create editor state and view.
107
+ // The view is recreated if the model or extensions are changed.
107
108
  useEffect(() => {
108
109
  if (!model || !rootRef.current) {
109
110
  return;
110
111
  }
111
112
 
113
+ //
114
+ // EditorState
112
115
  // https://codemirror.net/docs/ref/#state.EditorStateConfig
116
+ //
113
117
  const state = EditorState.create({
114
118
  doc: model.text(),
115
- // TODO(burdon): Composer should store and set selection when switching documents.
116
119
  selection,
117
120
  extensions: [
118
- readonly && EditorState.readOnly.of(readonly),
119
-
120
- // TODO(burdon): Factor out VIM mode? (manage via MarkdownPlugin).
121
- editorMode === 'vim' && vim(),
121
+ // TODO(burdon): Doesn't catch errors in keymap functions.
122
+ EditorView.exceptionSink.of((err) => {
123
+ log.catch(err);
124
+ }),
122
125
 
123
126
  // Theme.
124
- // TODO(burdon): Make theme configurable.
127
+ // TODO(burdon): Make configurable.
125
128
  EditorView.baseTheme(defaultTheme),
126
129
  EditorView.theme(theme ?? {}),
127
130
  EditorView.darkTheme.of(themeMode === 'dark'),
131
+ EditorView.editorAttributes.of({ class: slots.editor?.className ?? '' }),
132
+ EditorView.contentAttributes.of({ class: slots.content?.className ?? '' }),
133
+
134
+ // State.
135
+ EditorState.readOnly.of(!!readonly),
128
136
 
129
137
  // Storage and replication.
138
+ // NOTE: This must come before user extensions.
130
139
  model.extension,
131
140
 
141
+ // TODO(burdon): Factor out (requires special handling for Escape/focus).
142
+ editorMode === 'vim' && vim(),
143
+
132
144
  // Custom.
133
145
  ...extensions,
134
146
  ].filter(Boolean) as Extension[],
135
147
  });
136
148
 
137
- // NOTE: This repaints the editor.
138
- // If the new state is derived from the old state, it will likely not be visible other than the cursor resetting.
139
- // Ideally this should not happen except when changing between text objects.
140
- view?.destroy();
149
+ //
150
+ // EditorView
151
+ // https://codemirror.net/docs/ref/#view.EditorViewConfig
152
+ //
141
153
  const newView = new EditorView({
142
- parent: rootRef.current,
143
154
  state,
144
- // NOTE: Uncomment to spy on all transactions.
155
+ parent: rootRef.current,
156
+ scrollTo,
157
+ // NOTE: Uncomment to debug/monitor all transactions.
145
158
  // https://codemirror.net/docs/ref/#view.EditorView.dispatch
146
- // dispatch: (transaction, view) => {
147
- // view.update([transaction]);
148
- // },
159
+ dispatchTransactions: (trs, view) => {
160
+ if (debug) {
161
+ logChanges(trs);
162
+ }
163
+ view.update(trs);
164
+ },
149
165
  });
150
166
 
167
+ view?.destroy();
151
168
  setView(newView);
169
+
152
170
  return () => {
153
171
  newView?.destroy();
154
172
  setView(null);
155
173
  };
156
174
  }, [rootRef, model, readonly, editorMode, themeMode]);
157
175
 
176
+ // Handles tab/focus.
177
+ // Pressing Escape focuses the outer div (to support tab navigation); pressing Enter refocuses the editor.
158
178
  const handleKeyUp = useCallback(
159
179
  (event: KeyboardEvent) => {
160
180
  const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
161
181
  switch (key) {
162
- // TODO(burdon): Is this required (for vim mode?)
163
- // case 'Enter': {
164
- // view?.contentDOM.focus();
165
- // break;
166
- // }
182
+ case 'Enter': {
183
+ view?.focus();
184
+ break;
185
+ }
167
186
 
168
187
  case 'Escape': {
169
- editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey) && rootRef.current?.focus();
188
+ if (editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)) {
189
+ rootRef.current?.focus();
190
+ }
170
191
  break;
171
192
  }
172
193
  }
@@ -188,15 +209,16 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
188
209
  },
189
210
  );
190
211
 
212
+ // TODO(burdon): Single-line/scroll.
191
213
  export const TextEditor = forwardRef<EditorView, TextEditorProps>(
192
- ({ readonly, placeholder, extensions = [], theme = textTheme, slots, ...props }, forwardedRef) => {
214
+ ({ readonly, placeholder, lineWrapping, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
193
215
  const { themeMode } = useThemeContext();
194
216
  const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
195
217
  return (
196
218
  <BaseTextEditor
197
219
  ref={forwardedRef}
198
220
  readonly={readonly}
199
- extensions={[basicBundle({ readonly, themeMode, placeholder }), ...extensions]}
221
+ extensions={[basicBundle({ themeMode, placeholder, lineWrapping }), ...extensions]}
200
222
  theme={theme}
201
223
  slots={updatedSlots}
202
224
  {...props}
@@ -205,16 +227,15 @@ export const TextEditor = forwardRef<EditorView, TextEditorProps>(
205
227
  },
206
228
  );
207
229
 
208
- // TODO(burdon): Remove (Just provide bundle, slots).
209
230
  export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
210
- ({ readonly, placeholder, extensions = [], theme = markdownTheme, slots, ...props }, forwardedRef) => {
231
+ ({ readonly, placeholder, theme = markdownTheme, slots, extensions = [], ...props }, forwardedRef) => {
211
232
  const { themeMode } = useThemeContext();
212
233
  const updatedSlots = defaultsDeep({}, slots, defaultMarkdownSlots);
213
234
  return (
214
235
  <BaseTextEditor
215
236
  ref={forwardedRef}
216
237
  readonly={readonly}
217
- extensions={[markdownBundle({ readonly, themeMode, placeholder }), ...extensions]}
238
+ extensions={[markdownBundle({ themeMode, placeholder }), ...extensions]}
218
239
  theme={theme}
219
240
  slots={updatedSlots}
220
241
  {...props}
@@ -225,7 +246,11 @@ export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
225
246
 
226
247
  export const defaultSlots: TextEditorSlots = {
227
248
  root: {
228
- className: mx('w-full p-2 overflow-y-auto', inputSurface),
249
+ // TODO(burdon): Add focusRing by default/as property?
250
+ className: mx('flex flex-col grow overflow-y-auto', attentionSurface),
251
+ },
252
+ editor: {
253
+ className: 'h-full p-2',
229
254
  },
230
255
  };
231
256