@dxos/react-ui-editor 0.6.6-main.e1a6e1f → 0.6.6-staging.582ce24

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
@@ -1,7 +1,8 @@
1
+ import { type EditorStateConfig, type StateEffect } from '@codemirror/state';
1
2
  import { EditorView } from '@codemirror/view';
2
3
  import { useFocusableGroup } from '@fluentui/react-tabster';
3
4
  import { type DependencyList, type KeyboardEventHandler, type RefObject } from 'react';
4
- import { type TextEditorProps } from '../components';
5
+ import { type MaybeFunction } from '@dxos/util';
5
6
  export type UseTextEditor = {
6
7
  parentRef: RefObject<HTMLDivElement>;
7
8
  view?: EditorView;
@@ -10,9 +11,25 @@ export type UseTextEditor = {
10
11
  onKeyUp: KeyboardEventHandler<HTMLDivElement>;
11
12
  };
12
13
  };
13
- export type UseTextEditorProps = Omit<TextEditorProps, 'moveToEndOfLine' | 'dataTestId'>;
14
+ export type CursorInfo = {
15
+ from: number;
16
+ to: number;
17
+ line: number;
18
+ lines: number;
19
+ length: number;
20
+ after?: string;
21
+ };
22
+ export type UseTextEditorProps = Pick<EditorStateConfig, 'selection' | 'extensions'> & {
23
+ id?: string;
24
+ initialValue?: string;
25
+ className?: string;
26
+ autoFocus?: boolean;
27
+ scrollTo?: StateEffect<unknown>;
28
+ moveToEndOfLine?: boolean;
29
+ debug?: boolean;
30
+ };
14
31
  /**
15
32
  * Hook for creating editor.
16
33
  */
17
- export declare const useTextEditor: (cb?: () => UseTextEditorProps, deps?: DependencyList) => UseTextEditor;
34
+ export declare const useTextEditor: (props?: MaybeFunction<UseTextEditorProps>, deps?: DependencyList) => UseTextEditor;
18
35
  //# sourceMappingURL=useTextEditor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useTextEditor.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useTextEditor.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,SAAS,EAMf,MAAM,OAAO,CAAC;AAKf,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAIrD,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,EAAE,SAAS,CAAC,cAAc,CAAC,CAAC;IACrC,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,eAAe,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,GAAG;QACtD,QAAQ,EAAE,CAAC,CAAC;QACZ,OAAO,EAAE,oBAAoB,CAAC,cAAc,CAAC,CAAC;KAC/C,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,eAAe,EAAE,iBAAiB,GAAG,YAAY,CAAC,CAAC;AAEzF;;GAEG;AACH,eAAO,MAAM,aAAa,QAAQ,MAAM,kBAAkB,4BAA2C,aA+FpG,CAAC"}
