@dxos/react-ui-editor 0.4.8-main.05fda5d → 0.4.8-main.06fa7e4

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 (137) hide show
  1. package/dist/lib/browser/index.mjs +1938 -1920
  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/TextEditor.d.ts +18 -34
  5. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +80 -5
  7. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  8. package/dist/types/src/components/Toolbar/Toolbar.d.ts +12 -11
  9. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  10. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +4 -2
  11. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
  12. package/dist/types/src/extensions/automerge/automerge.d.ts +1 -1
  13. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  14. package/dist/types/src/extensions/automerge/automerge.stories.d.ts +2 -1
  15. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  16. package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
  17. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  18. package/dist/types/src/extensions/automerge/defs.d.ts +1 -15
  19. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  20. package/dist/types/src/extensions/automerge/index.d.ts +0 -1
  21. package/dist/types/src/extensions/automerge/index.d.ts.map +1 -1
  22. package/dist/types/src/extensions/automerge/sync.d.ts +2 -1
  23. package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
  24. package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -1
  25. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  26. package/dist/types/src/{hooks → extensions/awareness}/awareness-provider.d.ts +1 -1
  27. package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -0
  28. package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -0
  29. package/dist/types/src/extensions/awareness/index.d.ts +3 -0
  30. package/dist/types/src/extensions/awareness/index.d.ts.map +1 -0
  31. package/dist/types/src/extensions/comments.d.ts +2 -2
  32. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  33. package/dist/types/src/extensions/cursor.d.ts +1 -1
  34. package/dist/types/src/extensions/cursor.d.ts.map +1 -1
  35. package/dist/types/src/extensions/dnd.d.ts +7 -1
  36. package/dist/types/src/extensions/dnd.d.ts.map +1 -1
  37. package/dist/types/src/extensions/doc.d.ts +3 -0
  38. package/dist/types/src/extensions/doc.d.ts.map +1 -0
  39. package/dist/types/src/extensions/factories.d.ts +53 -0
  40. package/dist/types/src/extensions/factories.d.ts.map +1 -0
  41. package/dist/types/src/extensions/index.d.ts +4 -2
  42. package/dist/types/src/extensions/index.d.ts.map +1 -1
  43. package/dist/types/src/extensions/markdown/action.d.ts +9 -0
  44. package/dist/types/src/extensions/markdown/action.d.ts.map +1 -0
  45. package/dist/types/src/extensions/markdown/bundle.d.ts +5 -4
  46. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  47. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  48. package/dist/types/src/extensions/markdown/formatting.d.ts +8 -5
  49. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  50. package/dist/types/src/extensions/markdown/image.d.ts +6 -0
  51. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -1
  52. package/dist/types/src/extensions/markdown/index.d.ts +1 -0
  53. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  54. package/dist/types/src/extensions/markdown/table.d.ts +2 -2
  55. package/dist/types/src/extensions/markdown/table.d.ts.map +1 -1
  56. package/dist/types/src/extensions/modes.d.ts +2 -1
  57. package/dist/types/src/extensions/modes.d.ts.map +1 -1
  58. package/dist/types/src/extensions/state.d.ts +20 -0
  59. package/dist/types/src/extensions/state.d.ts.map +1 -0
  60. package/dist/types/src/extensions/types.d.ts +9 -0
  61. package/dist/types/src/extensions/types.d.ts.map +1 -0
  62. package/dist/types/src/hooks/index.d.ts +1 -4
  63. package/dist/types/src/hooks/index.d.ts.map +1 -1
  64. package/dist/types/src/hooks/useActionHandler.d.ts +2 -2
  65. package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -1
  66. package/dist/types/src/hooks/useDocAccessor.d.ts +9 -0
  67. package/dist/types/src/hooks/useDocAccessor.d.ts.map +1 -0
  68. package/dist/types/src/hooks/useTextEditor.d.ts +3 -44
  69. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  70. package/dist/types/src/hooks/useTextEditor.stories.d.ts +7 -0
  71. package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +1 -1
  72. package/dist/types/src/themes/default.d.ts +0 -3
  73. package/dist/types/src/themes/default.d.ts.map +1 -1
  74. package/dist/types/src/translations.d.ts +1 -0
  75. package/dist/types/src/translations.d.ts.map +1 -1
  76. package/package.json +26 -27
  77. package/src/components/TextEditor/TextEditor.stories.tsx +532 -23
  78. package/src/components/TextEditor/TextEditor.tsx +45 -156
  79. package/src/components/Toolbar/Toolbar.stories.tsx +54 -55
  80. package/src/components/Toolbar/Toolbar.tsx +102 -60
  81. package/src/extensions/automerge/automerge.stories.tsx +16 -17
  82. package/src/extensions/automerge/automerge.ts +6 -5
  83. package/src/extensions/automerge/cursor.ts +1 -1
  84. package/src/extensions/automerge/defs.ts +1 -22
  85. package/src/extensions/automerge/index.ts +0 -1
  86. package/src/extensions/automerge/sync.ts +2 -9
  87. package/src/extensions/automerge/update-automerge.ts +2 -1
  88. package/src/{hooks → extensions/awareness}/awareness-provider.ts +1 -1
  89. package/src/extensions/{awareness.ts → awareness/awareness.ts} +1 -1
  90. package/src/extensions/awareness/index.ts +6 -0
  91. package/src/extensions/command/state.ts +3 -3
  92. package/src/extensions/comments.ts +9 -5
  93. package/src/extensions/cursor.ts +1 -1
  94. package/src/extensions/dnd.ts +31 -9
  95. package/src/extensions/doc.ts +14 -0
  96. package/src/extensions/factories.ts +193 -0
  97. package/src/extensions/index.ts +4 -2
  98. package/src/extensions/markdown/action.ts +112 -0
  99. package/src/extensions/markdown/bundle.ts +17 -37
  100. package/src/extensions/markdown/decorate.ts +2 -4
  101. package/src/extensions/markdown/formatting.test.ts +13 -13
  102. package/src/extensions/markdown/formatting.ts +110 -97
  103. package/src/extensions/markdown/image.ts +7 -0
  104. package/src/extensions/markdown/index.ts +1 -0
  105. package/src/extensions/markdown/table.ts +11 -3
  106. package/src/extensions/modes.ts +11 -2
  107. package/src/extensions/state.ts +90 -0
  108. package/src/extensions/types.ts +16 -0
  109. package/src/hooks/index.ts +1 -5
  110. package/src/hooks/useActionHandler.ts +4 -85
  111. package/src/hooks/useDocAccessor.ts +27 -0
  112. package/src/hooks/useTextEditor.stories.tsx +40 -35
  113. package/src/hooks/useTextEditor.ts +14 -120
  114. package/src/index.ts +1 -1
  115. package/src/themes/default.ts +25 -36
  116. package/src/translations.ts +1 -0
  117. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +0 -103
  118. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +0 -1
  119. package/dist/types/src/extensions/awareness.d.ts.map +0 -1
  120. package/dist/types/src/extensions/cursor-line-margin.d.ts +0 -8
  121. package/dist/types/src/extensions/cursor-line-margin.d.ts.map +0 -1
  122. package/dist/types/src/extensions/outliner.d.ts +0 -4
  123. package/dist/types/src/extensions/outliner.d.ts.map +0 -1
  124. package/dist/types/src/hooks/awareness-provider.d.ts.map +0 -1
  125. package/dist/types/src/hooks/defs.d.ts +0 -20
  126. package/dist/types/src/hooks/defs.d.ts.map +0 -1
  127. package/dist/types/src/hooks/useEditorView.d.ts +0 -20
  128. package/dist/types/src/hooks/useEditorView.d.ts.map +0 -1
  129. package/dist/types/src/hooks/useTextModel.d.ts +0 -33
  130. package/dist/types/src/hooks/useTextModel.d.ts.map +0 -1
  131. package/src/components/TextEditor/MarkdownEditor.stories.tsx +0 -524
  132. package/src/extensions/cursor-line-margin.ts +0 -95
  133. package/src/extensions/outliner.ts +0 -12
  134. package/src/hooks/defs.ts +0 -30
  135. package/src/hooks/useEditorView.ts +0 -32
  136. package/src/hooks/useTextModel.ts +0 -121
  137. /package/dist/types/src/extensions/{awareness.d.ts → awareness/awareness.d.ts} +0 -0
