@dxos/react-ui-editor 0.6.5 → 0.6.6-staging.23d123d

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 (70) hide show
  1. package/dist/lib/browser/index.mjs +316 -324
  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/Toolbar/Toolbar.d.ts +5 -1
  5. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  6. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +7 -0
  7. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
  8. package/dist/types/src/components/index.d.ts +0 -1
  9. package/dist/types/src/components/index.d.ts.map +1 -1
  10. package/dist/types/src/extensions/autocomplete.d.ts +2 -1
  11. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  12. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  13. package/dist/types/src/extensions/automerge/automerge.stories.d.ts +9 -2
  14. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  15. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  16. package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -1
  17. package/dist/types/src/extensions/awareness/awareness.d.ts +6 -6
  18. package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -1
  19. package/dist/types/src/extensions/comments.d.ts +1 -2
  20. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  21. package/dist/types/src/extensions/cursor.d.ts +1 -1
  22. package/dist/types/src/extensions/cursor.d.ts.map +1 -1
  23. package/dist/types/src/extensions/debug.d.ts +3 -0
  24. package/dist/types/src/extensions/debug.d.ts.map +1 -0
  25. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  26. package/dist/types/src/extensions/index.d.ts +1 -0
  27. package/dist/types/src/extensions/index.d.ts.map +1 -1
  28. package/dist/types/src/extensions/markdown/action.d.ts +1 -1
  29. package/dist/types/src/extensions/markdown/action.d.ts.map +1 -1
  30. package/dist/types/src/extensions/modes.d.ts +7 -4
  31. package/dist/types/src/extensions/modes.d.ts.map +1 -1
  32. package/dist/types/src/hooks/{useTextEditor.stories.d.ts → InputMode.stories.d.ts} +9 -9
  33. package/dist/types/src/hooks/InputMode.stories.d.ts.map +1 -0
  34. package/dist/types/src/{components/TextEditor → hooks}/TextEditor.stories.d.ts +8 -16
  35. package/dist/types/src/hooks/TextEditor.stories.d.ts.map +1 -0
  36. package/dist/types/src/hooks/useTextEditor.d.ts +20 -3
  37. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  38. package/dist/types/src/themes/default.d.ts.map +1 -1
  39. package/dist/types/src/translations.d.ts +4 -0
  40. package/dist/types/src/translations.d.ts.map +1 -1
  41. package/package.json +27 -27
  42. package/src/components/Toolbar/Toolbar.stories.tsx +18 -8
  43. package/src/components/Toolbar/Toolbar.tsx +93 -3
  44. package/src/components/index.ts +0 -1
  45. package/src/extensions/autocomplete.ts +3 -3
  46. package/src/extensions/automerge/automerge.stories.tsx +25 -18
  47. package/src/extensions/automerge/automerge.ts +2 -0
  48. package/src/extensions/automerge/cursor.ts +3 -4
  49. package/src/extensions/awareness/awareness-provider.ts +2 -0
  50. package/src/extensions/awareness/awareness.ts +34 -30
  51. package/src/extensions/comments.ts +7 -14
  52. package/src/extensions/cursor.ts +1 -1
  53. package/src/extensions/debug.ts +15 -0
  54. package/src/extensions/factories.ts +19 -13
  55. package/src/extensions/index.ts +1 -0
  56. package/src/extensions/markdown/action.ts +1 -0
  57. package/src/extensions/modes.ts +9 -6
  58. package/src/hooks/{useTextEditor.stories.tsx → InputMode.stories.tsx} +41 -47
  59. package/src/{components/TextEditor → hooks}/TextEditor.stories.tsx +24 -30
  60. package/src/hooks/useTextEditor.ts +75 -23
  61. package/src/themes/default.ts +20 -4
  62. package/src/translations.ts +4 -0
  63. package/dist/types/src/components/TextEditor/TextEditor.d.ts +0 -34
  64. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +0 -1
  65. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +0 -1
  66. package/dist/types/src/components/TextEditor/index.d.ts +0 -2
  67. package/dist/types/src/components/TextEditor/index.d.ts.map +0 -1
  68. package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +0 -1
  69. package/src/components/TextEditor/TextEditor.tsx +0 -184
  70. package/src/components/TextEditor/index.ts +0 -5
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { EditorSelection, EditorState } from '@codemirror/state';
5
+ import { EditorState, type EditorStateConfig, type StateEffect } from '@codemirror/state';
6
6
  import { EditorView } from '@codemirror/view';
