@dxos/react-ui-editor 0.6.5 → 0.6.6-staging.23d123d
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 +316 -324
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +5 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +7 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +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/autocomplete.d.ts +2 -1
- package/dist/types/src/extensions/autocomplete.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 +9 -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/debug.d.ts +3 -0
- package/dist/types/src/extensions/debug.d.ts.map +1 -0
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +1 -0
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/action.d.ts +1 -1
- package/dist/types/src/extensions/markdown/action.d.ts.map +1 -1
- package/dist/types/src/extensions/modes.d.ts +7 -4
- package/dist/types/src/extensions/modes.d.ts.map +1 -1
- package/dist/types/src/hooks/{useTextEditor.stories.d.ts → InputMode.stories.d.ts} +9 -9
- package/dist/types/src/hooks/InputMode.stories.d.ts.map +1 -0
- package/dist/types/src/{components/TextEditor → hooks}/TextEditor.stories.d.ts +8 -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/dist/types/src/translations.d.ts +4 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/package.json +27 -27
- package/src/components/Toolbar/Toolbar.stories.tsx +18 -8
- package/src/components/Toolbar/Toolbar.tsx +93 -3
- package/src/components/index.ts +0 -1
- package/src/extensions/autocomplete.ts +3 -3
- 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 +7 -14
- package/src/extensions/cursor.ts +1 -1
- package/src/extensions/debug.ts +15 -0
- package/src/extensions/factories.ts +19 -13
- package/src/extensions/index.ts +1 -0
- package/src/extensions/markdown/action.ts +1 -0
- package/src/extensions/modes.ts +9 -6
- package/src/hooks/{useTextEditor.stories.tsx → InputMode.stories.tsx} +41 -47
- package/src/{components/TextEditor → hooks}/TextEditor.stories.tsx +24 -30
- package/src/hooks/useTextEditor.ts +75 -23
- package/src/themes/default.ts +20 -4
- package/src/translations.ts +4 -0
- 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
|
@@ -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';
|
|
@@ -40,6 +39,7 @@ import { callbackWrapper } from '../util';
|
|
|
40
39
|
// State management.
|
|
41
40
|
//
|
|
42
41
|
|
|
42
|
+
// TODO(wittjosiah): Factor out, not comments-specific.
|
|
43
43
|
const documentId = Facet.define<string | undefined, string | undefined>({ combine: (values) => values[0] });
|
|
44
44
|
|
|
45
45
|
type CommentState = {
|
|
@@ -612,21 +612,16 @@ const hasActiveSelection = (state: EditorState): boolean => {
|
|
|
612
612
|
};
|
|
613
613
|
|
|
614
614
|
class ExternalCommentSync implements PluginValue {
|
|
615
|
-
private unsubscribe: () => void;
|
|
615
|
+
private readonly unsubscribe: () => void;
|
|
616
616
|
|
|
617
617
|
constructor(
|
|
618
618
|
view: EditorView,
|
|
619
619
|
id: string,
|
|
620
620
|
subscribe: (sink: () => void) => UnsubscribeCallback,
|
|
621
|
-
|
|
621
|
+
getComments: () => Comment[],
|
|
622
622
|
) {
|
|
623
623
|
const updateComments = () => {
|
|
624
|
-
const
|
|
625
|
-
const comments = threads
|
|
626
|
-
.filter(nonNullable)
|
|
627
|
-
.filter((thread) => thread.anchor)
|
|
628
|
-
.map((thread) => ({ id: thread.id, cursor: thread.anchor! }));
|
|
629
|
-
|
|
624
|
+
const comments = getComments();
|
|
630
625
|
if (id === view.state.facet(documentId)) {
|
|
631
626
|
queueMicrotask(() => view.dispatch({ effects: setComments.of({ id, comments }) }));
|
|
632
627
|
}
|
|
@@ -643,12 +638,12 @@ class ExternalCommentSync implements PluginValue {
|
|
|
643
638
|
export const createExternalCommentSync = (
|
|
644
639
|
id: string,
|
|
645
640
|
subscribe: (sink: () => void) => UnsubscribeCallback,
|
|
646
|
-
|
|
641
|
+
getComments: () => Comment[],
|
|
647
642
|
): Extension =>
|
|
648
643
|
ViewPlugin.fromClass(
|
|
649
644
|
class {
|
|
650
645
|
constructor(view: EditorView) {
|
|
651
|
-
return new ExternalCommentSync(view, id, subscribe,
|
|
646
|
+
return new ExternalCommentSync(view, id, subscribe, getComments);
|
|
652
647
|
}
|
|
653
648
|
},
|
|
654
649
|
);
|
|
@@ -697,7 +692,7 @@ export const useComments = (view: EditorView | null | undefined, id: string, com
|
|
|
697
692
|
* Hook provides an extension to listen for comment clicks and invoke a handler.
|
|
698
693
|
*/
|
|
699
694
|
export const useCommentClickListener = (onCommentClick: (commentId: string) => void): Extension => {
|
|
700
|
-
|
|
695
|
+
return useMemo(
|
|
701
696
|
() =>
|
|
702
697
|
EditorView.updateListener.of((update) => {
|
|
703
698
|
update.transactions.forEach((transaction) => {
|
|
@@ -710,6 +705,4 @@ export const useCommentClickListener = (onCommentClick: (commentId: string) => v
|
|
|
710
705
|
}),
|
|
711
706
|
[onCommentClick],
|
|
712
707
|
);
|
|
713
|
-
|
|
714
|
-
return observer;
|
|
715
708
|
};
|
package/src/extensions/cursor.ts
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { syntaxTree } from '@codemirror/language';
|
|
6
|
+
import { type EditorState, type RangeSet, StateField, type Transaction } from '@codemirror/state';
|
|
7
|
+
|
|
8
|
+
// eslint-disable-next-line no-console
|
|
9
|
+
export const debugNodeLogger = (log: (...args: any[]) => void = console.log) => {
|
|
10
|
+
const logTokens = (state: EditorState) => syntaxTree(state).iterate({ enter: (node) => log(node.type) });
|
|
11
|
+
return StateField.define<any>({
|
|
12
|
+
create: (state) => logTokens(state),
|
|
13
|
+
update: (_: RangeSet<any>, tr: Transaction) => logTokens(tr.state),
|
|
14
|
+
});
|
|
15
|
+
};
|
|
@@ -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;
|
package/src/extensions/index.ts
CHANGED
package/src/extensions/modes.ts
CHANGED
|
@@ -9,28 +9,31 @@ import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
|
|
9
9
|
|
|
10
10
|
export const focusEvent = 'focus.container';
|
|
11
11
|
|
|
12
|
-
export
|
|
12
|
+
export const EditorViewModes = ['preview', 'readonly', 'source'] as const;
|
|
13
|
+
export type EditorViewMode = (typeof EditorViewModes)[number];
|
|
14
|
+
export const EditorInputModes = ['default', 'vim', 'vscode'] as const;
|
|
15
|
+
export type EditorInputMode = (typeof EditorInputModes)[number];
|
|
13
16
|
|
|
14
|
-
export type
|
|
17
|
+
export type EditorInputConfig = {
|
|
15
18
|
type: string;
|
|
16
19
|
noTabster?: boolean;
|
|
17
20
|
};
|
|
18
21
|
|
|
19
|
-
export const
|
|
22
|
+
export const editorInputMode = Facet.define<EditorInputConfig, EditorInputConfig>({
|
|
20
23
|
combine: (modes) => modes[0] ?? {},
|
|
21
24
|
});
|
|
22
25
|
|
|
23
|
-
export const
|
|
26
|
+
export const InputModeExtensions: { [mode: string]: Extension } = {
|
|
24
27
|
default: [],
|
|
25
28
|
vscode: [
|
|
26
29
|
// https://github.com/replit/codemirror-vscode-keymap
|
|
27
|
-
|
|
30
|
+
editorInputMode.of({ type: 'vscode' }),
|
|
28
31
|
keymap.of(vscodeKeymap),
|
|
29
32
|
],
|
|
30
33
|
vim: [
|
|
31
34
|
// https://github.com/replit/codemirror-vim
|
|
32
35
|
vim(),
|
|
33
|
-
|
|
36
|
+
editorInputMode.of({ type: 'vim', noTabster: true }),
|
|
34
37
|
keymap.of([
|
|
35
38
|
{
|
|
36
39
|
key: 'Alt-Escape',
|
|
@@ -8,41 +8,38 @@ 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 } from '../extensions';
|
|
17
16
|
import {
|
|
18
|
-
type
|
|
19
|
-
EditorModes,
|
|
17
|
+
type EditorInputMode,
|
|
20
18
|
decorateMarkdown,
|
|
21
19
|
createMarkdownExtensions,
|
|
22
20
|
formattingKeymap,
|
|
21
|
+
image,
|
|
23
22
|
table,
|
|
24
23
|
useFormattingState,
|
|
25
|
-
|
|
24
|
+
createBasicExtensions,
|
|
25
|
+
createThemeExtensions,
|
|
26
|
+
InputModeExtensions,
|
|
26
27
|
} from '../extensions';
|
|
27
28
|
import translations from '../translations';
|
|
28
29
|
|
|
29
|
-
type StoryProps = {
|
|
30
|
-
autoFocus?: boolean;
|
|
31
|
-
placeholder?: string;
|
|
32
|
-
doc?: string;
|
|
33
|
-
readonly?: boolean;
|
|
34
|
-
};
|
|
30
|
+
type StoryProps = { placeholder?: string; readonly?: boolean } & UseTextEditorProps;
|
|
35
31
|
|
|
36
|
-
const Story = ({ autoFocus,
|
|
32
|
+
const Story = ({ autoFocus, initialValue, placeholder, readonly }: StoryProps) => {
|
|
37
33
|
const { themeMode } = useThemeContext();
|
|
38
34
|
const [formattingState, trackFormatting] = useFormattingState();
|
|
39
|
-
const [
|
|
35
|
+
const [editorInputMode, setEditorInputMode] = useState<EditorInputMode>('default');
|
|
40
36
|
const { parentRef, view } = useTextEditor(
|
|
41
37
|
() => ({
|
|
42
38
|
autoFocus,
|
|
43
|
-
|
|
39
|
+
initialValue,
|
|
40
|
+
moveToEndOfLine: true,
|
|
44
41
|
extensions: [
|
|
45
|
-
|
|
42
|
+
editorInputMode ? InputModeExtensions[editorInputMode] : [],
|
|
46
43
|
createBasicExtensions({ placeholder, lineWrapping: true, readonly }),
|
|
47
44
|
createMarkdownExtensions({ themeMode }),
|
|
48
45
|
createThemeExtensions({ themeMode }),
|
|
@@ -53,7 +50,7 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
|
|
|
53
50
|
trackFormatting,
|
|
54
51
|
],
|
|
55
52
|
}),
|
|
56
|
-
[
|
|
53
|
+
[editorInputMode, themeMode, placeholder, readonly],
|
|
57
54
|
);
|
|
58
55
|
|
|
59
56
|
const handleAction = useActionHandler(view);
|
|
@@ -62,10 +59,13 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
|
|
|
62
59
|
// Also not sure if view is even guaranteed to exist at this point.
|
|
63
60
|
return (
|
|
64
61
|
<div role='none' className={mx('fixed inset-0 flex flex-col')}>
|
|
65
|
-
<
|
|
66
|
-
<Toolbar.
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+
|
|
69
69
|
<div role='none' className='grow overflow-hidden'>
|
|
70
70
|
<div className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />
|
|
71
71
|
</div>
|
|
@@ -73,17 +73,17 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
|
|
|
73
73
|
);
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
const EditorInputModeToolbar = ({
|
|
77
|
+
editorInputMode,
|
|
78
|
+
setEditorInputMode,
|
|
79
79
|
}: {
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
editorInputMode: EditorInputMode;
|
|
81
|
+
setEditorInputMode: (mode: EditorInputMode) => void;
|
|
82
82
|
}) => {
|
|
83
83
|
return (
|
|
84
|
-
<Select.Root value={
|
|
84
|
+
<Select.Root value={editorInputMode} onValueChange={(value) => setEditorInputMode(value as EditorInputMode)}>
|
|
85
85
|
<NaturalToolbar.Button asChild>
|
|
86
|
-
<Select.TriggerButton variant='ghost'>{
|
|
86
|
+
<Select.TriggerButton variant='ghost'>{editorInputMode}</Select.TriggerButton>
|
|
87
87
|
</NaturalToolbar.Button>
|
|
88
88
|
<Select.Portal>
|
|
89
89
|
<Select.Content>
|
|
@@ -104,30 +104,24 @@ const EditorModeToolbar = ({
|
|
|
104
104
|
};
|
|
105
105
|
|
|
106
106
|
export default {
|
|
107
|
-
title: 'react-ui-editor/
|
|
108
|
-
decorators: [withTheme],
|
|
109
|
-
render: (args: StoryProps) => (
|
|
110
|
-
<Tooltip.Provider>
|
|
111
|
-
<Story {...args} />
|
|
112
|
-
</Tooltip.Provider>
|
|
113
|
-
),
|
|
107
|
+
title: 'react-ui-editor/InputMode',
|
|
108
|
+
decorators: [withTheme, withFullscreen()],
|
|
114
109
|
parameters: { translations, layout: 'fullscreen' },
|
|
110
|
+
render: Story,
|
|
115
111
|
};
|
|
116
112
|
|
|
117
113
|
export const Default = {
|
|
118
|
-
render: () => {
|
|
119
|
-
const { parentRef } = useTextEditor();
|
|
120
|
-
return <div className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />;
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
export const Basic = {
|
|
125
114
|
render: () => {
|
|
126
115
|
const { themeMode } = useThemeContext();
|
|
127
|
-
const { parentRef } = useTextEditor(
|
|
128
|
-
extensions: [
|
|
129
|
-
|
|
130
|
-
|
|
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')} />;
|
|
131
125
|
},
|
|
132
126
|
};
|
|
133
127
|
|
|
@@ -135,6 +129,6 @@ export const Markdown = {
|
|
|
135
129
|
args: {
|
|
136
130
|
autoFocus: true,
|
|
137
131
|
placeholder: 'Text...',
|
|
138
|
-
|
|
132
|
+
initialValue: '# Demo\n\nThis is a document.\n\n',
|
|
139
133
|
},
|
|
140
134
|
};
|
|
@@ -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 {
|
|
8
|
+
import { effect, useSignal } from '@preact/signals-react';
|
|
9
9
|
import defaultsDeep from 'lodash.defaultsdeep';
|
|
10
|
-
import React, { type FC, type KeyboardEvent, StrictMode, useMemo,
|
|
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,9 +21,9 @@ 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 {
|
|
24
|
+
import { useTextEditor, type UseTextEditorProps } from './useTextEditor';
|
|
25
25
|
import {
|
|
26
|
-
|
|
26
|
+
InputModeExtensions,
|
|
27
27
|
annotations,
|
|
28
28
|
autocomplete,
|
|
29
29
|
blast,
|
|
@@ -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 '
|
|
55
|
-
import translations from '
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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/
|
|
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
|
|
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
|
-
|
|
481
|
+
_comments.value = [..._comments.value, { id, cursor }];
|
|
488
482
|
return id;
|
|
489
483
|
},
|
|
490
484
|
onSelect: (state) => {
|
|
@@ -511,7 +505,7 @@ export const Vim = {
|
|
|
511
505
|
render: () => (
|
|
512
506
|
<Story
|
|
513
507
|
text={str('# Vim Mode', '', 'The distant future. The year 2000.', '', text.paragraphs)}
|
|
514
|
-
extensions={[defaults,
|
|
508
|
+
extensions={[defaults, InputModeExtensions.vim]}
|
|
515
509
|
/>
|
|
516
510
|
),
|
|
517
511
|
};
|