@dxos/react-ui-editor 0.4.7-main.733903e → 0.4.7-main.76c1dea
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 +997 -728
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +9 -0
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +0 -9
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/index.d.ts +0 -2
- package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
- package/dist/types/src/{components/TextEditor → extensions/automerge}/automerge.stories.d.ts +5 -2
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -0
- package/dist/types/src/extensions/command/command.d.ts +10 -0
- package/dist/types/src/extensions/command/command.d.ts.map +1 -0
- package/dist/types/src/extensions/command/hint.d.ts +7 -0
- package/dist/types/src/extensions/command/hint.d.ts.map +1 -0
- package/dist/types/src/extensions/command/index.d.ts +2 -0
- package/dist/types/src/extensions/command/index.d.ts.map +1 -0
- package/dist/types/src/extensions/command/state.d.ts +18 -0
- package/dist/types/src/extensions/command/state.d.ts.map +1 -0
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/cursor-line-margin.d.ts.map +1 -1
- package/dist/types/src/extensions/dnd.d.ts +3 -0
- package/dist/types/src/extensions/dnd.d.ts.map +1 -0
- package/dist/types/src/extensions/index.d.ts +2 -1
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/listener.d.ts +1 -1
- package/dist/types/src/extensions/listener.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts +2 -2
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts +4 -0
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
- package/dist/types/src/extensions/mention.d.ts.map +1 -1
- package/dist/types/src/extensions/util/dom.d.ts +15 -0
- package/dist/types/src/extensions/util/dom.d.ts.map +1 -0
- package/dist/types/src/hooks/defs.d.ts +5 -11
- package/dist/types/src/hooks/defs.d.ts.map +1 -1
- package/dist/types/src/hooks/useEditorView.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextEditor.d.ts +20 -5
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
- package/dist/types/src/{components/TextEditor/hooks.stories.d.ts → hooks/useTextEditor.stories.d.ts} +2 -3
- package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/index.d.ts +0 -1
- package/dist/types/src/styles/index.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/package.json +31 -31
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +95 -8
- package/src/components/TextEditor/TextEditor.stories.tsx +0 -15
- package/src/components/TextEditor/TextEditor.tsx +13 -6
- package/src/components/TextEditor/index.ts +0 -3
- package/src/components/Toolbar/Toolbar.stories.tsx +2 -1
- package/src/{components/TextEditor → extensions/automerge}/automerge.stories.tsx +32 -37
- package/src/extensions/awareness.ts +3 -3
- package/src/extensions/command/command.ts +35 -0
- package/src/extensions/command/hint.ts +76 -0
- package/src/extensions/command/index.ts +5 -0
- package/src/extensions/command/state.ts +106 -0
- package/src/extensions/comments.ts +2 -1
- package/src/extensions/cursor-line-margin.ts +5 -2
- package/src/extensions/dnd.ts +17 -0
- package/src/extensions/index.ts +2 -1
- package/src/extensions/listener.ts +3 -3
- package/src/extensions/markdown/bundle.ts +4 -15
- package/src/extensions/markdown/decorate.ts +76 -32
- package/src/extensions/mention.ts +3 -1
- package/src/extensions/util/dom.ts +36 -0
- package/src/hooks/defs.ts +6 -20
- package/src/hooks/useActionHandler.ts +11 -11
- package/src/hooks/useEditorView.ts +0 -1
- package/src/{components/TextEditor/hooks.stories.tsx → hooks/useTextEditor.stories.tsx} +8 -8
- package/src/hooks/useTextEditor.ts +69 -14
- package/src/hooks/useTextModel.ts +20 -18
- package/src/index.ts +2 -0
- package/src/styles/index.ts +0 -1
- package/src/themes/default.ts +1 -0
- package/src/util.ts +1 -1
- package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +0 -1
- package/dist/types/src/extensions/basic.d.ts +0 -9
- package/dist/types/src/extensions/basic.d.ts.map +0 -1
- package/dist/types/src/styles/cursors.d.ts +0 -5
- package/dist/types/src/styles/cursors.d.ts.map +0 -1
- package/src/extensions/basic.ts +0 -41
- package/src/styles/cursors.ts +0 -21
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { syntaxTree } from '@codemirror/language';
|
|
6
|
-
import { RangeSetBuilder, type EditorState } from '@codemirror/state';
|
|
6
|
+
import { RangeSetBuilder, type EditorState, StateEffect } from '@codemirror/state';
|
|
7
7
|
import { EditorView, Decoration, type DecorationSet, WidgetType, ViewPlugin, type ViewUpdate } from '@codemirror/view';
|
|
8
8
|
|
|
9
9
|
import { mx } from '@dxos/react-ui-theme';
|
|
@@ -81,20 +81,21 @@ const horizontalRule = Decoration.replace({ widget: new HorizontalRuleWidget() }
|
|
|
81
81
|
const checkedTask = Decoration.replace({ widget: new CheckboxWidget(true) });
|
|
82
82
|
const uncheckedTask = Decoration.replace({ widget: new CheckboxWidget(false) });
|
|
83
83
|
|
|
84
|
-
const editingRange = (state: EditorState, range: { from: number; to: number }) => {
|
|
84
|
+
const editingRange = (state: EditorState, range: { from: number; to: number }, focus: boolean) => {
|
|
85
85
|
const {
|
|
86
86
|
readOnly,
|
|
87
87
|
selection: {
|
|
88
88
|
main: { head },
|
|
89
89
|
},
|
|
90
90
|
} = state;
|
|
91
|
-
return !readOnly && head >= range.from && head <= range.to;
|
|
91
|
+
return focus && !readOnly && head >= range.from && head <= range.to;
|
|
92
92
|
};
|
|
93
93
|
|
|
94
94
|
const MarksByParent = new Set(['CodeMark', 'EmphasisMark', 'StrikethroughMark', 'SubscriptMark', 'SuperscriptMark']);
|
|
95
95
|
|
|
96
|
-
const buildDecorations = (view: EditorView, options: DecorateOptions
|
|
97
|
-
const
|
|
96
|
+
const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boolean) => {
|
|
97
|
+
const deco = new RangeSetBuilder<Decoration>();
|
|
98
|
+
const atomicDeco = new RangeSetBuilder<Decoration>();
|
|
98
99
|
const { state } = view;
|
|
99
100
|
|
|
100
101
|
for (const { from, to } of view.visibleRanges) {
|
|
@@ -103,7 +104,7 @@ const buildDecorations = (view: EditorView, options: DecorateOptions): Decoratio
|
|
|
103
104
|
to,
|
|
104
105
|
enter: (node) => {
|
|
105
106
|
if (node.name === 'FencedCode') {
|
|
106
|
-
const editing = editingRange(state, node);
|
|
107
|
+
const editing = editingRange(state, node, focus);
|
|
107
108
|
// FencedCode > CodeMark > [CodeInfo] > CodeText > CodeMark
|
|
108
109
|
for (const block of view.viewportLineBlocks) {
|
|
109
110
|
if (block.to < node.from) {
|
|
@@ -113,27 +114,23 @@ const buildDecorations = (view: EditorView, options: DecorateOptions): Decoratio
|
|
|
113
114
|
break;
|
|
114
115
|
}
|
|
115
116
|
const first = block.from <= node.from;
|
|
116
|
-
const last = block.to >= node.to;
|
|
117
|
-
|
|
118
|
-
block.from,
|
|
119
|
-
block.from,
|
|
120
|
-
first ? fencedCodeLineFirst : last ? fencedCodeLineLast : fencedCodeLine,
|
|
121
|
-
);
|
|
117
|
+
const last = block.to >= node.to && /^(\s>)*```$/.test(state.doc.sliceString(block.from, block.to));
|
|
118
|
+
deco.add(block.from, block.from, first ? fencedCodeLineFirst : last ? fencedCodeLineLast : fencedCodeLine);
|
|
122
119
|
if (!editing && (first || last)) {
|
|
123
|
-
|
|
120
|
+
atomicDeco.add(block.from, block.to, hide);
|
|
124
121
|
}
|
|
125
122
|
}
|
|
126
123
|
return false;
|
|
127
124
|
} else if (node.name === 'Link') {
|
|
128
125
|
const marks = node.node.getChildren('LinkMark');
|
|
129
126
|
const urlNode = node.node.getChild('URL');
|
|
130
|
-
const editing = editingRange(state, node);
|
|
127
|
+
const editing = editingRange(state, node, focus);
|
|
131
128
|
if (urlNode && marks.length >= 2) {
|
|
132
129
|
const url = state.sliceDoc(urlNode.from, urlNode.to);
|
|
133
130
|
if (!editing) {
|
|
134
|
-
|
|
131
|
+
atomicDeco.add(node.from, marks[0].to, hide);
|
|
135
132
|
}
|
|
136
|
-
|
|
133
|
+
deco.add(
|
|
137
134
|
marks[0].to,
|
|
138
135
|
marks[1].from,
|
|
139
136
|
Decoration.mark({
|
|
@@ -147,7 +144,7 @@ const buildDecorations = (view: EditorView, options: DecorateOptions): Decoratio
|
|
|
147
144
|
}),
|
|
148
145
|
);
|
|
149
146
|
if (!editing) {
|
|
150
|
-
|
|
147
|
+
atomicDeco.add(
|
|
151
148
|
marks[1].from,
|
|
152
149
|
node.to,
|
|
153
150
|
options.renderLinkButton
|
|
@@ -160,54 +157,101 @@ const buildDecorations = (view: EditorView, options: DecorateOptions): Decoratio
|
|
|
160
157
|
}
|
|
161
158
|
} else if (node.name === 'HeaderMark') {
|
|
162
159
|
const parent = node.node.parent!;
|
|
163
|
-
if (/^ATX/.test(parent.name) && !editingRange(state, state.doc.lineAt(node.from))) {
|
|
160
|
+
if (/^ATX/.test(parent.name) && !editingRange(state, state.doc.lineAt(node.from), focus)) {
|
|
164
161
|
const next = state.doc.sliceString(node.to, node.to + 1);
|
|
165
|
-
|
|
162
|
+
atomicDeco.add(node.from, node.to + (next === ' ' ? 1 : 0), hide);
|
|
166
163
|
}
|
|
167
164
|
} else if (node.name === 'HorizontalRule') {
|
|
168
|
-
if (!editingRange(state, node)) {
|
|
169
|
-
|
|
165
|
+
if (!editingRange(state, node, focus)) {
|
|
166
|
+
deco.add(node.from, node.to, horizontalRule);
|
|
170
167
|
}
|
|
171
168
|
} else if (node.name === 'TaskMarker') {
|
|
172
169
|
// Check if cursor is inside text.
|
|
173
|
-
if (!editingRange(state, node)) {
|
|
170
|
+
if (!editingRange(state, node, focus)) {
|
|
174
171
|
const checked = state.doc.sliceString(node.from + 1, node.to - 1) === 'x';
|
|
175
|
-
|
|
176
|
-
|
|
172
|
+
atomicDeco.add(node.from - 2, node.from - 1, Decoration.mark({ class: 'cm-task' }));
|
|
173
|
+
atomicDeco.add(node.from, node.to, checked ? checkedTask : uncheckedTask);
|
|
177
174
|
}
|
|
178
175
|
} else if (MarksByParent.has(node.name)) {
|
|
179
|
-
if (!editingRange(state, node.node.parent
|
|
180
|
-
|
|
176
|
+
if (!editingRange(state, node.node.parent!, focus)) {
|
|
177
|
+
atomicDeco.add(node.from, node.to, hide);
|
|
181
178
|
}
|
|
182
179
|
}
|
|
183
180
|
},
|
|
184
181
|
});
|
|
185
182
|
}
|
|
186
183
|
|
|
187
|
-
return
|
|
184
|
+
return { deco: deco.finish(), atomicDeco: atomicDeco.finish() };
|
|
188
185
|
};
|
|
189
186
|
|
|
190
187
|
export interface DecorateOptions {
|
|
191
188
|
renderLinkButton?: (el: Element, url: string) => void;
|
|
189
|
+
/**
|
|
190
|
+
* Prevents triggering decorations as the cursor moves through the document.
|
|
191
|
+
*/
|
|
192
|
+
selectionChangeDelay?: number;
|
|
192
193
|
}
|
|
193
194
|
|
|
195
|
+
const forceUpdate = StateEffect.define<null>();
|
|
196
|
+
|
|
194
197
|
export const decorateMarkdown = (options: DecorateOptions = {}) => {
|
|
195
198
|
return [
|
|
196
199
|
ViewPlugin.fromClass(
|
|
197
200
|
class {
|
|
198
|
-
|
|
201
|
+
deco: DecorationSet;
|
|
202
|
+
atomicDeco: DecorationSet;
|
|
203
|
+
pendingUpdate?: NodeJS.Timeout;
|
|
204
|
+
|
|
199
205
|
constructor(view: EditorView) {
|
|
200
|
-
this.
|
|
206
|
+
({ deco: this.deco, atomicDeco: this.atomicDeco } = buildDecorations(view, options, view.hasFocus));
|
|
201
207
|
}
|
|
202
208
|
|
|
203
209
|
update(update: ViewUpdate) {
|
|
204
|
-
if (
|
|
205
|
-
|
|
210
|
+
if (
|
|
211
|
+
update.docChanged ||
|
|
212
|
+
update.viewportChanged ||
|
|
213
|
+
update.focusChanged ||
|
|
214
|
+
update.transactions.some((tr) => tr.effects.some((e) => e.is(forceUpdate))) ||
|
|
215
|
+
(update.selectionSet && !options.selectionChangeDelay)
|
|
216
|
+
) {
|
|
217
|
+
({ deco: this.deco, atomicDeco: this.atomicDeco } = buildDecorations(
|
|
218
|
+
update.view,
|
|
219
|
+
options,
|
|
220
|
+
update.view.hasFocus,
|
|
221
|
+
));
|
|
222
|
+
|
|
223
|
+
this.clearUpdate();
|
|
224
|
+
} else if (update.selectionSet) {
|
|
225
|
+
this.scheduleUpdate(update.view);
|
|
206
226
|
}
|
|
207
227
|
}
|
|
228
|
+
|
|
229
|
+
// TODO(burdon): BUG: If the cursor is at the end of a link at the end of a line,
|
|
230
|
+
// the cursor will float in space or be in the wrong position after the decoration is applied.
|
|
231
|
+
scheduleUpdate(view: EditorView) {
|
|
232
|
+
this.clearUpdate();
|
|
233
|
+
this.pendingUpdate = setTimeout(() => {
|
|
234
|
+
view.dispatch({ effects: forceUpdate.of(null) });
|
|
235
|
+
}, options.selectionChangeDelay);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
clearUpdate() {
|
|
239
|
+
if (this.pendingUpdate) {
|
|
240
|
+
clearTimeout(this.pendingUpdate);
|
|
241
|
+
this.pendingUpdate = undefined;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
destroy() {
|
|
246
|
+
this.clearUpdate();
|
|
247
|
+
}
|
|
208
248
|
},
|
|
209
249
|
{
|
|
210
|
-
|
|
250
|
+
provide: (plugin) => [
|
|
251
|
+
EditorView.atomicRanges.of((view) => view.plugin(plugin)?.atomicDeco ?? Decoration.none),
|
|
252
|
+
EditorView.decorations.of((view) => view.plugin(plugin)?.atomicDeco ?? Decoration.none),
|
|
253
|
+
EditorView.decorations.of((view) => view.plugin(plugin)?.deco ?? Decoration.none),
|
|
254
|
+
],
|
|
211
255
|
},
|
|
212
256
|
),
|
|
213
257
|
formattingStyles,
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import { autocompletion, type CompletionContext, type CompletionResult } from '@codemirror/autocomplete';
|
|
6
6
|
import type { Extension } from '@codemirror/state';
|
|
7
7
|
|
|
8
|
+
import { log } from '@dxos/log';
|
|
9
|
+
|
|
8
10
|
export type MentionOptions = {
|
|
9
11
|
onSearch: (text: string) => string[];
|
|
10
12
|
};
|
|
@@ -21,7 +23,7 @@ export const mention = ({ onSearch }: MentionOptions): Extension => {
|
|
|
21
23
|
icons: false,
|
|
22
24
|
override: [
|
|
23
25
|
(context: CompletionContext): CompletionResult | null => {
|
|
24
|
-
|
|
26
|
+
log.info('completion context', { context });
|
|
25
27
|
|
|
26
28
|
const match = context.matchBefore(/@(\w+)?/);
|
|
27
29
|
if (!match || (match.from === match.to && !context.explicit)) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// Copied from @codemirror/view
|
|
6
|
+
|
|
7
|
+
export interface Rect {
|
|
8
|
+
readonly left: number;
|
|
9
|
+
readonly right: number;
|
|
10
|
+
readonly top: number;
|
|
11
|
+
readonly bottom: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const flattenRect = (rect: Rect, left: boolean) => {
|
|
15
|
+
const x = left ? rect.left : rect.right;
|
|
16
|
+
return { left: x, right: x, top: rect.top, bottom: rect.bottom };
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let scratchRange: Range | null;
|
|
20
|
+
|
|
21
|
+
export const textRange = (node: Text, from: number, to = from) => {
|
|
22
|
+
const range = scratchRange || (scratchRange = document.createRange());
|
|
23
|
+
range.setEnd(node, to);
|
|
24
|
+
range.setStart(node, from);
|
|
25
|
+
return range;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const clientRectsFor = (dom: Node) => {
|
|
29
|
+
if (dom.nodeType === 3) {
|
|
30
|
+
return textRange(dom as Text, 0, dom.nodeValue!.length).getClientRects();
|
|
31
|
+
} else if (dom.nodeType === 1) {
|
|
32
|
+
return (dom as HTMLElement).getClientRects();
|
|
33
|
+
} else {
|
|
34
|
+
return [] as any as DOMRectList;
|
|
35
|
+
}
|
|
36
|
+
};
|
package/src/hooks/defs.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type Extension
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
6
6
|
|
|
7
7
|
import { type DocAccessor } from '@dxos/echo-schema';
|
|
8
8
|
|
|
@@ -18,27 +18,13 @@ export type Comment = {
|
|
|
18
18
|
cursor?: string;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* @deprecated
|
|
23
|
+
*/
|
|
24
|
+
// TODO(burdon): Remove.
|
|
21
25
|
export type EditorModel = {
|
|
22
26
|
id: string;
|
|
23
27
|
text: () => string;
|
|
24
|
-
extension?: Extension;
|
|
25
|
-
|
|
26
|
-
// TODO(burdon): Remove.
|
|
27
28
|
content: string | DocAccessor;
|
|
28
|
-
|
|
29
|
-
// TODO(burdon): Remove.
|
|
30
|
-
peer?: {
|
|
31
|
-
id: string;
|
|
32
|
-
name?: string;
|
|
33
|
-
};
|
|
29
|
+
extension?: Extension;
|
|
34
30
|
};
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* State field makes the model available to other extensions.
|
|
38
|
-
* @deprecated
|
|
39
|
-
*/
|
|
40
|
-
// TODO(burdon): Remove.
|
|
41
|
-
export const modelState = StateField.define<EditorModel | undefined>({
|
|
42
|
-
create: () => undefined,
|
|
43
|
-
update: (model) => model,
|
|
44
|
-
});
|
|
@@ -6,23 +6,23 @@ import type { EditorView } from '@codemirror/view';
|
|
|
6
6
|
|
|
7
7
|
import type { ToolbarProps } from '../components';
|
|
8
8
|
import {
|
|
9
|
-
createComment,
|
|
10
|
-
insertTable,
|
|
11
|
-
setHeading,
|
|
12
|
-
setStyle,
|
|
13
|
-
toggleStyle,
|
|
14
|
-
addLink,
|
|
15
|
-
removeLink,
|
|
16
9
|
Inline,
|
|
17
10
|
List,
|
|
18
|
-
addList,
|
|
19
|
-
removeList,
|
|
20
|
-
toggleList,
|
|
21
11
|
addBlockquote,
|
|
22
|
-
removeBlockquote,
|
|
23
12
|
addCodeblock,
|
|
13
|
+
addLink,
|
|
14
|
+
addList,
|
|
15
|
+
createComment,
|
|
16
|
+
insertTable,
|
|
17
|
+
removeBlockquote,
|
|
24
18
|
removeCodeblock,
|
|
19
|
+
removeLink,
|
|
20
|
+
removeList,
|
|
21
|
+
setHeading,
|
|
22
|
+
setStyle,
|
|
25
23
|
toggleBlockquote,
|
|
24
|
+
toggleList,
|
|
25
|
+
toggleStyle,
|
|
26
26
|
} from '../extensions';
|
|
27
27
|
|
|
28
28
|
export const useActionHandler = (view?: EditorView | null): ToolbarProps['onAction'] => {
|
|
@@ -21,7 +21,6 @@ import { type MutableRefObject, useRef, useState } from 'react';
|
|
|
21
21
|
* };
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
|
-
// TODO(burdon): Replace with useTextEditor.
|
|
25
24
|
export const useEditorView = (id: string): [MutableRefObject<EditorView | null>, boolean] => {
|
|
26
25
|
const editorRef = useRef<EditorView | null>(null);
|
|
27
26
|
const [prev, setPrev] = useState<[EditorView | null, string]>([null, '']);
|
|
@@ -10,7 +10,9 @@ import { Toolbar as NaturalToolbar, Select, useThemeContext, Tooltip } from '@dx
|
|
|
10
10
|
import { attentionSurface, mx, textBlockWidth } from '@dxos/react-ui-theme';
|
|
11
11
|
import { withTheme } from '@dxos/storybook-utils';
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { useActionHandler } from './useActionHandler';
|
|
14
|
+
import { createBasicExtensions, createThemeExtensions, useTextEditor } from './useTextEditor';
|
|
15
|
+
import { Toolbar } from '../components';
|
|
14
16
|
import {
|
|
15
17
|
type EditorMode,
|
|
16
18
|
EditorModes,
|
|
@@ -20,11 +22,9 @@ import {
|
|
|
20
22
|
image,
|
|
21
23
|
table,
|
|
22
24
|
useFormattingState,
|
|
23
|
-
} from '
|
|
24
|
-
import {
|
|
25
|
-
import
|
|
26
|
-
import translations from '../../translations';
|
|
27
|
-
import { Toolbar } from '../Toolbar';
|
|
25
|
+
} from '../extensions';
|
|
26
|
+
import { markdownTheme } from '../themes';
|
|
27
|
+
import translations from '../translations';
|
|
28
28
|
|
|
29
29
|
// TODO(burdon): Demo toolbar with hooks.
|
|
30
30
|
// TODO(burdon): Build components from hooks and adapters for model/extensions, etc.
|
|
@@ -46,7 +46,7 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
|
|
|
46
46
|
const extensions = useMemo(
|
|
47
47
|
() => [
|
|
48
48
|
editorMode ? EditorModes[editorMode] : [],
|
|
49
|
-
|
|
49
|
+
createBasicExtensions({ readonly }),
|
|
50
50
|
createThemeExtensions({
|
|
51
51
|
themeMode,
|
|
52
52
|
theme: markdownTheme,
|
|
@@ -116,7 +116,7 @@ const EditorModeToolbar = ({
|
|
|
116
116
|
|
|
117
117
|
export default {
|
|
118
118
|
title: 'react-ui-editor/useTextEditor',
|
|
119
|
-
component: TextEditor,
|
|
119
|
+
// component: TextEditor,
|
|
120
120
|
decorators: [withTheme],
|
|
121
121
|
render: (args: StoryProps) => (
|
|
122
122
|
<Tooltip.Provider>
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { closeBrackets } from '@codemirror/autocomplete';
|
|
6
|
+
import { history } from '@codemirror/commands';
|
|
7
|
+
import { bracketMatching } from '@codemirror/language';
|
|
5
8
|
import {
|
|
6
9
|
EditorSelection,
|
|
7
10
|
EditorState,
|
|
@@ -9,7 +12,17 @@ import {
|
|
|
9
12
|
type Extension,
|
|
10
13
|
type StateEffect,
|
|
11
14
|
} from '@codemirror/state';
|
|
12
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
crosshairCursor,
|
|
17
|
+
drawSelection,
|
|
18
|
+
dropCursor,
|
|
19
|
+
EditorView,
|
|
20
|
+
highlightActiveLine,
|
|
21
|
+
lineNumbers,
|
|
22
|
+
placeholder,
|
|
23
|
+
scrollPastEnd,
|
|
24
|
+
} from '@codemirror/view';
|
|
25
|
+
import defaultsDeep from 'lodash.defaultsdeep';
|
|
13
26
|
import { type DependencyList, type RefObject, useEffect, useRef, useState } from 'react';
|
|
14
27
|
|
|
15
28
|
import { log } from '@dxos/log';
|
|
@@ -38,7 +51,7 @@ export type UseTextEditor = {
|
|
|
38
51
|
// TODO(wittjosiah): Does not work in strict mode.
|
|
39
52
|
export const useTextEditor = (
|
|
40
53
|
{ autoFocus, scrollTo, debug, doc, selection, extensions }: UseTextEditorOptions = {},
|
|
41
|
-
deps
|
|
54
|
+
deps: DependencyList = [],
|
|
42
55
|
): UseTextEditor => {
|
|
43
56
|
const onUpdate = useRef<() => void>();
|
|
44
57
|
const [view, setView] = useState<EditorView>();
|
|
@@ -114,23 +127,66 @@ export const useTextEditor = (
|
|
|
114
127
|
|
|
115
128
|
// TODO(burdon): Factor out extension factories.
|
|
116
129
|
|
|
117
|
-
|
|
130
|
+
/**
|
|
131
|
+
* https://codemirror.net/docs/extensions
|
|
132
|
+
* https://github.com/codemirror/basic-setup
|
|
133
|
+
*/
|
|
134
|
+
// TODO(burdon): Reconcile with createMarkdownExtensions.
|
|
135
|
+
export type BasicExtensionsOptions = {
|
|
136
|
+
allowMultipleSelections?: boolean;
|
|
137
|
+
bracketMatching?: boolean;
|
|
138
|
+
closeBrackets?: boolean;
|
|
139
|
+
crosshairCursor?: boolean;
|
|
140
|
+
dropCursor?: boolean;
|
|
141
|
+
drawSelection?: boolean;
|
|
142
|
+
editable?: boolean;
|
|
143
|
+
highlightActiveLine?: boolean;
|
|
144
|
+
history?: boolean;
|
|
145
|
+
lineNumbers?: boolean;
|
|
146
|
+
lineWrapping?: boolean;
|
|
147
|
+
placeholder?: string;
|
|
118
148
|
readonly?: boolean;
|
|
149
|
+
scrollPastEnd?: boolean;
|
|
150
|
+
tabSize?: number;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const defaults: BasicExtensionsOptions = {
|
|
154
|
+
bracketMatching: true,
|
|
155
|
+
closeBrackets: true,
|
|
156
|
+
drawSelection: true,
|
|
157
|
+
editable: true,
|
|
158
|
+
history: true,
|
|
159
|
+
lineWrapping: true,
|
|
119
160
|
};
|
|
120
161
|
|
|
121
|
-
|
|
122
|
-
|
|
162
|
+
export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extension => {
|
|
163
|
+
const props: BasicExtensionsOptions = defaultsDeep({}, _props, defaults);
|
|
123
164
|
return [
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
165
|
+
// TODO(burdon): Doesn't catch errors in keymap functions.
|
|
166
|
+
EditorView.exceptionSink.of((err) => {
|
|
167
|
+
log.catch(err);
|
|
168
|
+
}),
|
|
169
|
+
|
|
170
|
+
props.allowMultipleSelections && EditorState.allowMultipleSelections.of(true),
|
|
171
|
+
props.bracketMatching && bracketMatching(),
|
|
172
|
+
props.closeBrackets && closeBrackets(),
|
|
173
|
+
props.crosshairCursor && crosshairCursor(),
|
|
174
|
+
props.dropCursor && dropCursor(),
|
|
175
|
+
props.drawSelection && drawSelection(),
|
|
176
|
+
props.highlightActiveLine && highlightActiveLine(),
|
|
177
|
+
props.history && history(),
|
|
178
|
+
props.lineNumbers && lineNumbers(),
|
|
179
|
+
props.lineWrapping && EditorView.lineWrapping,
|
|
180
|
+
props.placeholder && placeholder(props.placeholder),
|
|
181
|
+
props.readonly && [EditorState.readOnly.of(true), EditorView.editable.of(false)],
|
|
182
|
+
props.scrollPastEnd && scrollPastEnd(),
|
|
183
|
+
props.tabSize && EditorState.tabSize.of(props.tabSize),
|
|
184
|
+
].filter(isNotFalsy);
|
|
128
185
|
};
|
|
129
186
|
|
|
130
187
|
export type ThemeExtensionsOptions = {
|
|
131
188
|
theme?: ThemeStyles;
|
|
132
189
|
themeMode?: ThemeMode;
|
|
133
|
-
lineWrap?: boolean;
|
|
134
190
|
slots?: {
|
|
135
191
|
editor?: {
|
|
136
192
|
className?: string;
|
|
@@ -143,11 +199,10 @@ export type ThemeExtensionsOptions = {
|
|
|
143
199
|
|
|
144
200
|
export const createThemeExtensions = ({ theme, themeMode, slots }: ThemeExtensionsOptions = {}): Extension => {
|
|
145
201
|
return [
|
|
146
|
-
//
|
|
147
202
|
EditorView.baseTheme(defaultTheme),
|
|
148
|
-
theme && EditorView.theme(theme),
|
|
149
203
|
EditorView.darkTheme.of(themeMode === 'dark'),
|
|
150
|
-
EditorView.
|
|
151
|
-
EditorView.
|
|
204
|
+
theme && EditorView.theme(theme),
|
|
205
|
+
slots?.editor?.className && EditorView.editorAttributes.of({ class: slots.editor.className }),
|
|
206
|
+
slots?.content?.className && EditorView.contentAttributes.of({ class: slots.content.className }),
|
|
152
207
|
].filter(isNotFalsy);
|
|
153
208
|
};
|
|
@@ -11,12 +11,13 @@ import { type AutomergeTextCompat, getRawDoc } from '@dxos/echo-schema';
|
|
|
11
11
|
import { invariant } from '@dxos/invariant';
|
|
12
12
|
import { isAutomergeObject, type Space, type TextObject } from '@dxos/react-client/echo';
|
|
13
13
|
import { type Identity } from '@dxos/react-client/halo';
|
|
14
|
+
import { type HuePalette, hueTokens } from '@dxos/react-ui-theme';
|
|
15
|
+
import { hexToHue } from '@dxos/util';
|
|
14
16
|
|
|
15
17
|
import { SpaceAwarenessProvider } from './awareness-provider';
|
|
16
|
-
import { type EditorModel
|
|
18
|
+
import { type EditorModel } from './defs';
|
|
17
19
|
import { automerge, awareness } from '../extensions';
|
|
18
20
|
import { type DocAccessor } from '../extensions/automerge/defs';
|
|
19
|
-
import { cursorColor } from '../styles';
|
|
20
21
|
|
|
21
22
|
export type useTextExtensionsProps = {
|
|
22
23
|
id: string;
|
|
@@ -29,6 +30,10 @@ export type useTextExtensionsProps = {
|
|
|
29
30
|
export const useTextExtensions = ({ id, text, space, identity }: useTextExtensionsProps): Extension[] => {
|
|
30
31
|
const extensions: Extension[] = [automerge(text)];
|
|
31
32
|
|
|
33
|
+
const peerId = identity?.identityKey.toHex();
|
|
34
|
+
const { cursorLightValue, cursorDarkValue } =
|
|
35
|
+
hueTokens[(identity?.profile?.data?.hue as HuePalette | undefined) ?? hexToHue(peerId ?? '0')];
|
|
36
|
+
|
|
32
37
|
if (space && identity) {
|
|
33
38
|
const awarenessProvider = new SpaceAwarenessProvider({
|
|
34
39
|
space,
|
|
@@ -36,8 +41,8 @@ export const useTextExtensions = ({ id, text, space, identity }: useTextExtensio
|
|
|
36
41
|
peerId: identity.identityKey.toHex(),
|
|
37
42
|
info: {
|
|
38
43
|
displayName: identity.profile?.displayName ?? generateName(identity.identityKey.toHex()),
|
|
39
|
-
color:
|
|
40
|
-
lightColor:
|
|
44
|
+
color: cursorDarkValue,
|
|
45
|
+
lightColor: cursorLightValue,
|
|
41
46
|
},
|
|
42
47
|
});
|
|
43
48
|
|
|
@@ -64,6 +69,7 @@ export const useTextModel = (props: UseTextModelProps): EditorModel | undefined
|
|
|
64
69
|
* For use primarily in stories & tests so the dependence on TextObject can be avoided.
|
|
65
70
|
* @deprecated
|
|
66
71
|
*/
|
|
72
|
+
// TODO(burdon): Remove.
|
|
67
73
|
export const useInMemoryTextModel = ({
|
|
68
74
|
id,
|
|
69
75
|
defaultContent,
|
|
@@ -72,10 +78,10 @@ export const useInMemoryTextModel = ({
|
|
|
72
78
|
defaultContent?: string;
|
|
73
79
|
}): EditorModel & { setContent: Dispatch<SetStateAction<string>> } => {
|
|
74
80
|
const [content, setContent] = useState(defaultContent ?? '');
|
|
75
|
-
return { id, content,
|
|
81
|
+
return { id, content, text: () => content, setContent };
|
|
76
82
|
};
|
|
77
83
|
|
|
78
|
-
const createModel = ({ space, identity, text }: UseTextModelProps) => {
|
|
84
|
+
const createModel = ({ space, identity, text }: UseTextModelProps): EditorModel | undefined => {
|
|
79
85
|
if (!text) {
|
|
80
86
|
return undefined;
|
|
81
87
|
}
|
|
@@ -84,6 +90,10 @@ const createModel = ({ space, identity, text }: UseTextModelProps) => {
|
|
|
84
90
|
const obj = text as any as AutomergeTextCompat;
|
|
85
91
|
const doc = getRawDoc(obj, [obj.field]);
|
|
86
92
|
|
|
93
|
+
const peerId = identity?.identityKey.toHex();
|
|
94
|
+
const { cursorLightValue, cursorDarkValue } =
|
|
95
|
+
hueTokens[(identity?.profile?.data?.hue as HuePalette | undefined) ?? hexToHue(peerId ?? '0')];
|
|
96
|
+
|
|
87
97
|
const awarenessProvider =
|
|
88
98
|
space &&
|
|
89
99
|
new SpaceAwarenessProvider({
|
|
@@ -91,29 +101,21 @@ const createModel = ({ space, identity, text }: UseTextModelProps) => {
|
|
|
91
101
|
channel: `automerge.awareness.${obj.id}`,
|
|
92
102
|
info: {
|
|
93
103
|
displayName: identity ? identity.profile?.displayName ?? generateName(identity.identityKey.toHex()) : undefined,
|
|
94
|
-
color:
|
|
95
|
-
lightColor:
|
|
104
|
+
color: cursorLightValue,
|
|
105
|
+
lightColor: cursorDarkValue,
|
|
96
106
|
},
|
|
97
107
|
peerId: identity?.identityKey.toHex() ?? 'Anonymous',
|
|
98
108
|
});
|
|
99
109
|
|
|
100
|
-
const extensions = [
|
|
110
|
+
const extensions = [automerge({ handle: doc.handle, path: doc.path })];
|
|
101
111
|
if (awarenessProvider) {
|
|
102
112
|
extensions.push(awareness(awarenessProvider));
|
|
103
113
|
}
|
|
104
114
|
|
|
105
|
-
|
|
115
|
+
return {
|
|
106
116
|
id: obj.id,
|
|
107
117
|
content: doc,
|
|
108
118
|
text: () => get(doc.handle.docSync(), doc.path),
|
|
109
119
|
extension: extensions,
|
|
110
|
-
peer: identity
|
|
111
|
-
? {
|
|
112
|
-
id: identity.identityKey.toHex(),
|
|
113
|
-
name: identity.profile?.displayName,
|
|
114
|
-
}
|
|
115
|
-
: undefined,
|
|
116
120
|
};
|
|
117
|
-
|
|
118
|
-
return model;
|
|
119
121
|
};
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
//
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
|
+
|
|
4
5
|
import translations from './translations';
|
|
5
6
|
|
|
6
7
|
export { type Extension, type EditorState } from '@codemirror/state';
|
|
7
8
|
export { type EditorView, keymap } from '@codemirror/view';
|
|
9
|
+
export { tags } from '@lezer/highlight';
|
|
8
10
|
|
|
9
11
|
export { TextKind } from '@dxos/protocols/proto/dxos/echo/model/text';
|
|
10
12
|
export { Doc, YText, YXmlFragment } from '@dxos/text-model';
|
package/src/styles/index.ts
CHANGED
package/src/themes/default.ts
CHANGED
package/src/util.ts
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"automerge.stories.d.ts","sourceRoot":"","sources":["../../../../../src/components/TextEditor/automerge.stories.tsx"],"names":[],"mappings":"AAIA,OAAO,YAAY,CAAC;AAGpB,OAAO,uBAAuB,CAAC;AAc/B,OAAO,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAW9D,KAAK,WAAW,GAAG;IACjB,MAAM,EAAE,WAAW,CAAC;CACrB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DF,wBAKE;AAEF,eAAO,MAAM,OAAO,IAAK,CAAC;AAoB1B,eAAO,MAAM,QAAQ;;;CAmBpB,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.stories.d.ts","sourceRoot":"","sources":["../../../../../src/components/TextEditor/hooks.stories.tsx"],"names":[],"mappings":"AAIA,OAAO,YAAY,CAAC;AAEpB,OAAO,KAA4B,MAAM,OAAO,CAAC;AA4BjD,KAAK,UAAU,GAAG;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;;;;;mBAiFe,UAAU;;;;;;;;;;;;;;;;;;;;;;;;AAJ3B,wBAUE;AAEF,eAAO,MAAM,OAAO;;;;;;CAMnB,CAAC"}
|