@dxos/react-ui-editor 0.6.6-main.e1a6e1f → 0.6.6-staging.41c080b

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 (49) hide show
  1. package/dist/lib/browser/index.mjs +203 -307
  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/index.d.ts +0 -1
  5. package/dist/types/src/components/index.d.ts.map +1 -1
  6. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  7. package/dist/types/src/extensions/automerge/automerge.stories.d.ts +5 -2
  8. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  9. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  10. package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -1
  11. package/dist/types/src/extensions/awareness/awareness.d.ts +6 -6
  12. package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -1
  13. package/dist/types/src/extensions/comments.d.ts +1 -2
  14. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  15. package/dist/types/src/extensions/cursor.d.ts +1 -1
  16. package/dist/types/src/extensions/cursor.d.ts.map +1 -1
  17. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  18. package/dist/types/src/extensions/markdown/link.d.ts +1 -3
  19. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
  20. package/dist/types/src/hooks/{useTextEditor.stories.d.ts → InputMode.stories.d.ts} +5 -9
  21. package/dist/types/src/hooks/InputMode.stories.d.ts.map +1 -0
  22. package/dist/types/src/{components/TextEditor → hooks}/TextEditor.stories.d.ts +4 -16
  23. package/dist/types/src/hooks/TextEditor.stories.d.ts.map +1 -0
  24. package/dist/types/src/hooks/useTextEditor.d.ts +20 -3
  25. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  26. package/dist/types/src/themes/default.d.ts.map +1 -1
  27. package/package.json +25 -25
  28. package/src/components/Toolbar/Toolbar.stories.tsx +1 -1
  29. package/src/components/index.ts +0 -1
  30. package/src/extensions/automerge/automerge.stories.tsx +25 -18
  31. package/src/extensions/automerge/automerge.ts +2 -0
  32. package/src/extensions/automerge/cursor.ts +3 -4
  33. package/src/extensions/awareness/awareness-provider.ts +2 -0
  34. package/src/extensions/awareness/awareness.ts +34 -30
  35. package/src/extensions/comments.ts +6 -14
  36. package/src/extensions/cursor.ts +1 -1
  37. package/src/extensions/factories.ts +19 -13
  38. package/src/hooks/{useTextEditor.stories.tsx → InputMode.stories.tsx} +30 -35
  39. package/src/{components/TextEditor → hooks}/TextEditor.stories.tsx +22 -28
  40. package/src/hooks/useTextEditor.ts +75 -23
  41. package/src/themes/default.ts +20 -4
  42. package/dist/types/src/components/TextEditor/TextEditor.d.ts +0 -34
  43. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +0 -1
  44. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +0 -1
  45. package/dist/types/src/components/TextEditor/index.d.ts +0 -2
  46. package/dist/types/src/components/TextEditor/index.d.ts.map +0 -1
  47. package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +0 -1
  48. package/src/components/TextEditor/TextEditor.tsx +0 -184
  49. package/src/components/TextEditor/index.ts +0 -5
@@ -5,9 +5,9 @@
5
5
  import '@dxosTheme';
6
6
  import { markdown } from '@codemirror/lang-markdown';
7
7
  import { ArrowSquareOut, X } from '@phosphor-icons/react';
8
- import { type EditorView } from 'codemirror';
8
+ import { effect, useSignal } from '@preact/signals-react';
9
9
  import defaultsDeep from 'lodash.defaultsdeep';
10
- import React, { type FC, type KeyboardEvent, StrictMode, useMemo, useRef, useState } from 'react';
10
+ import React, { type FC, type KeyboardEvent, StrictMode, useMemo, useState } from 'react';
11
11
  import { createRoot } from 'react-dom/client';
12
12
 
13
13
  import { TextType } from '@braneframe/types';
@@ -21,7 +21,7 @@ import { Button, DensityProvider, Input, ThemeProvider, useThemeContext } from '
21
21
  import { baseSurface, defaultTx, getSize, mx, textBlockWidth } from '@dxos/react-ui-theme';
22
22
  import { withTheme } from '@dxos/storybook-utils';
23
23
 
