@dxos/react-ui-editor 0.3.11-main.00a28cb → 0.3.11-main.0252f93

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 (161) hide show
  1. package/dist/lib/browser/index.mjs +1304 -1092
  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 -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/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 +8 -10
  21. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  22. package/dist/types/src/extensions/automerge/semaphore.d.ts +7 -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 +23 -15
  31. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  32. package/dist/types/src/extensions/cursor.d.ts +3 -3
  33. package/dist/types/src/extensions/cursor.d.ts.map +1 -1
  34. package/dist/types/src/extensions/index.d.ts +0 -7
  35. package/dist/types/src/extensions/index.d.ts.map +1 -1
  36. package/dist/types/src/extensions/markdown/bundle.d.ts +3 -6
  37. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  38. package/dist/types/src/extensions/markdown/code.d.ts.map +1 -0
  39. package/dist/types/src/extensions/markdown/formatting.d.ts +11 -0
  40. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -0
  41. package/dist/types/src/extensions/markdown/heading.d.ts +6 -0
  42. package/dist/types/src/extensions/markdown/heading.d.ts.map +1 -0
  43. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  44. package/dist/types/src/extensions/markdown/hr.d.ts.map +1 -0
  45. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -0
  46. package/dist/types/src/extensions/markdown/index.d.ts +8 -0
  47. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  48. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -0
  49. package/dist/types/src/extensions/markdown/table.d.ts.map +1 -0
  50. package/dist/types/src/extensions/markdown/tasklist.d.ts +3 -0
  51. package/dist/types/src/extensions/markdown/tasklist.d.ts.map +1 -0
  52. package/dist/types/src/extensions/mention.d.ts.map +1 -1
  53. package/dist/types/src/hooks/awareness-provider.d.ts +7 -4
  54. package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -1
  55. package/dist/types/src/hooks/defs.d.ts +25 -0
  56. package/dist/types/src/hooks/defs.d.ts.map +1 -0
  57. package/dist/types/src/hooks/index.d.ts +3 -2
  58. package/dist/types/src/hooks/index.d.ts.map +1 -1
  59. package/dist/types/src/hooks/useActionHandler.d.ts +4 -0
  60. package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -0
  61. package/dist/types/src/hooks/useTextEditor.d.ts +17 -0
  62. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
  63. package/dist/types/src/hooks/useTextModel.d.ts +2 -27
  64. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  65. package/dist/types/src/styles/markdown.d.ts +2 -2
  66. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  67. package/dist/types/src/themes/default.d.ts.map +1 -1
  68. package/dist/types/src/util.d.ts +2 -0
  69. package/dist/types/src/util.d.ts.map +1 -1
  70. package/package.json +45 -36
  71. package/src/components/TextEditor/MarkdownEditor.stories.tsx +64 -75
  72. package/src/components/TextEditor/TextEditor.tsx +103 -71
  73. package/src/components/TextEditor/comments.stories.tsx +46 -44
  74. package/src/components/Toolbar/Toolbar.stories.tsx +86 -0
  75. package/src/components/Toolbar/Toolbar.tsx +115 -0
  76. package/src/extensions/autocomplete.ts +6 -3
  77. package/src/extensions/automerge/automerge.spec.tsx +76 -0
  78. package/src/extensions/automerge/automerge.test.ts +13 -0
  79. package/src/extensions/automerge/automerge.ts +50 -39
  80. package/src/extensions/automerge/cursor.ts +3 -3
  81. package/src/extensions/automerge/defs.ts +11 -36
  82. package/src/extensions/automerge/semaphore.ts +31 -28
  83. package/src/extensions/automerge/update-automerge.ts +14 -8
  84. package/src/extensions/automerge/update-codemirror.ts +5 -5
  85. package/src/extensions/awareness.ts +17 -17
  86. package/src/extensions/basic.ts +22 -17
  87. package/src/extensions/comments.ts +247 -179
  88. package/src/extensions/cursor.ts +7 -3
  89. package/src/extensions/index.ts +0 -7
  90. package/src/extensions/markdown/bundle.ts +20 -30
  91. package/src/extensions/markdown/code.ts +142 -0
  92. package/src/extensions/markdown/formatting.ts +159 -0
  93. package/src/extensions/markdown/heading.ts +59 -0
  94. package/src/extensions/markdown/highlight.ts +9 -15
  95. package/src/extensions/markdown/hr.ts +71 -0
  96. package/src/extensions/markdown/index.ts +8 -0
  97. package/src/extensions/{link.ts → markdown/link.ts} +5 -2
  98. package/src/extensions/{tasklist.ts → markdown/tasklist.ts} +48 -34
  99. package/src/extensions/mention.ts +9 -2
  100. package/src/hooks/awareness-provider.ts +24 -20
  101. package/src/hooks/defs.ts +42 -0
  102. package/src/hooks/index.ts +3 -2
  103. package/src/hooks/useActionHandler.ts +45 -0
  104. package/src/hooks/useTextEditor.ts +29 -0
  105. package/src/hooks/useTextModel.ts +49 -55
  106. package/src/styles/markdown.ts +9 -10
  107. package/src/themes/default.ts +1 -3
  108. package/src/util.ts +26 -3
  109. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
  110. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
  111. package/dist/types/src/components/TextEditor/yjs.stories.d.ts +0 -12
  112. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
  113. package/dist/types/src/extensions/code.d.ts.map +0 -1
  114. package/dist/types/src/extensions/hr.d.ts.map +0 -1
  115. package/dist/types/src/extensions/image.d.ts.map +0 -1
  116. package/dist/types/src/extensions/link.d.ts.map +0 -1
  117. package/dist/types/src/extensions/table.d.ts.map +0 -1
  118. package/dist/types/src/extensions/tasklist.d.ts +0 -7
  119. package/dist/types/src/extensions/tasklist.d.ts.map +0 -1
  120. package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
  121. package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
  122. package/dist/types/src/extensions/yjs/index.d.ts +0 -2
  123. package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
  124. package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
  125. package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
  126. package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
  127. package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
  128. package/dist/types/src/hooks/automerge.d.ts +0 -3
  129. package/dist/types/src/hooks/automerge.d.ts.map +0 -1
  130. package/dist/types/src/hooks/yjs.d.ts +0 -27
  131. package/dist/types/src/hooks/yjs.d.ts.map +0 -1
  132. package/dist/types/src/testing/index.d.ts +0 -3
  133. package/dist/types/src/testing/index.d.ts.map +0 -1
  134. package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
  135. package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
  136. package/dist/types/src/testing/proto/index.d.ts +0 -2
  137. package/dist/types/src/testing/proto/index.d.ts.map +0 -1
  138. package/dist/types/src/testing/replicator.d.ts +0 -45
  139. package/dist/types/src/testing/replicator.d.ts.map +0 -1
  140. package/src/components/TextEditor/codemirror.stories.ts +0 -115
  141. package/src/components/TextEditor/yjs.stories.tsx +0 -56
  142. package/src/extensions/code.ts +0 -104
  143. package/src/extensions/hr.ts +0 -72
  144. package/src/extensions/yjs/cursor.ts +0 -22
  145. package/src/extensions/yjs/index.ts +0 -5
  146. package/src/extensions/yjs/yjs.test.ts +0 -35
  147. package/src/extensions/yjs/yjs.ts +0 -16
  148. package/src/hooks/automerge.ts +0 -52
  149. package/src/hooks/yjs.ts +0 -156
  150. package/src/testing/index.ts +0 -6
  151. package/src/testing/proto/gen/schema.ts +0 -49
  152. package/src/testing/proto/index.ts +0 -5
  153. package/src/testing/proto/schema.proto +0 -15
  154. package/src/testing/replicator.ts +0 -184
  155. /package/dist/types/src/extensions/{code.d.ts → markdown/code.d.ts} +0 -0
  156. /package/dist/types/src/extensions/{hr.d.ts → markdown/hr.d.ts} +0 -0
  157. /package/dist/types/src/extensions/{image.d.ts → markdown/image.d.ts} +0 -0
  158. /package/dist/types/src/extensions/{link.d.ts → markdown/link.d.ts} +0 -0
  159. /package/dist/types/src/extensions/{table.d.ts → markdown/table.d.ts} +0 -0
  160. /package/src/extensions/{image.ts → markdown/image.ts} +0 -0
  161. /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,15 +34,10 @@ 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
