@dxos/react-ui-editor 0.4.8-main.4d33a73 → 0.4.8-main.4f9d99e

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 (43) hide show
  1. package/dist/lib/browser/index.mjs +1437 -1438
  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.map +1 -1
  5. package/dist/types/src/components/Toolbar/Toolbar.d.ts +1 -6
  6. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  7. package/dist/types/src/extensions/dnd.d.ts +7 -1
  8. package/dist/types/src/extensions/dnd.d.ts.map +1 -1
  9. package/dist/types/src/extensions/factories.d.ts +4 -0
  10. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  11. package/dist/types/src/extensions/index.d.ts +0 -1
  12. package/dist/types/src/extensions/index.d.ts.map +1 -1
  13. package/dist/types/src/extensions/markdown/action.d.ts +9 -0
  14. package/dist/types/src/extensions/markdown/action.d.ts.map +1 -0
  15. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  16. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  17. package/dist/types/src/extensions/markdown/index.d.ts +1 -0
  18. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  19. package/dist/types/src/extensions/modes.d.ts +1 -1
  20. package/dist/types/src/extensions/modes.d.ts.map +1 -1
  21. package/dist/types/src/hooks/useActionHandler.d.ts +2 -2
  22. package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -1
  23. package/dist/types/src/hooks/useDocAccessor.d.ts +4 -2
  24. package/dist/types/src/hooks/useDocAccessor.d.ts.map +1 -1
  25. package/dist/types/src/themes/default.d.ts.map +1 -1
  26. package/package.json +25 -24
  27. package/src/components/TextEditor/TextEditor.stories.tsx +14 -2
  28. package/src/components/Toolbar/Toolbar.tsx +2 -26
  29. package/src/extensions/command/state.ts +2 -2
  30. package/src/extensions/dnd.ts +31 -9
  31. package/src/extensions/factories.ts +28 -7
  32. package/src/extensions/index.ts +0 -1
  33. package/src/extensions/markdown/action.ts +112 -0
  34. package/src/extensions/markdown/bundle.ts +8 -11
  35. package/src/extensions/markdown/formatting.ts +11 -11
  36. package/src/extensions/markdown/index.ts +1 -0
  37. package/src/extensions/modes.ts +8 -1
  38. package/src/hooks/useActionHandler.ts +4 -89
  39. package/src/hooks/useDocAccessor.ts +8 -2
  40. package/src/themes/default.ts +24 -8
  41. package/dist/types/src/extensions/cursor-line-margin.d.ts +0 -7
  42. package/dist/types/src/extensions/cursor-line-margin.d.ts.map +0 -1
  43. package/src/extensions/cursor-line-margin.ts +0 -94
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-editor",
3
- "version": "0.4.8-main.4d33a73",
3
+ "version": "0.4.8-main.4f9d99e",
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-main.4d33a73",
42
- "@dxos/debug": "0.4.8-main.4d33a73",
43
- "@dxos/context": "0.4.8-main.4d33a73",
44
- "@dxos/automerge": "0.4.8-main.4d33a73",
45
- "@dxos/display-name": "0.4.8-main.4d33a73",
46
- "@dxos/echo-schema": "0.4.8-main.4d33a73",
47
- "@dxos/invariant": "0.4.8-main.4d33a73",
48
- "@dxos/protocols": "0.4.8-main.4d33a73",
49
- "@dxos/react-async": "0.4.8-main.4d33a73",
50
- "@dxos/log": "0.4.8-main.4d33a73",
51
- "@dxos/react-ui-theme": "0.4.8-main.4d33a73",
52
- "@dxos/react-ui": "0.4.8-main.4d33a73",
53
- "@dxos/text-model": "0.4.8-main.4d33a73",
54
- "@dxos/util": "0.4.8-main.4d33a73"
42
+ "@dxos/async": "0.4.8-main.4f9d99e",
43
+ "@dxos/automerge": "0.4.8-main.4f9d99e",
44
+ "@dxos/context": "0.4.8-main.4f9d99e",
45
+ "@dxos/debug": "0.4.8-main.4f9d99e",
46
+ "@dxos/display-name": "0.4.8-main.4f9d99e",
47
+ "@dxos/echo-schema": "0.4.8-main.4f9d99e",
48
+ "@dxos/invariant": "0.4.8-main.4f9d99e",
49
+ "@dxos/log": "0.4.8-main.4f9d99e",
50
+ "@dxos/react-async": "0.4.8-main.4f9d99e",
51
+ "@dxos/react-ui": "0.4.8-main.4f9d99e",
52
+ "@dxos/react-ui-theme": "0.4.8-main.4f9d99e",
53
+ "@dxos/protocols": "0.4.8-main.4f9d99e",
54
+ "@dxos/text-model": "0.4.8-main.4f9d99e",
55
+ "@dxos/util": "0.4.8-main.4f9d99e"
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-main.4d33a73",
82
- "@dxos/echo-signals": "0.4.8-main.4d33a73",
83
- "@dxos/keyboard": "0.4.8-main.4d33a73",
84
- "@dxos/random": "0.4.8-main.4d33a73",
85
- "@dxos/echo-typegen": "0.4.8-main.4d33a73",
86
- "@dxos/react-client": "0.4.8-main.4d33a73",
87
- "@dxos/storybook-utils": "0.4.8-main.4d33a73",
88
- "@dxos/react-ui": "0.4.8-main.4d33a73"
82
+ "@dxos/config": "0.4.8-main.4f9d99e",
83
+ "@dxos/echo-signals": "0.4.8-main.4f9d99e",
84
+ "@dxos/echo-typegen": "0.4.8-main.4f9d99e",
85
+ "@dxos/keyboard": "0.4.8-main.4f9d99e",
86
+ "@dxos/react-client": "0.4.8-main.4f9d99e",
87
+ "@dxos/react-ui": "0.4.8-main.4f9d99e",
88
+ "@dxos/random": "0.4.8-main.4f9d99e",
89
+ "@dxos/storybook-utils": "0.4.8-main.4f9d99e"
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-main.4d33a73"
95
+ "@dxos/react-client": "0.4.8-main.4f9d99e"
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(',');
@@ -42,7 +42,7 @@ import {
42
42
  } from '@dxos/react-ui';