24
- import { TextEditor, type TextEditorProps } from './TextEditor';
24
+ import { useTextEditor, type UseTextEditorProps } from './useTextEditor';
25
25
  import {
26
26
  InputModeExtensions,
27
27
  annotations,
@@ -32,6 +32,7 @@ import {
32
32
  comments,
33
33
  createBasicExtensions,
34
34
  createDataExtensions,
35
+ createExternalCommentSync,
35
36
  createMarkdownExtensions,
36
37
  createThemeExtensions,
37
38
  decorateMarkdown,
@@ -45,14 +46,13 @@ import {
45
46
  state,
46
47
  table,
47
48
  typewriter,
48
- useComments,
49
49
  type CommandAction,
50
50
  type CommandOptions,
51
51
  type Comment,
52
52
  type CommentsOptions,
53
53
  type SelectionState,
54
- } from '../../extensions';
55
- import translations from '../../translations';
54
+ } from '../extensions';
55
+ import translations from '../translations';
56
56
 
57
57
  faker.seed(101);
58
58
 
@@ -242,25 +242,19 @@ const renderLinkButton = (el: Element, url: string) => {
242
242
  type StoryProps = {
243
243
  id?: string;
244
244
  text?: string;
245
- comments?: Comment[];
246
245
  readonly?: boolean;
247
246
  placeholder?: string;
248
- } & Pick<TextEditorProps, 'selection' | 'extensions'>;
247
+ } & Pick<UseTextEditorProps, 'selection' | 'extensions'>;
249
248
 
250
249
  const Story = ({
251
250
  id = 'editor-' + PublicKey.random().toHex().slice(0, 8),
252
251
  text,
253
- comments,
254
252
  extensions: _extensions = [],
255
253
  readonly,
256
254
  placeholder = 'New document.',
257
- ...props
255
+ selection,
258
256
  }: StoryProps) => {
259
257
  const [object] = useState(createEchoObject(create(TextType, { content: text ?? '' })));
260
-
261
- const viewRef = useRef<EditorView>(null);
262
- useComments(viewRef.current, id, comments);
263
-
264
258
  const { themeMode } = useThemeContext();
265
259
  const extensions = useMemo(
266
260
  () => [
@@ -278,21 +272,16 @@ const Story = ({
278
272
  [_extensions, object],
279
273
  );
280
274
 
281
- return (
282
- <TextEditor
283
- {...props}
284
- id={id}
285
- ref={viewRef}
286
- doc={text}
287
- extensions={extensions}
288
- className={mx(textBlockWidth, 'min-bs-dvh')}
289
- />
275
+ const { parentRef, focusAttributes } = useTextEditor(
276
+ () => ({ id, initialValue: text, extensions, selection }),
277
+ [extensions],
290
278
  );
279
+
280
+ return <div role='none' ref={parentRef} className={mx(textBlockWidth, 'min-bs-dvh')} {...focusAttributes} />;
291
281
  };
292
282
 
293
283
  export default {
294
- title: 'react-ui-editor/TextEditor',
295
- component: TextEditor,
284
+ title: 'react-ui-editor/useTextEditor',
296
285
  decorators: [withTheme],
297
286
  render: Story,
298
287
  parameters: { translations, layout: 'fullscreen' },
@@ -474,17 +463,22 @@ export const Command = {
474
463
 
475
464
  export const Comments = {
476
465
  render: () => {
477
- const [_comments, setComments] = useState<Comment[]>([]);
466
+ const _comments = useSignal<Comment[]>([]);
478
467
  return (
479
468
  <Story
480
469
  text={str('# Comments', '', text.paragraphs, text.footer)}
481
- comments={_comments}
482
470
  extensions={[
471
+ createExternalCommentSync(
472
+ 'test',
473
+ (sink) => effect(() => sink()),
474
+ () => _comments.value,
475
+ ),
483
476
  comments({
477
+ id: 'test',
484
478
  onHover: onCommentsHover,
485
479
  onCreate: ({ cursor }) => {
486
480
  const id = PublicKey.random().toHex();
487
- setComments((commentRanges) => [...commentRanges, { id, cursor }]);
481
+ _comments.value = [..._comments.value, { id, cursor }];
488
482
  return id;
489
483
  },
490
484
  onSelect: (state) => {
@@ -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
  //
@@ -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;AAuBpB,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, editorInputMode, 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(editorInputMode).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, 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';