@dxos/react-ui-editor 0.4.8-main.ec0899a → 0.4.8-next.fff1521

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 (48) hide show
  1. package/dist/lib/browser/index.mjs +449 -273
  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 +2 -2
  5. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +2 -1
  7. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  8. package/dist/types/src/components/Toolbar/Toolbar.d.ts +11 -5
  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.stories.d.ts +1 -0
  13. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  14. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  15. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  16. package/dist/types/src/extensions/index.d.ts +1 -0
  17. package/dist/types/src/extensions/index.d.ts.map +1 -1
  18. package/dist/types/src/extensions/markdown/formatting.d.ts +7 -4
  19. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  20. package/dist/types/src/extensions/markdown/image.d.ts +6 -0
  21. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -1
  22. package/dist/types/src/extensions/modes.d.ts +1 -0
  23. package/dist/types/src/extensions/modes.d.ts.map +1 -1
  24. package/dist/types/src/extensions/state.d.ts +20 -0
  25. package/dist/types/src/extensions/state.d.ts.map +1 -0
  26. package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -1
  27. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  28. package/dist/types/src/hooks/useTextEditor.stories.d.ts +1 -0
  29. package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +1 -1
  30. package/dist/types/src/translations.d.ts +1 -0
  31. package/dist/types/src/translations.d.ts.map +1 -1
  32. package/package.json +25 -27
  33. package/src/components/TextEditor/TextEditor.stories.tsx +16 -4
  34. package/src/components/TextEditor/TextEditor.tsx +13 -12
  35. package/src/components/Toolbar/Toolbar.stories.tsx +23 -24
  36. package/src/components/Toolbar/Toolbar.tsx +96 -35
  37. package/src/extensions/comments.ts +2 -2
  38. package/src/extensions/factories.ts +0 -3
  39. package/src/extensions/index.ts +1 -0
  40. package/src/extensions/markdown/formatting.test.ts +13 -13
  41. package/src/extensions/markdown/formatting.ts +91 -83
  42. package/src/extensions/markdown/image.ts +6 -0
  43. package/src/extensions/modes.ts +3 -1
  44. package/src/extensions/state.ts +90 -0
  45. package/src/hooks/useActionHandler.ts +5 -1
  46. package/src/hooks/useTextEditor.stories.tsx +3 -3
  47. package/src/hooks/useTextEditor.ts +3 -0
  48. package/src/translations.ts +1 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../../src/extensions/state.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,SAAS,EAAe,MAAM,mBAAmB,CAAC;AAYhE,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,SAAS,EAAE;QACT,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IACtD,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,cAAc,GAAG,SAAS,CAAC;CACtD,CAAC;AAGF,eAAO,MAAM,6BAA6B,EAAE,YAU3C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,KAAK,4BAA4B,QAAQ,YAAY,CAAC,KAAQ,SA0C1E,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"useActionHandler.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useActionHandler.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAqBlD,eAAO,MAAM,gBAAgB,UAAW,UAAU,GAAG,IAAI,KAAG,YAAY,CAAC,UAAU,CAiElF,CAAC"}
