@dxos/react-ui-editor 0.3.11-main.c8e9429 → 0.3.11-main.cb8120f

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 (164) hide show
  1. package/dist/lib/browser/index.mjs +1306 -1084
  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 +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 +1 -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/extensions/outliner.d.ts +4 -0
  54. package/dist/types/src/extensions/outliner.d.ts.map +1 -0
  55. package/dist/types/src/hooks/awareness-provider.d.ts +7 -4
  56. package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -1
  57. package/dist/types/src/hooks/defs.d.ts +25 -0
  58. package/dist/types/src/hooks/defs.d.ts.map +1 -0
  59. package/dist/types/src/hooks/index.d.ts +3 -2
  60. package/dist/types/src/hooks/index.d.ts.map +1 -1
  61. package/dist/types/src/hooks/useActionHandler.d.ts +4 -0
  62. package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -0
  63. package/dist/types/src/hooks/useTextEditor.d.ts +17 -0
  64. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
  65. package/dist/types/src/hooks/useTextModel.d.ts +2 -27
  66. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  67. package/dist/types/src/styles/markdown.d.ts +2 -2
  68. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  69. package/dist/types/src/themes/default.d.ts.map +1 -1
  70. package/dist/types/src/util.d.ts +2 -0
  71. package/dist/types/src/util.d.ts.map +1 -1
  72. package/package.json +44 -35
  73. package/src/components/TextEditor/MarkdownEditor.stories.tsx +72 -80
  74. package/src/components/TextEditor/TextEditor.tsx +94 -67
  75. package/src/components/TextEditor/comments.stories.tsx +45 -43
  76. package/src/components/Toolbar/Toolbar.stories.tsx +86 -0
  77. package/src/components/Toolbar/Toolbar.tsx +115 -0
  78. package/src/extensions/autocomplete.ts +6 -3
  79. package/src/extensions/automerge/automerge.spec.tsx +76 -0
  80. package/src/extensions/automerge/automerge.test.ts +13 -0
  81. package/src/extensions/automerge/automerge.ts +50 -39
  82. package/src/extensions/automerge/cursor.ts +3 -3
  83. package/src/extensions/automerge/defs.ts +11 -36
  84. package/src/extensions/automerge/semaphore.ts +31 -28
  85. package/src/extensions/automerge/update-automerge.ts +14 -8
  86. package/src/extensions/automerge/update-codemirror.ts +5 -5
  87. package/src/extensions/awareness.ts +17 -17
  88. package/src/extensions/basic.ts +22 -17
  89. package/src/extensions/comments.ts +247 -179
  90. package/src/extensions/cursor.ts +7 -3
  91. package/src/extensions/index.ts +1 -7
  92. package/src/extensions/markdown/bundle.ts +20 -30
  93. package/src/extensions/markdown/code.ts +142 -0
  94. package/src/extensions/markdown/formatting.ts +159 -0
  95. package/src/extensions/markdown/heading.ts +59 -0
  96. package/src/extensions/markdown/highlight.ts +9 -15
  97. package/src/extensions/markdown/hr.ts +71 -0
  98. package/src/extensions/markdown/index.ts +8 -0
  99. package/src/extensions/{link.ts → markdown/link.ts} +5 -2
  100. package/src/extensions/{tasklist.ts → markdown/tasklist.ts} +48 -34
  101. package/src/extensions/mention.ts +9 -2
  102. package/src/extensions/outliner.ts +12 -0
  103. package/src/hooks/awareness-provider.ts +24 -20
  104. package/src/hooks/defs.ts +42 -0
  105. package/src/hooks/index.ts +3 -2
  106. package/src/hooks/useActionHandler.ts +45 -0
  107. package/src/hooks/useTextEditor.ts +29 -0
  108. package/src/hooks/useTextModel.ts +49 -55
  109. package/src/styles/markdown.ts +9 -10
  110. package/src/themes/default.ts +1 -3
  111. package/src/util.ts +26 -3
  112. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
  113. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
  114. package/dist/types/src/components/TextEditor/yjs.stories.d.ts +0 -12
  115. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
  116. package/dist/types/src/extensions/code.d.ts.map +0 -1
  117. package/dist/types/src/extensions/hr.d.ts.map +0 -1
  118. package/dist/types/src/extensions/image.d.ts.map +0 -1
  119. package/dist/types/src/extensions/link.d.ts.map +0 -1
  120. package/dist/types/src/extensions/table.d.ts.map +0 -1
  121. package/dist/types/src/extensions/tasklist.d.ts +0 -7
  122. package/dist/types/src/extensions/tasklist.d.ts.map +0 -1
  123. package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
  124. package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
  125. package/dist/types/src/extensions/yjs/index.d.ts +0 -2
  126. package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
  127. package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
  128. package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
  129. package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
  130. package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
  131. package/dist/types/src/hooks/automerge.d.ts +0 -3
  132. package/dist/types/src/hooks/automerge.d.ts.map +0 -1
  133. package/dist/types/src/hooks/yjs.d.ts +0 -27
  134. package/dist/types/src/hooks/yjs.d.ts.map +0 -1
  135. package/dist/types/src/testing/index.d.ts +0 -3
  136. package/dist/types/src/testing/index.d.ts.map +0 -1
  137. package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
  138. package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
  139. package/dist/types/src/testing/proto/index.d.ts +0 -2
  140. package/dist/types/src/testing/proto/index.d.ts.map +0 -1
  141. package/dist/types/src/testing/replicator.d.ts +0 -45
  142. package/dist/types/src/testing/replicator.d.ts.map +0 -1
  143. package/src/components/TextEditor/codemirror.stories.ts +0 -115
  144. package/src/components/TextEditor/yjs.stories.tsx +0 -56
  145. package/src/extensions/code.ts +0 -104
  146. package/src/extensions/hr.ts +0 -72
  147. package/src/extensions/yjs/cursor.ts +0 -22
  148. package/src/extensions/yjs/index.ts +0 -5
  149. package/src/extensions/yjs/yjs.test.ts +0 -35
  150. package/src/extensions/yjs/yjs.ts +0 -16
  151. package/src/hooks/automerge.ts +0 -52
  152. package/src/hooks/yjs.ts +0 -156
  153. package/src/testing/index.ts +0 -6
  154. package/src/testing/proto/gen/schema.ts +0 -49
  155. package/src/testing/proto/index.ts +0 -5
  156. package/src/testing/proto/schema.proto +0 -15
  157. package/src/testing/replicator.ts +0 -184
  158. /package/dist/types/src/extensions/{code.d.ts → markdown/code.d.ts} +0 -0
  159. /package/dist/types/src/extensions/{hr.d.ts → markdown/hr.d.ts} +0 -0
  160. /package/dist/types/src/extensions/{image.d.ts → markdown/image.d.ts} +0 -0
  161. /package/dist/types/src/extensions/{link.d.ts → markdown/link.d.ts} +0 -0
  162. /package/dist/types/src/extensions/{table.d.ts → markdown/table.d.ts} +0 -0
  163. /package/src/extensions/{image.ts → markdown/image.ts} +0 -0
  164. /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,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 { nonNullable } 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,123 @@ 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
