@dxos/react-ui-editor 0.4.6-main.9c97a50 → 0.4.6-main.e47f68d

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 (30) hide show
  1. package/dist/lib/browser/index.mjs +77 -53
  2. package/dist/lib/browser/index.mjs.map +3 -3
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/TextEditor/automerge.stories.d.ts +3 -5
  5. package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -1
  6. package/dist/types/src/extensions/automerge/automerge.d.ts +2 -7
  7. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  8. package/dist/types/src/extensions/automerge/cursor.d.ts +2 -3
  9. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  10. package/dist/types/src/extensions/automerge/defs.d.ts +9 -2
  11. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  12. package/dist/types/src/hooks/defs.d.ts +1 -0
  13. package/dist/types/src/hooks/defs.d.ts.map +1 -1
  14. package/dist/types/src/hooks/useEditorView.d.ts +1 -0
  15. package/dist/types/src/hooks/useEditorView.d.ts.map +1 -1
  16. package/dist/types/src/hooks/useTextEditor.d.ts +2 -2
  17. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  18. package/dist/types/src/hooks/useTextModel.d.ts +12 -4
  19. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  20. package/dist/types/src/themes/default.d.ts.map +1 -1
  21. package/package.json +24 -24
  22. package/src/components/TextEditor/automerge.stories.tsx +15 -16
  23. package/src/extensions/automerge/automerge.ts +10 -15
  24. package/src/extensions/automerge/cursor.ts +2 -3
  25. package/src/extensions/automerge/defs.ts +15 -2
  26. package/src/hooks/defs.ts +2 -0
  27. package/src/hooks/useEditorView.ts +2 -0
  28. package/src/hooks/useTextEditor.ts +6 -10
  29. package/src/hooks/useTextModel.ts +34 -5
  30. package/src/themes/default.ts +7 -0
@@ -7,11 +7,9 @@ import '@dxosTheme';
7
7
  import { BroadcastChannelNetworkAdapter } from '@automerge/automerge-repo-network-broadcastchannel';
8
8
  import '@preact/signals-react';
9
9
  import { EditorView } from '@codemirror/view'; // Register react integration.
10
- import get from 'lodash.get';
11
10
  import React, { useEffect, useMemo, useState } from 'react';
12
11
 
13
- import { type Prop } from '@dxos/automerge/automerge';
14
- import { Repo, type DocHandle } from '@dxos/automerge/automerge-repo';
12
+ import { Repo } from '@dxos/automerge/automerge-repo';
15
13
  import { Filter } from '@dxos/echo-schema';
16
14
  import { type PublicKey } from '@dxos/keys';
17
15
  import { Expando, TextObject, useSpace } from '@dxos/react-client/echo';
@@ -20,7 +18,8 @@ import { ClientRepeater } from '@dxos/react-client/testing';
20
18
  import { withTheme } from '@dxos/storybook-utils';
21
19
 
22
20
  import { MarkdownEditor } from './TextEditor';
23
- import { type IDocHandle, automerge, awareness, createBasicBundle } from '../../extensions';
21
+ import { automerge, awareness, createBasicBundle } from '../../extensions';
22
+ import { DocAccessor } from '../../extensions/automerge/defs';
24
23
  import { useTextEditor, useTextModel } from '../../hooks';
25
24
  import { defaultTheme } from '../../themes';
26
25
  import translations from '../../translations';
@@ -32,24 +31,24 @@ type TestObject = {
32
31
  };
33
32
 
34
33
  type EditorProps = {
35
- handle: IDocHandle;
36
- path?: Prop[];
34
+ source: DocAccessor;
37
35
  };
38
36
 
