@dxos/react-ui-editor 0.4.3-main.1ddf290 → 0.4.3-main.2a46fc0

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.
@@ -4,13 +4,12 @@
4
4
 
5
5
  import { EditorState, type Extension, type StateEffect } from '@codemirror/state';
6
6
  import { EditorView } from '@codemirror/view';
7
- // import { useFocusableGroup } from '@fluentui/react-tabster';
8
- import { vim } from '@replit/codemirror-vim';
7
+ import { useFocusableGroup } from '@fluentui/react-tabster';
9
8
  import defaultsDeep from 'lodash.defaultsdeep';
10
9
  import React, {
11
10
  type ComponentProps,
12
- type KeyboardEvent,
13
11
  forwardRef,
12
+ type KeyboardEventHandler,
14
13
  useCallback,
15
14
  useEffect,
16
15
  useImperativeHandle,
@@ -23,16 +22,12 @@ import { useThemeContext } from '@dxos/react-ui';
23
22
  import { focusRing } from '@dxos/react-ui-theme';
24
23
  import { isNotFalsy } from '@dxos/util';
25
24
 
26
- import { createBasicBundle, createMarkdownExtensions } from '../../extensions';
25
+ import { createBasicBundle, createMarkdownExtensions, editorMode } from '../../extensions';
27
26
  import { type EditorModel } from '../../hooks';
28
27
  import { type ThemeStyles } from '../../styles';
29
28
  import { defaultTheme, markdownTheme, textTheme } from '../../themes';
30
29
  import { logChanges } from '../../util';
31
30
 
32
- // TODO(burdon): Change to enum?
33
- export const EditorModes = ['default', 'vim'] as const;
34
- export type EditorMode = (typeof EditorModes)[number];
35
-
36
31
  export type CursorInfo = {
37
32
  from: number;
38
33
  to: number;
@@ -52,7 +47,6 @@ export type TextEditorSlots = {
52
47
  };
53
48
  };
54
49
 
55
- // TODO(burdon): Spellcheck?
56
50
  export type TextEditorProps = {
57
51
  model: EditorModel; // TODO(burdon): Optional (e.g., just provide content if readonly).
58
52
  readonly?: boolean; // TODO(burdon): Move into model.
@@ -60,7 +54,6 @@ export type TextEditorProps = {
60
54
  lineWrapping?: boolean;
61
55
  scrollTo?: StateEffect<any>; // TODO(burdon): Restore scroll position: scrollTo EditorView.scrollSnapshot().
62
56
  selection?: { anchor: number; head?: number };
63
- editorMode?: EditorMode; // TODO(burdon): Factor out.
64
57
  placeholder?: string;
65
58
  theme?: ThemeStyles;
66
59
  slots?: TextEditorSlots;
@@ -74,26 +67,10 @@ export type TextEditorProps = {
74
67
  // TODO(burdon): Replace with useTextEditor.
75
68
  export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
76
69
  (
77
- {
78
- model,
79
- readonly,
80
- autoFocus,
81
- scrollTo,
82
- selection,
83
- editorMode,
84
- theme,
85
- slots = defaultSlots,
86
- extensions = [],
87
- debug,
88
- },
70
+ { model, readonly, autoFocus, scrollTo, selection, theme, slots = defaultSlots, extensions = [], debug },
89
71
  forwardedRef,
90
72
  ) => {
91
- // TODO(burdon): Hook causes error even if properties are not spread into div.
92
- // Uncaught TypeError: Cannot read properties of undefined (reading 'relatedTarget')
93
- // Uses event.detail, which is deprecated (not event.details). At runtime the event has a property `details`.
94
- // https://github.com/microsoft/tabster/blob/master/src/State/FocusedElement.ts#L348 (e.detail.relatedTarget)
95
- // https://github.com/microsoft/keyborg/blob/49e49b2c3ba0a5f6cc518ac46825d7551def8109/src/FocusEvent.ts#L58
96
- // const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
73
+ const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
97
74
  const { themeMode } = useThemeContext();
98
75
 
99
76
  const rootRef = useRef<HTMLDivElement>(null);
@@ -143,6 +120,15 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
143
120
  EditorView.editorAttributes.of({ class: slots.editor?.className ?? '' }),
144
121
  EditorView.contentAttributes.of({ class: slots.content?.className ?? '' }),
145
122
 
123
+ // Focus.
124
+ EditorView.updateListener.of((update) => {
125
+ update.transactions.forEach((transaction) => {
126
+ if (transaction.isUserEvent('focus.container')) {
127
+ rootRef.current?.focus();
128
+ }
129
+ });
130
+ }),
131
+
146
132
  // State.
147
133
  EditorView.editable.of(!readonly),
148
134
  EditorState.readOnly.of(!!readonly),
@@ -151,9 +137,6 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
151
137
  // NOTE: This must come before user extensions.
152
138
  model.extension,
153
139
 
154
- // TODO(burdon): Factor out? (Requires special handling for Escape/Enter below).
155
- editorMode === 'vim' && vim(),
156
-
157
140
  // Custom.
158
141
  ...extensions,
159
142
  ].filter(isNotFalsy),
@@ -180,44 +163,40 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
180
163
  view?.destroy();
181
164
  setView(newView);
182
165
 
166
+ // Remove tabster attribute (rely on custom keymap).
167
+ if (state.facet(editorMode).noTabster) {
168
+ rootRef.current.removeAttribute('data-tabster');
169
+ }
170
+
183
171
  return () => {
184
172
  newView?.destroy();
185
173
  setView(null);
186
174
  };
187
- }, [rootRef, model, readonly, editorMode, themeMode]);
175
+ }, [rootRef, model, readonly, themeMode]);
188
176
 
