@dxos/react-ui-editor 0.4.7-main.33db07e → 0.4.7-main.344d0d8
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 +346 -148
- 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 +6 -0
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- 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/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 -0
- package/dist/types/src/extensions/index.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/useTextModel.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/package.json +25 -25
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +72 -4
- package/src/extensions/awareness.ts +3 -3
- package/src/extensions/command/command.ts +38 -0
- package/src/extensions/command/hint.ts +75 -0
- package/src/extensions/command/index.ts +5 -0
- package/src/extensions/command/state.ts +106 -0
- package/src/extensions/dnd.ts +15 -0
- package/src/extensions/index.ts +2 -0
- package/src/extensions/util/dom.ts +36 -0
- package/src/hooks/useTextModel.ts +12 -5
- package/src/styles/index.ts +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/styles/cursors.ts +0 -21
|
@@ -3,17 +3,19 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import '@dxosTheme';
|
|
6
|
+
|
|
6
7
|
import { type EditorView } from '@codemirror/view';
|
|
7
|
-
import { ArrowSquareOut } from '@phosphor-icons/react';
|
|
8
|
+
import { ArrowSquareOut, X } from '@phosphor-icons/react';
|
|
8
9
|
import defaultsDeep from 'lodash.defaultsdeep';
|
|
9
|
-
import React, { type FC, StrictMode, useRef, useState } from 'react';
|
|
10
|
+
import React, { type FC, type KeyboardEvent, StrictMode, useRef, useState } from 'react';
|
|
10
11
|
import { createRoot } from 'react-dom/client';
|
|
11
12
|
|
|
12
13
|
import { TextObject } from '@dxos/echo-schema';
|
|
13
14
|
import { keySymbols, parseShortcut } from '@dxos/keyboard';
|
|
14
15
|
import { PublicKey } from '@dxos/keys';
|
|
15
16
|
import { faker } from '@dxos/random';
|
|
16
|
-
import {
|
|
17
|
+
import { Button, DensityProvider, Input, ThemeProvider } from '@dxos/react-ui';
|
|
18
|
+
import { baseSurface, defaultTx, getSize, mx, textBlockWidth } from '@dxos/react-ui-theme';
|
|
17
19
|
import { withTheme } from '@dxos/storybook-utils';
|
|
18
20
|
|
|
19
21
|
import { MarkdownEditor, type TextEditorProps } from './TextEditor';
|
|
@@ -24,6 +26,8 @@ import {
|
|
|
24
26
|
decorateMarkdown,
|
|
25
27
|
comments,
|
|
26
28
|
defaultOptions,
|
|
29
|
+
dnd,
|
|
30
|
+
command,
|
|
27
31
|
image,
|
|
28
32
|
linkTooltip,
|
|
29
33
|
mention,
|
|
@@ -33,6 +37,8 @@ import {
|
|
|
33
37
|
formatting,
|
|
34
38
|
annotations,
|
|
35
39
|
EditorModes,
|
|
40
|
+
type CommandOptions,
|
|
41
|
+
type CommandAction,
|
|
36
42
|
} from '../../extensions';
|
|
37
43
|
import { type Comment, useTextModel } from '../../hooks';
|
|
38
44
|
import translations from '../../translations';
|
|
@@ -238,7 +244,7 @@ const Story = ({ id = 'test', text, comments, placeholder = 'New document.', ...
|
|
|
238
244
|
placeholder={placeholder}
|
|
239
245
|
slots={{
|
|
240
246
|
root: { className: mx(textBlockWidth, 'min-bs-dvh') },
|
|
241
|
-
editor: { className: 'min-bs-dvh
|
|
247
|
+
editor: { className: 'min-bs-dvh px-8 bg-white dark:bg-black' },
|
|
242
248
|
}}
|
|
243
249
|
{...props}
|
|
244
250
|
/>
|
|
@@ -353,6 +359,64 @@ export const Mention = {
|
|
|
353
359
|
),
|
|
354
360
|
};
|
|
355
361
|
|
|
362
|
+
const CommandDialog: FC<{ onClose: (action?: CommandAction) => void }> = ({ onClose }) => {
|
|
363
|
+
const [text, setText] = useState('');
|
|
364
|
+
const handleInsert = () => {
|
|
365
|
+
onClose(text.length ? { insert: text + '\n' } : undefined);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
|
369
|
+
switch (event.key) {
|
|
370
|
+
case 'Enter': {
|
|
371
|
+
handleInsert();
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
case 'Escape': {
|
|
375
|
+
onClose();
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
return (
|
|
382
|
+
<DensityProvider density='fine'>
|
|
383
|
+
<div className={mx('flex items-center p-2 gap-2 border rounded-md', baseSurface)}>
|
|
384
|
+
<Input.Root>
|
|
385
|
+
<Input.TextInput
|
|
386
|
+
autoFocus={true}
|
|
387
|
+
placeholder='Enter command.'
|
|
388
|
+
value={text}
|
|
389
|
+
onChange={({ target: { value } }) => setText(value)}
|
|
390
|
+
onKeyDown={handleKeyDown}
|
|
391
|
+
/>
|
|
392
|
+
</Input.Root>
|
|
393
|
+
<Button variant='ghost' classNames='pli-0' onClick={() => onClose()}>
|
|
394
|
+
<X className={getSize(5)} />
|
|
395
|
+
</Button>
|
|
396
|
+
</div>
|
|
397
|
+
</DensityProvider>
|
|
398
|
+
);
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const renderCommandDialog: CommandOptions['onRender'] = (el, onClose) => {
|
|
402
|
+
createRoot(el).render(
|
|
403
|
+
<StrictMode>
|
|
404
|
+
<ThemeProvider tx={defaultTx}>
|
|
405
|
+
<CommandDialog onClose={onClose} />
|
|
406
|
+
</ThemeProvider>
|
|
407
|
+
</StrictMode>,
|
|
408
|
+
);
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
export const Command = {
|
|
412
|
+
render: () => (
|
|
413
|
+
<Story
|
|
414
|
+
text={str('# Command', '')}
|
|
415
|
+
extensions={[command({ onRender: renderCommandDialog, onHint: () => 'Press / for commands.' })]}
|
|
416
|
+
/>
|
|
417
|
+
),
|
|
418
|
+
};
|
|
419
|
+
|
|
356
420
|
export const Comments = {
|
|
357
421
|
render: () => {
|
|
358
422
|
const [_comments, setComments] = useState<Comment[]>([]);
|
|
@@ -402,6 +466,10 @@ export const Annotations = {
|
|
|
402
466
|
render: () => <Story text={str('# Annotations', '', large)} extensions={[annotations({ match: /volup/gi })]} />,
|
|
403
467
|
};
|
|
404
468
|
|
|
469
|
+
export const DND = {
|
|
470
|
+
render: () => <Story text={str('# DND', '')} extensions={[dnd()]} />,
|
|
471
|
+
};
|
|
472
|
+
|
|
405
473
|
const typewriterItems = localStorage.getItem('dxos.org/plugin/markdown/typewriter')?.split(',');
|
|
406
474
|
|
|
407
475
|
export const Typewriter = {
|
|
@@ -164,7 +164,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
164
164
|
from: start,
|
|
165
165
|
to: startLine.from + startLine.length,
|
|
166
166
|
value: Decoration.mark({
|
|
167
|
-
attributes: { style: `background-color: ${
|
|
167
|
+
attributes: { style: `background-color: ${lightColor}` },
|
|
168
168
|
class: 'cm-collab-selection',
|
|
169
169
|
}),
|
|
170
170
|
});
|
|
@@ -174,7 +174,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
174
174
|
from: endLine.from,
|
|
175
175
|
to: end,
|
|
176
176
|
value: Decoration.mark({
|
|
177
|
-
attributes: { style: `background-color: ${
|
|
177
|
+
attributes: { style: `background-color: ${lightColor}` },
|
|
178
178
|
class: 'cm-collab-selection',
|
|
179
179
|
}),
|
|
180
180
|
});
|
|
@@ -185,7 +185,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
185
185
|
from: linePos,
|
|
186
186
|
to: linePos,
|
|
187
187
|
value: Decoration.line({
|
|
188
|
-
attributes: { style: `background-color: ${
|
|
188
|
+
attributes: { style: `background-color: ${lightColor}`, class: 'cm-collab-selectionLine' },
|
|
189
189
|
}),
|
|
190
190
|
});
|
|
191
191
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
6
|
+
import { EditorView, keymap } from '@codemirror/view';
|
|
7
|
+
|
|
8
|
+
import { hintViewPlugin } from './hint';
|
|
9
|
+
import { closeEffect, commandConfig, commandKeymap, commandState } from './state';
|
|
10
|
+
|
|
11
|
+
// TODO(burdon): Create knowledge base for notes.
|
|
12
|
+
// https://discuss.codemirror.net/t/inline-code-hints-like-vscode/5533/4
|
|
13
|
+
// https://github.com/saminzadeh/codemirror-extension-inline-suggestion
|
|
14
|
+
// https://github.com/ChromeDevTools/devtools-frontend/blob/main/front_end/ui/components/text_editor/config.ts#L370
|
|
15
|
+
|
|
16
|
+
// TODO(burdon): Insert text at cursor.
|
|
17
|
+
|
|
18
|
+
export type CommandAction = {
|
|
19
|
+
insert?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type CommandOptions = {
|
|
23
|
+
onRender: (el: HTMLElement, cb: (action?: CommandAction) => void) => void;
|
|
24
|
+
onHint: () => string | undefined;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const command = (options: CommandOptions): Extension => {
|
|
28
|
+
return [
|
|
29
|
+
commandState,
|
|
30
|
+
keymap.of(commandKeymap),
|
|
31
|
+
hintViewPlugin(options),
|
|
32
|
+
// Close tooltip on focus change.
|
|
33
|
+
EditorView.focusChangeEffect.of((_, focusing) => {
|
|
34
|
+
return focusing ? closeEffect.of(null) : null;
|
|
35
|
+
}),
|
|
36
|
+
commandConfig.of(options),
|
|
37
|
+
];
|
|
38
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { RangeSetBuilder } from '@codemirror/state';
|
|
6
|
+
import { Decoration, EditorView, ViewPlugin, type ViewUpdate, WidgetType } from '@codemirror/view';
|
|
7
|
+
|
|
8
|
+
import { type CommandOptions } from './command';
|
|
9
|
+
import { commandState } from './state';
|
|
10
|
+
import { clientRectsFor, flattenRect } from '../util/dom';
|
|
11
|
+
|
|
12
|
+
class CommandHint extends WidgetType {
|
|
13
|
+
constructor(readonly content: string | HTMLElement) {
|
|
14
|
+
super();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
toDOM() {
|
|
18
|
+
const wrap = document.createElement('span');
|
|
19
|
+
wrap.className = 'cm-placeholder';
|
|
20
|
+
wrap.style.pointerEvents = 'none';
|
|
21
|
+
wrap.appendChild(typeof this.content === 'string' ? document.createTextNode(this.content) : this.content);
|
|
22
|
+
if (typeof this.content === 'string') {
|
|
23
|
+
wrap.setAttribute('aria-label', 'placeholder ' + this.content);
|
|
24
|
+
} else {
|
|
25
|
+
wrap.setAttribute('aria-hidden', 'true');
|
|
26
|
+
}
|
|
27
|
+
return wrap;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
override coordsAt(dom: HTMLElement) {
|
|
31
|
+
const rects = dom.firstChild ? clientRectsFor(dom.firstChild) : [];
|
|
32
|
+
if (!rects.length) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const style = window.getComputedStyle(dom.parentNode as HTMLElement);
|
|
36
|
+
const rect = flattenRect(rects[0], style.direction !== 'rtl');
|
|
37
|
+
const lineHeight = parseInt(style.lineHeight);
|
|
38
|
+
if (rect.bottom - rect.top > lineHeight * 1.5) {
|
|
39
|
+
return { left: rect.left, right: rect.right, top: rect.top, bottom: rect.top + lineHeight };
|
|
40
|
+
}
|
|
41
|
+
return rect;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
override ignoreEvent() {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const hintViewPlugin = ({ onHint }: CommandOptions) =>
|
|
50
|
+
ViewPlugin.fromClass(
|
|
51
|
+
class {
|
|
52
|
+
deco = Decoration.none;
|
|
53
|
+
update(update: ViewUpdate) {
|
|
54
|
+
const builder = new RangeSetBuilder<Decoration>();
|
|
55
|
+
const cState = update.view.state.field(commandState, false);
|
|
56
|
+
if (!cState?.tooltip) {
|
|
57
|
+
const selection = update.view.state.selection.main;
|
|
58
|
+
const line = update.view.state.doc.lineAt(selection.from);
|
|
59
|
+
// Only show if blank line.
|
|
60
|
+
// TODO(burdon): Show after delay or if blank line above?
|
|
61
|
+
if (selection.from === selection.to && line.from === line.to) {
|
|
62
|
+
const hint = onHint();
|
|
63
|
+
if (hint) {
|
|
64
|
+
builder.add(selection.from, selection.to, Decoration.widget({ widget: new CommandHint(hint) }));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.deco = builder.finish();
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
provide: (plugin) => [EditorView.decorations.of((view) => view.plugin(plugin)?.deco ?? Decoration.none)],
|
|
74
|
+
},
|
|
75
|
+
);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Facet, StateEffect, StateField } from '@codemirror/state';
|
|
6
|
+
import {
|
|
7
|
+
type Command,
|
|
8
|
+
type EditorView,
|
|
9
|
+
type KeyBinding,
|
|
10
|
+
showTooltip,
|
|
11
|
+
type Tooltip,
|
|
12
|
+
type TooltipView,
|
|
13
|
+
} from '@codemirror/view';
|
|
14
|
+
|
|
15
|
+
import { type CommandOptions } from './command';
|
|
16
|
+
|
|
17
|
+
type CommandState = {
|
|
18
|
+
tooltip?: Tooltip | null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const commandConfig = Facet.define<CommandOptions, Required<CommandOptions>>({
|
|
22
|
+
combine: (providers) => providers[0],
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const commandState = StateField.define<CommandState>({
|
|
26
|
+
create: () => ({}),
|
|
27
|
+
update: (state, tr) => {
|
|
28
|
+
for (const effect of tr.effects) {
|
|
29
|
+
if (effect.is(closeEffect)) {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (effect.is(openEffect)) {
|
|
34
|
+
const options = tr.state.facet(commandConfig);
|
|
35
|
+
const { pos, fullWidth } = effect.value;
|
|
36
|
+
const tooltip: Tooltip = {
|
|
37
|
+
pos,
|
|
38
|
+
above: false,
|
|
39
|
+
arrow: false,
|
|
40
|
+
strictSide: true,
|
|
41
|
+
create: (view: EditorView) => {
|
|
42
|
+
const dom = document.createElement('div');
|
|
43
|
+
const tooltipView: TooltipView = {
|
|
44
|
+
dom,
|
|
45
|
+
mount: (view: EditorView) => {
|
|
46
|
+
if (fullWidth) {
|
|
47
|
+
const parent = dom.parentElement!;
|
|
48
|
+
const { paddingLeft, paddingRight } = window.getComputedStyle(parent);
|
|
49
|
+
const widthWithoutPadding = parent.clientWidth - parseFloat(paddingLeft) - parseFloat(paddingRight);
|
|
50
|
+
dom.style.width = `${widthWithoutPadding}px`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Render react component.
|
|
54
|
+
options.onRender(dom, (action) => {
|
|
55
|
+
view.dispatch({ effects: closeEffect.of(null) });
|
|
56
|
+
if (action?.insert?.length) {
|
|
57
|
+
view.dispatch({
|
|
58
|
+
changes: { from: pos, insert: action.insert },
|
|
59
|
+
selection: { anchor: pos + action.insert.length },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// TODO(burdon): Truncates text if focus immediately.
|
|
63
|
+
setTimeout(() => view.focus());
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return tooltipView;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
return { tooltip };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return state;
|
|
76
|
+
},
|
|
77
|
+
provide: (f) => [showTooltip.from(f, (value) => value.tooltip ?? null)],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export const openEffect = StateEffect.define<{ pos: number; fullWidth?: boolean }>();
|
|
81
|
+
export const closeEffect = StateEffect.define<null>();
|
|
82
|
+
|
|
83
|
+
export const openCommand: Command = (view: EditorView) => {
|
|
84
|
+
if (view.state.field(commandState, false)) {
|
|
85
|
+
const selection = view.state.selection.main;
|
|
86
|
+
const line = view.state.doc.lineAt(selection.from);
|
|
87
|
+
if (line.from === selection.from && line.from === line.to) {
|
|
88
|
+
view.dispatch({ effects: openEffect.of({ pos: selection.anchor, fullWidth: true }) });
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const closeCommand: Command = (view: EditorView) => {
|
|
96
|
+
if (view.state.field(commandState, false)) {
|
|
97
|
+
view.dispatch({ effects: closeEffect.of(null) });
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const commandKeymap: readonly KeyBinding[] = [
|
|
104
|
+
{ key: '/', run: openCommand },
|
|
105
|
+
{ key: 'Escape', run: closeCommand },
|
|
106
|
+
];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type { Extension } from '@codemirror/state';
|
|
6
|
+
import { EditorView } from '@codemirror/view';
|
|
7
|
+
|
|
8
|
+
// TODO(burdon): DND experiment.
|
|
9
|
+
export const dnd = (): Extension => {
|
|
10
|
+
return EditorView.domEventHandlers({
|
|
11
|
+
drop: (event, view) => {
|
|
12
|
+
console.log(event);
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
};
|
package/src/extensions/index.ts
CHANGED
|
@@ -8,9 +8,11 @@ export * from './automerge';
|
|
|
8
8
|
export * from './awareness';
|
|
9
9
|
export * from './basic';
|
|
10
10
|
export * from './blast';
|
|
11
|
+
export * from './command';
|
|
11
12
|
export * from './comments';
|
|
12
13
|
export * from './cursor';
|
|
13
14
|
export * from './cursor-line-margin';
|
|
15
|
+
export * from './dnd';
|
|
14
16
|
export * from './listener';
|
|
15
17
|
export * from './markdown';
|
|
16
18
|
export * from './mention';
|
|
@@ -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
|
+
};
|
|
@@ -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 { hueTokens } from '@dxos/react-ui-theme';
|
|
15
|
+
import { hexToHue } from '@dxos/util';
|
|
14
16
|
|
|
15
17
|
import { SpaceAwarenessProvider } from './awareness-provider';
|
|
16
18
|
import { type EditorModel, modelState } 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,9 @@ 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 } = hueTokens[hexToHue(peerId ?? '0')];
|
|
35
|
+
|
|
32
36
|
if (space && identity) {
|
|
33
37
|
const awarenessProvider = new SpaceAwarenessProvider({
|
|
34
38
|
space,
|
|
@@ -36,8 +40,8 @@ export const useTextExtensions = ({ id, text, space, identity }: useTextExtensio
|
|
|
36
40
|
peerId: identity.identityKey.toHex(),
|
|
37
41
|
info: {
|
|
38
42
|
displayName: identity.profile?.displayName ?? generateName(identity.identityKey.toHex()),
|
|
39
|
-
color:
|
|
40
|
-
lightColor:
|
|
43
|
+
color: cursorDarkValue,
|
|
44
|
+
lightColor: cursorLightValue,
|
|
41
45
|
},
|
|
42
46
|
});
|
|
43
47
|
|
|
@@ -84,6 +88,9 @@ const createModel = ({ space, identity, text }: UseTextModelProps) => {
|
|
|
84
88
|
const obj = text as any as AutomergeTextCompat;
|
|
85
89
|
const doc = getRawDoc(obj, [obj.field]);
|
|
86
90
|
|
|
91
|
+
const peerId = identity?.identityKey.toHex();
|
|
92
|
+
const { cursorLightValue, cursorDarkValue } = hueTokens[hexToHue(peerId ?? '0')];
|
|
93
|
+
|
|
87
94
|
const awarenessProvider =
|
|
88
95
|
space &&
|
|
89
96
|
new SpaceAwarenessProvider({
|
|
@@ -91,8 +98,8 @@ const createModel = ({ space, identity, text }: UseTextModelProps) => {
|
|
|
91
98
|
channel: `automerge.awareness.${obj.id}`,
|
|
92
99
|
info: {
|
|
93
100
|
displayName: identity ? identity.profile?.displayName ?? generateName(identity.identityKey.toHex()) : undefined,
|
|
94
|
-
color:
|
|
95
|
-
lightColor:
|
|
101
|
+
color: cursorLightValue,
|
|
102
|
+
lightColor: cursorDarkValue,
|
|
96
103
|
},
|
|
97
104
|
peerId: identity?.identityKey.toHex() ?? 'Anonymous',
|
|
98
105
|
});
|
package/src/styles/index.ts
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cursors.d.ts","sourceRoot":"","sources":["../../../../src/styles/cursors.ts"],"names":[],"mappings":"AAoBA,eAAO,MAAM,WAAW;;;CAAsD,CAAC"}
|
package/src/styles/cursors.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2023 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
// Copied from https://github.com/yjs/y-codemirror.next#example.
|
|
6
|
-
|
|
7
|
-
import * as random from 'lib0/random';
|
|
8
|
-
|
|
9
|
-
const cursorColors = [
|
|
10
|
-
{ color: '#30bced', light: '#30bced33' },
|
|
11
|
-
{ color: '#6eeb83', light: '#6eeb8333' },
|
|
12
|
-
{ color: '#ffbc42', light: '#ffbc4233' },
|
|
13
|
-
{ color: '#ecd444', light: '#ecd44433' },
|
|
14
|
-
{ color: '#ee6352', light: '#ee635233' },
|
|
15
|
-
{ color: '#9ac2c9', light: '#9ac2c933' },
|
|
16
|
-
{ color: '#8acb88', light: '#8acb8833' },
|
|
17
|
-
{ color: '#1be7ff', light: '#1be7ff33' },
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
// Select a random color for this user.
|
|
21
|
-
export const cursorColor = cursorColors[random.uint32() % cursorColors.length];
|