@dxos/react-ui-editor 0.4.8-main.c67d72c → 0.4.8-main.cd60bd2

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 (71) hide show
  1. package/dist/lib/browser/index.mjs +1926 -1743
  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 +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.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/dnd.d.ts +7 -1
  16. package/dist/types/src/extensions/dnd.d.ts.map +1 -1
  17. package/dist/types/src/extensions/factories.d.ts +5 -1
  18. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  19. package/dist/types/src/extensions/index.d.ts +1 -1
  20. package/dist/types/src/extensions/index.d.ts.map +1 -1
  21. package/dist/types/src/extensions/markdown/action.d.ts +9 -0
  22. package/dist/types/src/extensions/markdown/action.d.ts.map +1 -0
  23. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  24. package/dist/types/src/extensions/markdown/formatting.d.ts +7 -4
  25. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  26. package/dist/types/src/extensions/markdown/image.d.ts +6 -0
  27. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -1
  28. package/dist/types/src/extensions/markdown/index.d.ts +1 -0
  29. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  30. package/dist/types/src/extensions/modes.d.ts +2 -1
  31. package/dist/types/src/extensions/modes.d.ts.map +1 -1
  32. package/dist/types/src/extensions/state.d.ts +20 -0
  33. package/dist/types/src/extensions/state.d.ts.map +1 -0
  34. package/dist/types/src/hooks/useActionHandler.d.ts +2 -2
  35. package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -1
  36. package/dist/types/src/hooks/useDocAccessor.d.ts +4 -2
  37. package/dist/types/src/hooks/useDocAccessor.d.ts.map +1 -1
  38. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  39. package/dist/types/src/hooks/useTextEditor.stories.d.ts +1 -0
  40. package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +1 -1
  41. package/dist/types/src/themes/default.d.ts.map +1 -1
  42. package/dist/types/src/translations.d.ts +1 -0
  43. package/dist/types/src/translations.d.ts.map +1 -1
  44. package/package.json +26 -24
  45. package/src/components/TextEditor/TextEditor.stories.tsx +30 -6
  46. package/src/components/TextEditor/TextEditor.tsx +13 -12
  47. package/src/components/Toolbar/Toolbar.stories.tsx +23 -24
  48. package/src/components/Toolbar/Toolbar.tsx +102 -60
  49. package/src/extensions/automerge/automerge.ts +4 -4
  50. package/src/extensions/command/state.ts +2 -2
  51. package/src/extensions/comments.ts +2 -2
  52. package/src/extensions/dnd.ts +31 -9
  53. package/src/extensions/factories.ts +41 -15
  54. package/src/extensions/index.ts +1 -1
  55. package/src/extensions/markdown/action.ts +112 -0
  56. package/src/extensions/markdown/bundle.ts +8 -11
  57. package/src/extensions/markdown/formatting.test.ts +13 -13
  58. package/src/extensions/markdown/formatting.ts +95 -87
  59. package/src/extensions/markdown/image.ts +6 -0
  60. package/src/extensions/markdown/index.ts +1 -0
  61. package/src/extensions/modes.ts +11 -2
  62. package/src/extensions/state.ts +90 -0
  63. package/src/hooks/useActionHandler.ts +4 -85
  64. package/src/hooks/useDocAccessor.ts +8 -2
  65. package/src/hooks/useTextEditor.stories.tsx +3 -3
  66. package/src/hooks/useTextEditor.ts +3 -0
  67. package/src/themes/default.ts +24 -8
  68. package/src/translations.ts +1 -0
  69. package/dist/types/src/extensions/cursor-line-margin.d.ts +0 -7
  70. package/dist/types/src/extensions/cursor-line-margin.d.ts.map +0 -1
  71. package/src/extensions/cursor-line-margin.ts +0 -94
@@ -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 };
@@ -60,16 +60,16 @@ export const automerge = (accessor: DocAccessor): Extension => {
60
60
  ViewPlugin.fromClass(
61
61
  class {
62
62
  constructor(private readonly _view: EditorView) {
63
- accessor.handle.addListener('change', this._handleChange.bind(this));
63
+ accessor.handle.addListener('change', this._handleChange);
64
64
  }
65
65
 
66
66
  destroy() {
67
- accessor.handle.removeListener('change', this._handleChange.bind(this));
67
+ accessor.handle.removeListener('change', this._handleChange);
68
68
  }
69
69
 
70
- _handleChange() {
70
+ readonly _handleChange = () => {
71
71
  syncer.reconcile(this._view, false);
72
- }
72
+ };
73
73
  },
74
74
  ),
75
75
 
@@ -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
  };
