@dxos/react-ui-editor 0.3.11-main.6fccc32 → 0.3.11-main.73ee990

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 +1259 -1060
  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 +13 -14
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +14 -6
  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 +1 -1
  10. package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/Toolbar/Toolbar.d.ts +9 -0
  12. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -0
  13. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +14 -0
  14. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -0
  15. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  16. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  17. package/dist/types/src/extensions/automerge/defs.d.ts +7 -9
  18. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  19. package/dist/types/src/extensions/automerge/index.d.ts +0 -1
  20. package/dist/types/src/extensions/automerge/index.d.ts.map +1 -1
  21. package/dist/types/src/extensions/automerge/semaphore.d.ts +7 -12
  22. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
  23. package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -2
  24. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  25. package/dist/types/src/extensions/awareness.d.ts +1 -1
  26. package/dist/types/src/extensions/awareness.d.ts.map +1 -1
  27. package/dist/types/src/extensions/basic.d.ts +4 -5
  28. package/dist/types/src/extensions/basic.d.ts.map +1 -1
  29. package/dist/types/src/extensions/comments.d.ts +35 -18
  30. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  31. package/dist/types/src/extensions/cursor.d.ts +3 -3
  32. package/dist/types/src/extensions/cursor.d.ts.map +1 -1
  33. package/dist/types/src/extensions/index.d.ts +1 -7
  34. package/dist/types/src/extensions/index.d.ts.map +1 -1
  35. package/dist/types/src/extensions/markdown/bundle.d.ts +3 -6
  36. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  37. package/dist/types/src/extensions/markdown/code.d.ts.map +1 -0
  38. package/dist/types/src/extensions/markdown/formatting.d.ts +11 -0
  39. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -0
  40. package/dist/types/src/extensions/markdown/heading.d.ts +6 -0
  41. package/dist/types/src/extensions/markdown/heading.d.ts.map +1 -0
  42. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  43. package/dist/types/src/extensions/markdown/hr.d.ts.map +1 -0
  44. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -0
  45. package/dist/types/src/extensions/markdown/index.d.ts +8 -0
  46. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  47. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -0
  48. package/dist/types/src/extensions/markdown/table.d.ts.map +1 -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/extensions/outliner.d.ts +4 -0
  52. package/dist/types/src/extensions/outliner.d.ts.map +1 -0
  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 +34 -37
  71. package/src/components/TextEditor/MarkdownEditor.stories.tsx +72 -80
  72. package/src/components/TextEditor/TextEditor.tsx +96 -70
  73. package/src/components/TextEditor/comments.stories.tsx +113 -71
  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.ts +39 -46
  78. package/src/extensions/automerge/defs.ts +10 -31
  79. package/src/extensions/automerge/index.ts +0 -1
  80. package/src/extensions/automerge/semaphore.ts +23 -20
  81. package/src/extensions/automerge/update-automerge.ts +14 -8
  82. package/src/extensions/awareness.ts +15 -14
  83. package/src/extensions/basic.ts +22 -17
  84. package/src/extensions/comments.ts +269 -162
  85. package/src/extensions/cursor.ts +7 -3
  86. package/src/extensions/index.ts +1 -7
  87. package/src/extensions/markdown/bundle.ts +20 -30
  88. package/src/extensions/markdown/code.ts +142 -0
  89. package/src/extensions/markdown/formatting.ts +159 -0
  90. package/src/extensions/markdown/heading.ts +59 -0
  91. package/src/extensions/markdown/highlight.ts +9 -15
  92. package/src/extensions/markdown/hr.ts +71 -0
  93. package/src/extensions/markdown/index.ts +8 -0
  94. package/src/extensions/{link.ts → markdown/link.ts} +8 -5
  95. package/src/extensions/markdown/tasklist.ts +110 -0
  96. package/src/extensions/mention.ts +9 -2
  97. package/src/extensions/outliner.ts +12 -0
  98. package/src/hooks/awareness-provider.ts +24 -20
  99. package/src/hooks/defs.ts +42 -0
  100. package/src/hooks/index.ts +4 -2
  101. package/src/hooks/useActionHandler.ts +45 -0
  102. package/src/hooks/useTextEditor.ts +29 -0
  103. package/src/hooks/useTextModel.ts +47 -56
  104. package/src/styles/markdown.ts +9 -10
  105. package/src/themes/default.ts +1 -11
  106. package/src/util.ts +26 -3
  107. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
  108. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
  109. package/dist/types/src/components/TextEditor/yjs.stories.d.ts +0 -12
  110. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
  111. package/dist/types/src/extensions/code.d.ts.map +0 -1
  112. package/dist/types/src/extensions/hr.d.ts.map +0 -1
  113. package/dist/types/src/extensions/image.d.ts.map +0 -1
  114. package/dist/types/src/extensions/link.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.map +0 -1
  117. package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
  118. package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
  119. package/dist/types/src/extensions/yjs/index.d.ts +0 -2
  120. package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
  121. package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
  122. package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
  123. package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
  124. package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
  125. package/dist/types/src/hooks/automerge.d.ts +0 -3
  126. package/dist/types/src/hooks/automerge.d.ts.map +0 -1
  127. package/dist/types/src/hooks/yjs.d.ts +0 -27
  128. package/dist/types/src/hooks/yjs.d.ts.map +0 -1
  129. package/dist/types/src/testing/index.d.ts +0 -3
  130. package/dist/types/src/testing/index.d.ts.map +0 -1
  131. package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
  132. package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
  133. package/dist/types/src/testing/proto/index.d.ts +0 -2
  134. package/dist/types/src/testing/proto/index.d.ts.map +0 -1
  135. package/dist/types/src/testing/replicator.d.ts +0 -45
  136. package/dist/types/src/testing/replicator.d.ts.map +0 -1
  137. package/src/components/TextEditor/codemirror.stories.ts +0 -115
  138. package/src/components/TextEditor/yjs.stories.tsx +0 -56
  139. package/src/extensions/code.ts +0 -106
  140. package/src/extensions/hr.ts +0 -73
  141. package/src/extensions/tasklist.ts +0 -119
  142. package/src/extensions/yjs/cursor.ts +0 -22
  143. package/src/extensions/yjs/index.ts +0 -5
  144. package/src/extensions/yjs/yjs.test.ts +0 -35
  145. package/src/extensions/yjs/yjs.ts +0 -16
  146. package/src/hooks/automerge.ts +0 -52
  147. package/src/hooks/yjs.ts +0 -156
  148. package/src/testing/index.ts +0 -6
  149. package/src/testing/proto/gen/schema.ts +0 -49
  150. package/src/testing/proto/index.ts +0 -5
  151. package/src/testing/proto/schema.proto +0 -15
  152. package/src/testing/replicator.ts +0 -184
  153. /package/dist/types/src/extensions/{code.d.ts → markdown/code.d.ts} +0 -0
  154. /package/dist/types/src/extensions/{hr.d.ts → markdown/hr.d.ts} +0 -0
  155. /package/dist/types/src/extensions/{image.d.ts → markdown/image.d.ts} +0 -0
  156. /package/dist/types/src/extensions/{link.d.ts → markdown/link.d.ts} +0 -0
  157. /package/dist/types/src/extensions/{table.d.ts → markdown/table.d.ts} +0 -0
  158. /package/dist/types/src/extensions/{tasklist.d.ts → markdown/tasklist.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,27 +19,25 @@ 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
- listener,
30
32
  link,
31
33
  mention,
32
34
  table,
33
35
  tasklist,
34
36
  typewriter,
35
- type CommentsOptions,
36
- type LinkOptions,
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,15 @@ const onRenderLink: LinkOptions['onRender'] = (el, url) => {
210
210
 
211
211
  type StoryProps = {
212
212
  text?: string;
213
+ comments?: Comment[];
213
214
  automerge?: boolean;
214
- } & Pick<TextEditorProps, 'comments' | 'readonly' | 'extensions' | 'slots'>;
215
+ } & Pick<TextEditorProps, 'readonly' | 'placeholder' | 'slots' | 'extensions'>;
215
216
 
216
- const Story = ({ text, automerge, ...props }: StoryProps) => {
217
+ const Story = ({ text, comments, automerge, placeholder = 'New document.', ...props }: StoryProps) => {
217
218
  const [item] = useState({ text: new TextObject(text, undefined, undefined, { automerge }) });
219
+ const view = useRef<EditorView>(null);
218
220
  const model = useTextModel({ text: item.text });
221
+ useComments(view.current, comments);
219
222
  if (!model) {
220
223
  return null;
221
224
  }
@@ -224,7 +227,7 @@ const Story = ({ text, automerge, ...props }: StoryProps) => {
224
227
  <div className={mx(fixedInsetFlexLayout, groupSurface)}>
225
228
  <div className='flex h-full justify-center'>
226
229
  <div className='flex flex-col h-full w-[800px]'>
227
- <MarkdownEditor model={model} {...props} />
230
+ <MarkdownEditor ref={view} model={model} placeholder={placeholder} {...props} />
228
231
  </div>
229
232
  </div>
230
233
  </div>
@@ -238,11 +241,13 @@ export default {
238
241
  render: Story,
239
242
  };
240
243
 
241
- const extensions = [
244
+ const defaults = [
242
245
  autocomplete({
243
246
  onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
244
247
  }),
245
248
  code(),
249
+ formatting(),
250
+ heading(),
246
251
  hr(),
247
252
  image(),
248
253
  link({ onRender: onRenderLink, onHover: onHoverLinkTooltip }),
@@ -251,35 +256,25 @@ const extensions = [
251
256
  ];
252
257
 
253
258
  export const Default = {
254
- render: () => <Story text={document} extensions={extensions} />,
259
+ render: () => <Story text={document} extensions={defaults} />,
255
260
  };
256
261
 
257
262
  export const Readonly = {
258
- render: () => <Story text={document} extensions={extensions} readonly />,
263
+ render: () => <Story text={document} extensions={defaults} readonly />,
259
264
  };
260
265
 
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={[]} />,
266
+ export const NoExtensions = {
267
+ render: () => <Story text={document} />,
265
268
  };
266
269
 
270
+ const large = faker.helpers.multiple(() => faker.lorem.paragraph({ min: 8, max: 16 }), { count: 20 }).join('\n\n');
271
+
267
272
  export const Empty = {
268
273
  render: () => <Story />,
269
274
  };
270
275
 
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
- ),
276
+ export const Scrolling = {
277
+ render: () => <Story text={str('# Large Document', '', large)} extensions={[]} />,
283
278
  };
284
279
 
285
280
  export const Links = {
@@ -292,37 +287,36 @@ export const Links = {
292
287
  };
293
288
 
294
289
  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()]} />,
290
+ render: () => <Story text={str(text.code, text.footer)} extensions={[code()]} />,
300
291
  };
301
292
 
302
293
  export const Image = {
303
- render: () => <Story text={str(text.image, text.footer)} readonly extensions={[image()]} />,
294
+ render: () => <Story text={str(text.image, text.footer)} extensions={[image()]} />,
304
295
  };
305
296
 
306
- export const Tasklist = {
297
+ export const Lists = {
307
298
  render: () => (
308
- <Story
309
- text={str(text.tasks, '', text.list, text.footer)}
310
- extensions={[
311
- tasklist(),
312
- listener({
313
- onChange: (text) => {
314
- console.log(text);
315
- },
316
- }),
317
- ]}
318
- />
299
+ <Story text={str(text.tasks, '', text.list, '', text.numbered, text.footer)} extensions={[tasklist()]} />
319
300
  ),
320
301
  };
321
302
 
303
+ export const Table = {
304
+ render: () => <Story text={str(text.table, text.footer)} extensions={[table()]} />,
305
+ };
306
+
307
+ // export const Outliner = {
308
+ // render: () => (
309
+ // <Story
310
+ // text={str('# Outliner', '', 'Block', ': this is a block', ': with multiple lines', text.footer)}
311
+ // extensions={[outliner()]}
312
+ // />
313
+ // ),
314
+ // };
315
+
322
316
  export const Autocomplete = {
323
317
  render: () => (
324
318
  <Story
325
- text={str('# Autocomplete', '', 'Press CTRL-SPACE', text.footer)}
319
+ text={str('# Autocomplete', '', 'Press Ctrl-Space...', text.footer)}
326
320
  extensions={[
327
321
  link({ onRender: onRenderLink }),
328
322
  autocomplete({
@@ -333,8 +327,6 @@ export const Autocomplete = {
333
327
  ),
334
328
  };
335
329
 
336
- const names = ['adam', 'alice', 'alison', 'bob', 'carol', 'charlie', 'sayuri', 'shoko'];
337
-
338
330
  export const Mention = {
339
331
  render: () => (
340
332
  <Story
@@ -350,18 +342,18 @@ export const Mention = {
350
342
 
351
343
  export const Comments = {
352
344
  render: () => {
353
- const [commentRanges, setCommentRanges] = useState<CommentRange[]>([]);
345
+ const [_comments, setComments] = useState<Comment[]>([]);
354
346
 
355
347
  return (
356
348
  <Story
357
349
  text={str('# Comments', '', text.paragraphs, text.footer)}
358
- comments={commentRanges}
350
+ comments={_comments}
359
351
  extensions={[
360
352
  comments({
361
353
  onHover: onCommentsHover,
362
354
  onCreate: (range) => {
363
355
  const id = PublicKey.random().toHex();
364
- setCommentRanges((commentRanges) => [...commentRanges, { id, cursor: range }]);
356
+ setComments((commentRanges) => [...commentRanges, { id, cursor: range }]);
365
357
  return id;
366
358
  },
367
359
  onSelect: (state) => {
@@ -370,9 +362,9 @@ export const Comments = {
370
362
  console.log(
371
363
  'update',
372
364
  JSON.stringify({
373
- active: state.active?.slice(0, 8),
374
- closest: state.closest?.slice(0, 8),
375
- ranges: state.ranges.length,
365
+ comments: state.comments.length,
366
+ active: state.selection.current?.slice(0, 8),
367
+ closest: state.selection.closest?.slice(0, 8),
376
368
  }),
377
369
  );
378
370
  }
@@ -384,8 +376,24 @@ export const Comments = {
384
376
  },
385
377
  };
386
378
 
379
+ export const HorizontalRule = {
380
+ render: () => (
381
+ <Story
382
+ text={str('# Horizontal Rule', '', text.paragraphs, '---', text.paragraphs, '---', text.paragraphs)}
383
+ extensions={[hr()]}
384
+ />
385
+ ),
386
+ };
387
+
388
+ const typewriterItems = localStorage.getItem('dxos.org/plugin/markdown/typewriter')?.split(',');
389
+
387
390
  export const Typewriter = {
388
- render: () => <Story text={str('# Typewriter', '', text.paragraphs, text.footer)} extensions={[typewriter()]} />,
391
+ render: () => (
392
+ <Story
393
+ text={str('# Typewriter', '', text.paragraphs, text.footer)}
394
+ extensions={[typewriter({ items: typewriterItems })]}
395
+ />
396
+ ),
389
397
  };
390
398
 
391
399
  export const Blast = {
@@ -393,9 +401,7 @@ export const Blast = {
393
401
  <Story
394
402
  text={str('# Blast', '', text.paragraphs, text.code, text.paragraphs)}
395
403
  extensions={[
396
- typewriter({
397
- items: localStorage.getItem('dxos.composer.extension.demo')?.split(','),
398
- }),
404
+ typewriter({ items: typewriterItems }),
399
405
  blast(
400
406
  defaultsDeep(
401
407
  {
@@ -412,17 +418,3 @@ export const Blast = {
412
418
  />
413
419
  ),
414
420
  };
415
-
416
- export const Diagnostics = {
417
- render: () => (
418
- <Story
419
- text={document}
420
- extensions={[
421
- // Cursor moved.
422
- EditorView.updateListener.of((update) => {
423
- console.log('update', update.view.state.selection.main.head);
424
- }),
425
- ]}
426
- />
427
- ),
428
- };
@@ -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
- focus?: boolean;
53
- selection?: { anchor: number; head?: number };
56
+ model: EditorModel; // TODO(burdon): Optional (e.g., just provide content if readonly).
54
57
  readonly?: boolean; // TODO(burdon): Move into model.
55
- comments?: CommentRange[]; // TODO(burdon): Move into extension.
56
- extensions?: Extension[];
57
- editorMode?: EditorMode;
58
+ autoFocus?: boolean;
59
+ lineWrapping?: boolean;
60
+ scrollTo?: StateEffect<any>; // TODO(burdon): Restore scroll position: scrollTo EditorView.scrollSnapshot().
61
+ selection?: { anchor: number; head?: number };
62
+ editorMode?: EditorMode; // TODO(burdon): Factor out.
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,123 @@ export type TextEditorProps = {
65
72
  */
66
73
  export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
67
74
  (
68
- { model, focus, selection, readonly, 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);
77
- useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
78
94
 
79
- // Focus.
80
- useEffect(() => {
81
- if (view && focus) {
82
- view.focus();
83
- }
84
- }, [view, focus]);
85
-
86
- // TODO(burdon): Factor out as extension.
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]);
95
+ // The view ref can be used to focus the editor.
96
+ // NOTE: This does not cause the parent to re-render, so the ref is not available immediately.
97
+ useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
97
98
 
98
- // TODO(burdon): Factor out as extension.
99
+ // Set focus.
99
100
  useEffect(() => {
100
- if (view && comments !== undefined) {
101
- view.dispatch({
102
- effects: setCommentRange.of({ model, comments }),
103
- });
101
+ if (autoFocus) {
102
+ view?.focus();
104
103
  }
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
+ EditorView.editable.of(!readonly),
136
+ EditorState.readOnly.of(!!readonly),
128
137
 
129
138
  // Storage and replication.
139
+ // NOTE: This must come before user extensions.
130
140
  model.extension,
131
141
 
142
+ // TODO(burdon): Factor out (requires special handling for Escape/focus).
143
+ editorMode === 'vim' && vim(),
144
+
132
145
  // Custom.
133
146
  ...extensions,
134
147
  ].filter(Boolean) as Extension[],
135
148
  });
136
149
 
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();
150
+ //
151
+ // EditorView
152
+ // https://codemirror.net/docs/ref/#view.EditorViewConfig
153
+ //
141
154
  const newView = new EditorView({
142
- parent: rootRef.current,
143
155
  state,
144
- // NOTE: Uncomment to spy on all transactions.
156
+ parent: rootRef.current,
157
+ scrollTo,
158
+ // NOTE: Uncomment to debug/monitor all transactions.
145
159
  // https://codemirror.net/docs/ref/#view.EditorView.dispatch
146
- // dispatch: (transaction, view) => {
147
- // view.update([transaction]);
148
- // },
160
+ dispatchTransactions: (trs, view) => {
161
+ if (debug) {
162
+ logChanges(trs);
163
+ }
164
+ view.update(trs);
165
+ },
149
166
  });
150
167
 
168
+ view?.destroy();
151
169
  setView(newView);
170
+
152
171
  return () => {
153
172
  newView?.destroy();
154
173
  setView(null);
155
174
  };
156
175
  }, [rootRef, model, readonly, editorMode, themeMode]);
157
176
 
177
+ // Handles tab/focus.
178
+ // Pressing Escape focuses the outer div (to support tab navigation); pressing Enter refocuses the editor.
158
179
  const handleKeyUp = useCallback(
159
180
  (event: KeyboardEvent) => {
160
181
  const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
161
182
  switch (key) {
162
- // TODO(burdon): Is this required (for vim mode?)
163
- // case 'Enter': {
164
- // view?.contentDOM.focus();
165
- // break;
166
- // }
183
+ case 'Enter': {
184
+ view?.focus();
185
+ break;
186
+ }
167
187
 
168
188
  case 'Escape': {
169
- editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey) && rootRef.current?.focus();
189
+ if (editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)) {
190
+ rootRef.current?.focus();
191
+ }
170
192
  break;
171
193
  }
172
194
  }
@@ -178,25 +200,26 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
178
200
  <div
179
201
  key={model.id}
180
202
  role='none'
181
- ref={rootRef}
182
203
  tabIndex={0}
183
204
  onKeyUp={handleKeyUp}
184
205
  {...slots.root}
185
206
  {...(editorMode !== 'vim' && tabsterDOMAttribute)}
207
+ ref={rootRef}
186
208
  />
187
209
  );
188
210
  },
189
211
  );
190
212
 
213
+ // TODO(burdon): Single-line/scroll.
191
214
  export const TextEditor = forwardRef<EditorView, TextEditorProps>(
192
- ({ readonly, placeholder, extensions = [], theme = textTheme, slots, ...props }, forwardedRef) => {
215
+ ({ readonly, placeholder, lineWrapping, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
193
216
  const { themeMode } = useThemeContext();
194
217
  const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
195
218
  return (
196
219
  <BaseTextEditor
197
220
  ref={forwardedRef}
198
221
  readonly={readonly}
199
- extensions={[basicBundle({ readonly, themeMode, placeholder }), ...extensions]}
222
+ extensions={[basicBundle({ themeMode, placeholder, lineWrapping }), ...extensions]}
200
223
  theme={theme}
201
224
  slots={updatedSlots}
202
225
  {...props}
@@ -205,16 +228,15 @@ export const TextEditor = forwardRef<EditorView, TextEditorProps>(
205
228
  },
206
229
  );
207
230
 
208
- // TODO(burdon): Remove (Just provide bundle, slots).
209
231
  export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
210
- ({ readonly, placeholder, extensions = [], theme = markdownTheme, slots, ...props }, forwardedRef) => {
232
+ ({ readonly, placeholder, theme = markdownTheme, slots, extensions = [], ...props }, forwardedRef) => {
211
233
  const { themeMode } = useThemeContext();
212
234
  const updatedSlots = defaultsDeep({}, slots, defaultMarkdownSlots);
213
235
  return (
214
236
  <BaseTextEditor
215
237
  ref={forwardedRef}
216
238
  readonly={readonly}
217
- extensions={[markdownBundle({ readonly, themeMode, placeholder }), ...extensions]}
239
+ extensions={[markdownBundle({ themeMode, placeholder }), ...extensions]}
218
240
  theme={theme}
219
241
  slots={updatedSlots}
220
242
  {...props}
@@ -225,7 +247,11 @@ export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
225
247
 
226
248
  export const defaultSlots: TextEditorSlots = {
227
249
  root: {
228
- className: mx('p-2 overflow-y-auto', inputSurface),
250
+ // TODO(burdon): Add focusRing by default/as property?
251
+ className: mx('flex flex-col grow overflow-y-auto', attentionSurface),
252
+ },
253
+ editor: {
254
+ className: 'h-full p-2',
229
255
  },
230
256
  };
231
257