1
+ {"version":3,"file":"useActionHandler.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useActionHandler.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAqBlD,eAAO,MAAM,gBAAgB,UAAW,UAAU,GAAG,IAAI,KAAG,YAAY,CAAC,UAAU,CAqElF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"useTextEditor.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useTextEditor.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,SAAS,EAAwC,MAAM,OAAO,CAAC;AAKlG,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAIrD,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,EAAE,SAAS,CAAC,cAAc,CAAC,CAAC;IACrC,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,eAAe,EAAE,iBAAiB,GAAG,YAAY,CAAC,CAAC;AAEzF;;GAEG;AACH,eAAO,MAAM,aAAa,QAAQ,MAAM,kBAAkB,4BAA2C,aAyEpG,CAAC"}
1
+ {"version":3,"file":"useTextEditor.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useTextEditor.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,SAAS,EAAwC,MAAM,OAAO,CAAC;AAKlG,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAIrD,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,EAAE,SAAS,CAAC,cAAc,CAAC,CAAC;IACrC,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,eAAe,EAAE,iBAAiB,GAAG,YAAY,CAAC,CAAC;AAEzF;;GAEG;AACH,eAAO,MAAM,aAAa,QAAQ,MAAM,kBAAkB,4BAA2C,aA4EpG,CAAC"}
@@ -24,6 +24,7 @@ declare const _default: {
24
24
  'blockquote label': string;
25
25
  'codeblock label': string;
26
26
  'comment label': string;
27
+ 'image label': string;
27
28
  'heading label': string;
28
29
  'table label': string;
29
30
  };
@@ -1 +1 @@
1
- {"version":3,"file":"useTextEditor.stories.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useTextEditor.stories.tsx"],"names":[],"mappings":"AAIA,OAAO,YAAY,CAAC;AAwBpB,KAAK,UAAU,GAAG;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;;;;mBA2Ee,UAAU;;;;;;;;;;;;;;;;;;;;;;;;AAH3B,wBASE;AAEF,eAAO,MAAM,OAAO;;CAKnB,CAAC;AAEF,eAAO,MAAM,KAAK;;CAQjB,CAAC;AAEF,eAAO,MAAM,QAAQ;;;;;;CAMpB,CAAC"}
1
+ {"version":3,"file":"useTextEditor.stories.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useTextEditor.stories.tsx"],"names":[],"mappings":"AAIA,OAAO,YAAY,CAAC;AAwBpB,KAAK,UAAU,GAAG;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;;;;mBA2Ee,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;AAH3B,wBASE;AAEF,eAAO,MAAM,OAAO;;CAKnB,CAAC;AAEF,eAAO,MAAM,KAAK;;CAQjB,CAAC;AAEF,eAAO,MAAM,QAAQ;;;;;;CAMpB,CAAC"}
@@ -13,6 +13,7 @@ declare const _default: {
13
13
  'blockquote label': string;
14
14
  'codeblock label': string;
15
15
  'comment label': string;
16
+ 'image label': string;
16
17
  'heading label': string;
17
18
  'table label': string;
18
19
  };
