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

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 (54) hide show
  1. package/dist/lib/browser/index.mjs +362 -45
  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 +1 -2
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  7. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +1 -1
  8. package/dist/types/src/components/TextEditor/hooks.stories.d.ts +23 -0
  9. package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -0
  10. package/dist/types/src/components/Toolbar/Toolbar.d.ts +23 -7
  11. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  12. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +10 -1
  13. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
  14. package/dist/types/src/components/Toolbar/index.d.ts +2 -0
  15. package/dist/types/src/components/Toolbar/index.d.ts.map +1 -0
  16. package/dist/types/src/components/index.d.ts +1 -0
  17. package/dist/types/src/components/index.d.ts.map +1 -1
  18. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  19. package/dist/types/src/extensions/basic.d.ts +1 -1
  20. package/dist/types/src/extensions/basic.d.ts.map +1 -1
  21. package/dist/types/src/extensions/comments.d.ts +1 -1
  22. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  23. package/dist/types/src/extensions/markdown/bundle.d.ts +2 -2
  24. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  25. package/dist/types/src/extensions/markdown/formatting.d.ts +2 -0
  26. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  27. package/dist/types/src/hooks/index.d.ts +1 -0
  28. package/dist/types/src/hooks/index.d.ts.map +1 -1
  29. package/dist/types/src/hooks/useActionHandler.d.ts +2 -2
  30. package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -1
  31. package/dist/types/src/hooks/useEditorView.d.ts +17 -0
  32. package/dist/types/src/hooks/useEditorView.d.ts.map +1 -0
  33. package/dist/types/src/hooks/useTextEditor.d.ts +36 -14
  34. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  35. package/dist/types/src/themes/default.d.ts.map +1 -1
  36. package/package.json +26 -25
  37. package/src/components/TextEditor/MarkdownEditor.stories.tsx +2 -3
  38. package/src/components/TextEditor/TextEditor.tsx +23 -13
  39. package/src/components/TextEditor/comments.stories.tsx +3 -3
  40. package/src/components/TextEditor/hooks.stories.tsx +106 -0
  41. package/src/components/Toolbar/Toolbar.stories.tsx +7 -3
  42. package/src/components/Toolbar/Toolbar.tsx +120 -69
  43. package/src/components/Toolbar/index.ts +5 -0
  44. package/src/components/index.ts +1 -0
  45. package/src/extensions/automerge/update-automerge.ts +7 -7
  46. package/src/extensions/basic.ts +15 -13
  47. package/src/extensions/comments.ts +2 -2
  48. package/src/extensions/markdown/bundle.ts +10 -5
  49. package/src/extensions/markdown/formatting.ts +31 -2
  50. package/src/hooks/index.ts +1 -0
  51. package/src/hooks/useActionHandler.ts +22 -5
  52. package/src/hooks/useEditorView.ts +29 -0
  53. package/src/hooks/useTextEditor.ts +143 -19
  54. package/src/themes/default.ts +3 -1