90
  const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
72
91
  const { themeMode } = useThemeContext();
73
92
 
74
- // The editor view ref should only be used as an escape hatch.
75
93
  const rootRef = useRef<HTMLDivElement>(null);
76
94
  const [view, setView] = useState<EditorView | null>(null);
95
+
96
+ // The view ref can be used to focus the editor.
77
97
  // NOTE: This does not cause the parent to re-render, so the ref is not available immediately.
78
98
  useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
79
99
 
100
+ // Set focus.
80
101
  useEffect(() => {
81
- if (autofocus) {
102
+ if (autoFocus) {
82
103
  view?.focus();
83
104
  }
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]);
105
+ }, [view, autoFocus]);
106
106
 
107
+ // Create editor state and view.
108
+ // The view is recreated if the model or extensions are changed.
107
109
  useEffect(() => {
108
110
  if (!model || !rootRef.current) {
109
111
  return;
110
112
  }
111
113
 
114
+ //
115
+ // EditorState
112
116
  // https://codemirror.net/docs/ref/#state.EditorStateConfig
117
+ //
113
118
  const state = EditorState.create({
114
119
  doc: model.text(),
115
- // TODO(burdon): Composer should store and set selection when switching documents.
116
120
  selection,
117
121
  extensions: [
118
- readonly && EditorState.readOnly.of(readonly),
119
-
120
- // TODO(burdon): Factor out VIM mode? (manage via MarkdownPlugin).
121
- editorMode === 'vim' && vim(),
122
+ // TODO(burdon): Doesn't catch errors in keymap functions.
123
+ EditorView.exceptionSink.of((err) => {
124
+ log.catch(err);
125
+ }),
122
126
 
123
127
  // Theme.
124
- // TODO(burdon): Make theme configurable.
128
+ // TODO(burdon): Make configurable.
125
129
  EditorView.baseTheme(defaultTheme),
126
130
  EditorView.theme(theme ?? {}),
127
131
  EditorView.darkTheme.of(themeMode === 'dark'),
132
+ EditorView.editorAttributes.of({ class: slots.editor?.className ?? '' }),
133
+ EditorView.contentAttributes.of({ class: slots.content?.className ?? '' }),
134
+
135
+ // State.
136
+ EditorView.editable.of(!readonly),
137
+ EditorState.readOnly.of(!!readonly),
128
138
 
129
139
  // Storage and replication.
140
+ // NOTE: This must come before user extensions.
130
141
  model.extension,
131
142
 
143
+ // TODO(burdon): Factor out (requires special handling for Escape/focus).
144
+ editorMode === 'vim' && vim(),
145
+
132
146
  // Custom.
133
147
  ...extensions,
134
- ].filter(Boolean) as Extension[],
148
+ ].filter(nonNullable) as Extension[], // TODO(burdon): ???
135
149
  });
136
150
 
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();
151
+ //
152
+ // EditorView
153
+ // https://codemirror.net/docs/ref/#view.EditorViewConfig
154
+ //
141
155
  const newView = new EditorView({
142
- parent: rootRef.current,
143
156
  state,
144
- // NOTE: Uncomment to spy on all transactions.
157
+ parent: rootRef.current,
158
+ scrollTo,
159
+ // NOTE: Uncomment to debug/monitor all transactions.
145
160
  // https://codemirror.net/docs/ref/#view.EditorView.dispatch
146
- // dispatch: (transaction, view) => {
147
- // view.update([transaction]);
148
- // },
161
+ dispatchTransactions: (trs, view) => {
162
+ if (debug) {
163
+ logChanges(trs);
164
+ }
165
+ view.update(trs);
166
+ },
149
167
  });
150
168
 
169
+ view?.destroy();
151
170
  setView(newView);
171
+
152
172
  return () => {
153
173
  newView?.destroy();
154
174
  setView(null);
155
175
  };
156
176
  }, [rootRef, model, readonly, editorMode, themeMode]);
