@dxos/react-ui-editor 0.3.11-main.08776d7 → 0.3.11-main.0999ea0

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