@dxos/react-ui-editor 0.4.8-next.fff1521 → 0.4.9-main.2d20855

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 (55) hide show
  1. package/dist/lib/browser/index.mjs +1687 -1690
  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.stories.d.ts +3 -0
  5. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/Toolbar/Toolbar.d.ts +1 -6
  7. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  8. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +3 -0
  9. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
  10. package/dist/types/src/extensions/automerge/automerge.stories.d.ts +3 -0
  11. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  12. package/dist/types/src/extensions/dnd.d.ts +7 -1
  13. package/dist/types/src/extensions/dnd.d.ts.map +1 -1
  14. package/dist/types/src/extensions/factories.d.ts +5 -1
  15. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  16. package/dist/types/src/extensions/index.d.ts +0 -1
  17. package/dist/types/src/extensions/index.d.ts.map +1 -1
  18. package/dist/types/src/extensions/markdown/action.d.ts +9 -0
  19. package/dist/types/src/extensions/markdown/action.d.ts.map +1 -0
  20. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  21. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  22. package/dist/types/src/extensions/markdown/index.d.ts +1 -0
  23. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  24. package/dist/types/src/extensions/modes.d.ts +1 -1
  25. package/dist/types/src/extensions/modes.d.ts.map +1 -1
  26. package/dist/types/src/hooks/useActionHandler.d.ts +2 -2
  27. package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -1
  28. package/dist/types/src/hooks/useDocAccessor.d.ts +4 -2
  29. package/dist/types/src/hooks/useDocAccessor.d.ts.map +1 -1
  30. package/dist/types/src/hooks/useTextEditor.stories.d.ts +3 -0
  31. package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +1 -1
  32. package/dist/types/src/themes/default.d.ts.map +1 -1
  33. package/dist/types/src/translations.d.ts +3 -0
  34. package/dist/types/src/translations.d.ts.map +1 -1
  35. package/package.json +25 -24
  36. package/src/components/TextEditor/TextEditor.stories.tsx +14 -2
  37. package/src/components/Toolbar/Toolbar.stories.tsx +0 -1
  38. package/src/components/Toolbar/Toolbar.tsx +46 -59
  39. package/src/extensions/automerge/automerge.ts +4 -4
  40. package/src/extensions/command/state.ts +2 -2
  41. package/src/extensions/dnd.ts +31 -9
  42. package/src/extensions/factories.ts +41 -15
  43. package/src/extensions/index.ts +0 -1
  44. package/src/extensions/markdown/action.ts +112 -0
  45. package/src/extensions/markdown/bundle.ts +8 -11
  46. package/src/extensions/markdown/formatting.ts +11 -11
  47. package/src/extensions/markdown/index.ts +1 -0
  48. package/src/extensions/modes.ts +8 -1
  49. package/src/hooks/useActionHandler.ts +4 -89
  50. package/src/hooks/useDocAccessor.ts +8 -2
  51. package/src/themes/default.ts +24 -8
  52. package/src/translations.ts +3 -0
  53. package/dist/types/src/extensions/cursor-line-margin.d.ts +0 -7
  54. package/dist/types/src/extensions/cursor-line-margin.d.ts.map +0 -1
  55. package/src/extensions/cursor-line-margin.ts +0 -94
@@ -16,6 +16,9 @@ declare const _default: {
16
16
  'image label': string;
17
17
  'heading label': string;
18
18
  'table label': string;
19
+ 'heading level label_zero': string;
20
+ 'heading level label_one': string;
21
+ 'heading level label_other': string;
19
22
  };
20
23
  };
21
24
  }[];