@@ -7,11 +7,13 @@ import {
7
7
  ChatText,
8
8
  Code,
9
9
  CodeBlock,
10
+ Image,
10
11
  Link,
11
12
  ListBullets,
12
13
  ListChecks,
13
14
  ListNumbers,
14
15
  Paragraph,
16
+ Quotes,
15
17
  TextStrikethrough,
16
18
  Table,
17
19
  TextB,
@@ -22,24 +24,25 @@ import {
22
24
  TextHFive,
23
25
  TextHSix,
24
26
  TextItalic,
25
- Quotes,
26
27
  } from '@phosphor-icons/react';
27
28
  import { createContext } from '@radix-ui/react-context';
28
- import React, { type PropsWithChildren, useRef, useState } from 'react';
29
+ import React, { type PropsWithChildren, useEffect, useRef, useState } from 'react';
30
+ import { useDropzone } from 'react-dropzone';
29
31
 
30
32
  import {
31
33
  DensityProvider,
32
34
  ElevationProvider,
33
35
  Select,
34
- type ThemedClassName,
35
36
  Toolbar as NaturalToolbar,
36
37
  Tooltip,
37
- type ToolbarToggleGroupItemProps,
38
+ type ThemedClassName,
39
+ type ToolbarToggleGroupItemProps as NaturalToolbarToggleGroupItemProps,
40
+ type ToolbarButtonProps as NaturalToolbarButtonProps,
38
41
  useTranslation,
39
42
  } from '@dxos/react-ui';
40
43
  import { getSize } from '@dxos/react-ui-theme';
41
44
 
42
- import { type Formatting } from '../../extensions';
45
+ import { type Action, type ActionType, type Formatting } from '../../extensions';
43
46
  import { translationKey } from '../../translations';
44
47
 
45
48
  const tooltipProps = { side: 'top' as const, classNames: 'z-10' };
@@ -54,33 +57,9 @@ const HeadingIcons: { [key: string]: Icon } = {
54
57
  '6': TextHSix,
55
58
  };
56
59
 
57
- // TODO(burdon): Revert to string to make extensible?
58
- export type ActionType =
59
- | 'blockquote'
60
- | 'strong'
61
- | 'codeblock'
62
- | 'comment'
63
- | 'heading'
64
- | 'image'
65
- | 'emphasis'
66
- | 'code'
67
- | 'link'
68
- | 'list-bullet'
69
- | 'list-ordered'
70
- | 'list-task'
71
- | 'mention'
72
- | 'prompt'
73
- | 'strikethrough'
74
- | 'table';
75
-
76
- export type Action = {
77
- type: ActionType;
78
- data?: any;
79
- };
80
-
81
60
  export type ToolbarProps = ThemedClassName<
82
61
  PropsWithChildren<{
83
- state: Formatting | null;
62
+ state: Formatting | undefined;
84
63
  onAction?: (action: Action) => void;
85
64
  }>
86
65
  >;
@@ -111,9 +90,9 @@ type ButtonProps = {
111
90
  disabled?: (state: Formatting) => boolean;
112
91
  };
113
92
 
114
- type ToolbarButtonProps = ToolbarToggleGroupItemProps & { Icon: Icon };
93
+ type ToolbarToggleButtonProps = NaturalToolbarToggleGroupItemProps & { Icon: Icon };
115
94
 
116
- const ToolbarButton = ({ Icon, children, ...props }: ToolbarButtonProps) => {
95
+ const ToolbarToggleButton = ({ Icon, children, ...props }: ToolbarToggleButtonProps) => {
117
96
  return (
118
97
  <Tooltip.Root>
119
98
  <Tooltip.Trigger asChild>
@@ -132,6 +111,27 @@ const ToolbarButton = ({ Icon, children, ...props }: ToolbarButtonProps) => {
132
111
  );
133
112
  };
134
113
 
114
+ type ToolbarButtonProps = NaturalToolbarButtonProps & { Icon: Icon };
115
+
116
+ const ToolbarButton = ({ Icon, children, ...props }: ToolbarButtonProps) => {
117
+ return (
118
+ <Tooltip.Root>
119
+ <Tooltip.Trigger asChild>
120
+ <NaturalToolbar.Button variant='ghost' {...props} classNames={buttonStyles}>
121
+ <Icon className={iconStyles} />
122
+ <span className='sr-only'>{children}</span>
123
+ </NaturalToolbar.Button>
124
+ </Tooltip.Trigger>
125
+ <Tooltip.Portal>
126
+ <Tooltip.Content {...tooltipProps}>
127
+ {children}
128
+ <Tooltip.Arrow />
129
+ </Tooltip.Content>
130
+ </Tooltip.Portal>
131
+ </Tooltip.Root>
132
+ );
133
+ };
134
+
135
135
  const ToolbarSeparator = () => <div role='separator' className='grow' />;
136
136
 
137
137
  const MarkdownHeading = () => {
@@ -227,7 +227,7 @@ const MarkdownStyles = () => {
227
227
  value={markdownStyles.filter(({ getState }) => state && getState(state)).map(({ type }) => type)}
228
228
  >
229
229
  {markdownStyles.map(({ type, getState, Icon }) => (
230
- <ToolbarButton
230
+ <ToolbarToggleButton
231
231
  key={type}
232
232
  value={type}
233
233
  Icon={Icon}
@@ -235,7 +235,7 @@ const MarkdownStyles = () => {
235
235
  onClick={state ? () => onAction?.({ type, data: !getState(state) }) : undefined}
236
236
  >
237
237
  {t(`${type} label`)}
238
- </ToolbarButton>
238
+ </ToolbarToggleButton>
239
239
  ))}
240
240
  </NaturalToolbar.ToggleGroup>
241
241
  );
@@ -253,14 +253,14 @@ const MarkdownLists = () => {
253
253
  return (
254
254
  <NaturalToolbar.ToggleGroup type='single' value={state?.listStyle ? `list-${state.listStyle}` : ''}>
255
255
  {markdownLists.map(({ type, getState, Icon }) => (
256
- <ToolbarButton
256
+ <ToolbarToggleButton
257
257
  key={type}
258
258
  value={type}
259
259
  Icon={Icon}
260
260
  onClick={state ? () => onAction?.({ type, data: !getState(state) }) : undefined}
261
261
  >
262
262
  {t(`${type} label`)}
263
- </ToolbarButton>
263
+ </ToolbarToggleButton>
264
264
  ))}
265
265
  </NaturalToolbar.ToggleGroup>
266
266
  );
@@ -292,7 +292,7 @@ const MarkdownBlocks = () => {
292
292
  return (
293
293
  <NaturalToolbar.ToggleGroup type='single' value={value?.type ?? ''}>
294
294
  {markdownBlocks.map(({ type, disabled, getState, Icon }) => (
295
- <ToolbarButton
295
+ <ToolbarToggleButton
296
296
  key={type}
297
297
  value={type}
298
298
  Icon={Icon}
@@ -300,7 +300,7 @@ const MarkdownBlocks = () => {
300
300
  onClick={state ? () => onAction?.({ type, data: !getState(state) }) : undefined}
301
301
  >
302
302
  {t(`${type} label`)}
303
- </ToolbarButton>
303
+ </ToolbarToggleButton>
304
304
  ))}
305
305
  </NaturalToolbar.ToggleGroup>
306
306
  );
@@ -315,38 +315,80 @@ const MarkdownStandard = () => (
315
315
  </>
316
316
  );
317
317
 
318
- const MarkdownExtended = () => {
318
+ // TODO(burdon): Make extensible.
319
+ export type MarkdownCustomOptions = {
320
+ onUpload?: (file: File) => Promise<{ url?: string }>;
321
+ };
322
+
323
+ const MarkdownCustom = ({ onUpload }: MarkdownCustomOptions = {}) => {
319
324
  const { onAction } = useToolbarContext('MarkdownStyles');
320
325
  const { t } = useTranslation(translationKey);
326
+ // https://react-dropzone.js.org/#src
327
+ const { acceptedFiles, getInputProps, open } = useDropzone({
328
+ multiple: false,
329
+ noDrag: true,
330
+ accept: {
331
+ 'image/*': ['.jpg', '.jpeg', '.png', '.gif'],
332
+ },
333
+ });
334
+
335
+ useEffect(() => {
336
+ if (onUpload && acceptedFiles.length) {
337
+ requestAnimationFrame(async () => {
338
+ // NOTE: Clone file since react-dropzone patches in a non-standard `path` property, which confuses IPFS.
339
+ const f = acceptedFiles[0];
340
+ const file = new File([f], f.name, {
341
+ type: f.type,
342
+ lastModified: f.lastModified,
343
+ });
344
+
345
+ const { url } = await onUpload(file);
346
+ if (url) {
347
+ onAction?.({ type: 'image', data: url });
348
+ }
349
+ });
350
+ }
351
+ }, [acceptedFiles]);
352
+
321
353
  return (
322
- <Tooltip.Root>
323
- <Tooltip.Trigger asChild>
324
- <NaturalToolbar.Button
325
- variant='ghost'
326
- data-testid='editor.toolbar.comment'
327
- onClick={() => onAction?.({ type: 'comment' })}
328
- classNames={buttonStyles}
329
- >
330
- <ChatText className={iconStyles} />
331
- <span className='sr-only'>{t('comment label')}</span>
332
- </NaturalToolbar.Button>
333
- </Tooltip.Trigger>
334
- <Tooltip.Portal>
335
- <Tooltip.Content {...tooltipProps}>
336
- {t('comment label')}
337
- <Tooltip.Arrow />
338
- </Tooltip.Content>
339
- </Tooltip.Portal>
340
- </Tooltip.Root>
354
+ <>
355
+ <input {...getInputProps()} />
356
+ <ToolbarButton value='image' Icon={Image} onClick={() => open()}>
357
+ {t('image label')}
358
+ </ToolbarButton>
359
+ </>
360
+ );
361
+ };
362
+
363
+ // TODO(burdon): Make extensible.
364
+ const MarkdownActions = () => {
365
+ const { onAction } = useToolbarContext('MarkdownStyles');
366
+ const { t } = useTranslation(translationKey);
367
+ return (
368
+ <>
369
+ {/* TODO(burdon): Toggle readonly state. */}
370
+ {/* <ToolbarButton value='comment' Icon={BookOpenText} onClick={() => onAction?.({ type: 'comment' })}> */}
371
+ {/* {t('comment label')} */}
372
+ {/* </ToolbarButton> */}
373
+ <ToolbarButton
374
+ value='comment'
375
+ Icon={ChatText}
376
+ data-testid='editor.toolbar.comment'
377
+ onClick={() => onAction?.({ type: 'comment' })}
378
+ >
379
+ {t('comment label')}
380
+ </ToolbarButton>
381
+ </>
341
382
  );
342
383
  };
343
384
 
344
385
  export const Toolbar = {
345
386
  Root: ToolbarRoot,
346
- Button: ToolbarButton,
387
+ Button: ToolbarToggleButton,
347
388
  Separator: ToolbarSeparator,
348
389
  Markdown: MarkdownStandard,
349
- Extended: MarkdownExtended,
390
+ Custom: MarkdownCustom,
391
+ Actions: MarkdownActions,
350
392
  };
351
393
 
352
394
  export { useToolbarContext };
@@ -9,7 +9,7 @@ import '@preact/signals-react';
9
9
  import React, { useEffect, useMemo, useState } from 'react';
10
10
 
11
11
  import { Repo } from '@dxos/automerge/automerge-repo';
12
- import { createDocAccessor, Filter } from '@dxos/echo-schema';
12
+ import { createDocAccessor, Filter, DocAccessor } from '@dxos/echo-schema';
13
13
  import { type PublicKey } from '@dxos/keys';
14
14
  import { Expando, TextObject, useSpace } from '@dxos/react-client/echo';
15
15
  import { ClientRepeater } from '@dxos/react-client/testing';
@@ -17,9 +17,9 @@ import { useThemeContext } from '@dxos/react-ui';
17
17
  import { withTheme } from '@dxos/storybook-utils';
18
18
 
19
19
  import { automerge } from './automerge';
20
- import { DocAccessor } from './defs';
21
- import { createBasicExtensions, createThemeExtensions, useTextEditor } from '../../hooks';
20
+ import { useTextEditor } from '../../hooks';
22
21
  import translations from '../../translations';
22
+ import { createBasicExtensions, createThemeExtensions } from '../factories';
23
23
 
24
24
  const initialContent = 'Hello world!';
25
25
 
@@ -34,21 +34,19 @@ type EditorProps = {
34
34
 
35
35
  const Editor = ({ source, autoFocus }: EditorProps) => {
36
36
  const { themeMode } = useThemeContext();
37
- const extensions = useMemo(
38
- () => [
39
- createBasicExtensions({ placeholder: 'Type here...' }),
40
- createThemeExtensions({ themeMode, slots: { editor: { className: 'p-2 bg-white dark:bg-black' } } }),
41
- automerge(source),
42
- ],
43
- [themeMode, source],
37
+ const { parentRef } = useTextEditor(
38
+ () => ({
39
+ doc: DocAccessor.getValue(source),
40
+ extensions: [
41
+ createBasicExtensions({ placeholder: 'Type here...' }),
42
+ createThemeExtensions({ themeMode, slots: { editor: { className: 'p-2 bg-white dark:bg-black' } } }),
43
+ automerge(source),
44
+ ],
45
+ autoFocus,
46
+ }),
47
+ [source, themeMode],
44
48
  );
45
49
 
46
- const { parentRef } = useTextEditor({
47
- doc: DocAccessor.getValue(source),
48
- extensions,
49
- autoFocus,
50
- });
51
-
52
50
  return <div ref={parentRef} />;
53
51
  };
54
52
 
@@ -114,7 +112,8 @@ const EchoStory = ({ spaceKey }: { spaceKey: PublicKey }) => {
114
112
 
115
113
  export const Default = {};
116
114
 
117
- // TODO(burdon): Feed already added error.
115
+ // TODO(burdon): Error:
116
+ // chunk-6NX3RPDS.mjs:2021 ControlPipeline#5 Error: invariant violation: Feed already added
118
117
  export const WithEcho = {
119
118
  decorators: [withTheme],
120
119
  render: () => {
@@ -8,9 +8,10 @@ import { StateField, type Extension } from '@codemirror/state';
8
8
  import { EditorView, ViewPlugin } from '@codemirror/view';
9
9
 
10
10
  import { next as A } from '@dxos/automerge/automerge';
11
+ import { type DocAccessor } from '@dxos/echo-schema';
11
12
 
12
13
  import { cursorConverter } from './cursor';
13
- import { updateHeadsEffect, isReconcile, type State, type DocAccessor } from './defs';
14
+ import { updateHeadsEffect, isReconcile, type State } from './defs';
14
15
  import { Syncer } from './sync';
15
16
  import { Cursor } from '../cursor';
16
17
 
@@ -59,16 +60,16 @@ export const automerge = (accessor: DocAccessor): Extension => {
59
60
  ViewPlugin.fromClass(
60
61
  class {
61
62
  constructor(private readonly _view: EditorView) {
62
- accessor.handle.addListener('change', this._handleChange.bind(this));
63
+ accessor.handle.addListener('change', this._handleChange);
63
64
  }
64
65
 
65
66
  destroy() {
66
- accessor.handle.removeListener('change', this._handleChange.bind(this));
67
+ accessor.handle.removeListener('change', this._handleChange);
67
68
  }
68
69
 
69
- _handleChange() {
70
+ readonly _handleChange = () => {
70
71
  syncer.reconcile(this._view, false);
71
- }
72
+ };
72
73
  },
73
74
  ),
74
75
 
@@ -5,9 +5,9 @@
5
5
  import get from 'lodash.get';
6
6
 
7
7
  import { next as A } from '@dxos/automerge/automerge';
8
+ import { type DocAccessor } from '@dxos/echo-schema';
8
9
  import { log } from '@dxos/log';
9
10
 
10
- import { type DocAccessor } from './defs';
11
11
  import { type CursorConverter } from '../cursor';
12
12
 
13
13
  export const cursorConverter = ({ handle, path }: DocAccessor): CursorConverter => ({
@@ -5,9 +5,8 @@
5
5
  //
6
6
 
7
7
  import { Annotation, StateEffect, type StateField, type EditorState, type Transaction } from '@codemirror/state';
8
- import get from 'lodash.get';
9
8
 
10
- import { type ChangeFn, type ChangeOptions, type Doc, type Heads, type Prop } from '@dxos/automerge/automerge';
9
+ import { type Heads, type Prop } from '@dxos/automerge/automerge';
11
10
 
12
11
  export type State = {
13
12
  path: Prop[];
@@ -31,23 +30,3 @@ export const reconcileAnnotation = Annotation.define<boolean>();
31
30
  export const isReconcile = (tr: Transaction): boolean => {
32
31
  return !!tr.annotation(reconcileAnnotation);
33
32
  };
34
-
35
- // TODO(burdon): Reconcile with echo.
36
- export interface DocAccessor<T = any> {
37
- handle: IDocHandle<T>;
38
- get path(): string[]; // TODO(burdon): Getter or prop?
39
- }
40
-
41
- // TODO(burdon): Reconcile with echo.
42
- export const DocAccessor = {
43
- getValue: (accessor: DocAccessor) => get(accessor.handle, accessor.path),
44
- };
45
-
46
- // TODO(burdon): Reconcile with echo.
47
- export interface IDocHandle<T = any> {
48
- docSync(): Doc<T> | undefined;
49
- change(callback: ChangeFn<T>, options?: ChangeOptions<T>): void;
50
- changeAt(heads: Heads, callback: ChangeFn<T>, options?: ChangeOptions<T>): string[] | undefined;
51
- addListener(event: 'change', listener: () => void): void;
52
- removeListener(event: 'change', listener: () => void): void;
53
- }
@@ -3,4 +3,3 @@
3
3
  //
4
4
 
5
5
  export { automerge } from './automerge';
6
- export { type IDocHandle } from './defs';
@@ -8,16 +8,9 @@ import { type StateField } from '@codemirror/state';
8
8
  import { type EditorView } from '@codemirror/view';
9
9
 
10
10
  import { next as A } from '@dxos/automerge/automerge';
11
+ import { type IDocHandle } from '@dxos/echo-schema';
11
12
 
12
- import {
13
- getLastHeads,
14
- getPath,
15
- type IDocHandle,
16
- type State,
17
- reconcileAnnotation,
18
- updateHeads,
19
- isReconcile,
20
- } from './defs';
13
+ import { getLastHeads, getPath, type State, reconcileAnnotation, updateHeads, isReconcile } from './defs';
21
14
  import { updateAutomerge } from './update-automerge';
22
15
  import { updateCodeMirror } from './update-codemirror';
23
16
 
@@ -7,8 +7,9 @@
7
7
  import { type EditorState, type StateField, type Transaction, type Text } from '@codemirror/state';
8
8
 
9
9
  import { next as A, type Heads } from '@dxos/automerge/automerge';
10
+ import { type IDocHandle } from '@dxos/echo-schema';
10
11
 
11
- import { type IDocHandle, type State } from './defs';
12
+ import { type State } from './defs';
12
13
 
13
14
  export const updateAutomerge = (
14
15
  field: StateField<State>,
@@ -9,7 +9,7 @@ import { log } from '@dxos/log';
9
9
  import { type Space } from '@dxos/react-client/echo';
10
10
  import { type GossipMessage } from '@dxos/react-client/mesh';
11
11
 
12
- import { type AwarenessInfo, type AwarenessPosition, type AwarenessProvider, type AwarenessState } from '../extensions';
12
+ import { type AwarenessInfo, type AwarenessPosition, type AwarenessProvider, type AwarenessState } from './awareness';
13
13
 
14
14
  type ProtocolMessage =
15
15
  | {
@@ -16,7 +16,7 @@ import {
16
16
  import { Event } from '@dxos/async';
17
17
  import { Context } from '@dxos/context';
18
18
 
19
- import { Cursor, type CursorConverter } from './cursor';
19
+ import { Cursor, type CursorConverter } from '../cursor';
20
20
 
21
21
  export interface AwarenessProvider {
22
22
  remoteStateChange: Event<void>;
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './awareness';
6
+ export * from './awareness-provider';
@@ -59,8 +59,8 @@ export const commandState = StateField.define<CommandState>({
59
59
  selection: { anchor: pos + action.insert.length },
60
60
  });
61
61
  }
62
- // TODO(burdon): Truncates text if set focus immediately.
63
- setTimeout(() => view.focus());
62
+ // NOTE: Truncates text if set focus immediately.
63
+ requestAnimationFrame(() => view.focus());
64
64
  });
65
65
  },
66
66
  };
@@ -74,7 +74,7 @@ export const commandState = StateField.define<CommandState>({
74
74
 
75
75
  return state;
76
76
  },
77
- provide: (f) => [showTooltip.from(f, (value) => value.tooltip ?? null)],
77
+ provide: (field) => [showTooltip.from(field, (value) => value.tooltip ?? null)],
78
78
  });
79
79
 
80
80
  export const openEffect = StateEffect.define<{ pos: number; fullWidth?: boolean }>();
@@ -13,7 +13,7 @@ import { log } from '@dxos/log';
13
13
  import { nonNullable } from '@dxos/util';
14
14
 
15
15
  import { Cursor } from './cursor';
16
- import { type Comment, type Range } from '../hooks';
16
+ import { type Comment, type Range } from './types';
17
17
  import { getToken } from '../styles';
18
18
  import { callbackWrapper } from '../util';
19
19
 
@@ -508,12 +508,16 @@ export const focusComment = (view: EditorView, id: string, center = true) => {
508
508
  /**
509
509
  * Update comments state field.
510
510
  */
511
- export const useComments = (view: EditorView | null, id: string, comments: Comment[] = []) => {
511
+ export const useComments = (view: EditorView | null | undefined, id: string, comments?: Comment[]) => {
512
512
  useEffect(() => {
513
513
  if (view) {
514
- view.dispatch({
515
- effects: setComments.of({ id, comments }),
516
- });
514
+ // Check same document.
515
+ // NOTE: Hook might be called before editor state is updated.
516
+ if (id === view.state.facet(documentId)) {
517
+ view.dispatch({
518
+ effects: setComments.of({ id, comments: comments ?? [] }),
519
+ });
520
+ }
517
521
  }
518
522
  }, [id, view, comments]);
519
523
  };
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { type EditorState, Facet } from '@codemirror/state';
6
6
 
7
- import type { Range } from '../hooks';
7
+ import type { Range } from './types';
8
8
 
9
9
  /**
10
10
  * Converts indexes into the text document into stable peer-independent cursors.
@@ -3,15 +3,37 @@
3
3
  //
4
4
 
5
5
  import type { Extension } from '@codemirror/state';
6
- import { EditorView } from '@codemirror/view';
6
+ import { dropCursor, EditorView } from '@codemirror/view';
7
7
 
8
- import { log } from '@dxos/log';
8
+ import { getToken } from '../styles';
9
9
 
10
- // TODO(burdon): DND experiment.
11
- export const dnd = (): Extension => {
12
- return EditorView.domEventHandlers({
13
- drop: (event, view) => {
14
- log.info('domEventHandlersDrop', { event });
15
- },
16
- });
10
+ export type DNDOptions = { onDrop?: (view: EditorView, event: { files: FileList }) => void };
11
+
12
+ const styles = EditorView.baseTheme({
13
+ '.cm-dropCursor': {
14
+ borderLeft: `2px solid ${getToken('extend.colors.primary.500')}`,
15
+ color: getToken('extend.colors.primary.500'),
16
+ padding: '0 4px',
17
+ },
18
+ '.cm-dropCursor:after': {
19
+ content: '"←"',
20
+ },
21
+ });
22
+
23
+ export const dropFile = (options: DNDOptions = {}): Extension => {
24
+ return [
25
+ styles,
26
+ dropCursor(),
27
+ EditorView.domEventHandlers({
28
+ drop: (event, view) => {
29
+ event.preventDefault();
30
+ const files = event.dataTransfer?.files;
31
+ const pos = view.posAtCoords(event);
32
+ if (files?.length && pos !== null) {
33
+ view.dispatch({ selection: { anchor: pos } });
34
+ options.onDrop?.(view, { files });
35
+ }
36
+ },
37
+ }),
38
+ ];
17
39
  };
@@ -0,0 +1,14 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { Facet } from '@codemirror/state';
6
+
7
+ import { invariant } from '@dxos/invariant';
8
+
9
+ export const documentId = Facet.define<string, string>({
10
+ combine: (providers) => {
11
+ invariant(providers.length <= 1);
12
+ return providers[0];
13
+ },
14
+ });