43
43
  import { getSize } from '@dxos/react-ui-theme';
44
44
 
45
- import { type Formatting } from '../../extensions';
45
+ import { type Action, type ActionType, type Formatting } from '../../extensions';
46
46
  import { translationKey } from '../../translations';
47
47
 
48
48
  const tooltipProps = { side: 'top' as const, classNames: 'z-10' };
@@ -57,30 +57,6 @@ const HeadingIcons: { [key: string]: Icon } = {
57
57
  '6': TextHSix,
58
58
  };
59
59
 
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
60
  export type ToolbarProps = ThemedClassName<
85
61
  PropsWithChildren<{
86
62
  state: Formatting | undefined;
@@ -358,7 +334,7 @@ const MarkdownCustom = ({ onUpload }: MarkdownCustomOptions = {}) => {
358
334
 
359
335
  useEffect(() => {
360
336
  if (onUpload && acceptedFiles.length) {
361
- setTimeout(async () => {
337
+ requestAnimationFrame(async () => {
362
338
  // NOTE: Clone file since react-dropzone patches in a non-standard `path` property, which confuses IPFS.
363
339
  const f = acceptedFiles[0];
364
340
  const file = new File([f], f.name, {
@@ -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,9 +2,10 @@
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 { 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,
@@ -12,6 +13,7 @@ import {
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,8 +41,8 @@ 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;
@@ -53,21 +53,27 @@ export type BasicExtensionsOptions = {
53
53
  editable?: boolean;
54
54
  highlightActiveLine?: boolean;
55
55
  history?: boolean;
56
+ indentWithTab?: boolean;
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
67
  const defaults: 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
+ standardKeymap: true,
70
75
  lineWrapping: true,
76
+ search: true,
71
77
  };
72
78
 
73
79
  export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extension => {
@@ -92,6 +98,22 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
92
98
  props.readonly && [EditorState.readOnly.of(true), EditorView.editable.of(false)],
93
99
  props.scrollPastEnd && scrollPastEnd(),
94
100
  props.tabSize && EditorState.tabSize.of(props.tabSize),
101
+
102
+ // https://codemirror.net/docs/ref/#view.KeyBinding
103
+ keymap.of(
104
+ [
105
+ // https://codemirror.net/docs/ref/#commands.standardKeymap
106
+ ...(props.standardKeymap ? standardKeymap : []),
107
+ // https://codemirror.net/docs/ref/#commands.indentWithTab
108
+ ...(props.indentWithTab ? [indentWithTab] : []),
109
+ // https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
110
+ ...(props.closeBrackets ? closeBracketsKeymap : []),
111
+ // https://codemirror.net/docs/ref/#commands.historyKeymap
112
+ ...(props.history ? historyKeymap : []),
113
+ // https://codemirror.net/docs/ref/#search.searchKeymap
114
+ ...(props.search ? searchKeymap : []),
115
+ ].filter(isNotFalsy),
116
+ ),
95
117
  ].filter(isNotFalsy);
96
118
  };
97
119
 
@@ -135,12 +157,11 @@ export const createThemeExtensions = ({ theme, themeMode, slots: _slots }: Theme
135
157
 
136
158
  export type DataExtensionsProps = {
137
159
  id: string;
138
- text: DocAccessor; // TODO(burdon): Rename content.
160
+ text: DocAccessor;
139
161
  space?: Space;
140
162
  identity?: Identity | null;
141
163
  };
142
164
 
143
- // TODO(burdon): Factor out automerge defs and extension (not hook).
144
165
  // TODO(burdon): Move out of react-ui-editor (remove echo deps).
145
166
  export const createDataExtensions = ({ id, text, space, identity }: DataExtensionsProps): Extension[] => {
146
167
  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
+ };
@@ -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
  };
@@ -15,9 +15,7 @@ import {
15
15
  } from '@codemirror/state';
16
16
  import { EditorView, keymap } from '@codemirror/view';
17
17
  import { type SyntaxNodeRef, type SyntaxNode } from '@lezer/common';
18
- import { useMemo } from 'react';
19
-
20
- import { useStateRef } from '@dxos/react-async';
18
+ import { useMemo, useState } from 'react';
21
19
 
22
20
  // Markdown refs:
23
21
  // https://github.github.com/gfm
@@ -1225,20 +1223,22 @@ export const getFormatting = (state: EditorState): Formatting => {
1225
1223
  * Hook provides an extension to compute the current formatting state.
1226
1224
  */
1227
1225
  export const useFormattingState = (): [Formatting | undefined, Extension] => {
1228
- const [state, setState, stateRef] = useStateRef<Formatting>();
1226
+ const [state, setState] = useState<Formatting>();
1227
+
1229
1228
  const observer = useMemo(
1230
1229
  () =>
1231
1230
  EditorView.updateListener.of((update) => {
1232
1231
  if (update.docChanged || update.selectionSet) {
1233
- const newState = getFormatting(update.state);
1234
- if (!stateRef.current || !formattingEquals(stateRef.current, newState)) {
1235
- // TODO(burdon): Error on first click on toolbar.
1236
- // Warning: React has detected a change in the order of Hooks called by ForwardRef. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks
1237
- setState(newState);
1238
- }
1232
+ setState((prevState) => {
1233
+ const newState = getFormatting(update.state);
1234
+ if (!prevState || !formattingEquals(prevState, newState)) {
1235
+ return newState;
1236
+ }
1237
+ return prevState;
1238
+ });
1239
1239
  }
1240
1240
  }),
1241
- [],
1241
+ [setState],
1242
1242
  );
1243
1243
 
1244
1244
  return [state, observer];
@@ -2,6 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ export * from './action';
5
6
  export * from './bundle';
6
7
  export * from './decorate';
7
8
  export * from './formatting';
@@ -5,10 +5,11 @@
5
5
  import { type Extension, Facet } from '@codemirror/state';
6
6
  import { keymap } from '@codemirror/view';
7
7
  import { vim } from '@replit/codemirror-vim';
8
+ import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
8
9
 
9
10
  export const focusEvent = 'focus.container';
10
11
 
11
- export type EditorMode = 'default' | 'vim' | undefined;
12
+ export type EditorMode = 'default' | 'vim' | 'vscode' | undefined;
12
13
 
13
14
  export type EditorConfig = {
14
15
  type: string;
@@ -21,7 +22,13 @@ export const editorMode = Facet.define<EditorConfig, EditorConfig>({
21
22
 
22
23
  export const EditorModes: { [mode: string]: Extension } = {
23
24
  default: [],
25
+ vscode: [
26
+ // https://github.com/replit/codemirror-vscode-keymap
27
+ editorMode.of({ type: 'vscode' }),
28
+ keymap.of(vscodeKeymap),
29
+ ],
24
30
  vim: [
31
+ // https://github.com/replit/codemirror-vim
25
32
  vim(),
26
33
  editorMode.of({ type: 'vim', noTabster: true }),
27
34
  keymap.of([
@@ -2,96 +2,11 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import type { EditorView } from '@codemirror/view';
5
+ import { type EditorView } from '@codemirror/view';
6
6
 
7
- import type { ToolbarProps } from '../components';
8
- import {
9
- Inline,
10
- List,
11
- addBlockquote,
12
- addCodeblock,
13
- addLink,
14
- addList,
15
- createComment,
16
- insertTable,
17
- removeBlockquote,
18
- removeCodeblock,
19
- removeLink,
20
- removeList,
21
- setHeading,
22
- setStyle,
23
- toggleBlockquote,
24
- toggleList,
25
- toggleStyle,
26
- } from '../extensions';
7
+ import { type ToolbarProps } from '../components';
8
+ import { processAction } from '../extensions';
27
9
 
28
10
  export const useActionHandler = (view?: EditorView | null): ToolbarProps['onAction'] => {
29
- return (action) => {
30
- if (!view) {
31
- return;
32
- }
33
-
34
- let inlineType, listType;
35
- switch (action.type) {
36
- case 'heading':
37
- setHeading(parseInt(action.data))(view);
38
- break;
39
-
40
- case 'strong':
41
- case 'emphasis':
42
- case 'strikethrough':
43
- case 'code':
44
- inlineType =
45
- action.type === 'strong'
46
- ? Inline.Strong
47
- : action.type === 'emphasis'
48
- ? Inline.Emphasis
49
- : action.type === 'strikethrough'
50
- ? Inline.Strikethrough
51
- : Inline.Code;
52
- (typeof action.data === 'boolean' ? setStyle(inlineType, action.data) : toggleStyle(inlineType))(view);
53
- break;
54
-
55
- case 'list-ordered':
56
- case 'list-bullet':
57
- case 'list-task':
58
- listType =
59
- action.type === 'list-ordered' ? List.Ordered : action.type === 'list-bullet' ? List.Bullet : List.Task;
60
- (action.data === false
61
- ? removeList(listType)
62
- : action.data === true
63
- ? addList(listType)
64
- : toggleList(listType))(view);
65
- break;
66
-
67
- case 'blockquote':
68
- (action.data === false ? removeBlockquote : action.data === true ? addBlockquote : toggleBlockquote)(view);
69
- break;
70
- case 'codeblock':
71
- (action.data === false ? removeCodeblock : addCodeblock)(view);
72
- break;
73
- case 'table':
74
- insertTable(view);
75
- break;
76
-
77
- case 'link':
78
- (action.data === false ? removeLink : addLink())(view);
79
- break;
80
-
81
- case 'image':
82
- addLink({ url: action.data, image: true })(view);
83
- break;
84
-
85
- case 'comment':
86
- createComment(view);
87
- break;
88
- }
89
-
90
- // TODO(burdon): Hack otherwise remains on heading selector.
91
- setTimeout(() => {
92
- if (!view.hasFocus) {
93
- view.focus();
94
- }
95
- });
96
- };
11
+ return (action) => view && processAction(view, action);
97
12
  };