@dxos/react-ui-editor 0.8.2-main.f11618f → 0.8.2-staging.7ac8446
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 +371 -499
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +379 -515
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +371 -499
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/{stories/InputMode.stories.d.ts → InputMode.stories.d.ts} +1 -1
- package/dist/types/src/InputMode.stories.d.ts.map +1 -0
- package/dist/types/src/{stories/TextEditorBasic.stories.d.ts → TextEditor.stories.d.ts} +35 -2
- package/dist/types/src/TextEditor.stories.d.ts.map +1 -0
- package/dist/types/src/components/EditorToolbar/util.d.ts +3 -3
- package/dist/types/src/components/EditorToolbar/util.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/{view-mode.d.ts → viewMode.d.ts} +1 -1
- package/dist/types/src/components/EditorToolbar/viewMode.d.ts.map +1 -0
- package/dist/types/src/defaults.d.ts +0 -1
- package/dist/types/src/defaults.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/command/command.d.ts +10 -5
- package/dist/types/src/extensions/command/command.d.ts.map +1 -1
- package/dist/types/src/extensions/command/hint.d.ts +2 -4
- package/dist/types/src/extensions/command/hint.d.ts.map +1 -1
- package/dist/types/src/extensions/command/index.d.ts +0 -1
- package/dist/types/src/extensions/command/index.d.ts.map +1 -1
- package/dist/types/src/extensions/command/menu.d.ts +2 -7
- package/dist/types/src/extensions/command/menu.d.ts.map +1 -1
- package/dist/types/src/extensions/command/preview.d.ts +12 -0
- package/dist/types/src/extensions/command/preview.d.ts.map +1 -0
- package/dist/types/src/extensions/command/state.d.ts +11 -9
- package/dist/types/src/extensions/command/state.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +7 -9
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +0 -1
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts +1 -4
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts +2 -2
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/link.d.ts +1 -4
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
- package/dist/types/src/fragments.d.ts +3 -0
- package/dist/types/src/fragments.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextEditor.d.ts +1 -2
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +0 -5
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/src/util/react.d.ts +1 -6
- package/dist/types/src/util/react.d.ts.map +1 -1
- package/package.json +27 -33
- package/src/{stories/InputMode.stories.tsx → InputMode.stories.tsx} +4 -4
- package/src/TextEditor.stories.tsx +856 -0
- package/src/components/EditorToolbar/EditorToolbar.tsx +2 -2
- package/src/components/EditorToolbar/util.ts +3 -3
- package/src/defaults.ts +3 -5
- package/src/extensions/automerge/automerge.stories.tsx +11 -3
- package/src/extensions/command/command.ts +27 -9
- package/src/extensions/command/hint.ts +30 -33
- package/src/extensions/command/index.ts +0 -1
- package/src/extensions/command/menu.ts +8 -11
- package/src/extensions/command/preview.ts +79 -0
- package/src/extensions/command/state.ts +61 -41
- package/src/extensions/comments.ts +9 -9
- package/src/extensions/folding.tsx +1 -1
- package/src/extensions/index.ts +0 -1
- package/src/extensions/markdown/decorate.ts +3 -4
- package/src/extensions/markdown/formatting.ts +2 -2
- package/src/extensions/markdown/image.ts +11 -12
- package/src/extensions/markdown/link.ts +24 -33
- package/src/fragments.ts +19 -0
- package/src/hooks/useTextEditor.ts +3 -4
- package/src/types.ts +0 -7
- package/src/util/react.tsx +2 -20
- package/dist/lib/browser/testing/index.mjs +0 -67
- package/dist/lib/browser/testing/index.mjs.map +0 -7
- package/dist/lib/node/testing/index.cjs +0 -101
- package/dist/lib/node/testing/index.cjs.map +0 -7
- package/dist/lib/node-esm/testing/index.mjs +0 -69
- package/dist/lib/node-esm/testing/index.mjs.map +0 -7
- package/dist/types/src/components/EditorToolbar/view-mode.d.ts.map +0 -1
- package/dist/types/src/extensions/command/action.d.ts +0 -17
- package/dist/types/src/extensions/command/action.d.ts.map +0 -1
- package/dist/types/src/extensions/preview/index.d.ts +0 -2
- package/dist/types/src/extensions/preview/index.d.ts.map +0 -1
- package/dist/types/src/extensions/preview/preview.d.ts +0 -39
- package/dist/types/src/extensions/preview/preview.d.ts.map +0 -1
- package/dist/types/src/stories/InputMode.stories.d.ts.map +0 -1
- package/dist/types/src/stories/TextEditorBasic.stories.d.ts.map +0 -1
- package/dist/types/src/stories/TextEditorComments.stories.d.ts +0 -13
- package/dist/types/src/stories/TextEditorComments.stories.d.ts.map +0 -1
- package/dist/types/src/stories/TextEditorPreview.stories.d.ts +0 -13
- package/dist/types/src/stories/TextEditorPreview.stories.d.ts.map +0 -1
- package/dist/types/src/stories/TextEditorSpecial.stories.d.ts +0 -19
- package/dist/types/src/stories/TextEditorSpecial.stories.d.ts.map +0 -1
- package/dist/types/src/stories/story-utils.d.ts +0 -53
- package/dist/types/src/stories/story-utils.d.ts.map +0 -1
- package/dist/types/src/testing/RefPopover.d.ts +0 -21
- package/dist/types/src/testing/RefPopover.d.ts.map +0 -1
- package/dist/types/src/testing/index.d.ts +0 -2
- package/dist/types/src/testing/index.d.ts.map +0 -1
- package/src/extensions/command/action.ts +0 -49
- package/src/extensions/preview/index.ts +0 -5
- package/src/extensions/preview/preview.ts +0 -271
- package/src/stories/TextEditorBasic.stories.tsx +0 -289
- package/src/stories/TextEditorComments.stories.tsx +0 -99
- package/src/stories/TextEditorPreview.stories.tsx +0 -239
- package/src/stories/TextEditorSpecial.stories.tsx +0 -107
- package/src/stories/story-utils.tsx +0 -329
- package/src/testing/RefPopover.tsx +0 -74
- package/src/testing/index.ts +0 -5
- /package/src/components/EditorToolbar/{view-mode.ts → viewMode.ts} +0 -0
@@ -26,8 +26,8 @@ import {
|
|
26
26
|
type EditorToolbarProps,
|
27
27
|
editorToolbarSearch,
|
28
28
|
} from './util';
|
29
|
-
import { createViewMode } from './
|
30
|
-
import { stackItemContentToolbarClassNames } from '../../
|
29
|
+
import { createViewMode } from './viewMode';
|
30
|
+
import { stackItemContentToolbarClassNames } from '../../fragments';
|
31
31
|
|
32
32
|
const createToolbar = ({
|
33
33
|
state,
|
@@ -4,7 +4,7 @@
|
|
4
4
|
|
5
5
|
import { useMemo } from 'react';
|
6
6
|
|
7
|
-
import {
|
7
|
+
import { create, type ReactiveObject } from '@dxos/live-object';
|
8
8
|
import { type Label, type ThemedClassName } from '@dxos/react-ui';
|
9
9
|
import {
|
10
10
|
type MenuSeparator,
|
@@ -23,7 +23,7 @@ export type EditorToolbarState = Formatting &
|
|
23
23
|
Partial<{ comment: boolean; viewMode: EditorViewMode; selection: boolean }>;
|
24
24
|
|
25
25
|
export const useEditorToolbarState = (initialState: Partial<EditorToolbarState> = {}) => {
|
26
|
-
return useMemo(() =>
|
26
|
+
return useMemo(() => create<EditorToolbarState>(initialState), []);
|
27
27
|
};
|
28
28
|
|
29
29
|
export type EditorToolbarFeatureFlags = Partial<{
|
@@ -37,7 +37,7 @@ export type EditorToolbarFeatureFlags = Partial<{
|
|
37
37
|
}>;
|
38
38
|
|
39
39
|
export type EditorToolbarActionGraphProps = {
|
40
|
-
state:
|
40
|
+
state: ReactiveObject<EditorToolbarState>;
|
41
41
|
// TODO(wittjosiah): Control positioning.
|
42
42
|
customActions?: () => ActionGraphProps;
|
43
43
|
onAction: (action: EditorAction) => void;
|
package/src/defaults.ts
CHANGED
@@ -17,9 +17,7 @@ const margin = '!mt-[1rem]';
|
|
17
17
|
* NOTE: Max width - 4rem = 2rem left/right margin (or 2rem gutter plus 1rem left/right margin).
|
18
18
|
*/
|
19
19
|
// TOOD(burdon): Adjust depending on
|
20
|
-
export const
|
21
|
-
|
22
|
-
export const editorContent = mx(margin, editorWidth);
|
20
|
+
export const editorContent = mx(margin, '!mli-auto w-full max-w-[min(50rem,100%-4rem)]');
|
23
21
|
|
24
22
|
/**
|
25
23
|
* Margin for numbers.
|
@@ -52,6 +50,6 @@ export const stackItemContentEditorClassNames = (role?: string) =>
|
|
52
50
|
|
53
51
|
export const stackItemContentToolbarClassNames = (role?: string) =>
|
54
52
|
mx(
|
55
|
-
'attention-surface is-full border-be !border-separator
|
56
|
-
role === 'section' && 'sticky block-start-0 -mbe-px min-is-0',
|
53
|
+
'attention-surface is-full border-be !border-separator',
|
54
|
+
role === 'section' && 'sticky block-start-0 z-[1] -mbe-px min-is-0',
|
57
55
|
);
|
@@ -10,7 +10,15 @@ import React, { useEffect, useState } from 'react';
|
|
10
10
|
import { Repo } from '@dxos/automerge/automerge-repo';
|
11
11
|
import { BroadcastChannelNetworkAdapter } from '@dxos/automerge/automerge-repo-network-broadcastchannel';
|
12
12
|
import { Expando } from '@dxos/echo-schema';
|
13
|
-
import {
|
13
|
+
import {
|
14
|
+
DocAccessor,
|
15
|
+
Filter,
|
16
|
+
create,
|
17
|
+
createDocAccessor,
|
18
|
+
useQuery,
|
19
|
+
useSpace,
|
20
|
+
type Space,
|
21
|
+
} from '@dxos/react-client/echo';
|
14
22
|
import { useIdentity, type Identity } from '@dxos/react-client/halo';
|
15
23
|
import { ClientRepeater, type ClientRepeatedComponentProps } from '@dxos/react-client/testing';
|
16
24
|
import { useThemeContext } from '@dxos/react-ui';
|
@@ -131,9 +139,9 @@ export const WithEcho = {
|
|
131
139
|
createSpace
|
132
140
|
onSpaceCreated={async ({ space }) => {
|
133
141
|
space.db.add(
|
134
|
-
|
142
|
+
create({
|
135
143
|
type: 'test',
|
136
|
-
content:
|
144
|
+
content: create(Expando, { content: initialContent }),
|
137
145
|
}),
|
138
146
|
);
|
139
147
|
}}
|
@@ -5,25 +5,35 @@
|
|
5
5
|
import { type Extension } from '@codemirror/state';
|
6
6
|
import { EditorView, keymap } from '@codemirror/view';
|
7
7
|
|
8
|
-
import {
|
9
|
-
import {
|
10
|
-
import {
|
11
|
-
import { commandConfig,
|
8
|
+
import { hintViewPlugin } from './hint';
|
9
|
+
import { floatingMenu } from './menu';
|
10
|
+
import { preview, type PreviewOptions } from './preview';
|
11
|
+
import { closeEffect, commandConfig, commandKeyBindings, commandState } from './state';
|
12
12
|
|
13
13
|
// TODO(burdon): Create knowledge base for CM notes and ideas.
|
14
14
|
// https://discuss.codemirror.net/t/inline-code-hints-like-vscode/5533/4
|
15
15
|
// https://github.com/saminzadeh/codemirror-extension-inline-suggestion
|
16
16
|
// https://github.com/ChromeDevTools/devtools-frontend/blob/main/front_end/ui/components/text_editor/config.ts#L370
|
17
17
|
|
18
|
-
|
18
|
+
// TODO(burdon): Discriminated union.
|
19
|
+
export type CommandAction = {
|
20
|
+
insert?: string;
|
21
|
+
};
|
22
|
+
|
23
|
+
export type CommandOptions = {
|
24
|
+
onHint: () => string | undefined;
|
25
|
+
onRenderDialog: (el: HTMLElement, cb: (action?: CommandAction) => void) => void;
|
26
|
+
onRenderMenu: (el: HTMLElement, cb: () => void) => void;
|
27
|
+
} & Pick<PreviewOptions, 'onRenderPreview'>;
|
19
28
|
|
20
|
-
export const command = (options: CommandOptions
|
29
|
+
export const command = (options: CommandOptions): Extension => {
|
21
30
|
return [
|
22
|
-
keymap.of(commandKeyBindings),
|
23
31
|
commandConfig.of(options),
|
24
32
|
commandState,
|
25
|
-
|
26
|
-
|
33
|
+
keymap.of(commandKeyBindings),
|
34
|
+
preview(options),
|
35
|
+
floatingMenu(options),
|
36
|
+
hintViewPlugin(options),
|
27
37
|
EditorView.focusChangeEffect.of((_, focusing) => {
|
28
38
|
return focusing ? closeEffect.of(null) : null;
|
29
39
|
}),
|
@@ -31,6 +41,14 @@ export const command = (options: CommandOptions = {}): Extension => {
|
|
31
41
|
'.cm-tooltip': {
|
32
42
|
background: 'transparent',
|
33
43
|
},
|
44
|
+
'.cm-preview': {
|
45
|
+
marginLeft: '-1rem',
|
46
|
+
marginRight: '-1rem',
|
47
|
+
padding: '1rem',
|
48
|
+
borderRadius: '1rem',
|
49
|
+
background: 'var(--dx-modalSurface)',
|
50
|
+
border: '1px solid var(--dx-separator)',
|
51
|
+
},
|
34
52
|
}),
|
35
53
|
];
|
36
54
|
};
|
@@ -5,42 +5,10 @@
|
|
5
5
|
import { RangeSetBuilder } from '@codemirror/state';
|
6
6
|
import { Decoration, EditorView, ViewPlugin, type ViewUpdate, WidgetType } from '@codemirror/view';
|
7
7
|
|
8
|
+
import { type CommandOptions } from './command';
|
8
9
|
import { commandState } from './state';
|
9
10
|
import { clientRectsFor, flattenRect } from '../../util';
|
10
11
|
|
11
|
-
export type HintOptions = {
|
12
|
-
onHint: () => string | undefined;
|
13
|
-
};
|
14
|
-
|
15
|
-
export const hintViewPlugin = ({ onHint }: HintOptions) =>
|
16
|
-
ViewPlugin.fromClass(
|
17
|
-
class {
|
18
|
-
deco = Decoration.none;
|
19
|
-
update(update: ViewUpdate) {
|
20
|
-
const builder = new RangeSetBuilder<Decoration>();
|
21
|
-
const cState = update.view.state.field(commandState, false);
|
22
|
-
if (!cState?.tooltip) {
|
23
|
-
const selection = update.view.state.selection.main;
|
24
|
-
const line = update.view.state.doc.lineAt(selection.from);
|
25
|
-
// Only show if blank line.
|
26
|
-
// TODO(burdon): Clashes with placeholder if pos === 0.
|
27
|
-
// TODO(burdon): Show after delay or if blank line above?
|
28
|
-
if (selection.from === selection.to && line.from === line.to) {
|
29
|
-
const hint = onHint();
|
30
|
-
if (hint) {
|
31
|
-
builder.add(selection.from, selection.to, Decoration.widget({ widget: new CommandHint(hint) }));
|
32
|
-
}
|
33
|
-
}
|
34
|
-
}
|
35
|
-
|
36
|
-
this.deco = builder.finish();
|
37
|
-
}
|
38
|
-
},
|
39
|
-
{
|
40
|
-
provide: (plugin) => [EditorView.decorations.of((view) => view.plugin(plugin)?.deco ?? Decoration.none)],
|
41
|
-
},
|
42
|
-
);
|
43
|
-
|
44
12
|
class CommandHint extends WidgetType {
|
45
13
|
constructor(readonly content: string | HTMLElement) {
|
46
14
|
super();
|
@@ -80,3 +48,32 @@ class CommandHint extends WidgetType {
|
|
80
48
|
return false;
|
81
49
|
}
|
82
50
|
}
|
51
|
+
|
52
|
+
export const hintViewPlugin = ({ onHint }: CommandOptions) =>
|
53
|
+
ViewPlugin.fromClass(
|
54
|
+
class {
|
55
|
+
deco = Decoration.none;
|
56
|
+
update(update: ViewUpdate) {
|
57
|
+
const builder = new RangeSetBuilder<Decoration>();
|
58
|
+
const cState = update.view.state.field(commandState, false);
|
59
|
+
if (!cState?.tooltip) {
|
60
|
+
const selection = update.view.state.selection.main;
|
61
|
+
const line = update.view.state.doc.lineAt(selection.from);
|
62
|
+
// Only show if blank line.
|
63
|
+
// TODO(burdon): Clashes with placeholder if pos === 0.
|
64
|
+
// TODO(burdon): Show after delay or if blank line above?
|
65
|
+
if (selection.from === selection.to && line.from === line.to) {
|
66
|
+
const hint = onHint();
|
67
|
+
if (hint) {
|
68
|
+
builder.add(selection.from, selection.to, Decoration.widget({ widget: new CommandHint(hint) }));
|
69
|
+
}
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
this.deco = builder.finish();
|
74
|
+
}
|
75
|
+
},
|
76
|
+
{
|
77
|
+
provide: (plugin) => [EditorView.decorations.of((view) => view.plugin(plugin)?.deco ?? Decoration.none)],
|
78
|
+
},
|
79
|
+
);
|
@@ -4,16 +4,12 @@
|
|
4
4
|
|
5
5
|
import { type BlockInfo, type EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view';
|
6
6
|
|
7
|
-
import {
|
8
|
-
import {
|
9
|
-
|
10
|
-
export type FloatingMenuOptions = {
|
11
|
-
renderMenu: RenderCallback<{ onAction: () => void }>;
|
12
|
-
};
|
7
|
+
import { type CommandOptions } from './command';
|
8
|
+
import { closeEffect, openCommand, openEffect } from './state';
|
13
9
|
|
14
10
|
// TODO(burdon): Trigger completion on click.
|
15
11
|
// TODO(burdon): Hide when dialog is open.
|
16
|
-
export const floatingMenu = (options:
|
12
|
+
export const floatingMenu = (options: CommandOptions) =>
|
17
13
|
ViewPlugin.fromClass(
|
18
14
|
class {
|
19
15
|
button: HTMLElement;
|
@@ -35,11 +31,13 @@ export const floatingMenu = (options: FloatingMenuOptions) =>
|
|
35
31
|
this.button.style.zIndex = '10';
|
36
32
|
this.button.style.display = 'none';
|
37
33
|
|
38
|
-
options.
|
34
|
+
options.onRenderMenu(this.button, () => {
|
35
|
+
openCommand(view);
|
36
|
+
});
|
39
37
|
container.appendChild(this.button);
|
40
38
|
|
41
39
|
// Listen for scroll events.
|
42
|
-
container.addEventListener('scroll', this.scheduleUpdate
|
40
|
+
container.addEventListener('scroll', this.scheduleUpdate);
|
43
41
|
this.scheduleUpdate();
|
44
42
|
}
|
45
43
|
|
@@ -58,8 +56,7 @@ export const floatingMenu = (options: FloatingMenuOptions) =>
|
|
58
56
|
if (this.rafId != null) {
|
59
57
|
cancelAnimationFrame(this.rafId);
|
60
58
|
}
|
61
|
-
|
62
|
-
this.rafId = requestAnimationFrame(this.updateButtonPosition.bind(this));
|
59
|
+
this.rafId = requestAnimationFrame(() => this.updateButtonPosition());
|
63
60
|
}
|
64
61
|
|
65
62
|
updateButtonPosition() {
|
@@ -0,0 +1,79 @@
|
|
1
|
+
//
|
2
|
+
// Copyright 2023 DXOS.org
|
3
|
+
//
|
4
|
+
|
5
|
+
import { syntaxTree } from '@codemirror/language';
|
6
|
+
import {
|
7
|
+
type EditorState,
|
8
|
+
type Extension,
|
9
|
+
type RangeSet,
|
10
|
+
RangeSetBuilder,
|
11
|
+
StateField,
|
12
|
+
type Transaction,
|
13
|
+
} from '@codemirror/state';
|
14
|
+
import { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemirror/view';
|
15
|
+
|
16
|
+
export type PreviewOptions = {
|
17
|
+
onRenderPreview: (el: HTMLElement, props: { url: string; text: string }) => void;
|
18
|
+
};
|
19
|
+
|
20
|
+
/**
|
21
|
+
* Create image decorations.
|
22
|
+
*/
|
23
|
+
export const preview = (options: PreviewOptions): Extension => {
|
24
|
+
return [
|
25
|
+
StateField.define<DecorationSet>({
|
26
|
+
create: (state) => buildDecorations(state, options),
|
27
|
+
update: (_: RangeSet<Decoration>, tr: Transaction) => buildDecorations(tr.state, options),
|
28
|
+
// TODO(burdon): Make atomic.
|
29
|
+
provide: (field) => EditorView.decorations.from(field),
|
30
|
+
}),
|
31
|
+
];
|
32
|
+
};
|
33
|
+
|
34
|
+
// TODO(burdon): Make atomic.
|
35
|
+
const buildDecorations = (state: EditorState, options: PreviewOptions) => {
|
36
|
+
const builder = new RangeSetBuilder<Decoration>();
|
37
|
+
syntaxTree(state).iterate({
|
38
|
+
enter: (node) => {
|
39
|
+
if (node.name === 'Link') {
|
40
|
+
const urlNode = node.node.getChild('URL');
|
41
|
+
if (urlNode) {
|
42
|
+
const text = state.sliceDoc(node.from + 1, urlNode.from - 2);
|
43
|
+
const url = state.sliceDoc(urlNode.from, urlNode.to);
|
44
|
+
builder.add(
|
45
|
+
node.from,
|
46
|
+
node.to,
|
47
|
+
Decoration.replace({
|
48
|
+
block: true, // Prevent cursor from entering.
|
49
|
+
widget: new PreviewWidget(options.onRenderPreview, url, text),
|
50
|
+
}),
|
51
|
+
);
|
52
|
+
}
|
53
|
+
}
|
54
|
+
},
|
55
|
+
});
|
56
|
+
|
57
|
+
return builder.finish();
|
58
|
+
};
|
59
|
+
|
60
|
+
class PreviewWidget extends WidgetType {
|
61
|
+
constructor(
|
62
|
+
readonly _onRenderPreview: PreviewOptions['onRenderPreview'],
|
63
|
+
readonly _url: string,
|
64
|
+
readonly _text: string,
|
65
|
+
) {
|
66
|
+
super();
|
67
|
+
}
|
68
|
+
|
69
|
+
override eq(other: this) {
|
70
|
+
return this._url === (other as any as PreviewWidget)._url;
|
71
|
+
}
|
72
|
+
|
73
|
+
override toDOM(view: EditorView) {
|
74
|
+
const root = document.createElement('div');
|
75
|
+
root.classList.add('cm-preview');
|
76
|
+
this._onRenderPreview(root, { url: this._url, text: this._text });
|
77
|
+
return root;
|
78
|
+
}
|
79
|
+
}
|
@@ -2,24 +2,25 @@
|
|
2
2
|
// Copyright 2024 DXOS.org
|
3
3
|
//
|
4
4
|
|
5
|
-
import { StateField } from '@codemirror/state';
|
6
|
-
import {
|
5
|
+
import { StateEffect, StateField } from '@codemirror/state';
|
6
|
+
import {
|
7
|
+
showTooltip,
|
8
|
+
type Command,
|
9
|
+
type EditorView,
|
10
|
+
type KeyBinding,
|
11
|
+
type Tooltip,
|
12
|
+
type TooltipView,
|
13
|
+
} from '@codemirror/view';
|
7
14
|
|
8
|
-
import { closeEffect, type Action, openEffect } from './action';
|
9
15
|
import { type CommandOptions } from './command';
|
10
|
-
import { type RenderCallback } from '../../types';
|
11
16
|
import { singleValueFacet } from '../../util';
|
12
17
|
|
13
|
-
export const commandConfig = singleValueFacet<CommandOptions>();
|
14
|
-
|
15
|
-
export type PopupOptions = {
|
16
|
-
renderDialog: RenderCallback<{ onAction: (action?: Action) => void }>;
|
17
|
-
};
|
18
|
-
|
19
18
|
type CommandState = {
|
20
19
|
tooltip?: Tooltip | null;
|
21
20
|
};
|
22
21
|
|
22
|
+
export const commandConfig = singleValueFacet<CommandOptions>();
|
23
|
+
|
23
24
|
export const commandState = StateField.define<CommandState>({
|
24
25
|
create: () => ({}),
|
25
26
|
update: (state, tr) => {
|
@@ -28,8 +29,8 @@ export const commandState = StateField.define<CommandState>({
|
|
28
29
|
return {};
|
29
30
|
}
|
30
31
|
|
31
|
-
|
32
|
-
|
32
|
+
if (effect.is(openEffect)) {
|
33
|
+
const options = tr.state.facet(commandConfig);
|
33
34
|
const { pos, fullWidth } = effect.value;
|
34
35
|
const tooltip: Tooltip = {
|
35
36
|
pos,
|
@@ -37,49 +38,38 @@ export const commandState = StateField.define<CommandState>({
|
|
37
38
|
arrow: false,
|
38
39
|
strictSide: true,
|
39
40
|
create: (view: EditorView) => {
|
40
|
-
const
|
41
|
-
|
41
|
+
const dom = document.createElement('div');
|
42
42
|
const tooltipView: TooltipView = {
|
43
|
-
dom
|
43
|
+
dom,
|
44
44
|
mount: (view: EditorView) => {
|
45
45
|
if (fullWidth) {
|
46
|
-
const parent =
|
46
|
+
const parent = dom.parentElement!;
|
47
47
|
const { paddingLeft, paddingRight } = window.getComputedStyle(parent);
|
48
48
|
const widthWithoutPadding = parent.clientWidth - parseFloat(paddingLeft) - parseFloat(paddingRight);
|
49
|
-
|
49
|
+
dom.style.width = `${widthWithoutPadding}px`;
|
50
50
|
}
|
51
51
|
|
52
52
|
// Render react component.
|
53
|
-
|
54
|
-
|
55
|
-
{
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
}
|
68
|
-
}
|
69
|
-
|
70
|
-
// NOTE: Truncates text if set focus immediately.
|
71
|
-
requestAnimationFrame(() => view.focus());
|
72
|
-
},
|
73
|
-
},
|
74
|
-
view,
|
75
|
-
);
|
53
|
+
options.onRenderDialog(dom, (action) => {
|
54
|
+
view.dispatch({ effects: closeEffect.of(null) });
|
55
|
+
if (action?.insert?.length) {
|
56
|
+
// Insert into editor.
|
57
|
+
const text = action.insert + '\n';
|
58
|
+
view.dispatch({
|
59
|
+
changes: { from: pos, insert: text },
|
60
|
+
selection: { anchor: pos + text.length },
|
61
|
+
});
|
62
|
+
}
|
63
|
+
|
64
|
+
// NOTE: Truncates text if set focus immediately.
|
65
|
+
requestAnimationFrame(() => view.focus());
|
66
|
+
});
|
76
67
|
},
|
77
68
|
};
|
78
69
|
|
79
70
|
return tooltipView;
|
80
71
|
},
|
81
72
|
};
|
82
|
-
|
83
73
|
return { tooltip };
|
84
74
|
}
|
85
75
|
}
|
@@ -88,3 +78,33 @@ export const commandState = StateField.define<CommandState>({
|
|
88
78
|
},
|
89
79
|
provide: (field) => [showTooltip.from(field, (value) => value.tooltip ?? null)],
|
90
80
|
});
|
81
|
+
|
82
|
+
export const openEffect = StateEffect.define<{ pos: number; fullWidth?: boolean }>();
|
83
|
+
export const closeEffect = StateEffect.define<null>();
|
84
|
+
|
85
|
+
export const openCommand: Command = (view: EditorView) => {
|
86
|
+
if (view.state.field(commandState, false)) {
|
87
|
+
const selection = view.state.selection.main;
|
88
|
+
const line = view.state.doc.lineAt(selection.from);
|
89
|
+
if (line.from === selection.from && line.from === line.to) {
|
90
|
+
view.dispatch({ effects: openEffect.of({ pos: selection.anchor, fullWidth: true }) });
|
91
|
+
return true;
|
92
|
+
}
|
93
|
+
}
|
94
|
+
|
95
|
+
return false;
|
96
|
+
};
|
97
|
+
|
98
|
+
export const closeCommand: Command = (view: EditorView) => {
|
99
|
+
if (view.state.field(commandState, false)) {
|
100
|
+
view.dispatch({ effects: closeEffect.of(null) });
|
101
|
+
return true;
|
102
|
+
}
|
103
|
+
|
104
|
+
return false;
|
105
|
+
};
|
106
|
+
|
107
|
+
export const commandKeyBindings: readonly KeyBinding[] = [
|
108
|
+
{ key: '/', run: openCommand },
|
109
|
+
{ key: 'Escape', run: closeCommand },
|
110
|
+
];
|
@@ -25,13 +25,13 @@ import sortBy from 'lodash.sortby';
|
|
25
25
|
import { useEffect, useMemo } from 'react';
|
26
26
|
|
27
27
|
import { debounce, type CleanupFn } from '@dxos/async';
|
28
|
-
import { type
|
28
|
+
import { type ReactiveObject } from '@dxos/live-object';
|
29
29
|
import { log } from '@dxos/log';
|
30
30
|
import { isNonNullable } from '@dxos/util';
|
31
31
|
|
32
32
|
import { documentId } from './selection';
|
33
33
|
import { type EditorToolbarState } from '../components';
|
34
|
-
import { type
|
34
|
+
import { type Comment, type Range } from '../types';
|
35
35
|
import { Cursor, overlap, singleValueFacet, callbackWrapper } from '../util';
|
36
36
|
|
37
37
|
//
|
@@ -345,10 +345,6 @@ export type CommentsOptions = {
|
|
345
345
|
* Key shortcut to create a new thread.
|
346
346
|
*/
|
347
347
|
key?: string;
|
348
|
-
/**
|
349
|
-
* Called to render tooltip.
|
350
|
-
*/
|
351
|
-
renderTooltip?: RenderCallback<{ shortcut: string }>;
|
352
348
|
/**
|
353
349
|
* Called to create a new thread and return the thread id.
|
354
350
|
*/
|
@@ -365,6 +361,10 @@ export type CommentsOptions = {
|
|
365
361
|
* Called to notify which thread is currently closest to the cursor.
|
366
362
|
*/
|
367
363
|
onSelect?: (state: CommentsState) => void;
|
364
|
+
/**
|
365
|
+
* Called to render tooltip.
|
366
|
+
*/
|
367
|
+
onHover?: (el: Element, shortcut: string) => void;
|
368
368
|
};
|
369
369
|
|
370
370
|
const optionsFacet = singleValueFacet<CommentsOptions>();
|
@@ -408,7 +408,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
408
408
|
// Hover tooltip (for key shortcut hints, etc.)
|
409
409
|
// TODO(burdon): Factor out to generic hints extension for current selection/line.
|
410
410
|
//
|
411
|
-
options.
|
411
|
+
options.onHover &&
|
412
412
|
hoverTooltip(
|
413
413
|
(view, pos) => {
|
414
414
|
const selection = view.state.selection.main;
|
@@ -419,7 +419,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
419
419
|
above: true,
|
420
420
|
create: () => {
|
421
421
|
const el = document.createElement('div');
|
422
|
-
options.
|
422
|
+
options.onHover!(el, shortcut);
|
423
423
|
return { dom: el, offset: { x: 0, y: 8 } };
|
424
424
|
},
|
425
425
|
};
|
@@ -606,7 +606,7 @@ export const createExternalCommentSync = (
|
|
606
606
|
},
|
607
607
|
);
|
608
608
|
|
609
|
-
export const useCommentState = (state:
|
609
|
+
export const useCommentState = (state: ReactiveObject<EditorToolbarState>): Extension => {
|
610
610
|
return useMemo(
|
611
611
|
() =>
|
612
612
|
EditorView.updateListener.of((update) => {
|
@@ -29,7 +29,7 @@ export const folding = (_props: FoldingOptions = {}): Extension => [
|
|
29
29
|
const el = createElement('div', { className: 'flex h-full items-center' });
|
30
30
|
return renderRoot(
|
31
31
|
el,
|
32
|
-
<Icon icon='ph--caret-right--
|
32
|
+
<Icon icon='ph--caret-right--regular' size={3} classNames={['mx-3 cursor-pointer', open && 'rotate-90']} />,
|
33
33
|
);
|
34
34
|
},
|
35
35
|
}),
|
package/src/extensions/index.ts
CHANGED
@@ -15,7 +15,6 @@ import { image } from './image';
|
|
15
15
|
import { formattingStyles, bulletListIndentationWidth, orderedListIndentationWidth } from './styles';
|
16
16
|
import { table } from './table';
|
17
17
|
import { theme, type HeadingLevel } from '../../styles';
|
18
|
-
import { type RenderCallback } from '../../types';
|
19
18
|
import { wrapWithCatch } from '../../util';
|
20
19
|
|
21
20
|
/**
|
@@ -46,7 +45,7 @@ class HorizontalRuleWidget extends WidgetType {
|
|
46
45
|
class LinkButton extends WidgetType {
|
47
46
|
constructor(
|
48
47
|
private readonly url: string,
|
49
|
-
private readonly render:
|
48
|
+
private readonly render: (el: HTMLElement, url: string) => void,
|
50
49
|
) {
|
51
50
|
super();
|
52
51
|
}
|
@@ -58,7 +57,7 @@ class LinkButton extends WidgetType {
|
|
58
57
|
// TODO(burdon): Create icon and link directly without react?
|
59
58
|
override toDOM(view: EditorView) {
|
60
59
|
const el = document.createElement('span');
|
61
|
-
this.render(el,
|
60
|
+
this.render(el, this.url);
|
62
61
|
return el;
|
63
62
|
}
|
64
63
|
}
|
@@ -520,7 +519,7 @@ export interface DecorateOptions {
|
|
520
519
|
*/
|
521
520
|
selectionChangeDelay?: number;
|
522
521
|
numberedHeadings?: { from: number; to?: number };
|
523
|
-
renderLinkButton?:
|
522
|
+
renderLinkButton?: (el: Element, url: string) => void;
|
524
523
|
}
|
525
524
|
|
526
525
|
export const decorateMarkdown = (options: DecorateOptions = {}) => {
|
@@ -17,7 +17,7 @@ import { EditorView, keymap } from '@codemirror/view';
|
|
17
17
|
import { type SyntaxNodeRef, type SyntaxNode } from '@lezer/common';
|
18
18
|
import { useMemo } from 'react';
|
19
19
|
|
20
|
-
import { type
|
20
|
+
import { type ReactiveObject } from '@dxos/live-object';
|
21
21
|
|
22
22
|
import { type EditorToolbarState } from '../../components';
|
23
23
|
|
@@ -1250,7 +1250,7 @@ export const getFormatting = (state: EditorState): Formatting => {
|
|
1250
1250
|
/**
|
1251
1251
|
* Hook provides an extension to compute the current formatting state.
|
1252
1252
|
*/
|
1253
|
-
export const useFormattingState = (state:
|
1253
|
+
export const useFormattingState = (state: ReactiveObject<EditorToolbarState>): Extension => {
|
1254
1254
|
return useMemo(
|
1255
1255
|
() =>
|
1256
1256
|
EditorView.updateListener.of((update) => {
|