157
177
 
178
+ // Handles tab/focus.
179
+ // Pressing Escape focuses the outer div (to support tab navigation); pressing Enter refocuses the editor.
158
180
  const handleKeyUp = useCallback(
159
181
  (event: KeyboardEvent) => {
160
182
  const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
161
183
  switch (key) {
162
- // TODO(burdon): Is this required (for vim mode?)
163
- // case 'Enter': {
164
- // view?.contentDOM.focus();
165
- // break;
166
- // }
184
+ case 'Enter': {
185
+ view?.focus();
186
+ break;
187
+ }
167
188
 
168
189
  case 'Escape': {
169
- editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey) && rootRef.current?.focus();
190
+ if (editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)) {
191
+ rootRef.current?.focus();
192
+ }
170
193
  break;
171
194
  }
172
195
  }
@@ -188,15 +211,16 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
188
211
  },
189
212
  );
190
213
 
214
+ // TODO(burdon): Single-line/scroll.
191
215
  export const TextEditor = forwardRef<EditorView, TextEditorProps>(
192
- ({ readonly, placeholder, extensions = [], theme = textTheme, slots, ...props }, forwardedRef) => {
216
+ ({ readonly, placeholder, lineWrapping, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
193
217
  const { themeMode } = useThemeContext();
194
218
  const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
195
219
  return (
196
220
  <BaseTextEditor
197
221
  ref={forwardedRef}
198
222
  readonly={readonly}
199
- extensions={[basicBundle({ readonly, themeMode, placeholder }), ...extensions]}
223
+ extensions={[basicBundle({ themeMode, placeholder, lineWrapping }), ...extensions]}
200
224
  theme={theme}
201
225
  slots={updatedSlots}
202
226
  {...props}
@@ -205,16 +229,15 @@ export const TextEditor = forwardRef<EditorView, TextEditorProps>(
205
229
  },
206
230
  );
207
231
 
208
- // TODO(burdon): Remove (Just provide bundle, slots).
209
232
  export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
210
- ({ readonly, placeholder, extensions = [], theme = markdownTheme, slots, ...props }, forwardedRef) => {
233
+ ({ readonly, placeholder, theme = markdownTheme, slots, extensions = [], ...props }, forwardedRef) => {
211
234
  const { themeMode } = useThemeContext();
212
235
  const updatedSlots = defaultsDeep({}, slots, defaultMarkdownSlots);
213
236
  return (
214
237
  <BaseTextEditor
215
238
  ref={forwardedRef}
216
239
  readonly={readonly}
217
- extensions={[markdownBundle({ readonly, themeMode, placeholder }), ...extensions]}
240
+ extensions={[markdownBundle({ themeMode, placeholder }), ...extensions]}
218
241
  theme={theme}
219
242
  slots={updatedSlots}
220
243
  {...props}
@@ -225,7 +248,11 @@ export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
225
248
 
226
249
  export const defaultSlots: TextEditorSlots = {
227
250
  root: {
228
- className: mx('w-full p-2 overflow-y-auto', inputSurface),
251
+ // TODO(burdon): Add focusRing by default/as property?
252
+ className: mx('flex flex-col grow overflow-y-auto', attentionSurface),
253
+ },
254
+ editor: {
255
+ className: 'h-full p-2',
229
256
  },
230
257
  };
231
258