7
7
  import { useFocusableGroup } from '@fluentui/react-tabster';
8
8
  import {
@@ -17,10 +17,10 @@ import {
17
17
  } from 'react';
18
18
 
19
19
  import { log } from '@dxos/log';
20
- import { isNotFalsy } from '@dxos/util';
20
+ import { useDefaultValue } from '@dxos/react-ui';
21
+ import { isNotFalsy, type MaybeFunction } from '@dxos/util';
21
22
 
22
- import { type TextEditorProps } from '../components';
23
- import { documentId } from '../extensions';
23
+ import { documentId, editorInputMode } from '../extensions';
24
24
  import { logChanges } from '../util';
25
25
 
26
26
  export type UseTextEditor = {
@@ -32,14 +32,50 @@ export type UseTextEditor = {
32
32
  };
33
33
  };
34
34
 
35
- export type UseTextEditorProps = Omit<TextEditorProps, 'moveToEndOfLine' | 'dataTestId'>;
35
+ export type CursorInfo = {
36
+ from: number;
37
+ to: number;
38
+ line: number;
39
+ lines: number;
40
+ length: number;
41
+ after?: string;
42
+ };
43
+
44
+ export type UseTextEditorProps = Pick<EditorStateConfig, 'selection' | 'extensions'> & {
45
+ id?: string;
46
+ initialValue?: string;
47
+ className?: string;
48
+ autoFocus?: boolean;
49
+ scrollTo?: StateEffect<unknown>;
50
+ moveToEndOfLine?: boolean;
51
+ debug?: boolean;
52
+ };
53
+
54
+ let instanceCount = 0;
36
55
 
37
56
  /**
38
57
  * Hook for creating editor.
39
58
  */
40
- export const useTextEditor = (cb: () => UseTextEditorProps = () => ({}), deps: DependencyList = []): UseTextEditor => {
41
- let { id, doc, selection, extensions, autoFocus, scrollTo, debug } = useMemo<UseTextEditorProps>(cb, deps ?? []);
42
-
59
+ export const useTextEditor = (
60
+ props: MaybeFunction<UseTextEditorProps> = {},
61
+ deps: DependencyList = [],
62
+ ): UseTextEditor => {
63
+ const {
64
+ id,
65
+ initialValue,
66
+ selection,
67
+ extensions,
68
+ autoFocus,
69
+ scrollTo: _scrollTo,
70
+ moveToEndOfLine,
71
+ debug,
72
+ } = useMemo<UseTextEditorProps>(() => {
73
+ return typeof props === 'function' ? props() : props;
74
+ }, deps ?? []);
75
+
76
+ // NOTE: Increments by 2 in strict mode.
77
+ const [instanceId] = useState(() => `text-editor-${++instanceCount}`);
78
+ const scrollTo = useDefaultValue(_scrollTo, EditorView.scrollIntoView(0, { yMargin: 0 }));
43
79
  const onUpdate = useRef<() => void>();
44
80
  const [view, setView] = useState<EditorView>();
45
81
  const parentRef = useRef<HTMLDivElement>(null);
@@ -47,12 +83,20 @@ export const useTextEditor = (cb: () => UseTextEditorProps = () => ({}), deps: D
47
83
  useEffect(() => {
48
84
  let view: EditorView;
49
85
  if (parentRef.current) {
50
- log('create', { id });
86
+ log('create', { id, instanceId, doc: initialValue?.length ?? 0 });
87
+
88
+ let initialSelection = selection;
89
+ if (moveToEndOfLine && selection === undefined) {
90
+ const index = initialValue?.indexOf('\n');
91
+ const anchor = !index || index === -1 ? 0 : index;
92
+ initialSelection = { anchor };
93
+ }
51
94
 
52
95
  // https://codemirror.net/docs/ref/#state.EditorStateConfig
53
96
  // NOTE: Don't set selection here in case it is invalid (and crashes the state); dispatch below.
54
97
  const state = EditorState.create({
55
- doc,
98
+ doc: initialValue,
99
+ selection: initialSelection,
56
100
  extensions: [
57
101
  id && documentId.of(id),
58
102
  // TODO(burdon): Doesn't catch errors in keymap functions.
@@ -70,6 +114,7 @@ export const useTextEditor = (cb: () => UseTextEditorProps = () => ({}), deps: D
70
114
  view = new EditorView({
71
115
  parent: parentRef.current,
72
116
  scrollTo,
117
+ selection: initialSelection,
73
118
  state,
74
119
  // NOTE: Uncomment to debug/monitor all transactions.
75
120
  // https://codemirror.net/docs/ref/#view.EditorView.dispatch
@@ -81,6 +126,12 @@ export const useTextEditor = (cb: () => UseTextEditorProps = () => ({}), deps: D
81
126
  },
82
127
  });
83
128
 
129
+ // Move to end of line after document loaded.
130
+ if (!initialValue && moveToEndOfLine) {
131
+ const { to } = view.state.doc.lineAt(0);
132
+ view.dispatch({ selection: { anchor: to } });
133
+ }
134
+
84
135
  setView(view);
85
136
  }
86
137
 
@@ -92,27 +143,29 @@ export const useTextEditor = (cb: () => UseTextEditorProps = () => ({}), deps: D
92
143
 
93
144
  useEffect(() => {
94
145
  if (view) {
95
- // Select end of line if not specified.
96
- if (!selection && !view.state.selection.main.anchor) {
97
- selection = EditorSelection.single(view.state.doc.line(1).to);
98
- }
99
-
100
- // Set selection after first update (since content may rerender on focus).
101
- // TODO(burdon): Make invisible until first render?
102
- if (selection || scrollTo) {
146
+ // TODO(burdon): Set selection after first update (since content may rerender on focus)?
147
+ if (scrollTo) {
103
148
  onUpdate.current = () => {
104
149
  onUpdate.current = undefined;
105
- view.dispatch({ selection, effects: scrollTo && [scrollTo], scrollIntoView: !scrollTo });
150
+ view.dispatch({ effects: scrollTo && [scrollTo], scrollIntoView: !scrollTo });
106
151
  };
107
152
  }
108
153
 
109
- if (autoFocus) {
110
- view.focus();
154
+ // Remove tabster attribute (rely on custom keymap).
155
+ if (view.state.facet(editorInputMode).noTabster) {
156
+ parentRef.current?.removeAttribute('data-tabster');
111
157
  }
112
158
  }
113
- }, [view, autoFocus, selection, scrollTo]);
159
+ }, [view, selection, scrollTo]);
160
+
161
+ useEffect(() => {
162
+ if (view && autoFocus) {
163
+ view.focus();
164
+ }
165
+ }, [autoFocus, view]);
114
166
 
115
167
  const focusableGroup = useFocusableGroup({ tabBehavior: 'limited' });
168
+
116
169
  // Focus editor on Enter (e.g., when tabbing to this component).
117
170
  const handleKeyUp = useCallback<KeyboardEventHandler<HTMLDivElement>>(
118
171
  (event) => {
@@ -130,6 +183,5 @@ export const useTextEditor = (cb: () => UseTextEditorProps = () => ({}), deps: D
130
183
  );
131
184
 
132
185
  const focusAttributes = { tabIndex: 0 as const, ...focusableGroup, onKeyUp: handleKeyUp };
133
-
134
186
  return { parentRef, view, focusAttributes };
135
187
  };
@@ -152,19 +152,34 @@ export const defaultTheme: ThemeStyles = {
152
152
  //
153
153
  '.cm-tooltip': {
154
154
  border: 'none',
155
- background: 'unset',
155
+ },
156
+ '&light .cm-tooltip': {
157
+ background: `${get(tokens, 'extend.colors.neutral.100')} !important`,
158
+ },
159
+ '&dark .cm-tooltip': {
160
+ background: `${get(tokens, 'extend.colors.neutral.900')} !important`,
156
161
  },
157
162
  '.cm-tooltip-below': {},
158
163
 
159
164
  //
160
165
  // autocomplete
166
+ // https://github.com/codemirror/autocomplete/blob/main/src/completion.ts
161
167
  //
162
168
  '.cm-tooltip-autocomplete': {
163
169
  marginTop: '4px',
164
170
  marginLeft: '-3px',
165
171
  },
166
- '.cm-tooltip-autocomplete ul li': {},
167
- '.cm-tooltip-autocomplete ul li[aria-selected]': {},
172
+ '.cm-tooltip-autocomplete > ul': {
173
+ maxHeight: '20em !important',
174
+ },
175
+ '.cm-tooltip-autocomplete > ul > li': {},
176
+ '.cm-tooltip-autocomplete > ul > li[aria-selected]': {},
177
+ // TODO(burdon): Can we add a class prefix to avoid adding !important?
178
+ '.cm-tooltip.cm-tooltip-autocomplete > ul > completion-section': {
179
+ paddingLeft: '4px !important',
180
+ borderBottom: 'none !important',
181
+ color: get(tokens, 'extend.colors.primary.500'),
182
+ },
168
183
  '.cm-completionIcon': {
169
184
  display: 'none',
170
185
  },
@@ -172,7 +187,8 @@ export const defaultTheme: ThemeStyles = {
172
187
  fontFamily: get(tokens, 'fontFamily.body', []).join(','),
173
188
  },
174
189
  '.cm-completionMatchedText': {
175
- textDecoration: 'none',
190
+ textDecoration: 'none !important',
191
+ opacity: 0.5,
176
192
  },
177
193
 
178
194
  //
@@ -25,6 +25,10 @@ export default [
25
25
  'heading level label_zero': 'Paragraph',
26
26
  'heading level label_one': 'Heading level {{count}}',
27
27
  'heading level label_other': 'Heading level {{count}}',
28
+ 'view mode label': 'Editor view',
29
+ 'preview mode label': 'Live preview',
30
+ 'readonly mode label': 'Read only',
31
+ 'source mode label': 'Source',
28
32
  },
29
33
  },
30
34
  },
@@ -1,34 +0,0 @@
1
- import { type EditorStateConfig, type StateEffect } from '@codemirror/state';
2
- import { EditorView } from '@codemirror/view';
3
- import React from 'react';
4
- export type CursorInfo = {
5
- from: number;
6
- to: number;
7
- line: number;
8
- lines: number;
9
- length: number;
10
- after?: string;
11
- };
12
- export type TextEditorProps = Pick<EditorStateConfig, 'doc' | 'selection' | 'extensions'> & {
13
- id?: string;
14
- className?: string;
15
- autoFocus?: boolean;
16
- scrollTo?: StateEffect<unknown>;
17
- moveToEndOfLine?: boolean;
18
- debug?: boolean;
19
- dataTestId?: string;
20
- };
21
- /**
22
- * Thin wrapper for text editor.
23
- * Handles tabster and focus management.
24
- */
25
- export declare const TextEditor: React.ForwardRefExoticComponent<Pick<EditorStateConfig, "doc" | "selection" | "extensions"> & {
26
- id?: string | undefined;
27
- className?: string | undefined;
28
- autoFocus?: boolean | undefined;
29
- scrollTo?: StateEffect<unknown> | undefined;
30
- moveToEndOfLine?: boolean | undefined;
31
- debug?: boolean | undefined;
32
- dataTestId?: string | undefined;
33
- } & React.RefAttributes<EditorView | null>>;
34
- //# sourceMappingURL=TextEditor.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"TextEditor.d.ts","sourceRoot":"","sources":["../../../../../src/components/TextEditor/TextEditor.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAe,KAAK,iBAAiB,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC1F,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,KAQN,MAAM,OAAO,CAAC;AASf,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,iBAAiB,EAAE,KAAK,GAAG,WAAW,GAAG,YAAY,CAAC,GAAG;IAC1F,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAChC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAIF;;;GAGG;AAEH,eAAO,MAAM,UAAU;;;;;;;;2CAqItB,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"TextEditor.stories.d.ts","sourceRoot":"","sources":["../../../../../src/components/TextEditor/TextEditor.stories.tsx"],"names":[],"mappings":"AAIA,OAAO,YAAY,CAAC;AAGpB,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,KAA6E,MAAM,OAAO,CAAC;AAclG,OAAO,EAAc,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,EA0BL,KAAK,OAAO,EAGb,MAAM,kBAAkB,CAAC;AA4L1B,KAAK,UAAU,GAAG;IAChB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,IAAI,CAAC,eAAe,EAAE,WAAW,GAAG,YAAY,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CtD,wBAME;AAeF,eAAO,MAAM,OAAO;;CAEnB,CAAC;AAEF,eAAO,MAAM,QAAQ;;CAEpB,CAAC;AAEF,eAAO,MAAM,YAAY;;CAExB,CAAC;AASF,eAAO,MAAM,KAAK;;CAEjB,CAAC;AAIF,eAAO,MAAM,SAAS;;CAUrB,CAAC;AAEF,eAAO,MAAM,mBAAmB;;CAE/B,CAAC;AAEF,eAAO,MAAM,KAAK;;CAEjB,CAAC;AAEF,eAAO,MAAM,KAAK;;CAEjB,CAAC;AAEF,eAAO,MAAM,IAAI;;CAEhB,CAAC;AAEF,eAAO,MAAM,KAAK;;CAIjB,CAAC;AAEF,eAAO,MAAM,KAAK;;CAEjB,CAAC;AAEF,eAAO,MAAM,YAAY;;CAYxB,CAAC;AAEF,eAAO,MAAM,YAAY;;CAWxB,CAAC;AAEF,eAAO,MAAM,OAAO;;CAWnB,CAAC;AAmDF,eAAO,MAAM,OAAO;;CAOnB,CAAC;AAEF,eAAO,MAAM,QAAQ;;CAiCpB,CAAC;AAEF,eAAO,MAAM,GAAG;;CAOf,CAAC;AAEF,eAAO,MAAM,WAAW;;CAEvB,CAAC;AAEF,eAAO,MAAM,GAAG;;CAaf,CAAC;AAIF,eAAO,MAAM,QAAQ;;CAgBpB,CAAC;AAEF,eAAO,MAAM,UAAU;;CAOtB,CAAC;AAEF,eAAO,MAAM,KAAK;;CAqBjB,CAAC"}
@@ -1,2 +0,0 @@
1
- export * from './TextEditor';
2
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/TextEditor/index.ts"],"names":[],"mappings":"AAIA,cAAc,cAAc,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"useTextEditor.stories.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useTextEditor.stories.tsx"],"names":[],"mappings":";AAIA,OAAO,YAAY,CAAC;AAwBpB,KAAK,UAAU,GAAG;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;;;;mBA2Ee,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAH3B,wBASE;AAEF,eAAO,MAAM,OAAO;;CAKnB,CAAC;AAEF,eAAO,MAAM,KAAK;;CAQjB,CAAC;AAEF,eAAO,MAAM,QAAQ;;;;;;CAMpB,CAAC"}
@@ -1,184 +0,0 @@
1
- //
2
- // Copyright 2023 DXOS.org
3
- //
4
-
5
- import { EditorState, type EditorStateConfig, type StateEffect } from '@codemirror/state';
6
- import { EditorView } from '@codemirror/view';
7
- import { useFocusableGroup } from '@fluentui/react-tabster';
8
- import React, {
9
- type KeyboardEventHandler,
10
- forwardRef,
11
- useCallback,
12
- useEffect,
13
- useImperativeHandle,
14
- useRef,
15
- useState,
16
- } from 'react';
17
-
18
- import { log } from '@dxos/log';
19
- import { useDefaultValue } from '@dxos/react-ui';
20
- import { isNotFalsy } from '@dxos/util';
21
-
22
- import { documentId, editorMode, focusEvent } from '../../extensions';
23
- import { logChanges } from '../../util';
24
-
25
- export type CursorInfo = {
26
- from: number;
27
- to: number;
28
- line: number;
29
- lines: number;
30
- length: number;
31
- after?: string;
32
- };
33
-
34
- export type TextEditorProps = Pick<EditorStateConfig, 'doc' | 'selection' | 'extensions'> & {
35
- id?: string;
36
- className?: string;
37
- autoFocus?: boolean;
38
- scrollTo?: StateEffect<unknown>;
39
- moveToEndOfLine?: boolean;
40
- debug?: boolean;
41
- dataTestId?: string;
42
- };
43
-
44
- let instanceCount = 0;
45
-
46
- /**
47
- * Thin wrapper for text editor.
48
- * Handles tabster and focus management.
49
- */
50
- // TODO(burdon): Use useTextEditor internally.
51
- export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
52
- (
53
- {
54
- id,
55
- // TODO(wittjosiah): Rename initialText?
56
- doc,
57
- selection,
58
- extensions,
59
- className,
60
- autoFocus,
61
- scrollTo: propsScrollTo,
62
- moveToEndOfLine,
63
- debug,
64
- dataTestId,
65
- },
66
- forwardedRef,
67
- ) => {
68
- // NOTE: Increments by 2 in strict mode.
69
- const [instanceId] = useState(() => `text-editor-${++instanceCount}`);
70
- const scrollTo = useDefaultValue(propsScrollTo, EditorView.scrollIntoView(0, { yMargin: 0 }));
71
-
72
- // TODO(burdon): Make tabster optional.
73
- const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
74
- const rootRef = useRef<HTMLDivElement>(null);
75
- const [view, setView] = useState<EditorView | null>(null);
76
-
77
- // The view ref can be used to focus the editor.
78
- // NOTE: Ref updates do not cause the parent to re-render; also the ref is not available immediately.
79
- useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
80
-
81
- // Set focus.
82
- useEffect(() => {
83
- if (autoFocus) {
84
- view?.focus();
85
- }
86
- }, [view, autoFocus]);
87
-
88
- // Create editor state and view.
89
- // The view is recreated if the model or extensions are changed.
90
- useEffect(() => {
91
- log('create', { id, instanceId });
92
-
93
- //
94
- // EditorState
95
- // https://codemirror.net/docs/ref/#state.EditorStateConfig
96
- // NOTE: Don't set selection here in case it is invalid (and crashes the state); dispatch below.
97
- //
98
- const state = EditorState.create({
99
- doc,
100
- extensions: [
101
- id && documentId.of(id),
102
- // TODO(burdon): NOTE: Doesn't catch errors in keymap functions.
103
- EditorView.exceptionSink.of((err) => {
104
- log.catch(err);
105
- }),
106
-
107
- // Focus.
108
- EditorView.updateListener.of((update) => {
109
- update.transactions.forEach((transaction) => {
110
- if (transaction.isUserEvent(focusEvent)) {
111
- rootRef.current?.focus();
112
- }
113
- });
114
- }),
115
-
116
- extensions,
117
- ].filter(isNotFalsy),
118
- });
119
-
120
- //
121
- // EditorView
122
- // https://codemirror.net/docs/ref/#view.EditorViewConfig
123
- //
124
- const view = new EditorView({
125
- state,
126
- parent: rootRef.current!,
127
- scrollTo,
128
-
129
- // NOTE: Uncomment to debug/monitor all transactions.
130
- // https://codemirror.net/docs/ref/#view.EditorView.dispatch
131
- dispatchTransactions: (trs, view) => {
132
- if (debug) {
133
- logChanges(trs);
134
- }
135
- view.update(trs);
136
- },
137
- });
138
-
139
- // Position cursor at end of first line.
140
- if (moveToEndOfLine && !(scrollTo || selection)) {
141
- const { to } = view.state.doc.lineAt(0);
142
- view.dispatch({ selection: { anchor: to } });
143
- }
144
-
145
- // Remove tabster attribute (rely on custom keymap).
146
- if (state.facet(editorMode).noTabster) {
147
- rootRef.current?.removeAttribute('data-tabster');
148
- }
149
-
150
- setView(view);
151
-
152
- return () => {
153
- log('destroy', { id, instanceId });
154
- view?.destroy();
155
- };
156
- }, [id, selection, scrollTo, editorMode, extensions]);
157
-
158
- // Focus editor on Enter (e.g., when tabbing to this component).
159
- const handleKeyUp = useCallback<KeyboardEventHandler<HTMLDivElement>>(
160
- (event) => {
161
- const { key } = event;
162
- switch (key) {
163
- case 'Enter': {
164
- view?.focus();
165
- break;
166
- }
167
- }
168
- },
169
- [view],
170
- );
171
-
172
- return (
173
- <div
174
- role='none'
175
- ref={rootRef}
176
- tabIndex={0}
177
- className={className}
178
- data-testid={dataTestId}
179
- {...tabsterDOMAttribute}
180
- onKeyUp={handleKeyUp}
181
- />
182
- );
183
- },
184
- );
@@ -1,5 +0,0 @@
1
- //
2
- // Copyright 2023 DXOS.org
3
- //
4
-
5
- export * from './TextEditor';