39
- const Editor = ({ handle, path = ['text'] }: EditorProps) => {
37
+ const Editor = ({ source }: EditorProps) => {
40
38
  const extensions = useMemo(
41
39
  () => [
42
40
  EditorView.baseTheme(defaultTheme),
43
41
  EditorView.editorAttributes.of({ class: 'p-2 bg-white' }),
44
42
  createBasicBundle({ placeholder: 'Type here...' }),
45
- automerge({ handle, path }),
43
+ automerge(source),
46
44
  awareness(),
47
45
  ],
48
- [handle, path],
46
+ [source],
49
47
  );
48
+
50
49
  const { parentRef } = useTextEditor({
51
50
  autoFocus: true,
52
- doc: get(handle.docSync()!, path),
51
+ doc: DocAccessor.getValue(source),
53
52
  extensions,
54
53
  });
55
54
 
@@ -57,8 +56,8 @@ const Editor = ({ handle, path = ['text'] }: EditorProps) => {
57
56
  };
58
57
 
59
58
  const Story = () => {
60
- const [object1, setObject1] = useState<DocHandle<TestObject>>();
61
- const [object2, setObject2] = useState<DocHandle<TestObject>>();
59
+ const [object1, setObject1] = useState<DocAccessor<TestObject>>();
60
+ const [object2, setObject2] = useState<DocAccessor<TestObject>>();
62
61
 
63
62
  useEffect(() => {
64
63
  queueMicrotask(async () => {
@@ -77,8 +76,8 @@ const Story = () => {
77
76
  const object2 = repo2.find(object1.url);
78
77
  await object2.whenReady();
79
78
 
80
- setObject1(object1);
81
- setObject2(object2);
79
+ setObject1({ handle: object1, path: ['text'] });
80
+ setObject2({ handle: object2, path: ['text'] });
82
81
  });
83
82
  }, []);
84
83
 
@@ -88,8 +87,8 @@ const Story = () => {
88
87
 
89
88
  return (
90
89
  <div role='none' className='grid grid-cols-2 bs-full is-full gap-2'>
91
- <Editor handle={object1} path={['text']} />
92
- <Editor handle={object2} path={['text']} />
90
+ <Editor source={object1} />
91
+ <Editor source={object2} />
93
92
  </div>
94
93
  );
95
94
  };
@@ -7,29 +7,24 @@
7
7
  import { StateField, type Extension } from '@codemirror/state';
8
8
  import { EditorView, ViewPlugin } from '@codemirror/view';
9
9
 
10
- import { next as A, type Prop } from '@dxos/automerge/automerge';
10
+ import { next as A } from '@dxos/automerge/automerge';
11
11
 
12
12
  import { cursorConverter } from './cursor';
13
- import { updateHeadsEffect, type IDocHandle, isReconcile, type State } from './defs';
13
+ import { updateHeadsEffect, isReconcile, type State, type DocAccessor } from './defs';
14
14
  import { Syncer } from './sync';
15
15
  import { Cursor } from '../cursor';
16
16
 
17
- export type AutomergeOptions = {
18
- handle: IDocHandle;
19
- path: Prop[];
20
- };
21
-
22
- export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
17
+ export const automerge = (accessor: DocAccessor): Extension => {
23
18
  const syncState = StateField.define<State>({
24
19
  create: () => ({
25
- path: path.slice(),
26
- lastHeads: A.getHeads(handle.docSync()!),
20
+ path: accessor.path.slice(),
21
+ lastHeads: A.getHeads(accessor.handle.docSync()!),
27
22
  unreconciledTransactions: [],
28
23
  }),
29
24
 
30
25
  update: (value, tr) => {
31
26
  const result: State = {
32
- path: path.slice(),
27
+ path: accessor.path.slice(),
33
28
  lastHeads: value.lastHeads,
34
29
  unreconciledTransactions: value.unreconciledTransactions.slice(),
35
30
  };
@@ -54,21 +49,21 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
54
49
  },
55
50
  });
56
51
 
57
- const syncer = new Syncer(handle, syncState);
52
+ const syncer = new Syncer(accessor.handle, syncState);
58
53
 
59
54
  return [
60
- Cursor.converter.of(cursorConverter(handle, path)),
55
+ Cursor.converter.of(cursorConverter(accessor)),
61
56
  syncState,
62
57
 
63
58
  // Reconcile external updates.
64
59
  ViewPlugin.fromClass(
65
60
  class {
66
61
  constructor(private readonly _view: EditorView) {
67
- handle.addListener('change', this._handleChange.bind(this));
62
+ accessor.handle.addListener('change', this._handleChange.bind(this));
68
63
  }
69
64
 
70
65
  destroy() {
71
- handle.removeListener('change', this._handleChange.bind(this));
66
+ accessor.handle.removeListener('change', this._handleChange.bind(this));
72
67
  }
73
68
 
74
69
  _handleChange() {
@@ -4,14 +4,13 @@
4
4
 
5
5
  import get from 'lodash.get';
6
6
 
7
- import type { Prop } from '@dxos/automerge/automerge';
8
7
  import { next as A } from '@dxos/automerge/automerge';
9
8
  import { log } from '@dxos/log';
10
9
 
11
- import { type IDocHandle } from './defs';
10
+ import { type DocAccessor } from './defs';
12
11
  import { type CursorConverter } from '../cursor';
13
12
 
14
- export const cursorConverter = (handle: IDocHandle, path: Prop[]): CursorConverter => ({
13
+ export const cursorConverter = ({ handle, path }: DocAccessor): CursorConverter => ({
15
14
  // TODO(burdon): Handle assoc to associate with a previous character.
16
15
  toCursor: (pos) => {
17
16
  const doc = handle.docSync();
@@ -5,6 +5,7 @@
5
5
  //
6
6
 
7
7
  import { Annotation, StateEffect, type StateField, type EditorState, type Transaction } from '@codemirror/state';
8
+ import get from 'lodash.get';
8
9
 
9
10
  import { type ChangeFn, type ChangeOptions, type Doc, type Heads, type Prop } from '@dxos/automerge/automerge';
10
11
 
@@ -31,10 +32,22 @@ export const isReconcile = (tr: Transaction): boolean => {
31
32
  return !!tr.annotation(reconcileAnnotation);
32
33
  };
33
34
 
34
- export type IDocHandle<T = any> = {
35
+ // TODO(burdon): Reconcile with echo.
36
+ export interface DocAccessor<T = any> {
37
+ handle: IDocHandle<T>;
38
+ get path(): string[]; // TODO(burdon): Getter or prop?
39
+ }
40
+
41
+ // TODO(burdon): Reconcile with echo.
42
+ export const DocAccessor = {
43
+ getValue: (accessor: DocAccessor) => get(accessor.handle, accessor.path),
44
+ };
45
+
46
+ // TODO(burdon): Reconcile with echo.
47
+ export interface IDocHandle<T = any> {
35
48
  docSync(): Doc<T> | undefined;
36
49
  change(callback: ChangeFn<T>, options?: ChangeOptions<T>): void;
37
50
  changeAt(heads: Heads, callback: ChangeFn<T>, options?: ChangeOptions<T>): string[] | undefined;
38
51
  addListener(event: 'change', listener: () => void): void;
39
52
  removeListener(event: 'change', listener: () => void): void;
40
- };
53
+ }
package/src/hooks/defs.ts CHANGED
@@ -35,7 +35,9 @@ export type EditorModel = {
35
35
 
36
36
  /**
37
37
  * State field makes the model available to other extensions.
38
+ * @deprecated
38
39
  */
40
+ // TODO(burdon): Remove.
39
41
  export const modelState = StateField.define<EditorModel | undefined>({
40
42
  create: () => undefined,
41
43
  update: (model) => model,
@@ -7,6 +7,7 @@ import { type MutableRefObject, useRef, useState } from 'react';
7
7
 
8
8
  /**
9
9
  * Hook for accessing the editor view.
10
+ * @deprecated
10
11
  *
11
12
  * ```tsx
12
13
  * const Test = () => {
@@ -20,6 +21,7 @@ import { type MutableRefObject, useRef, useState } from 'react';
20
21
  * };
21
22
  * ```
22
23
  */
24
+ // TODO(burdon): Replace with useTextEditor.
23
25
  export const useEditorView = (id: string): [MutableRefObject<EditorView | null>, boolean] => {
24
26
  const editorRef = useRef<EditorView | null>(null);
25
27
  const [prev, setPrev] = useState<[EditorView | null, string]>([null, '']);
@@ -10,7 +10,7 @@ import {
10
10
  type StateEffect,
11
11
  } from '@codemirror/state';
12
12
  import { EditorView } from '@codemirror/view';
13
- import { type RefObject, useEffect, useRef, useState } from 'react';
13
+ import { type DependencyList, type RefObject, useEffect, useRef, useState } from 'react';
14
14
 
15
15
  import { log } from '@dxos/log';
16
16
  import { type ThemeMode } from '@dxos/react-ui';
@@ -36,14 +36,10 @@ export type UseTextEditor = {
36
36
  * Hook for creating editor.
37
37
  */
38
38
  // TODO(wittjosiah): Does not work in strict mode.
39
- export const useTextEditor = ({
40
- autoFocus,
41
- scrollTo,
42
- debug,
43
- doc,
44
- selection,
45
- extensions,
46
- }: UseTextEditorOptions = {}): UseTextEditor => {
39
+ export const useTextEditor = (
40
+ { autoFocus, scrollTo, debug, doc, selection, extensions }: UseTextEditorOptions = {},
41
+ deps?: DependencyList,
42
+ ): UseTextEditor => {
47
43
  const onUpdate = useRef<() => void>();
48
44
  const [view, setView] = useState<EditorView>();
49
45
  const parentRef = useRef<HTMLDivElement>(null);
@@ -88,7 +84,7 @@ export const useTextEditor = ({
88
84
  return () => {
89
85
  view?.destroy();
90
86
  };
91
- }, [doc, extensions]);
87
+ }, deps);
92
88
 
93
89
  useEffect(() => {
94
90
  if (view) {
@@ -2,6 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ import { type Extension } from '@codemirror/state';
5
6
  import get from 'lodash.get';
6
7
  import { type Dispatch, type SetStateAction, useState, useMemo } from 'react';
7
8
 
@@ -14,13 +15,42 @@ import { type Identity } from '@dxos/react-client/halo';
14
15
  import { SpaceAwarenessProvider } from './awareness-provider';
15
16
  import { type EditorModel, modelState } from './defs';
16
17
  import { automerge, awareness } from '../extensions';
18
+ import { type DocAccessor } from '../extensions/automerge/defs';
17
19
  import { cursorColor } from '../styles';
18
20
 
19
- // TODO(burdon): Remove space/identity dependency. Define interface for the framework re content and presence.
20
- export type UseTextModelProps = {
21
- identity?: Identity | null;
21
+ export type useTextExtensionsProps = {
22
+ id: string;
23
+ text: DocAccessor;
22
24
  space?: Space;
25
+ identity?: Identity;
26
+ };
27
+
28
+ // TODO(burdon): Factor out automerge defs and extension (not hook).
29
+ export const useTextExtensions = ({ id, text, space, identity }: useTextExtensionsProps): Extension[] => {
30
+ const extensions: Extension[] = [automerge(text)];
31
+
32
+ if (space && identity) {
33
+ const awarenessProvider = new SpaceAwarenessProvider({
34
+ space,
35
+ channel: `awareness.${id}`,
36
+ peerId: identity.identityKey.toHex(),
37
+ info: {
38
+ displayName: identity.profile?.displayName ?? generateName(identity.identityKey.toHex()),
39
+ color: cursorColor.color,
40
+ lightColor: cursorColor.light,
41
+ },
42
+ });
43
+
44
+ extensions.push(awareness(awarenessProvider));
45
+ }
46
+
47
+ return extensions;
48
+ };
49
+
50
+ export type UseTextModelProps = {
23
51
  text?: TextObject;
52
+ space?: Space;
53
+ identity?: Identity | null;
24
54
  };
25
55
 
26
56
  /**
@@ -32,8 +62,7 @@ export const useTextModel = (props: UseTextModelProps): EditorModel | undefined
32
62
 
33
63
  /**
34
64
  * For use primarily in stories & tests so the dependence on TextObject can be avoided.
35
- * @param id
36
- * @param defaultContent
65
+ * @deprecated
37
66
  */
38
67
  export const useInMemoryTextModel = ({
39
68
  id,
@@ -100,6 +100,13 @@ export const defaultTheme: ThemeStyles = {
100
100
  background: 'transparent',
101
101
  },
102
102
 
103
+ //
104
+ // gutter
105
+ //
106
+ '.cm-gutterElement': {
107
+ width: '48px',
108
+ },
109
+
103
110
  //
104
111
  // Selection
105
112
  //