- import { type CommentRange, useTextModel } from '../../hooks';
39
-
40
- // Extensions:
41
- // TODO(burdon): Table of contents.
42
- // TODO(burdon): Front-matter
40
+ import { type Comment, useTextModel } from '../../hooks';
43
41
 
44
42
  faker.seed(101);
45
43
 
@@ -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<CommentRange[]>([]);
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) => {
@@ -367,9 +361,9 @@ export const Comments = {
367
361
  console.log(
368
362
  'update',
369
363
  JSON.stringify({
370
- active: state.active?.slice(0, 8),
371
- closest: state.closest?.slice(0, 8),
372
- ranges: state.ranges.length,
364
+ comments: state.comments.length,
365
+ active: state.selection.current?.slice(0, 8),
366
+ closest: state.selection.closest?.slice(0, 8),
373
367
  }),
374
368
  );
375
369
  }
@@ -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, setCommentRange } from '../../extensions';
26
- import { type CommentRange, 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,23 +44,28 @@ export type CursorInfo = {
41
44
 
42
45
  export type TextEditorSlots = {
43
46
  root?: Omit<ComponentProps<'div'>, 'ref'>;
44
- // editor?: {
45
- // className?: string;
46
- // };
47
+ editor?: {
48
+ className?: string;
49
+ };
50
+ content?: {
51
+ className?: string;
52
+ };
47
53
  };
48
54
 
49
55
  // TODO(burdon): Spellcheck?
50
56
  export type TextEditorProps = {
51
- model: EditorModel;
52
- autofocus?: boolean;
57
+ model: EditorModel; // TODO(burdon): Optional (e.g., just provide content if readonly).
53
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().
54
62
  selection?: { anchor: number; head?: number };
55
- comments?: CommentRange[]; // TODO(burdon): Move into extension?
56
- extensions?: Extension[];
57
- editorMode?: EditorMode;
63
+ editorMode?: EditorMode; // TODO(burdon): Factor out.
58
64
  placeholder?: string;
59
65
  theme?: ThemeStyles;
60
66
  slots?: TextEditorSlots;
67
+ extensions?: Extension[];
68
+ debug?: boolean;
61
69
  };
62
70
 
63
71
  /**
@@ -65,108 +73,129 @@ export type TextEditorProps = {
65
73
  */
66
74
  export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
67
75
  (
68
- { 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
+ },
69
88
  forwardedRef,
70
89
  ) => {
71
- 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' });
72
96
  const { themeMode } = useThemeContext();
73
97
 
74
- // The editor view ref should only be used as an escape hatch.
75
98
  const rootRef = useRef<HTMLDivElement>(null);
76
99
  const [view, setView] = useState<EditorView | null>(null);
100
+
101
+ // The view ref can be used to focus the editor.
77
102
  // NOTE: This does not cause the parent to re-render, so the ref is not available immediately.
78
103
  useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
79
104
 
105
+ // Set focus.
80
106
  useEffect(() => {
81
- if (autofocus) {
107
+ if (autoFocus) {
82
108
  view?.focus();
83
109
  }
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]);
110
+ }, [view, autoFocus]);
106
111
 
112
+ // Create editor state and view.
113
+ // The view is recreated if the model or extensions are changed.
107
114
  useEffect(() => {
108
115
  if (!model || !rootRef.current) {
109
116
  return;
110
117
  }
111
118
 
119
+ //
120
+ // EditorState
112
121
  // https://codemirror.net/docs/ref/#state.EditorStateConfig
122
+ //
113
123
  const state = EditorState.create({
114
124
  doc: model.text(),
115
- // TODO(burdon): Composer should store and set selection when switching documents.
116
125
  selection,
117
126
  extensions: [
118
- readonly && EditorState.readOnly.of(readonly),
119
-
120
- // TODO(burdon): Factor out VIM mode? (manage via MarkdownPlugin).
121
- editorMode === 'vim' && vim(),
127
+ // TODO(burdon): Doesn't catch errors in keymap functions.
128
+ EditorView.exceptionSink.of((err) => {
129
+ log.catch(err);
130
+ }),
122
131
 
123
132
  // Theme.
124
- // TODO(burdon): Make theme configurable.
133
+ // TODO(burdon): Make configurable.
125
134
  EditorView.baseTheme(defaultTheme),
126
135
  EditorView.theme(theme ?? {}),
127
136
  EditorView.darkTheme.of(themeMode === 'dark'),
137
+ EditorView.editorAttributes.of({ class: slots.editor?.className ?? '' }),
138
+ EditorView.contentAttributes.of({ class: slots.content?.className ?? '' }),
139
+
140
+ // State.
141
+ EditorView.editable.of(!readonly),
142
+ EditorState.readOnly.of(!!readonly),
128
143
 
129
144
  // Storage and replication.
145
+ // NOTE: This must come before user extensions.
130
146
  model.extension,
131
147
 
148
+ // TODO(burdon): Factor out? (Requires special handling for Escape/Enter below).
149
+ editorMode === 'vim' && vim(),
150
+
132
151
  // Custom.
133
152
  ...extensions,
134
- ].filter(Boolean) as Extension[],
153
+ ].filter(isNotFalsy),
135
154
  });
136
155
 
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();
156
+ //
157
+ // EditorView
158
+ // https://codemirror.net/docs/ref/#view.EditorViewConfig
159
+ //
141
160
  const newView = new EditorView({
142
- parent: rootRef.current,
143
161
  state,
144
- // NOTE: Uncomment to spy on all transactions.
162
+ parent: rootRef.current,
163
+ scrollTo,
164
+ // NOTE: Uncomment to debug/monitor all transactions.
145
165
  // https://codemirror.net/docs/ref/#view.EditorView.dispatch
146
- // dispatch: (transaction, view) => {
147
- // view.update([transaction]);
148
- // },
166
+ dispatchTransactions: (trs, view) => {
167
+ if (debug) {
168
+ logChanges(trs);
169
+ }
170
+ view.update(trs);
171
+ },
149
172
  });
150
173
 
174
+ view?.destroy();
151
175
  setView(newView);
176
+
152
177
  return () => {
153
178
  newView?.destroy();
154
179
  setView(null);
155
180
  };
156
181
  }, [rootRef, model, readonly, editorMode, themeMode]);
157
182
 
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?
158
186
  const handleKeyUp = useCallback(
159
187
  (event: KeyboardEvent) => {
160
188
  const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
161
189
  switch (key) {
162
- // TODO(burdon): Is this required (for vim mode?)
163
- // case 'Enter': {
164
- // view?.contentDOM.focus();
165
- // break;
166
- // }
190
+ case 'Enter': {
191
+ view?.focus();
192
+ break;
193
+ }
167
194
 
168
195
  case 'Escape': {
169
- editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey) && rootRef.current?.focus();
196
+ if (editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)) {
197
+ view?.focus();
198
+ }
170
199
  break;
171
200
  }
172
201
  }
@@ -179,9 +208,9 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
179
208
  key={model.id}
180
209
  role='none'
181
210
  tabIndex={0}
182
- onKeyUp={handleKeyUp}
183
211
  {...slots.root}
184
- {...(editorMode !== 'vim' && tabsterDOMAttribute)}
212
+ // {...(editorMode !== 'vim' && tabsterDOMAttribute)}
213
+ onKeyUp={handleKeyUp}
185
214
  ref={rootRef}
186
215
  />
187
216
  );