1
+ {"version":3,"file":"useTextEditor.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useTextEditor.ts"],"names":[],"mappings":"AAIA,OAAO,EAAe,KAAK,iBAAiB,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC1F,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,SAAS,EAMf,MAAM,OAAO,CAAC;AAIf,OAAO,EAAc,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAK5D,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,EAAE,SAAS,CAAC,cAAc,CAAC,CAAC;IACrC,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,eAAe,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,GAAG;QACtD,QAAQ,EAAE,CAAC,CAAC;QACZ,OAAO,EAAE,oBAAoB,CAAC,cAAc,CAAC,CAAC;KAC/C,CAAC;CACH,CAAC;AAEF,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,kBAAkB,GAAG,IAAI,CAAC,iBAAiB,EAAE,WAAW,GAAG,YAAY,CAAC,GAAG;IACrF,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,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;CACjB,CAAC;AAIF;;GAEG;AACH,eAAO,MAAM,aAAa,WACjB,cAAc,kBAAkB,CAAC,4BAEvC,aA6HF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"default.d.ts","sourceRoot":"","sources":["../../../../src/themes/default.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,WAAW,EAAU,MAAM,WAAW,CAAC;AAKrD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY,EAAE,WAgQ1B,CAAC"}
1
+ {"version":3,"file":"default.d.ts","sourceRoot":"","sources":["../../../../src/themes/default.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,WAAW,EAAU,MAAM,WAAW,CAAC;AAKrD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY,EAAE,WAgR1B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-editor",
3
- "version": "0.6.6-main.e1a6e1f",
3
+ "version": "0.6.6-staging.582ce24",
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",
@@ -47,19 +47,19 @@
47
47
  "lodash.sortby": "^4.7.0",
48
48
  "react-dropzone": "^14.2.3",
49
49
  "style-mod": "^4.1.0",
50
- "@dxos/async": "0.6.6-main.e1a6e1f",
51
- "@dxos/automerge": "0.6.6-main.e1a6e1f",
52
- "@dxos/context": "0.6.6-main.e1a6e1f",
53
- "@dxos/debug": "0.6.6-main.e1a6e1f",
54
- "@dxos/invariant": "0.6.6-main.e1a6e1f",
55
- "@dxos/echo-schema": "0.6.6-main.e1a6e1f",
56
- "@dxos/log": "0.6.6-main.e1a6e1f",
57
- "@dxos/display-name": "0.6.6-main.e1a6e1f",
58
- "@dxos/protocols": "0.6.6-main.e1a6e1f",
59
- "@dxos/react-async": "0.6.6-main.e1a6e1f",
60
- "@dxos/react-ui": "0.6.6-main.e1a6e1f",
61
- "@dxos/react-ui-theme": "0.6.6-main.e1a6e1f",
62
- "@dxos/util": "0.6.6-main.e1a6e1f"
50
+ "@dxos/automerge": "0.6.6-staging.582ce24",
51
+ "@dxos/context": "0.6.6-staging.582ce24",
52
+ "@dxos/async": "0.6.6-staging.582ce24",
53
+ "@dxos/invariant": "0.6.6-staging.582ce24",
54
+ "@dxos/debug": "0.6.6-staging.582ce24",
55
+ "@dxos/log": "0.6.6-staging.582ce24",
56
+ "@dxos/react-ui": "0.6.6-staging.582ce24",
57
+ "@dxos/protocols": "0.6.6-staging.582ce24",
58
+ "@dxos/display-name": "0.6.6-staging.582ce24",
59
+ "@dxos/react-async": "0.6.6-staging.582ce24",
60
+ "@dxos/react-ui-theme": "0.6.6-staging.582ce24",
61
+ "@dxos/util": "0.6.6-staging.582ce24",
62
+ "@dxos/echo-schema": "0.6.6-staging.582ce24"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@phosphor-icons/react": "^2.1.5",
@@ -85,22 +85,22 @@
85
85
  "vite-plugin-top-level-await": "^1.4.1",
86
86
  "vite-plugin-wasm": "^3.3.0",
87
87
  "vitest": "^1.5.0",
88
- "@dxos/automerge": "0.6.6-main.e1a6e1f",
89
- "@dxos/config": "0.6.6-main.e1a6e1f",
90
- "@dxos/echo-signals": "0.6.6-main.e1a6e1f",
91
- "@dxos/keyboard": "0.6.6-main.e1a6e1f",
92
- "@dxos/echo-typegen": "0.6.6-main.e1a6e1f",
93
- "@dxos/random": "0.6.6-main.e1a6e1f",
94
- "@dxos/react-client": "0.6.6-main.e1a6e1f",
95
- "@dxos/react-ui": "0.6.6-main.e1a6e1f",
96
- "@dxos/storybook-utils": "0.6.6-main.e1a6e1f",
97
- "@braneframe/types": "0.6.6-main.e1a6e1f"
88
+ "@dxos/automerge": "0.6.6-staging.582ce24",
89
+ "@dxos/echo-typegen": "0.6.6-staging.582ce24",
90
+ "@dxos/keyboard": "0.6.6-staging.582ce24",
91
+ "@dxos/echo-signals": "0.6.6-staging.582ce24",
92
+ "@dxos/random": "0.6.6-staging.582ce24",
93
+ "@dxos/react-client": "0.6.6-staging.582ce24",
94
+ "@dxos/react-ui": "0.6.6-staging.582ce24",
95
+ "@dxos/config": "0.6.6-staging.582ce24",
96
+ "@dxos/storybook-utils": "0.6.6-staging.582ce24",
97
+ "@braneframe/types": "0.6.6-staging.582ce24"
98
98
  },
99
99
  "peerDependencies": {
100
100
  "@phosphor-icons/react": "^2.1.5",
101
101
  "react": "^18.0.0",
102
102
  "react-dom": "^18.0.0",
103
- "@dxos/react-client": "0.6.6-main.e1a6e1f"
103
+ "@dxos/react-client": "0.6.6-staging.582ce24"
104
104
  },
105
105
  "publishConfig": {
106
106
  "access": "public"
@@ -45,7 +45,7 @@ const Story: FC<{ content: string }> = ({ content }) => {
45
45
  const { parentRef, view } = useTextEditor(() => {
46
46
  return {
47
47
  id: text.id,
48
- doc: text.content,
48
+ initialValue: text.content,
49
49
  extensions: [
50
50
  formattingObserver,
51
51
  createBasicExtensions({ readonly: viewMode === 'readonly' }),
@@ -2,5 +2,4 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- export * from './TextEditor';
6
5
  export * from './Toolbar';
@@ -12,15 +12,15 @@ import { Repo } from '@dxos/automerge/automerge-repo';
12
12
  import { BroadcastChannelNetworkAdapter } from '@dxos/automerge/automerge-repo-network-broadcastchannel';
13
13
  import { create, type Expando } from '@dxos/echo-schema';
14
14
  import { type PublicKey } from '@dxos/keys';
15
- import { Filter, DocAccessor, createDocAccessor, useSpace } from '@dxos/react-client/echo';
15
+ import { Filter, DocAccessor, createDocAccessor, useSpace, useQuery, type Space } from '@dxos/react-client/echo';
16
+ import { useIdentity, type Identity } from '@dxos/react-client/halo';
16
17
  import { ClientRepeater } from '@dxos/react-client/testing';
17
18
  import { useThemeContext } from '@dxos/react-ui';
18
19
  import { withTheme } from '@dxos/storybook-utils';
19
20
 
20
- import { automerge } from './automerge';
21
21
  import { useTextEditor } from '../../hooks';
22
22
  import translations from '../../translations';
23
- import { createBasicExtensions, createThemeExtensions } from '../factories';
23
+ import { createBasicExtensions, createDataExtensions, createThemeExtensions } from '../factories';
24
24
 
25
25
  const initialContent = 'Hello world!';
26
26
 
@@ -31,17 +31,26 @@ type TestObject = {
31
31
  type EditorProps = {
32
32
  source: DocAccessor;
33
33
  autoFocus?: boolean;
34
+ space?: Space;
35
+ identity?: Identity;
34
36
  };
35
37
 
36
- const Editor = ({ source, autoFocus }: EditorProps) => {
38
+ const Editor = ({ source, autoFocus, space, identity }: EditorProps) => {
37
39
  const { themeMode } = useThemeContext();
38
40
  const { parentRef } = useTextEditor(
39
41
  () => ({
40
- doc: DocAccessor.getValue(source),
42
+ initialValue: DocAccessor.getValue(source),
41
43
  extensions: [
42
44
  createBasicExtensions({ placeholder: 'Type here...' }),
43
- createThemeExtensions({ themeMode, slots: { editor: { className: 'w-full p-2 bg-white dark:bg-black' } } }),
44
- automerge(source),
45
+ createThemeExtensions({
46
+ themeMode,
47
+ slots: {
48
+ editor: { className: 'w-full bg-white dark:bg-black' },
49
+ // TODO(burdon): Sufficient padding so indicator isn't clipped.
50
+ content: { className: '!m-8' },
51
+ },
52
+ }),
53
+ createDataExtensions({ id: 'test', text: source, space, identity }),
45
54
  ],
46
55
  autoFocus,
47
56
  }),
@@ -94,25 +103,23 @@ export default {
94
103
  };
95
104
 
96
105
  const EchoStory = ({ spaceKey }: { spaceKey: PublicKey }) => {
106
+ const identity = useIdentity();
97
107
  const space = useSpace(spaceKey);
98
108
  const [source, setSource] = useState<DocAccessor>();
109
+ const objects = useQuery<Expando>(space, Filter.from({ type: 'test' }));
110
+
99
111
  useEffect(() => {
100
- setTimeout(async () => {
101
- if (space) {
102
- const { objects = [] } = await space.db.query<Expando>(Filter.from({ type: 'test' })).run();
103
- if (objects.length) {
104
- const source = createDocAccessor(objects[0].content, ['content']);
105
- setSource(source);
106
- }
107
- }
108
- });
109
- }, [space]);
112
+ if (!source && objects.length) {
113
+ const source = createDocAccessor(objects[0].content, ['content']);
114
+ setSource(source);
115
+ }
116
+ }, [objects, source]);
110
117
 
111
118
  if (!source) {
112
119
  return null;
113
120
  }
114
121
 
115
- return <Editor source={source} />;
122
+ return <Editor source={source} space={space} identity={identity ?? undefined} />;
116
123
  };
117
124
 
118
125
  export const Default = {};
@@ -54,6 +54,8 @@ export const automerge = (accessor: DocAccessor): Extension => {
54
54
 
55
55
  return [
56
56
  Cursor.converter.of(cursorConverter(accessor)),
57
+
58
+ // Track heads.
57
59
  syncState,
58
60
 
59
61
  // Reconcile external updates.
@@ -3,15 +3,14 @@
3
3
  //
4
4
 
5
5
  import { log } from '@dxos/log';
6
- import { toCursor, type DocAccessor, fromCursor } from '@dxos/react-client/echo';
6
+ import { type DocAccessor, fromCursor, toCursor } from '@dxos/react-client/echo';
7
7
 
8
8
  import { type CursorConverter } from '../cursor';
9
9
 
10
10
  export const cursorConverter = (accessor: DocAccessor): CursorConverter => ({
11
- // TODO(burdon): Handle assoc to associate with a previous character.
12
- toCursor: (pos) => {
11
+ toCursor: (pos, assoc) => {
13
12
  try {
14
- return toCursor(accessor, pos);
13
+ return toCursor(accessor, pos, assoc);
15
14
  } catch (err) {
16
15
  log.catch(err);
17
16
  return ''; // In case of invalid request (e.g., wrong document).
@@ -62,6 +62,8 @@ export class SpaceAwarenessProvider implements AwarenessProvider {
62
62
  state: this._localState,
63
63
  } satisfies ProtocolMessage);
64
64
 
65
+ // TODO(burdon): Replace with throttle.
66
+ // TODO(burdon): Send heads?
65
67
  await sleep(DEBOUNCE_INTERVAL);
66
68
  }
67
69
  });
@@ -51,10 +51,9 @@ export type AwarenessPosition = {
51
51
  };
52
52
 
53
53
  export type AwarenessInfo = {
54
- displayName?: string;
55
- // TODO(burdon): Rename light/dark.
56
- color?: string;
57
- lightColor?: string;
54
+ displayName: string;
55
+ darkColor: string;
56
+ lightColor: string;
58
57
  };
59
58
 
60
59
  export type AwarenessState = {
@@ -80,14 +79,14 @@ export const awareness = (provider = dummyProvider): Extension => {
80
79
  * Generates selection decorations from remote peers.
81
80
  */
82
81
  export class RemoteSelectionsDecorator implements PluginValue {
83
- public decorations: DecorationSet = RangeSet.of([]);
84
-
85
82
  private readonly _ctx = new Context();
83
+ private readonly _cursorConverter: CursorConverter;
84
+ private readonly _provider: AwarenessProvider;
85
+
86
+ private _lastAnchor?: number;
87
+ private _lastHead?: number;
86
88
 
87
- private _cursorConverter: CursorConverter;
88
- private _provider: AwarenessProvider;
89
- private _lastAnchor?: number = undefined;
90
- private _lastHead?: number = undefined;
89
+ public decorations: DecorationSet = RangeSet.of([]);
91
90
 
92
91
  constructor(view: EditorView) {
93
92
  this._cursorConverter = view.state.facet(Cursor.converter);
@@ -104,13 +103,13 @@ export class RemoteSelectionsDecorator implements PluginValue {
104
103
  }
105
104
 
106
105
  update(update: ViewUpdate) {
107
- this._updateLocalSelection(update);
108
- this._updateRemoteSelections(update);
106
+ this._updateLocalSelection(update.view);
107
+ this._updateRemoteSelections(update.view);
109
108
  }
110
109
 
111
- private _updateLocalSelection(update: ViewUpdate) {
112
- const hasFocus = update.view.hasFocus && update.view.dom.ownerDocument.hasFocus();
113
- const { anchor = undefined, head = undefined } = hasFocus ? update.state.selection.main : {};
110
+ private _updateLocalSelection(view: EditorView) {
111
+ const hasFocus = view.hasFocus && view.dom.ownerDocument.hasFocus();
112
+ const { anchor = undefined, head = undefined } = hasFocus ? view.state.selection.main : {};
114
113
  if (this._lastAnchor === anchor && this._lastHead === head) {
115
114
  return;
116
115
  }
@@ -122,14 +121,22 @@ export class RemoteSelectionsDecorator implements PluginValue {
122
121
  anchor !== undefined && head !== undefined
123
122
  ? {
124
123
  anchor: this._cursorConverter.toCursor(anchor),
125
- head: this._cursorConverter.toCursor(head),
124
+ head: this._cursorConverter.toCursor(head, -1),
126
125
  }
127
126
  : undefined,
128
127
  );
129
128
  }
130
129
 
131
- private _updateRemoteSelections(update: ViewUpdate) {
132
- const decorations: Range<Decoration>[] = [];
130
+ private _updateRemoteSelections(view: EditorView) {
131
+ const decorations: Range<Decoration>[] = [
132
+ // TODO(burdon): Factor out for testing.
133
+ // {
134
+ // from: 0,
135
+ // to: 0,
136
+ // value: Decoration.widget({ side: 0, block: false, widget: new RemoteCaretWidget('Test', 'red') }),
137
+ // },
138
+ ];
139
+
133
140
  const awarenessStates = this._provider.getRemoteStates();
134
141
  for (const state of awarenessStates) {
135
142
  const anchor = state.position?.anchor ? this._cursorConverter.fromCursor(state.position.anchor) : null;
@@ -138,15 +145,14 @@ export class RemoteSelectionsDecorator implements PluginValue {
138
145
  continue;
139
146
  }
140
147
 
141
- const start = Math.min(Math.min(anchor, head), update.view.state.doc.length);
142
- const end = Math.min(Math.max(anchor, head), update.view.state.doc.length);
148
+ const start = Math.min(Math.min(anchor, head), view.state.doc.length);
149
+ const end = Math.min(Math.max(anchor, head), view.state.doc.length);
143
150
 
144
- const startLine = update.view.state.doc.lineAt(start);
145
- const endLine = update.view.state.doc.lineAt(end);
151
+ const startLine = view.state.doc.lineAt(start);
152
+ const endLine = view.state.doc.lineAt(end);
146
153
 
147
- // TODO(burdon): Factor out styles.
148
- const color = state.info.color ?? '#30bced';
149
- const lightColor = state.info.lightColor ?? color + '33';
154
+ const darkColor = state.info.darkColor;
155
+ const lightColor = state.info.lightColor;
150
156
 
151
157
  if (startLine.number === endLine.number) {
152
158
  // Selected content in a single line.
@@ -180,7 +186,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
180
186
  });
181
187
 
182
188
  for (let i = startLine.number + 1; i < endLine.number; i++) {
183
- const linePos = update.view.state.doc.line(i).from;
189
+ const linePos = view.state.doc.line(i).from;
184
190
  decorations.push({
185
191
  from: linePos,
186
192
  to: linePos,
@@ -197,7 +203,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
197
203
  value: Decoration.widget({
198
204
  side: head - anchor > 0 ? -1 : 1, // The local cursor should be rendered outside the remote selection.
199
205
  block: false,
200
- widget: new RemoteCaretWidget(state.info.displayName ?? 'Anonymous', color),
206
+ widget: new RemoteCaretWidget(state.info.displayName ?? 'Anonymous', darkColor),
201
207
  }),
202
208
  });
203
209
  }
@@ -232,7 +238,6 @@ class RemoteCaretWidget extends WidgetType {
232
238
  span.appendChild(document.createTextNode('\u2060'));
233
239
  span.appendChild(info);
234
240
  span.appendChild(document.createTextNode('\u2060'));
235
-
236
241
  return span;
237
242
  }
238
243
 
@@ -296,12 +301,11 @@ const styles = EditorView.baseTheme({
296
301
  lineHeight: 'normal',
297
302
  userSelect: 'none',
298
303
  color: 'white',
299
- padding: '2px',
304
+ padding: '2px 6px',
300
305
  zIndex: 101,
301
306
  transition: 'opacity .3s ease-in-out',
302
307
  backgroundColor: 'inherit',
303
308
  borderRadius: '2px',
304
- // These should be separate.
305
309
  opacity: 0,
306
310
  transitionDelay: '0s',
307
311
  whiteSpace: 'nowrap',
@@ -25,7 +25,6 @@ import {
25
25
  import sortBy from 'lodash.sortby';
26
26
  import { useEffect, useMemo, useState } from 'react';
27
27
 
28
- import { type ThreadType } from '@braneframe/types';
29
28
  import { debounce, type UnsubscribeCallback } from '@dxos/async';
30
29
  import { log } from '@dxos/log';
31
30
  import { nonNullable } from '@dxos/util';
@@ -613,21 +612,16 @@ const hasActiveSelection = (state: EditorState): boolean => {
613
612
  };
614
613
 
615
614
  class ExternalCommentSync implements PluginValue {
616
- private unsubscribe: () => void;
615
+ private readonly unsubscribe: () => void;
617
616
 
618
617
  constructor(
619
618
  view: EditorView,
620
619
  id: string,
621
620
  subscribe: (sink: () => void) => UnsubscribeCallback,
622
- getThreads: () => ThreadType[],
621
+ getComments: () => Comment[],
623
622
  ) {
624
623
  const updateComments = () => {
625
- const threads = getThreads();
626
- const comments = threads
627
- .filter(nonNullable)
628
- .filter((thread) => thread.anchor)
629
- .map((thread) => ({ id: thread.id, cursor: thread.anchor! }));
630
-
624
+ const comments = getComments();
631
625
  if (id === view.state.facet(documentId)) {
632
626
  queueMicrotask(() => view.dispatch({ effects: setComments.of({ id, comments }) }));
633
627
  }
@@ -644,12 +638,12 @@ class ExternalCommentSync implements PluginValue {
644
638
  export const createExternalCommentSync = (
645
639
  id: string,
646
640
  subscribe: (sink: () => void) => UnsubscribeCallback,
647
- getThreads: () => ThreadType[],
641
+ getComments: () => Comment[],
648
642
  ): Extension =>
649
643
  ViewPlugin.fromClass(
650
644
  class {
651
645
  constructor(view: EditorView) {
652
- return new ExternalCommentSync(view, id, subscribe, getThreads);
646
+ return new ExternalCommentSync(view, id, subscribe, getComments);
653
647
  }
654
648
  },
655
649
  );
@@ -698,7 +692,7 @@ export const useComments = (view: EditorView | null | undefined, id: string, com
698
692
  * Hook provides an extension to listen for comment clicks and invoke a handler.
699
693
  */
700
694
  export const useCommentClickListener = (onCommentClick: (commentId: string) => void): Extension => {
701
- const observer = useMemo(
695
+ return useMemo(
702
696
  () =>
703
697
  EditorView.updateListener.of((update) => {
704
698
  update.transactions.forEach((transaction) => {
@@ -711,6 +705,4 @@ export const useCommentClickListener = (onCommentClick: (commentId: string) => v
711
705
  }),
712
706
  [onCommentClick],
713
707
  );
714
-
715
- return observer;
716
708
  };
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { type EditorState, Facet } from '@codemirror/state';
6
6
 
7
- import type { Range } from './types';
7
+ import { type Range } from './types';
8
8
 
9
9
  /**
10
10
  * Converts indexes into the text document into stable peer-independent cursors.
@@ -170,24 +170,30 @@ export type DataExtensionsProps<T> = {
170
170
 
171
171
  // TODO(burdon): Move out of react-ui-editor (remove echo deps).
172
172
  export const createDataExtensions = <T>({ id, text, space, identity }: DataExtensionsProps<T>): Extension[] => {
173
- const extensions: Extension[] = text ? [automerge(text)] : [];
173
+ const extensions: Extension[] = [];
174
+ if (text) {
175
+ extensions.push(automerge(text));
176
+ }
174
177
 
175
178
  if (space && identity) {
176
179
  const peerId = identity?.identityKey.toHex();
177
180
  const { cursorLightValue, cursorDarkValue } =
178
181
  hueTokens[(identity?.profile?.data?.hue as HuePalette | undefined) ?? hexToHue(peerId ?? '0')];
179
- const awarenessProvider = new SpaceAwarenessProvider({
180
- space,
181
- channel: `awareness.${id}`,
182
- peerId: identity.identityKey.toHex(),
183
- info: {
184
- displayName: identity.profile?.displayName ?? generateName(identity.identityKey.toHex()),
185
- color: cursorDarkValue,
186
- lightColor: cursorLightValue,
187
- },
188
- });
189
-
190
- extensions.push(awareness(awarenessProvider));
182
+
183
+ extensions.push(
184
+ awareness(
185
+ new SpaceAwarenessProvider({
186
+ space,
187
+ channel: `awareness.${id}`,
188
+ peerId: identity.identityKey.toHex(),
189
+ info: {
190
+ displayName: identity.profile?.displayName ?? generateName(identity.identityKey.toHex()),
191
+ darkColor: cursorDarkValue,
192
+ lightColor: cursorLightValue,
193
+ },
194
+ }),
195
+ ),
196
+ );
191
197
  }
192
198
 
193
199
  return extensions;
@@ -8,38 +8,36 @@ import React, { useState } from 'react';
8
8
 
9
9
  import { Toolbar as NaturalToolbar, Select, useThemeContext, Tooltip } from '@dxos/react-ui';
10
10
  import { attentionSurface, mx, textBlockWidth } from '@dxos/react-ui-theme';
11
- import { withTheme } from '@dxos/storybook-utils';
11
+ import { withFullscreen, withTheme } from '@dxos/storybook-utils';
12
12
 
13
13
  import { useActionHandler } from './useActionHandler';
14
- import { useTextEditor } from './useTextEditor';
14
+ import { useTextEditor, type UseTextEditorProps } from './useTextEditor';
15
15
  import { Toolbar } from '../components';
16
- import { createBasicExtensions, createThemeExtensions, InputModeExtensions } from '../extensions';
17
16
  import {
18
17
  type EditorInputMode,
19
18
  decorateMarkdown,
20
19
  createMarkdownExtensions,
21
20
  formattingKeymap,
21
+ image,
22
22
  table,
23
23
  useFormattingState,
24
- image,
24
+ createBasicExtensions,
25
+ createThemeExtensions,
26
+ InputModeExtensions,
25
27
  } from '../extensions';
26
28
  import translations from '../translations';
27
29
 
28
- type StoryProps = {
29
- autoFocus?: boolean;
30
- placeholder?: string;
31
- doc?: string;
32
- readonly?: boolean;
33
- };
30
+ type StoryProps = { placeholder?: string; readonly?: boolean } & UseTextEditorProps;
34
31
 
35
- const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
32
+ const Story = ({ autoFocus, initialValue, placeholder, readonly }: StoryProps) => {
36
33
  const { themeMode } = useThemeContext();
37
34
  const [formattingState, trackFormatting] = useFormattingState();
38
35
  const [editorInputMode, setEditorInputMode] = useState<EditorInputMode>('default');
39
36
  const { parentRef, view } = useTextEditor(
40
37
  () => ({
41
38
  autoFocus,
42
- doc,
39
+ initialValue,
40
+ moveToEndOfLine: true,
43
41
  extensions: [
44
42
  editorInputMode ? InputModeExtensions[editorInputMode] : [],
45
43
  createBasicExtensions({ placeholder, lineWrapping: true, readonly }),
@@ -61,10 +59,13 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
61
59
  // Also not sure if view is even guaranteed to exist at this point.
62
60
  return (
63
61
  <div role='none' className={mx('fixed inset-0 flex flex-col')}>
64
- <Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
65
- <Toolbar.Markdown />
66
- <EditorInputModeToolbar editorInputMode={editorInputMode} setEditorInputMode={setEditorInputMode} />
67
- </Toolbar.Root>
62
+ <Tooltip.Provider>
63
+ <Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
64
+ <Toolbar.Markdown />
65
+ <EditorInputModeToolbar editorInputMode={editorInputMode} setEditorInputMode={setEditorInputMode} />
66
+ </Toolbar.Root>
67
+ </Tooltip.Provider>
68
+
68
69
  <div role='none' className='grow overflow-hidden'>
69
70
  <div className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />
70
71
  </div>
@@ -103,30 +104,24 @@ const EditorInputModeToolbar = ({
103
104
  };
104
105
 
105
106
  export default {
106
- title: 'react-ui-editor/useTextEditor',
107
- decorators: [withTheme],
108
- render: (args: StoryProps) => (
109
- <Tooltip.Provider>
110
- <Story {...args} />
111
- </Tooltip.Provider>
112
- ),
107
+ title: 'react-ui-editor/InputMode',
108
+ decorators: [withTheme, withFullscreen()],
113
109
  parameters: { translations, layout: 'fullscreen' },
110
+ render: Story,
114
111
  };
115
112
 
116
113
  export const Default = {
117
- render: () => {
118
- const { parentRef } = useTextEditor();
119
- return <div className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />;
120
- },
121
- };
122
-
123
- export const Basic = {
124
114
  render: () => {
125
115
  const { themeMode } = useThemeContext();
126
- const { parentRef } = useTextEditor(() => ({
127
- extensions: [createBasicExtensions({ placeholder: 'Enter text...' }), createThemeExtensions({ themeMode })],
128
- }));
129
- return <div className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />;
116
+ const { parentRef } = useTextEditor({
117
+ extensions: [
118
+ //
119
+ createBasicExtensions({ placeholder: 'Enter text...' }),
120
+ createThemeExtensions({ themeMode }),
121
+ ],
122
+ });
123
+
124
+ return <div ref={parentRef} className={mx(textBlockWidth, attentionSurface, 'w-full')} />;
130
125
  },
131
126
  };
132
127
 
@@ -134,6 +129,6 @@ export const Markdown = {
134
129
  args: {
135
130
  autoFocus: true,
136
131
  placeholder: 'Text...',
137
- doc: '# Demo\n\nThis is a document.\n\n',
132
+ initialValue: '# Demo\n\nThis is a document.\n\n',
138
133
  },
139
134
  };