@@ -5,7 +5,6 @@
5
5
  import {
6
6
  type Icon,
7
7
  At,
8
- Brain,
9
8
  CaretRight,
10
9
  ChatText,
11
10
  Code,
@@ -19,15 +18,17 @@ import {
19
18
  TextB,
20
19
  TextItalic,
21
20
  } from '@phosphor-icons/react';
22
- import React, { type FC } from 'react';
21
+ import { createContext } from '@radix-ui/react-context';
22
+ import React, { type PropsWithChildren } from 'react';
23
23
 
24
24
  import { Button, type ButtonProps, DensityProvider, Select } from '@dxos/react-ui';
25
25
  import { getSize } from '@dxos/react-ui-theme';
26
26
 
27
- export type Action =
27
+ // TODO(burdon): Revert to string to make extensible?
28
+ export type ActionType =
28
29
  | 'blockquote'
29
30
  | 'bold'
30
- | 'code'
31
+ | 'codeblock'
31
32
  | 'comment'
32
33
  | 'heading'
33
34
  | 'image'
@@ -41,75 +42,125 @@ export type Action =
41
42
  | 'strikethrough'
42
43
  | 'table';
43
44
 
44
- export type ToolbarProps = {
45
- onAction?: (action: { type: Action; data?: any }) => void;
45
+ export type Action = {
46
+ type: ActionType;
47
+ data?: any;
46
48
  };
47
49
 
48
- const IconButton: FC<{ Icon: Icon; title: string } & NonNullable<Pick<ButtonProps, 'onClick'>>> = ({
49
- Icon,
50
- title,
51
- onClick,
52
- }) => (
53
- <Button variant='ghost' classNames='p-2' onClick={onClick} title={title}>
54
- <Icon className={getSize(5)} />
55
- </Button>
56
- );
50
+ export type ToolbarProps = PropsWithChildren<{
51
+ onAction?: (action: Action) => void;
52
+ }>;
53
+
54
+ const [ToolbarContextProvider, useToolbarContext] = createContext<ToolbarProps>('Toolbar');
57
55
 
58
- // TODO(burdon): Update state based on current selection?
59
- export const Toolbar = ({ onAction }: ToolbarProps) => {
56
+ const ToolbarRoot = ({ children, onAction }: ToolbarProps) => {
60
57
  return (
61
- <DensityProvider density='fine'>
62
- <div role='toolbar' className='flex w-full p-2 items-center gap-4'>
63
- <div>
64
- <Select.Root
65
- defaultValue='0'
66
- onValueChange={(value) => onAction?.({ type: 'heading', data: parseInt(value) })}
67
- >
68
- <Select.TriggerButton classNames='w-[8rem]' />
69
- <Select.Portal>
70
- <Select.Content>
71
- <Select.Viewport>
72
- <Select.Option value='0'>Paragraph</Select.Option>
73
- {[1, 2, 3, 4, 5, 6].map((level) => (
74
- <Select.Option key={level} value={String(level)}>
75
- Heading {level}
76
- </Select.Option>
77
- ))}
78
- </Select.Viewport>
79
- </Select.Content>
80
- </Select.Portal>
81
- </Select.Root>
82
- </div>
83
- <div>
84
- <IconButton Icon={TextB} title='Bold' onClick={() => onAction?.({ type: 'bold' })} />
85
- <IconButton Icon={TextItalic} title='Italic' onClick={() => onAction?.({ type: 'italic' })} />
86
- <IconButton
87
- Icon={TextStrikethrough}
88
- title='Strike-through'
89
- onClick={() => onAction?.({ type: 'strikethrough' })}
90
- />
91
- </div>
92
- <div>
93
- <IconButton Icon={ListBullets} title='Bullet list' onClick={() => onAction?.({ type: 'list' })} />
94
- <IconButton Icon={ListNumbers} title='Numbered list' onClick={() => onAction?.({ type: 'list-ordered' })} />
95
- <IconButton Icon={ListChecks} title='Task list' onClick={() => onAction?.({ type: 'list-tasks' })} />
96
- </div>
97
- <div>
98
- <IconButton Icon={CaretRight} title='Block quite' onClick={() => onAction?.({ type: 'blockquote' })} />
99
- <IconButton Icon={Code} title='Code block' onClick={() => onAction?.({ type: 'code' })} />
100
- <IconButton Icon={Table} title='Table' onClick={() => onAction?.({ type: 'table' })} />
58
+ <ToolbarContextProvider onAction={onAction}>
59
+ <DensityProvider density='fine'>
60
+ <div role='toolbar' className='flex w-full shrink-0 p-2 gap-4 items-center whitespace-nowrap overflow-hidden'>
61
+ {children}
101
62
  </div>
102
- <div>
103
- <IconButton Icon={Link} title='Link' onClick={() => onAction?.({ type: 'link' })} />
104
- <IconButton Icon={At} title='Mention' onClick={() => onAction?.({ type: 'mention' })} />
105
- <IconButton Icon={Image} title='Image' onClick={() => onAction?.({ type: 'image' })} />
106
- </div>
107
- <div className='grow' />
108
- <div>
109
- <IconButton Icon={Brain} title='AI prompt' onClick={() => onAction?.({ type: 'prompt' })} />
110
- <IconButton Icon={ChatText} title='Create comment' onClick={() => onAction?.({ type: 'comment' })} />
111
- </div>
112
- </div>
113
- </DensityProvider>
63
+ </DensityProvider>
64
+ </ToolbarContextProvider>
114
65
  );
115
66
  };
67
+
68
+ type ToolbarButtonProps = {
69
+ Icon: Icon;
70
+ onClick: () => Action | undefined;
71
+ } & NonNullable<Pick<ButtonProps, 'title'>>;
72
+
73
+ const ToolbarButton = ({ Icon, onClick, title }: ToolbarButtonProps) => {
74
+ const { onAction } = useToolbarContext('ToolbarButton');
75
+ const handleClick = () => {
76
+ const action = onClick();
77
+ if (action) {
78
+ onAction?.(action);
79
+ }
80
+ };
81
+
82
+ return (
83
+ <Button variant='ghost' classNames='p-2' onClick={handleClick} title={title}>
84
+ <Icon className={getSize(5)} />
85
+ </Button>
86
+ );
87
+ };
88
+
89
+ const ToolbarSeparator = () => <div className='grow' />;
90
+
91
+ const MarkdownHeading = () => {
92
+ const { onAction } = useToolbarContext('MarkdownFormatting');
93
+ return (
94
+ <Select.Root defaultValue='0' onValueChange={(value) => onAction?.({ type: 'heading', data: parseInt(value) })}>
95
+ <Select.TriggerButton classNames='w-[8rem]' />
96
+ <Select.Portal>
97
+ <Select.Content>
98
+ <Select.Viewport>
99
+ <Select.Option value='0'>Paragraph</Select.Option>
100
+ {[1, 2, 3, 4, 5, 6].map((level) => (
101
+ <Select.Option key={level} value={String(level)}>
102
+ Heading {level}
103
+ </Select.Option>
104
+ ))}
105
+ </Select.Viewport>
106
+ </Select.Content>
107
+ </Select.Portal>
108
+ </Select.Root>
109
+ );
110
+ };
111
+
112
+ const MarkdownStyles = () => (
113
+ <div role='none'>
114
+ <ToolbarButton Icon={TextB} title='Bold' onClick={() => ({ type: 'bold' })} />
115
+ <ToolbarButton Icon={TextItalic} title='Italic' onClick={() => ({ type: 'italic' })} />
116
+ <ToolbarButton Icon={TextStrikethrough} title='Strike-through' onClick={() => ({ type: 'strikethrough' })} />
117
+ </div>
118
+ );
119
+
120
+ const MarkdownLists = () => (
121
+ <div role='none'>
122
+ <ToolbarButton Icon={ListBullets} title='Bullet list' onClick={() => ({ type: 'list' })} />
123
+ <ToolbarButton Icon={ListNumbers} title='Numbered list' onClick={() => ({ type: 'list-ordered' })} />
124
+ <ToolbarButton Icon={ListChecks} title='Task list' onClick={() => ({ type: 'list-tasks' })} />
125
+ </div>
126
+ );
127
+
128
+ const MarkdownBlocks = () => (
129
+ <div role='none'>
130
+ <ToolbarButton Icon={CaretRight} title='Block quite' onClick={() => ({ type: 'blockquote' })} />
131
+ <ToolbarButton Icon={Code} title='Code block' onClick={() => ({ type: 'codeblock' })} />
132
+ <ToolbarButton Icon={Table} title='Table' onClick={() => ({ type: 'table' })} />
133
+ </div>
134
+ );
135
+
136
+ const MarkdownLinks = () => (
137
+ <div role='none'>
138
+ <ToolbarButton Icon={Link} title='Link' onClick={() => ({ type: 'link' })} />
139
+ <ToolbarButton Icon={At} title='Mention' onClick={() => ({ type: 'mention' })} />
140
+ <ToolbarButton Icon={Image} title='Image' onClick={() => ({ type: 'image' })} />
141
+ </div>
142
+ );
143
+
144
+ const MarkdownStandard = () => (
145
+ <>
146
+ <MarkdownHeading />
147
+ <MarkdownStyles />
148
+ <MarkdownLists />
149
+ <MarkdownBlocks />
150
+ <MarkdownLinks />
151
+ </>
152
+ );
153
+
154
+ const MarkdownExtended = () => (
155
+ <Toolbar.Button Icon={ChatText} title='Create comment' onClick={() => ({ type: 'comment' })} />
156
+ );
157
+
158
+ export const Toolbar = {
159
+ Root: ToolbarRoot,
160
+ Button: ToolbarButton,
161
+ Separator: ToolbarSeparator,
162
+ Markdown: MarkdownStandard,
163
+ Extended: MarkdownExtended,
164
+ };
165
+
166
+ export { useToolbarContext };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './Toolbar';
@@ -3,3 +3,4 @@
3
3
  //
4
4
 
5
5
  export * from './TextEditor';
6
+ export * from './Toolbar';
@@ -4,7 +4,7 @@
4
4
  // Ref: https://github.com/automerge/automerge-codemirror
5
5
  //
6
6
 
7
- import { type EditorState, type StateField, type Transaction } from '@codemirror/state';
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
10
 
@@ -14,7 +14,7 @@ export const updateAutomerge = (
14
14
  field: StateField<State>,
15
15
  handle: IDocHandle,
16
16
  transactions: Transaction[],
17
- state: EditorState, // TODO(burdon): Just pass in the state field value.
17
+ state: EditorState, // TODO(burdon): Just pass in the state field value?
18
18
  ): Heads | undefined => {
19
19
  const { lastHeads, path } = state.field(field);
20
20
 
@@ -32,16 +32,16 @@ export const updateAutomerge = (
32
32
  }
33
33
 
34
34
  const newHeads = handle.changeAt(lastHeads, (doc: A.Doc<unknown>) => {
35
- const t: any[] = [];
35
+ const invertedTransactions: { from: number; del: number; insert: Text }[] = [];
36
36
  for (const tr of transactions) {
37
37
  tr.changes.iterChanges((fromA, toA, _fromB, _toB, insert) => {
38
- t.push({ fromA, toA, insert });
38
+ invertedTransactions.push({ from: fromA, del: toA - fromA, insert });
39
39
  });
40
40
  }
41
41
 
42
- // Apply in reverse order to properly apply range.
43
- t.reverse().forEach(({ fromA, toA, insert }) => {
44
- A.splice(doc, path.slice(), fromA, toA - fromA, insert.toString());
42
+ // TODO(burdon): Hack to apply in reverse order to properly apply range.
43
+ invertedTransactions.reverse().forEach(({ from, del, insert }) => {
44
+ A.splice(doc, path.slice(), from, del, insert.toString());
45
45
  });
46
46
  });
47
47
 
@@ -10,6 +10,7 @@ import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
10
10
  import { drawSelection, EditorView, highlightActiveLine, placeholder } from '@codemirror/view';
11
11
 
12
12
  import { type ThemeMode } from '@dxos/react-ui';
13
+ import { isNotFalsy } from '@dxos/util';
13
14
 
14
15
  import { type TextEditorProps } from '../components';
15
16
 
@@ -17,21 +18,22 @@ export type BasicBundleOptions = {
17
18
  themeMode?: ThemeMode;
18
19
  } & Pick<TextEditorProps, 'placeholder' | 'lineWrapping'>;
19
20
 
20
- export const basicBundle = ({
21
+ export const createBasicBundle = ({
21
22
  themeMode,
22
23
  placeholder: _placeholder,
23
24
  lineWrapping = true,
24
- }: BasicBundleOptions): Extension[] => [
25
- lineWrapping ? EditorView.lineWrapping : [],
25
+ }: BasicBundleOptions = {}): Extension[] =>
26
+ [
27
+ lineWrapping && EditorView.lineWrapping,
26
28
 
27
- // https://codemirror.net/docs/ref/#codemirror.minimalSetup
28
- bracketMatching(),
29
- closeBrackets(),
30
- drawSelection(),
31
- highlightActiveLine(),
32
- history(),
33
- _placeholder ? placeholder(_placeholder) : [],
29
+ // https://codemirror.net/docs/ref/#codemirror.minimalSetup
30
+ bracketMatching(),
31
+ closeBrackets(),
32
+ drawSelection(),
33
+ highlightActiveLine(),
34
+ history(),
35
+ _placeholder && placeholder(_placeholder),
34
36
 
35
- // https://github.com/codemirror/theme-one-dark
36
- themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
37
- ];
37
+ // https://github.com/codemirror/theme-one-dark
38
+ themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
39
+ ].filter(isNotFalsy);
@@ -45,7 +45,7 @@ export type CommentsState = {
45
45
  selection: SelectionState;
46
46
  };
47
47
 
48
- export const setFocus = (view: EditorView, id: string, center = true) => {
48
+ export const focusComment = (view: EditorView, id: string, center = true) => {
49
49
  const comment = view.state.field(commentsState).comments.find((range) => range.comment.id === id);
50
50
  if (!comment?.comment.cursor) {
51
51
  return;
@@ -311,7 +311,7 @@ export const createComment: Command = (view) => {
311
311
  // Update range.
312
312
  view.dispatch({
313
313
  effects: setSelection.of({ current: id }),
314
- selection: { anchor: from },
314
+ selection: { anchor: to },
315
315
  });
316
316
 
317
317
  return true;
@@ -21,13 +21,14 @@ import {
21
21
  } from '@codemirror/view';
22
22
 
23
23
  import type { ThemeMode } from '@dxos/react-ui';
24
+ import { isNotFalsy } from '@dxos/util';
24
25
 
25
26
  import { markdownHighlightStyle, markdownTagsExtensions } from './highlight';
26
27
  import { type TextEditorProps } from '../../components';
27
28
 
28
29
  export type MarkdownBundleOptions = {
29
30
  themeMode?: ThemeMode;
30
- } & Pick<TextEditorProps, 'placeholder'>;
31
+ } & Pick<TextEditorProps, 'placeholder' | 'lineWrapping'>;
31
32
 
32
33
  /**
33
34
  * Markdown bundle.
@@ -36,11 +37,15 @@ export type MarkdownBundleOptions = {
36
37
  * https://codemirror.net/docs/ref/#codemirror.basicSetup
37
38
  */
38
39
  // TODO(burdon): Add Composer here: https://codemirror.net/docs/community
39
- export const markdownBundle = ({ themeMode, placeholder: _placeholder }: MarkdownBundleOptions): Extension[] => {
40
+ export const createMarkdownExtensions = ({
41
+ themeMode,
42
+ placeholder: _placeholder,
43
+ lineWrapping = true,
44
+ }: MarkdownBundleOptions = {}): Extension[] => {
40
45
  return [
46
+ lineWrapping && EditorView.lineWrapping,
41
47
  EditorState.allowMultipleSelections.of(true),
42
48
  EditorState.tabSize.of(2),
43
- EditorView.lineWrapping,
44
49
 
45
50
  // https://github.com/codemirror/basic-setup
46
51
  bracketMatching(),
@@ -51,7 +56,7 @@ export const markdownBundle = ({ themeMode, placeholder: _placeholder }: Markdow
51
56
  highlightActiveLine(),
52
57
  history(),
53
58
  indentOnInput(),
54
- _placeholder ? placeholder(_placeholder) : [],
59
+ _placeholder && placeholder(_placeholder),
55
60
 
56
61
  // Main extension.
57
62
  // https://github.com/codemirror/lang-markdown
@@ -92,5 +97,5 @@ export const markdownBundle = ({ themeMode, placeholder: _placeholder }: Markdow
92
97
  // https://codemirror.net/docs/ref/#commands.standardKeymap
93
98
  ...standardKeymap,
94
99
  ]),
95
- ];
100
+ ].filter(isNotFalsy);
96
101
  };
@@ -2,6 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { snippet } from '@codemirror/autocomplete';
5
6
  import { syntaxTree } from '@codemirror/language';
6
7
  import { type Extension, RangeSetBuilder } from '@codemirror/state';
7
8
  import {
@@ -27,10 +28,10 @@ export const setHeading =
27
28
  const changes = [];
28
29
  for (const range of ranges) {
29
30
  const { number } = doc.lineAt(range.anchor);
30
- const { from } = doc.line(number);
31
+ const { from, to } = doc.line(number);
31
32
 
32
33
  // Check heading doesn't already exist.
33
- const line = doc.sliceString(from, from + 7);
34
+ const line = doc.sliceString(from, to);
34
35
  const [_, marks, spaces] = line.match(/(#+)(\s+)/) ?? [];
35
36
  const current = marks?.length ?? 0;
36
37
  if (level !== current) {
@@ -76,6 +77,34 @@ export const toggleStyle =
76
77
  return true;
77
78
  };
78
79
 
80
+ // TODO(burdon): Define and trigger snippets for codeblock, table, etc.
81
+ const snippets = {
82
+ codeblock: snippet(['```#{lang}', '\t#{}', '```'].join('\n')),
83
+ table: snippet(
84
+ ['| #{col1} | #{col2} |', '| ---- | ---- |', '| #{val1} | #{val2} |', '| #{val3} | #{val4} |'].join('\n'),
85
+ ),
86
+ };
87
+
88
+ export const insertCodeblock = (view: EditorView) => {
89
+ const {
90
+ selection: { main },
91
+ doc,
92
+ } = view.state;
93
+ const { number } = doc.lineAt(main.anchor);
94
+ const { from } = doc.line(number);
95
+ snippets.codeblock(view, null, from, from);
96
+ };
97
+
98
+ export const insertTable = (view: EditorView) => {
99
+ const {
100
+ selection: { main },
101
+ doc,
102
+ } = view.state;
103
+ const { number } = doc.lineAt(main.anchor);
104
+ const { from } = doc.line(number);
105
+ snippets.table(view, null, from, from);
106
+ };
107
+
79
108
  export const toggleBold = toggleStyle('**');
80
109
  export const toggleItalic = toggleStyle('_');
81
110
  export const toggleStrikethrough = toggleStyle('~~');
@@ -6,5 +6,6 @@ export * from './awareness-provider';
6
6
  export * from './defs';
7
7
 
8
8
  export * from './useActionHandler';
9
+ export * from './useEditorView';
9
10
  export * from './useTextEditor';
10
11
  export * from './useTextModel';
@@ -4,10 +4,19 @@
4
4
 
5
5
  import type { EditorView } from '@codemirror/view';
6
6
 
7
- import type { ToolbarProps } from '../components/Toolbar/Toolbar';
8
- import { createComment, setHeading, toggleBold, toggleItalic, toggleList, toggleStrikethrough } from '../extensions';
9
-
10
- export const useActionHandler = (view: EditorView | null): ToolbarProps['onAction'] => {
7
+ import type { ToolbarProps } from '../components';
8
+ import {
9
+ createComment,
10
+ insertCodeblock,
11
+ insertTable,
12
+ setHeading,
13
+ toggleBold,
14
+ toggleItalic,
15
+ toggleList,
16
+ toggleStrikethrough,
17
+ } from '../extensions';
18
+
19
+ export const useActionHandler = (view?: EditorView | null): ToolbarProps['onAction'] => {
11
20
  return (action) => {
12
21
  if (!view) {
13
22
  return;
@@ -32,14 +41,22 @@ export const useActionHandler = (view: EditorView | null): ToolbarProps['onActio
32
41
  toggleList(view);
33
42
  break;
34
43
 
44
+ case 'codeblock':
45
+ insertCodeblock(view);
46
+ break;
47
+ case 'table':
48
+ insertTable(view);
49
+ break;
50
+
35
51
  case 'comment':
36
52
  createComment(view);
37
53
  break;
38
54
  }
39
55
 
40
- // TODO(burdon): Hack since otherwise focus doesn't leave heading selector.
56
+ // TODO(burdon): Hack otherwise remains on heading selector.
41
57
  setTimeout(() => {
42
58
  view.focus();
59
+ view.dispatch({ selection: view.state.selection });
43
60
  });
44
61
  };
45
62
  };
@@ -0,0 +1,29 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type EditorView } from '@codemirror/view';
6
+ import { type RefCallback, useState } from 'react';
7
+
8
+ /**
9
+ * Hook for accessing the editor view.
10
+ *
11
+ * ```tsx
12
+ * const Test = () => {
13
+ * const [editorRef, editorView] = useEditorView();
14
+ * useEffect(() => {
15
+ * editorView?.focus();
16
+ * }, [editorView]);
17
+ * return <TextEditor ref={editorRef} />;
18
+ * };
19
+ * ```
20
+ */
21
+ export const useEditorView = (): [RefCallback<EditorView | null>, EditorView | null] => {
22
+ const [view, setView] = useState<EditorView | null>(null);
23
+ return [
24
+ (ref: EditorView | null) => {
25
+ setView(ref);
26
+ },
27
+ view,
28
+ ];
29
+ };