@@ -189,14 +218,14 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
189
218
  );
190
219
 
191
220
  export const TextEditor = forwardRef<EditorView, TextEditorProps>(
192
- ({ readonly, placeholder, extensions = [], theme = textTheme, slots, ...props }, forwardedRef) => {
221
+ ({ readonly, placeholder, lineWrapping, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
193
222
  const { themeMode } = useThemeContext();
194
223
  const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
195
224
  return (
196
225
  <BaseTextEditor
197
226
  ref={forwardedRef}
198
227
  readonly={readonly}
199
- extensions={[basicBundle({ readonly, themeMode, placeholder }), ...extensions]}
228
+ extensions={[basicBundle({ themeMode, placeholder, lineWrapping }), ...extensions]}
200
229
  theme={theme}
201
230
  slots={updatedSlots}
202
231
  {...props}
@@ -205,16 +234,15 @@ export const TextEditor = forwardRef<EditorView, TextEditorProps>(
205
234
  },
206
235
  );
207
236
 
208
- // TODO(burdon): Remove (Just provide bundle, slots).
209
237
  export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
210
- ({ readonly, placeholder, extensions = [], theme = markdownTheme, slots, ...props }, forwardedRef) => {
238
+ ({ readonly, placeholder, theme = markdownTheme, slots, extensions = [], ...props }, forwardedRef) => {
211
239
  const { themeMode } = useThemeContext();
212
240
  const updatedSlots = defaultsDeep({}, slots, defaultMarkdownSlots);
213
241
  return (
214
242
  <BaseTextEditor
215
243
  ref={forwardedRef}
216
244
  readonly={readonly}
217
- extensions={[markdownBundle({ readonly, themeMode, placeholder }), ...extensions]}
245
+ extensions={[markdownBundle({ themeMode, placeholder }), ...extensions]}
218
246
  theme={theme}
219
247
  slots={updatedSlots}
220
248
  {...props}
@@ -225,7 +253,11 @@ export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
225
253
 
226
254
  export const defaultSlots: TextEditorSlots = {
227
255
  root: {
228
- 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',
229
261
  },
230
262
  };
231
263