189
- // Handles tab/focus.
190
- // Pressing Escape focuses the outer div (to support tab navigation); pressing Enter refocuses the editor.
191
- // TODO(burdon): Convert to keymap?
192
- const handleKeyUp = useCallback(
193
- (event: KeyboardEvent) => {
194
- const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
177
+ // Focus editor on Enter (e.g., when tabbing to this component).
178
+ const handleKeyUp = useCallback<KeyboardEventHandler<HTMLDivElement>>(
179
+ (event) => {
180
+ const { key } = event;
195
181
  switch (key) {
196
182
  case 'Enter': {
197
183
  view?.focus();
198
184
  break;
199
185
  }
200
-
201
- case 'Escape': {
202
- if (editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)) {
203
- view?.focus();
204
- }
205
- break;
206
- }
207
186
  }
208
187
  },
209
- [view, editorMode],
188
+ [view],
210
189
  );
211
190
 
212
191
  return (
213
192
  <div
214
- key={model.id}
215
193
  role='none'
194
+ ref={rootRef}
195
+ key={model.id}
216
196
  tabIndex={0}
217
- {...slots.root}
218
- // {...(editorMode !== 'vim' && tabsterDOMAttribute)}
219
197
  onKeyUp={handleKeyUp}
220
- ref={rootRef}
198
+ {...slots.root}
199
+ {...tabsterDOMAttribute}
221
200
  />
222
201
  );
223
202
  },
@@ -13,5 +13,6 @@ export * from './cursor';
13
13
  export * from './listener';
14
14
  export * from './markdown';
15
15
  export * from './mention';
16
+ export * from './modes';
16
17
  export * from './outliner';
17
18
  export * from './typewriter';
@@ -0,0 +1,36 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type Extension, Facet } from '@codemirror/state';
6
+ import { keymap } from '@codemirror/view';
7
+ import { vim } from '@replit/codemirror-vim';
8
+
9
+ export type EditorMode = 'default' | 'vim' | undefined;
10
+
11
+ export type EditorConfig = {
12
+ type: string;
13
+ noTabster?: boolean;
14
+ };
15
+
16
+ export const editorMode = Facet.define<EditorConfig, EditorConfig>({
17
+ combine: (modes) => modes[0] ?? {},
18
+ });
19
+
20
+ export const EditorModes: { [mode: string]: Extension } = {
21
+ default: [],
22
+ vim: [
23
+ vim(),
24
+ editorMode.of({ type: 'vim', noTabster: true }),
25
+ keymap.of([
26
+ {
27
+ key: 'Alt-Escape',
28
+ run: (view) => {
29
+ // Focus container for tab navigation.
30
+ view.dispatch({ userEvent: 'focus.container' });
31
+ return true;
32
+ },
33
+ },
34
+ ]),
35
+ ],
36
+ };
package/src/index.ts CHANGED
@@ -2,10 +2,11 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
+ export { type Extension, type EditorState } from '@codemirror/state';
6
+ export { type EditorView, keymap } from '@codemirror/view';
7
+
5
8
  export { TextKind } from '@dxos/protocols/proto/dxos/echo/model/text';
6
9
  export { Doc, YText, YXmlFragment } from '@dxos/text-model';
7
- export { type Extension } from '@codemirror/state';
8
- export { type EditorView, keymap } from '@codemirror/view';
9
10
 
10
11
  export * from './components';
11
12
  export * from './extensions';