@@ -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
  }
@@ -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
  };
@@ -2,16 +2,18 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { closeBrackets } from '@codemirror/autocomplete';
6
- import { history } from '@codemirror/commands';
5
+ import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
6
+ import { defaultKeymap, history, historyKeymap, indentWithTab, standardKeymap } from '@codemirror/commands';
7
7
  import { bracketMatching } from '@codemirror/language';
8
+ import { searchKeymap } from '@codemirror/search';
8
9
  import { EditorState, type Extension } from '@codemirror/state';
9
10
  import {
10
11
  EditorView,
11
- crosshairCursor,
12
+ type KeyBinding,
12
13
  drawSelection,
13
14
  dropCursor,
14
15
  highlightActiveLine,
16
+ keymap,
15
17
  lineNumbers,
16
18
  placeholder,
17
19
  scrollPastEnd,
@@ -32,8 +34,6 @@ import { awareness, SpaceAwarenessProvider } from './awareness';
32
34
  import { type ThemeStyles } from '../styles';
33
35
  import { defaultTheme } from '../themes';
34
36
 
35
- // TODO(burdon): Move into extensions folder.
36
-
37
37
  //
38
38
  // Basic
39
39
  //
@@ -41,39 +41,52 @@ import { defaultTheme } from '../themes';
41
41
  /**
42
42
  * https://codemirror.net/docs/extensions
43
43
  * https://github.com/codemirror/basic-setup
44
+ * https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts
44
45
  */
45
- // TODO(burdon): Reconcile with createMarkdownExtensions.
46
46
  export type BasicExtensionsOptions = {
47
47
  allowMultipleSelections?: boolean;
48
48
  bracketMatching?: boolean;
49
49
  closeBrackets?: boolean;
50
- crosshairCursor?: boolean;
51
50
  dropCursor?: boolean;
52
51
  drawSelection?: boolean;
53
52
  editable?: boolean;
54
53
  highlightActiveLine?: boolean;
55
54
  history?: boolean;
55
+ indentWithTab?: boolean;
56
+ keymap?: null | 'default' | 'standard';
56
57
  lineNumbers?: boolean;
57
58
  lineWrapping?: boolean;
58
59
  placeholder?: string;
59
60
  readonly?: boolean;
61
+ search?: boolean;
60
62
  scrollPastEnd?: boolean;
63
+ standardKeymap?: boolean;
61
64
  tabSize?: number;
62
65
  };
63
66
 
64
- const defaults: BasicExtensionsOptions = {
67
+ const defaultBasicOptions: BasicExtensionsOptions = {
68
+ allowMultipleSelections: true,
65
69
  bracketMatching: true,
66
70
  closeBrackets: true,
67
71
  drawSelection: true,
68
72
  editable: true,
69
73
  history: true,
74
+ keymap: 'standard',
70
75
  lineWrapping: true,
76
+ search: true,
77
+ };
78
+
79
+ const keymaps: { [key: string]: readonly KeyBinding[] } = {
80
+ // https://codemirror.net/docs/ref/#commands.standardKeymap
81
+ standard: standardKeymap,
82
+ // https://codemirror.net/docs/ref/#commands.defaultKeymap
83
+ default: defaultKeymap,
71
84
  };
72
85
 
73
86
  export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extension => {
74
- const props: BasicExtensionsOptions = defaultsDeep({}, _props, defaults);
87
+ const props: BasicExtensionsOptions = defaultsDeep({}, _props, defaultBasicOptions);
75
88
  return [
76
- // TODO(burdon): Doesn't catch errors in keymap functions.
89
+ // NOTE: Doesn't catch errors in keymap functions.
77
90
  EditorView.exceptionSink.of((err) => {
78
91
  log.catch(err);
79
92
  }),
@@ -81,7 +94,6 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
81
94
  props.allowMultipleSelections && EditorState.allowMultipleSelections.of(true),
82
95
  props.bracketMatching && bracketMatching(),
83
96
  props.closeBrackets && closeBrackets(),
84
- props.crosshairCursor && crosshairCursor(),
85
97
  props.dropCursor && dropCursor(),
86
98
  props.drawSelection && drawSelection(),
87
99
  props.highlightActiveLine && highlightActiveLine(),
@@ -92,6 +104,21 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
92
104
  props.readonly && [EditorState.readOnly.of(true), EditorView.editable.of(false)],
93
105
  props.scrollPastEnd && scrollPastEnd(),
94
106
  props.tabSize && EditorState.tabSize.of(props.tabSize),
107
+
108
+ // https://codemirror.net/docs/ref/#view.KeyBinding
109
+ keymap.of(
110
+ [
111
+ ...((props.keymap && keymaps[props.keymap]) ?? []),
112
+ // https://codemirror.net/docs/ref/#commands.indentWithTab
113
+ ...(props.indentWithTab ? [indentWithTab] : []),
114
+ // https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
115
+ ...(props.closeBrackets ? closeBracketsKeymap : []),
116
+ // https://codemirror.net/docs/ref/#commands.historyKeymap
117
+ ...(props.history ? historyKeymap : []),
118
+ // https://codemirror.net/docs/ref/#search.searchKeymap
119
+ ...(props.search ? searchKeymap : []),
120
+ ].filter(isNotFalsy),
121
+ ),
95
122
  ].filter(isNotFalsy);
96
123
  };
97
124
 
@@ -112,14 +139,14 @@ export type ThemeExtensionsOptions = {
112
139
  };
113
140
  };
114
141
 
115
- const defaultSlots = {
142
+ const defaultThemeSlots = {
116
143
  editor: {
117
144
  className: 'w-full bs-full',
118
145
  },
119
146
  };
120
147
 
121
148
  export const createThemeExtensions = ({ theme, themeMode, slots: _slots }: ThemeExtensionsOptions = {}): Extension => {
122
- const slots = defaultsDeep({}, _slots, defaultSlots);
149
+ const slots = defaultsDeep({}, _slots, defaultThemeSlots);
123
150
  return [
124
151
  EditorView.baseTheme(defaultTheme),
125
152
  EditorView.darkTheme.of(themeMode === 'dark'),
@@ -135,12 +162,11 @@ export const createThemeExtensions = ({ theme, themeMode, slots: _slots }: Theme
135
162
 
136
163
  export type DataExtensionsProps = {
137
164
  id: string;
138
- text: DocAccessor; // TODO(burdon): Rename content.
165
+ text: DocAccessor;
139
166
  space?: Space;
140
167
  identity?: Identity | null;
141
168
  };
142
169
 
143
- // TODO(burdon): Factor out automerge defs and extension (not hook).
144
170
  // TODO(burdon): Move out of react-ui-editor (remove echo deps).
145
171
  export const createDataExtensions = ({ id, text, space, identity }: DataExtensionsProps): Extension[] => {
146
172
  const extensions: Extension[] = [automerge(text)];
@@ -10,7 +10,6 @@ export * from './blast';
10
10
  export * from './command';
11
11
  export * from './comments';
12
12
  export * from './cursor';
13
- export * from './cursor-line-margin';
14
13
  export * from './doc';
15
14
  export * from './dnd';
16
15
  export * from './factories';
@@ -18,5 +17,6 @@ export * from './listener';
18
17
  export * from './markdown';
19
18
  export * from './mention';
20
19
  export * from './modes';
20
+ export * from './state';
21
21
  export * from './types';
22
22
  export * from './typewriter';
@@ -0,0 +1,112 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type EditorView } from '@codemirror/view';
6
+
7
+ import {
8
+ Inline,
9
+ List,
10
+ addBlockquote,
11
+ addCodeblock,
12
+ addLink,
13
+ addList,
14
+ insertTable,
15
+ removeBlockquote,
16
+ removeCodeblock,
17
+ removeLink,
18
+ removeList,
19
+ setHeading,
20
+ setStyle,
21
+ toggleBlockquote,
22
+ toggleList,
23
+ toggleStyle,
24
+ } from './formatting';
25
+ import { createComment } from '../comments';
26
+
27
+ export type ActionType =
28
+ | 'blockquote'
29
+ | 'strong'
30
+ | 'codeblock'
31
+ | 'comment'
32
+ | 'heading'
33
+ | 'image'
34
+ | 'emphasis'
35
+ | 'code'
36
+ | 'link'
37
+ | 'list-bullet'
38
+ | 'list-ordered'
39
+ | 'list-task'
40
+ | 'mention'
41
+ | 'prompt'
42
+ | 'strikethrough'
43
+ | 'table';
44
+
45
+ export type Action = {
46
+ type: ActionType;
47
+ data?: any;
48
+ };
49
+
50
+ export type ActionHandler = (view: EditorView, action: Action) => void;
51
+
52
+ export const processAction: ActionHandler = (view, action) => {
53
+ let inlineType, listType;
54
+ switch (action.type) {
55
+ case 'heading':
56
+ setHeading(parseInt(action.data))(view);
57
+ break;
58
+
59
+ case 'strong':
60
+ case 'emphasis':
61
+ case 'strikethrough':
62
+ case 'code':
63
+ inlineType =
64
+ action.type === 'strong'
65
+ ? Inline.Strong
66
+ : action.type === 'emphasis'
67
+ ? Inline.Emphasis
68
+ : action.type === 'strikethrough'
69
+ ? Inline.Strikethrough
70
+ : Inline.Code;
71
+ (typeof action.data === 'boolean' ? setStyle(inlineType, action.data) : toggleStyle(inlineType))(view);
72
+ break;
73
+
74
+ case 'list-ordered':
75
+ case 'list-bullet':
76
+ case 'list-task':
77
+ listType =
78
+ action.type === 'list-ordered' ? List.Ordered : action.type === 'list-bullet' ? List.Bullet : List.Task;
79
+ (action.data === false ? removeList(listType) : action.data === true ? addList(listType) : toggleList(listType))(
80
+ view,
81
+ );
82
+ break;
83
+
84
+ case 'blockquote':
85
+ (action.data === false ? removeBlockquote : action.data === true ? addBlockquote : toggleBlockquote)(view);
86
+ break;
87
+ case 'codeblock':
88
+ (action.data === false ? removeCodeblock : addCodeblock)(view);
89
+ break;
90
+ case 'table':
91
+ insertTable(view);
92
+ break;
93
+
94
+ case 'link':
95
+ (action.data === false ? removeLink : addLink())(view);
96
+ break;
97
+
98
+ case 'image':
99
+ addLink({ url: action.data, image: true })(view);
100
+ break;
101
+
102
+ case 'comment':
103
+ createComment(view);
104
+ break;
105
+ }
106
+
107
+ requestAnimationFrame(() => {
108
+ if (!view.hasFocus) {
109
+ view.focus();
110
+ }
111
+ });
112
+ };
@@ -2,12 +2,12 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { closeBracketsKeymap } from '@codemirror/autocomplete';
6
- import { historyKeymap, indentWithTab, standardKeymap } from '@codemirror/commands';
5
+ import { completionKeymap } from '@codemirror/autocomplete';
6
+ import { defaultKeymap, indentWithTab } from '@codemirror/commands';
7
7
  import { markdownLanguage, markdown } from '@codemirror/lang-markdown';
8
8
  import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
9
9
  import { languages } from '@codemirror/language-data';
10
- import { searchKeymap } from '@codemirror/search';
10
+ import { lintKeymap } from '@codemirror/lint';
11
11
  import { type Extension } from '@codemirror/state';
12
12
  import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
13
13
  import { keymap } from '@codemirror/view';
@@ -60,14 +60,11 @@ export const createMarkdownExtensions = ({ themeMode }: MarkdownBundleOptions =
60
60
  keymap.of([
61
61
  // https://codemirror.net/docs/ref/#commands.indentWithTab
62
62
  indentWithTab,
63
- // https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
64
- ...closeBracketsKeymap,
65
- // https://codemirror.net/docs/ref/#commands.historyKeymap
66
- ...historyKeymap,
67
- // https://codemirror.net/docs/ref/#search.searchKeymap
68
- ...searchKeymap,
69
- // https://codemirror.net/docs/ref/#commands.standardKeymap
70
- ...standardKeymap,
63
+
64
+ // https://codemirror.net/docs/ref/#commands.defaultKeymap
65
+ ...defaultKeymap,
66
+ ...completionKeymap,
67
+ ...lintKeymap,
71
68
  ]),
72
69
  ];
73
70
  };