@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.
- package/dist/lib/browser/index.mjs +203 -307
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/index.d.ts +0 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts +5 -2
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness/awareness.d.ts +6 -6
- package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +1 -2
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/cursor.d.ts +1 -1
- package/dist/types/src/extensions/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/link.d.ts +1 -3
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
- package/dist/types/src/hooks/{useTextEditor.stories.d.ts → InputMode.stories.d.ts} +5 -9
- package/dist/types/src/hooks/InputMode.stories.d.ts.map +1 -0
- package/dist/types/src/{components/TextEditor → hooks}/TextEditor.stories.d.ts +4 -16
- package/dist/types/src/hooks/TextEditor.stories.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextEditor.d.ts +20 -3
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/package.json +25 -25
- package/src/components/Toolbar/Toolbar.stories.tsx +1 -1
- package/src/components/index.ts +0 -1
- package/src/extensions/automerge/automerge.stories.tsx +25 -18
- package/src/extensions/automerge/automerge.ts +2 -0
- package/src/extensions/automerge/cursor.ts +3 -4
- package/src/extensions/awareness/awareness-provider.ts +2 -0
- package/src/extensions/awareness/awareness.ts +34 -30
- package/src/extensions/comments.ts +6 -14
- package/src/extensions/cursor.ts +1 -1
- package/src/extensions/factories.ts +19 -13
- package/src/hooks/{useTextEditor.stories.tsx → InputMode.stories.tsx} +30 -35
- package/src/{components/TextEditor → hooks}/TextEditor.stories.tsx +22 -28
- package/src/hooks/useTextEditor.ts +75 -23
- package/src/themes/default.ts +20 -4
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +0 -34
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/index.d.ts +0 -2
- package/dist/types/src/components/TextEditor/index.d.ts.map +0 -1
- package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +0 -1
- package/src/components/TextEditor/TextEditor.tsx +0 -184
- 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
|
|
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
|
|
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: (
|
|
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":"
|
|
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,
|
|
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-
|
|
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/
|
|
51
|
-
"@dxos/
|
|
52
|
-
"@dxos/
|
|
53
|
-
"@dxos/
|
|
54
|
-
"@dxos/
|
|
55
|
-
"@dxos/
|
|
56
|
-
"@dxos/
|
|
57
|
-
"@dxos/
|
|
58
|
-
"@dxos/
|
|
59
|
-
"@dxos/react-async": "0.6.6-
|
|
60
|
-
"@dxos/react-ui": "0.6.6-
|
|
61
|
-
"@dxos/
|
|
62
|
-
"@dxos/
|
|
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-
|
|
89
|
-
"@dxos/
|
|
90
|
-
"@dxos/
|
|
91
|
-
"@dxos/
|
|
92
|
-
"@dxos/
|
|
93
|
-
"@dxos/
|
|
94
|
-
"@dxos/react-
|
|
95
|
-
"@dxos/
|
|
96
|
-
"@dxos/storybook-utils": "0.6.6-
|
|
97
|
-
"@braneframe/types": "0.6.6-
|
|
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-
|
|
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
|
-
|
|
48
|
+
initialValue: text.content,
|
|
49
49
|
extensions: [
|
|
50
50
|
formattingObserver,
|
|
51
51
|
createBasicExtensions({ readonly: viewMode === 'readonly' }),
|
package/src/components/index.ts
CHANGED
|
@@ -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
|
-
|
|
42
|
+
initialValue: DocAccessor.getValue(source),
|
|
41
43
|
extensions: [
|
|
42
44
|
createBasicExtensions({ placeholder: 'Type here...' }),
|
|
43
|
-
createThemeExtensions({
|
|
44
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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 = {};
|
|
@@ -3,15 +3,14 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { log } from '@dxos/log';
|
|
6
|
-
import {
|
|
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
|
-
|
|
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).
|
|
@@ -51,10 +51,9 @@ export type AwarenessPosition = {
|
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
export type AwarenessInfo = {
|
|
54
|
-
displayName
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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(
|
|
112
|
-
const hasFocus =
|
|
113
|
-
const { anchor = undefined, head = undefined } = hasFocus ?
|
|
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(
|
|
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),
|
|
142
|
-
const end = Math.min(Math.max(anchor, head),
|
|
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 =
|
|
145
|
-
const endLine =
|
|
151
|
+
const startLine = view.state.doc.lineAt(start);
|
|
152
|
+
const endLine = view.state.doc.lineAt(end);
|
|
146
153
|
|
|
147
|
-
|
|
148
|
-
const
|
|
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 =
|
|
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',
|
|
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
|
-
|
|
621
|
+
getComments: () => Comment[],
|
|
623
622
|
) {
|
|
624
623
|
const updateComments = () => {
|
|
625
|
-
const
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
};
|
package/src/extensions/cursor.ts
CHANGED
|
@@ -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[] =
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
<
|
|
65
|
-
<Toolbar.
|
|
66
|
-
|
|
67
|
-
|
|
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/
|
|
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: [
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
132
|
+
initialValue: '# Demo\n\nThis is a document.\n\n',
|
|
138
133
|
},
|
|
139
134
|
};
|