@@ -1 +1 @@
1
- {"version":3,"file":"translations.d.ts","sourceRoot":"","sources":["../../../src/translations.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,cAAc,oBAAoB,CAAC;;;;;;;;;;;;;;;;;;;;AAEhD,wBAoBE"}
1
+ {"version":3,"file":"translations.d.ts","sourceRoot":"","sources":["../../../src/translations.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,cAAc,oBAAoB,CAAC;;;;;;;;;;;;;;;;;;;;;AAEhD,wBAqBE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-editor",
3
- "version": "0.4.8-main.ec0899a",
3
+ "version": "0.4.8-next.fff1521",
4
4
  "description": "Document editing experience within a DXOS shell.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -36,21 +36,22 @@
36
36
  "lodash.get": "^4.4.2",
37
37
  "lodash.pick": "^4.4.0",
38
38
  "lodash.sortby": "^4.7.0",
39
+ "react-dropzone": "^14.2.3",
39
40
  "style-mod": "^4.1.0",
40
- "@dxos/async": "0.4.8-main.ec0899a",
41
- "@dxos/context": "0.4.8-main.ec0899a",
42
- "@dxos/debug": "0.4.8-main.ec0899a",
43
- "@dxos/display-name": "0.4.8-main.ec0899a",
44
- "@dxos/automerge": "0.4.8-main.ec0899a",
45
- "@dxos/echo-schema": "0.4.8-main.ec0899a",
46
- "@dxos/invariant": "0.4.8-main.ec0899a",
47
- "@dxos/protocols": "0.4.8-main.ec0899a",
48
- "@dxos/log": "0.4.8-main.ec0899a",
49
- "@dxos/react-async": "0.4.8-main.ec0899a",
50
- "@dxos/react-ui": "0.4.8-main.ec0899a",
51
- "@dxos/text-model": "0.4.8-main.ec0899a",
52
- "@dxos/react-ui-theme": "0.4.8-main.ec0899a",
53
- "@dxos/util": "0.4.8-main.ec0899a"
41
+ "@dxos/async": "0.4.8-next.fff1521",
42
+ "@dxos/context": "0.4.8-next.fff1521",
43
+ "@dxos/debug": "0.4.8-next.fff1521",
44
+ "@dxos/display-name": "0.4.8-next.fff1521",
45
+ "@dxos/automerge": "0.4.8-next.fff1521",
46
+ "@dxos/echo-schema": "0.4.8-next.fff1521",
47
+ "@dxos/invariant": "0.4.8-next.fff1521",
48
+ "@dxos/protocols": "0.4.8-next.fff1521",
49
+ "@dxos/react-async": "0.4.8-next.fff1521",
50
+ "@dxos/react-ui": "0.4.8-next.fff1521",
51
+ "@dxos/react-ui-theme": "0.4.8-next.fff1521",
52
+ "@dxos/text-model": "0.4.8-next.fff1521",
53
+ "@dxos/log": "0.4.8-next.fff1521",
54
+ "@dxos/util": "0.4.8-next.fff1521"
54
55
  },
55
56
  "devDependencies": {
56
57
  "@automerge/automerge-repo-network-broadcastchannel": "^1.1.1",
@@ -68,7 +69,6 @@
68
69
  "@types/react-test-renderer": "^17.0.2",
69
70
  "@vitest/browser": "^1.3.1",
70
71
  "@vitest/ui": "^1.3.1",
71
- "deepsignal": "^1.5.0",
72
72
  "happy-dom": "^13.3.1",
73
73
  "jsdom": "^20.0.1",
74
74
  "react": "^18.2.0",
@@ -78,22 +78,20 @@
78
78
  "vite-plugin-top-level-await": "^1.4.1",
79
79
  "vite-plugin-wasm": "^3.3.0",
80
80
  "vitest": "^1.3.1",
81
- "@dxos/config": "0.4.8-main.ec0899a",
82
- "@dxos/echo-signals": "0.4.8-main.ec0899a",
83
- "@dxos/keyboard": "0.4.8-main.ec0899a",
84
- "@dxos/echo-typegen": "0.4.8-main.ec0899a",
85
- "@dxos/react-client": "0.4.8-main.ec0899a",
86
- "@dxos/react-ui": "0.4.8-main.ec0899a",
87
- "@dxos/storybook-utils": "0.4.8-main.ec0899a",
88
- "@dxos/random": "0.4.8-main.ec0899a"
81
+ "@dxos/config": "0.4.8-next.fff1521",
82
+ "@dxos/echo-signals": "0.4.8-next.fff1521",
83
+ "@dxos/echo-typegen": "0.4.8-next.fff1521",
84
+ "@dxos/random": "0.4.8-next.fff1521",
85
+ "@dxos/keyboard": "0.4.8-next.fff1521",
86
+ "@dxos/react-ui": "0.4.8-next.fff1521",
87
+ "@dxos/storybook-utils": "0.4.8-next.fff1521",
88
+ "@dxos/react-client": "0.4.8-next.fff1521"
89
89
  },
90
90
  "peerDependencies": {
91
91
  "@phosphor-icons/react": "^2.0.5",
92
- "@vitest/browser": "^1.3.1",
93
- "@vitest/ui": "^1.3.1",
94
92
  "react": "^18.2.0",
95
93
  "react-dom": "^18.2.0",
96
- "@dxos/react-client": "0.4.8-main.ec0899a"
94
+ "@dxos/react-client": "0.4.8-next.fff1521"
97
95
  },
98
96
  "publishConfig": {
99
97
  "access": "public"
@@ -20,6 +20,7 @@ import { withTheme } from '@dxos/storybook-utils';
20
20
 
21
21
  import { TextEditor, type TextEditorProps } from './TextEditor';
22
22
  import {
23
+ EditorModes,
23
24
  annotations,
24
25
  autocomplete,
25
26
  blast,
@@ -37,14 +38,15 @@ import {
37
38
  linkTooltip,
38
39
  listener,
39
40
  mention,
40
- useComments,
41
+ state,
41
42
  table,
42
43
  typewriter,
43
- EditorModes,
44
+ useComments,
44
45
  type CommandAction,
45
46
  type CommandOptions,
46
47
  type Comment,
47
48
  type CommentsOptions,
49
+ type SelectionState,
48
50
  } from '../../extensions';
49
51
  import { useDocAccessor } from '../../hooks';
50
52
  import translations from '../../translations';
@@ -241,7 +243,7 @@ type StoryProps = {
241
243
  } & Pick<TextEditorProps, 'extensions'>;
242
244
 
243
245
  const Story = ({
244
- id = 'test',
246
+ id = 'editor-' + PublicKey.random().toHex().slice(0, 8),
245
247
  text,
246
248
  comments,
247
249
  extensions: _extensions = [],
@@ -325,8 +327,18 @@ export const Empty = {
325
327
  render: () => <Story />,
326
328
  };
327
329
 
330
+ const global = new Map<string, SelectionState>();
331
+
328
332
  export const Scrolling = {
329
- render: () => <Story text={str('# Large Document', '', large)} />,
333
+ render: () => (
334
+ <Story
335
+ text={str('# Large Document', '', large)}
336
+ extensions={state({
337
+ setState: (id, state) => global.set(id, state),
338
+ getState: (id) => global.get(id),
339
+ })}
340
+ />
341
+ ),
330
342
  };
331
343
 
332
344
  export const ScrollingWithImages = {
@@ -18,7 +18,7 @@ import React, {
18
18
  import { log } from '@dxos/log';
19
19
  import { isNotFalsy } from '@dxos/util';
20
20
 
21
- import { documentId, editorMode } from '../../extensions';
21
+ import { documentId, editorMode, focusEvent } from '../../extensions';
22
22
  import { logChanges } from '../../util';
23
23
 
24
24
  export type CursorInfo = {
@@ -34,7 +34,7 @@ export type TextEditorProps = Pick<EditorStateConfig, 'doc' | 'selection' | 'ext
34
34
  id?: string;
35
35
  className?: string;
36
36
  autoFocus?: boolean;
37
- scrollTo?: StateEffect<any>; // TODO(burdon): Restore scroll position: scrollTo EditorView.scrollSnapshot().
37
+ scrollTo?: StateEffect<unknown>;
38
38
  moveToEndOfLine?: boolean;
39
39
  debug?: boolean;
40
40
  dataTestId?: string;
@@ -56,14 +56,14 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
56
56
  extensions,
57
57
  className,
58
58
  autoFocus,
59
- scrollTo = EditorView.scrollIntoView(0),
59
+ scrollTo = EditorView.scrollIntoView(0, { yMargin: 0 }),
60
60
  moveToEndOfLine,
61
61
  debug,
62
62
  dataTestId,
63
63
  },
64
64
  forwardedRef,
65
65
  ) => {
66
- // TODO(burdon): Increments by 2!
66
+ // NOTE: Increments by 2 in strict mode.
67
67
  const [instanceId] = useState(() => `text-editor-${++instanceCount}`);
68
68
 
69
69
  // TODO(burdon): Make tabster optional.
@@ -85,7 +85,7 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
85
85
  // Create editor state and view.
86
86
  // The view is recreated if the model or extensions are changed.
87
87
  useEffect(() => {
88
- log('updating', { id, instanceId });
88
+ log('create', { id, instanceId });
89
89
 
90
90
  //
91
91
  // EditorState
@@ -104,7 +104,7 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
104
104
  // Focus.
105
105
  EditorView.updateListener.of((update) => {
106
106
  update.transactions.forEach((transaction) => {
107
- if (transaction.isUserEvent('focus.container')) {
107
+ if (transaction.isUserEvent(focusEvent)) {
108
108
  rootRef.current?.focus();
109
109
  }
110
110
  });
@@ -122,6 +122,7 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
122
122
  state,
123
123
  parent: rootRef.current!,
124
124
  scrollTo,
125
+
125
126
  // NOTE: Uncomment to debug/monitor all transactions.
126
127
  // https://codemirror.net/docs/ref/#view.EditorView.dispatch
127
128
  dispatchTransactions: (trs, view) => {
@@ -132,12 +133,10 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
132
133
  },
133
134
  });
134
135
 
135
- setView(view);
136
-
137
- // Position cursor at end of line.
138
- if (moveToEndOfLine) {
136
+ // Position cursor at end of first line.
137
+ if (moveToEndOfLine && !(scrollTo || selection)) {
139
138
  const { to } = view.state.doc.lineAt(0);
140
- view?.dispatch({ selection: { anchor: to } });
139
+ view.dispatch({ selection: { anchor: to } });
141
140
  }
142
141
 
143
142
  // Remove tabster attribute (rely on custom keymap).
@@ -145,9 +144,11 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
145
144
  rootRef.current?.removeAttribute('data-tabster');
146
145
  }
147
146
 
147
+ setView(view);
148
+
148
149
  return () => {
150
+ log('destroy', { id, instanceId });
149
151
  view?.destroy();
150
- setView(null);
151
152
  };
152
153
  }, [doc, selection, extensions]);
153
154
 
@@ -16,17 +16,17 @@ import { withTheme } from '@dxos/storybook-utils';
16
16
  import { Toolbar } from './Toolbar';
17
17
  import {
18
18
  type Comment,
19
- decorateMarkdown,
20
19
  comments,
21
20
  createBasicExtensions,
22
21
  createDataExtensions,
22
+ createMarkdownExtensions,
23
23
  createThemeExtensions,
24
+ decorateMarkdown,
24
25
  formattingKeymap,
26
+ image,
25
27
  table,
26
28
  useComments,
27
29
  useFormattingState,
28
- createMarkdownExtensions,
29
- image,
30
30
  } from '../../extensions';
31
31
  import { useActionHandler, useDocAccessor, useTextEditor } from '../../hooks';
32
32
  import translations from '../../translations';
@@ -34,15 +34,17 @@ import translations from '../../translations';
34
34
  faker.seed(101);
35
35
 
36
36
  const Story: FC<{ content: string }> = ({ content }) => {
37
+ console.log('STORY');
37
38
  const { themeMode } = useThemeContext();
38
39
  const [item] = useState({ text: new TextObject(content) });
39
40
  const { id, doc, accessor } = useDocAccessor(item.text);
40
41
  const [formattingState, formattingObserver] = useFormattingState();
41
- const { parentRef, view } = useTextEditor(
42
- () => ({
42
+ const { parentRef, view } = useTextEditor(() => {
43
+ return {
43
44
  id,
44
45
  doc,
45
46
  extensions: [
47
+ formattingObserver,
46
48
  createBasicExtensions(),
47
49
  createMarkdownExtensions({ themeMode }),
48
50
  createThemeExtensions({ themeMode, slots: { editor: { className: 'p-2' } } }),
@@ -58,39 +60,36 @@ const Story: FC<{ content: string }> = ({ content }) => {
58
60
  formattingKeymap(),
59
61
  image(),
60
62
  table(),
61
- formattingObserver,
62
63
  ],
63
- }),
64
- [id, accessor, themeMode],
65
- );
64
+ };
65
+ }, [id, accessor, formattingObserver, themeMode]);
66
+
67
+ const handleAction = useActionHandler(view);
66
68
 
67
69
  const [_comments, setComments] = useState<Comment[]>([]);
68
70
  useComments(view, id, _comments);
69
71
 
70
- const handleAction = useActionHandler(view);
71
-
72
72
  return (
73
- <div role='none' className='fixed inset-0 flex flex-col'>
74
- <Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
75
- <Toolbar.Markdown />
76
- <Toolbar.Separator />
77
- <Toolbar.Extended />
78
- </Toolbar.Root>
79
- <div role='textbox' ref={parentRef} className={textBlockWidth} />;
80
- </div>
73
+ <Tooltip.Provider>
74
+ <div role='none' className='fixed inset-0 flex flex-col'>
75
+ <Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
76
+ <Toolbar.Markdown />
77
+ <Toolbar.Custom onUpload={async (file) => ({ url: file.name })} />
78
+ <Toolbar.Separator />
79
+ <Toolbar.Actions />
80
+ </Toolbar.Root>
81
+ <div ref={parentRef} className={textBlockWidth} />
82
+ </div>
83
+ </Tooltip.Provider>
81
84
  );
82
85
  };
83
86
 
84
87
  export default {
85
88
  title: 'react-ui-editor/Toolbar',
86
89
  component: Toolbar,
87
- render: (args: any) => (
88
- <Tooltip.Provider>
89
- <Story {...args} />
90
- </Tooltip.Provider>
91
- ),
92
90
  decorators: [withTheme],
93
91
  parameters: { translations, layout: 'fullscreen' },
92
+ render: (args: any) => <Story {...args} />,
94
93
  };
95
94
 
96
95
  const content = [
@@ -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,19 +24,20 @@ 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';
@@ -80,7 +83,7 @@ export type Action = {
80
83
 
81
84
  export type ToolbarProps = ThemedClassName<
82
85
  PropsWithChildren<{
83
- state: Formatting | null;
86
+ state: Formatting | undefined;
84
87
  onAction?: (action: Action) => void;
85
88
  }>
86
89
  >;
@@ -111,9 +114,9 @@ type ButtonProps = {
111
114
  disabled?: (state: Formatting) => boolean;
112
115
  };
113
116
 
114
- type ToolbarButtonProps = ToolbarToggleGroupItemProps & { Icon: Icon };
117
+ type ToolbarToggleButtonProps = NaturalToolbarToggleGroupItemProps & { Icon: Icon };
115
118
 
116
- const ToolbarButton = ({ Icon, children, ...props }: ToolbarButtonProps) => {
119
+ const ToolbarToggleButton = ({ Icon, children, ...props }: ToolbarToggleButtonProps) => {
117
120
  return (
118
121
  <Tooltip.Root>
119
122
  <Tooltip.Trigger asChild>
@@ -132,6 +135,27 @@ const ToolbarButton = ({ Icon, children, ...props }: ToolbarButtonProps) => {
132
135
  );
133
136
  };
134
137
 
138
+ type ToolbarButtonProps = NaturalToolbarButtonProps & { Icon: Icon };
139
+
140
+ const ToolbarButton = ({ Icon, children, ...props }: ToolbarButtonProps) => {
141
+ return (
142
+ <Tooltip.Root>
143
+ <Tooltip.Trigger asChild>
144
+ <NaturalToolbar.Button variant='ghost' {...props} classNames={buttonStyles}>
145
+ <Icon className={iconStyles} />
146
+ <span className='sr-only'>{children}</span>
147
+ </NaturalToolbar.Button>
148
+ </Tooltip.Trigger>
149
+ <Tooltip.Portal>
150
+ <Tooltip.Content {...tooltipProps}>
151
+ {children}
152
+ <Tooltip.Arrow />
153
+ </Tooltip.Content>
154
+ </Tooltip.Portal>
155
+ </Tooltip.Root>
156
+ );
157
+ };
158
+
135
159
  const ToolbarSeparator = () => <div role='separator' className='grow' />;
136
160
 
137
161
  const MarkdownHeading = () => {
@@ -227,7 +251,7 @@ const MarkdownStyles = () => {
227
251
  value={markdownStyles.filter(({ getState }) => state && getState(state)).map(({ type }) => type)}
228
252
  >
229
253
  {markdownStyles.map(({ type, getState, Icon }) => (
230
- <ToolbarButton
254
+ <ToolbarToggleButton
231
255
  key={type}
232
256
  value={type}
233
257
  Icon={Icon}
@@ -235,7 +259,7 @@ const MarkdownStyles = () => {
235
259
  onClick={state ? () => onAction?.({ type, data: !getState(state) }) : undefined}
236
260
  >
237
261
  {t(`${type} label`)}
238
- </ToolbarButton>
262
+ </ToolbarToggleButton>
239
263
  ))}
240
264
  </NaturalToolbar.ToggleGroup>
241
265
  );
@@ -253,14 +277,14 @@ const MarkdownLists = () => {
253
277
  return (
254
278
  <NaturalToolbar.ToggleGroup type='single' value={state?.listStyle ? `list-${state.listStyle}` : ''}>
255
279
  {markdownLists.map(({ type, getState, Icon }) => (
256
- <ToolbarButton
280
+ <ToolbarToggleButton
257
281
  key={type}
258
282
  value={type}
259
283
  Icon={Icon}
260
284
  onClick={state ? () => onAction?.({ type, data: !getState(state) }) : undefined}
261
285
  >
262
286
  {t(`${type} label`)}
263
- </ToolbarButton>
287
+ </ToolbarToggleButton>
264
288
  ))}
265
289
  </NaturalToolbar.ToggleGroup>
266
290
  );
@@ -292,7 +316,7 @@ const MarkdownBlocks = () => {
292
316
  return (
293
317
  <NaturalToolbar.ToggleGroup type='single' value={value?.type ?? ''}>
294
318
  {markdownBlocks.map(({ type, disabled, getState, Icon }) => (
295
- <ToolbarButton
319
+ <ToolbarToggleButton
296
320
  key={type}
297
321
  value={type}
298
322
  Icon={Icon}
@@ -300,7 +324,7 @@ const MarkdownBlocks = () => {
300
324
  onClick={state ? () => onAction?.({ type, data: !getState(state) }) : undefined}
301
325
  >
302
326
  {t(`${type} label`)}
303
- </ToolbarButton>
327
+ </ToolbarToggleButton>
304
328
  ))}
305
329
  </NaturalToolbar.ToggleGroup>
306
330
  );
@@ -315,38 +339,75 @@ const MarkdownStandard = () => (
315
339
  </>
316
340
  );
317
341
 
318
- const MarkdownExtended = () => {
342
+ // TODO(burdon): Make extensible.
343
+ export type MarkdownCustomOptions = {
344
+ onUpload?: (file: File) => Promise<{ url?: string }>;
345
+ };
346
+
347
+ const MarkdownCustom = ({ onUpload }: MarkdownCustomOptions = {}) => {
319
348
  const { onAction } = useToolbarContext('MarkdownStyles');
320
349
  const { t } = useTranslation(translationKey);
350
+ // https://react-dropzone.js.org/#src
351
+ const { acceptedFiles, getInputProps, open } = useDropzone({
352
+ multiple: false,
353
+ noDrag: true,
354
+ accept: {
355
+ 'image/*': ['.jpg', '.jpeg', '.png', '.gif'],
356
+ },
357
+ });
358
+
359
+ useEffect(() => {
360
+ if (onUpload && acceptedFiles.length) {
361
+ setTimeout(async () => {
362
+ // NOTE: Clone file since react-dropzone patches in a non-standard `path` property, which confuses IPFS.
363
+ const f = acceptedFiles[0];
364
+ const file = new File([f], f.name, {
365
+ type: f.type,
366
+ lastModified: f.lastModified,
367
+ });
368
+
369
+ const { url } = await onUpload(file);
370
+ if (url) {
371
+ onAction?.({ type: 'image', data: url });
372
+ }
373
+ });
374
+ }
375
+ }, [acceptedFiles]);
376
+
321
377
  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>
378
+ <>
379
+ <input {...getInputProps()} />
380
+ <ToolbarButton value='image' Icon={Image} onClick={() => open()}>
381
+ {t('image label')}
382
+ </ToolbarButton>
383
+ </>
384
+ );
385
+ };
386
+
387
+ // TODO(burdon): Make extensible.
388
+ const MarkdownActions = () => {
389
+ const { onAction } = useToolbarContext('MarkdownStyles');
390
+ const { t } = useTranslation(translationKey);
391
+ return (
392
+ <>
393
+ {/* TODO(burdon): Toggle readonly state. */}
394
+ {/* <ToolbarButton value='comment' Icon={BookOpenText} onClick={() => onAction?.({ type: 'comment' })}> */}
395
+ {/* {t('comment label')} */}
396
+ {/* </ToolbarButton> */}
397
+ <ToolbarButton value='comment' Icon={ChatText} onClick={() => onAction?.({ type: 'comment' })}>
398
+ {t('comment label')}
399
+ </ToolbarButton>
400
+ </>
341
401
  );
342
402
  };
343
403
 
344
404
  export const Toolbar = {
345
405
  Root: ToolbarRoot,
346
- Button: ToolbarButton,
406
+ Button: ToolbarToggleButton,
347
407
  Separator: ToolbarSeparator,
348
408
  Markdown: MarkdownStandard,
349
- Extended: MarkdownExtended,
409
+ Custom: MarkdownCustom,
410
+ Actions: MarkdownActions,
350
411
  };
351
412
 
352
413
  export { useToolbarContext };
@@ -508,14 +508,14 @@ 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 | undefined, id: string, comments: Comment[] = []) => {
511
+ export const useComments = (view: EditorView | null | undefined, id: string, comments?: Comment[]) => {
512
512
  useEffect(() => {
513
513
  if (view) {
514
514
  // Check same document.
515
515
  // NOTE: Hook might be called before editor state is updated.
516
516
  if (id === view.state.facet(documentId)) {
517
517
  view.dispatch({
518
- effects: setComments.of({ id, comments }),
518
+ effects: setComments.of({ id, comments: comments ?? [] }),
519
519
  });
520
520
  }
521
521
  }
@@ -116,9 +116,6 @@ const defaultSlots = {
116
116
  editor: {
117
117
  className: 'w-full bs-full',
118
118
  },
119
- content: {
120
- className: '!p-2',
121
- },
122
119
  };
123
120
 
124
121
  export const createThemeExtensions = ({ theme, themeMode, slots: _slots }: ThemeExtensionsOptions = {}): Extension => {
@@ -18,5 +18,6 @@ export * from './listener';
18
18
  export * from './markdown';
19
19
  export * from './mention';
20
20
  export * from './modes';
21
+ export * from './state';
21
22
  export * from './types';
22
23
  export * from './typewriter';