@@ -1 +1 @@
1
- {"version":3,"file":"translations.d.ts","sourceRoot":"","sources":["../../../src/translations.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,cAAc,oBAAoB,CAAC;;;;;;;;;;;;;;;;;;;;;AAEhD,wBAqBE"}
1
+ {"version":3,"file":"translations.d.ts","sourceRoot":"","sources":["../../../src/translations.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,cAAc,oBAAoB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;AAEhD,wBAwBE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-editor",
3
- "version": "0.4.8-next.fff1521",
3
+ "version": "0.4.9-main.2d20855",
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",
@@ -30,6 +30,7 @@
30
30
  "@lezer/markdown": "^1.2.0",
31
31
  "@radix-ui/react-context": "^1.0.0",
32
32
  "@replit/codemirror-vim": "^6.0.14",
33
+ "@replit/codemirror-vscode-keymap": "^6.0.2",
33
34
  "codemirror": "^6.0.1",
34
35
  "lib0": "^0.2.65",
35
36
  "lodash.defaultsdeep": "^4.6.1",
@@ -38,20 +39,20 @@
38
39
  "lodash.sortby": "^4.7.0",
39
40
  "react-dropzone": "^14.2.3",
40
41
  "style-mod": "^4.1.0",
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"
42
+ "@dxos/async": "0.4.9-main.2d20855",
43
+ "@dxos/debug": "0.4.9-main.2d20855",
44
+ "@dxos/automerge": "0.4.9-main.2d20855",
45
+ "@dxos/context": "0.4.9-main.2d20855",
46
+ "@dxos/display-name": "0.4.9-main.2d20855",
47
+ "@dxos/echo-schema": "0.4.9-main.2d20855",
48
+ "@dxos/invariant": "0.4.9-main.2d20855",
49
+ "@dxos/log": "0.4.9-main.2d20855",
50
+ "@dxos/react-async": "0.4.9-main.2d20855",
51
+ "@dxos/protocols": "0.4.9-main.2d20855",
52
+ "@dxos/react-ui": "0.4.9-main.2d20855",
53
+ "@dxos/react-ui-theme": "0.4.9-main.2d20855",
54
+ "@dxos/text-model": "0.4.9-main.2d20855",
55
+ "@dxos/util": "0.4.9-main.2d20855"
55
56
  },
56
57
  "devDependencies": {
57
58
  "@automerge/automerge-repo-network-broadcastchannel": "^1.1.1",
@@ -78,20 +79,20 @@
78
79
  "vite-plugin-top-level-await": "^1.4.1",
79
80
  "vite-plugin-wasm": "^3.3.0",
80
81
  "vitest": "^1.3.1",
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"
82
+ "@dxos/config": "0.4.9-main.2d20855",
83
+ "@dxos/echo-typegen": "0.4.9-main.2d20855",
84
+ "@dxos/echo-signals": "0.4.9-main.2d20855",
85
+ "@dxos/keyboard": "0.4.9-main.2d20855",
86
+ "@dxos/random": "0.4.9-main.2d20855",
87
+ "@dxos/react-client": "0.4.9-main.2d20855",
88
+ "@dxos/react-ui": "0.4.9-main.2d20855",
89
+ "@dxos/storybook-utils": "0.4.9-main.2d20855"
89
90
  },
90
91
  "peerDependencies": {
91
92
  "@phosphor-icons/react": "^2.0.5",
92
93
  "react": "^18.2.0",
93
94
  "react-dom": "^18.2.0",
94
- "@dxos/react-client": "0.4.8-next.fff1521"
95
+ "@dxos/react-client": "0.4.9-main.2d20855"
95
96
  },
96
97
  "publishConfig": {
97
98
  "access": "public"
@@ -13,6 +13,7 @@ import { createRoot } from 'react-dom/client';
13
13
  import { TextObject } from '@dxos/echo-schema';
14
14
  import { keySymbols, parseShortcut } from '@dxos/keyboard';
15
15
  import { PublicKey } from '@dxos/keys';
16
+ import { log } from '@dxos/log';
16
17
  import { faker } from '@dxos/random';
17
18
  import { Button, DensityProvider, Input, ThemeProvider, useThemeContext } from '@dxos/react-ui';
18
19
  import { baseSurface, defaultTx, getSize, mx, textBlockWidth } from '@dxos/react-ui-theme';
@@ -32,7 +33,7 @@ import {
32
33
  createThemeExtensions,
33
34
  decorateMarkdown,
34
35
  defaultOptions,
35
- dnd,
36
+ dropFile,
36
37
  formattingKeymap,
37
38
  image,
38
39
  linkTooltip,
@@ -501,7 +502,18 @@ export const Annotations = {
501
502
  };
502
503
 
503
504
  export const DND = {
504
- render: () => <Story text={str('# DND', '')} extensions={[dnd()]} />,
505
+ render: () => (
506
+ <Story
507
+ text={str('# DND', '')}
508
+ extensions={[
509
+ dropFile({
510
+ onDrop: (view, event) => {
511
+ log.info('drop', event);
512
+ },
513
+ }),
514
+ ]}
515
+ />
516
+ ),
505
517
  };
506
518
 
507
519
  const typewriterItems = localStorage.getItem('dxos.org/plugin/markdown/typewriter')?.split(',');
@@ -34,7 +34,6 @@ import translations from '../../translations';
34
34
  faker.seed(101);
35
35
 
36
36
  const Story: FC<{ content: string }> = ({ content }) => {
37
- console.log('STORY');
38
37
  const { themeMode } = useThemeContext();
39
38
  const [item] = useState({ text: new TextObject(content) });
40
39
  const { id, doc, accessor } = useDocAccessor(item.text);
@@ -24,6 +24,8 @@ import {
24
24
  TextHFive,
25
25
  TextHSix,
26
26
  TextItalic,
27
+ CaretDown,
28
+ Check,
27
29
  } from '@phosphor-icons/react';
28
30
  import { createContext } from '@radix-ui/react-context';
29
31
  import React, { type PropsWithChildren, useEffect, useRef, useState } from 'react';
@@ -32,17 +34,18 @@ import { useDropzone } from 'react-dropzone';
32
34
  import {
33
35
  DensityProvider,
34
36
  ElevationProvider,
35
- Select,
36
37
  Toolbar as NaturalToolbar,
37
38
  Tooltip,
38
39
  type ThemedClassName,
39
40
  type ToolbarToggleGroupItemProps as NaturalToolbarToggleGroupItemProps,
40
41
  type ToolbarButtonProps as NaturalToolbarButtonProps,
41
42
  useTranslation,
43
+ DropdownMenu,
44
+ Button,
42
45
  } from '@dxos/react-ui';
43
46
  import { getSize } from '@dxos/react-ui-theme';
44
47
 
45
- import { type Formatting } from '../../extensions';
48
+ import { type Action, type ActionType, type Formatting } from '../../extensions';
46
49
  import { translationKey } from '../../translations';
47
50
 
48
51
  const tooltipProps = { side: 'top' as const, classNames: 'z-10' };
@@ -57,30 +60,6 @@ const HeadingIcons: { [key: string]: Icon } = {
57
60
  '6': TextHSix,
58
61
  };
59
62
 
60
- // TODO(burdon): Revert to string to make extensible?
61
- export type ActionType =
62
- | 'blockquote'
63
- | 'strong'
64
- | 'codeblock'
65
- | 'comment'
66
- | 'heading'
67
- | 'image'
68
- | 'emphasis'
69
- | 'code'
70
- | 'link'
71
- | 'list-bullet'
72
- | 'list-ordered'
73
- | 'list-task'
74
- | 'mention'
75
- | 'prompt'
76
- | 'strikethrough'
77
- | 'table';
78
-
79
- export type Action = {
80
- type: ActionType;
81
- data?: any;
82
- };
83
-
84
63
  export type ToolbarProps = ThemedClassName<
85
64
  PropsWithChildren<{
86
65
  state: Formatting | undefined;
@@ -180,9 +159,9 @@ const MarkdownHeading = () => {
180
159
  }
181
160
  }}
182
161
  >
183
- <Select.Root
184
- disabled={value === null}
185
- value={value ?? '0'}
162
+ {/* TODO(thure): `Select` encounters a ref error if used here (repro: select a heading, then select another
163
+ heading). Determine the root cause and fix or report to Radix. */}
164
+ <DropdownMenu.Root
186
165
  open={selectOpen}
187
166
  onOpenChange={(nextOpen: boolean) => {
188
167
  if (!nextOpen) {
@@ -190,39 +169,42 @@ const MarkdownHeading = () => {
190
169
  }
191
170
  return setSelectOpen(nextOpen);
192
171
  }}
193
- onValueChange={(value) => onAction?.({ type: 'heading', data: parseInt(value) })}
194
172
  >
195
173
  <Tooltip.Trigger asChild>
196
174
  <NaturalToolbar.Button asChild>
197
- <Select.TriggerButton variant='ghost' classNames={buttonStyles}>
198
- <span className='sr-only'>{t('heading label')}</span>
199
- <HeadingIcon className={iconStyles} />
200
- </Select.TriggerButton>
175
+ <DropdownMenu.Trigger asChild>
176
+ <Button variant='ghost' classNames={buttonStyles} disabled={value === null}>
177
+ <span className='sr-only'>{t('heading label')}</span>
178
+ <HeadingIcon className={iconStyles} />
179
+ <CaretDown />
180
+ </Button>
181
+ </DropdownMenu.Trigger>
201
182
  </NaturalToolbar.Button>
202
183
  </Tooltip.Trigger>
203
- <Select.Portal>
204
- <Select.Content>
205
- <Select.ScrollUpButton />
206
- <Select.Viewport>
207
- <Select.Option value='0'>
208
- <Paragraph className={iconStyles} />
209
- </Select.Option>
210
- {[1, 2, 3, 4, 5, 6].map((level) => (
211
- <Select.Option key={level} value={String(level)}>
212
- {level === 1 && <TextHOne className={iconStyles} />}
213
- {level === 2 && <TextHTwo className={iconStyles} />}
214
- {level === 3 && <TextHThree className={iconStyles} />}
215
- {level === 4 && <TextHFour className={iconStyles} />}
216
- {level === 5 && <TextHFive className={iconStyles} />}
217
- {level === 6 && <TextHSix className={iconStyles} />}
218
- </Select.Option>
219
- ))}
220
- </Select.Viewport>
221
- <Select.ScrollDownButton />
222
- <Select.Arrow />
223
- </Select.Content>
224
- </Select.Portal>
225
- </Select.Root>
184
+ <DropdownMenu.Portal>
185
+ <DropdownMenu.Content classNames='is-min md:is-min' onCloseAutoFocus={(e) => e.preventDefault()}>
186
+ <DropdownMenu.Viewport>
187
+ {Object.keys(HeadingIcons).map((level) => {
188
+ const Icon = HeadingIcons[level];
189
+ return (
190
+ <DropdownMenu.CheckboxItem
191
+ key={level}
192
+ checked={value === level}
193
+ onClick={() => onAction?.({ type: 'heading', data: level })}
194
+ >
195
+ <span className='sr-only'>{t('heading level label', { count: parseInt(level) })}</span>
196
+ <Icon className={iconStyles} />
197
+ <DropdownMenu.ItemIndicator>
198
+ <Check />
199
+ </DropdownMenu.ItemIndicator>
200
+ </DropdownMenu.CheckboxItem>
201
+ );
202
+ })}
203
+ </DropdownMenu.Viewport>
204
+ <DropdownMenu.Arrow />
205
+ </DropdownMenu.Content>
206
+ </DropdownMenu.Portal>
207
+ </DropdownMenu.Root>
226
208
  <Tooltip.Portal>
227
209
  <Tooltip.Content {...tooltipProps}>
228
210
  {t('heading label')}
@@ -358,7 +340,7 @@ const MarkdownCustom = ({ onUpload }: MarkdownCustomOptions = {}) => {
358
340
 
359
341
  useEffect(() => {
360
342
  if (onUpload && acceptedFiles.length) {
361
- setTimeout(async () => {
343
+ requestAnimationFrame(async () => {
362
344
  // NOTE: Clone file since react-dropzone patches in a non-standard `path` property, which confuses IPFS.
363
345
  const f = acceptedFiles[0];
364
346
  const file = new File([f], f.name, {
@@ -394,7 +376,12 @@ const MarkdownActions = () => {
394
376
  {/* <ToolbarButton value='comment' Icon={BookOpenText} onClick={() => onAction?.({ type: 'comment' })}> */}
395
377
  {/* {t('comment label')} */}
396
378
  {/* </ToolbarButton> */}
397
- <ToolbarButton value='comment' Icon={ChatText} onClick={() => onAction?.({ type: 'comment' })}>
379
+ <ToolbarButton
380
+ value='comment'
381
+ Icon={ChatText}
382
+ data-testid='editor.toolbar.comment'
383
+ onClick={() => onAction?.({ type: 'comment' })}
384
+ >
398
385
  {t('comment label')}
399
386
  </ToolbarButton>
400
387
  </>
@@ -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
  };
@@ -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';
@@ -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
+ };