@dxos/react-ui-editor 0.8.1-main.ba2dec9 → 0.8.1-staging.31c3ee1
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 +283 -147
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +317 -185
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +283 -147
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/InputMode.stories.d.ts +2 -2
- package/dist/types/src/TextEditor.stories.d.ts +5 -40
- package/dist/types/src/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
- package/dist/types/src/defaults.d.ts +2 -0
- package/dist/types/src/defaults.d.ts.map +1 -1
- package/dist/types/src/extensions/command/command.d.ts +4 -2
- package/dist/types/src/extensions/command/command.d.ts.map +1 -1
- package/dist/types/src/extensions/command/hint.d.ts.map +1 -1
- package/dist/types/src/extensions/command/menu.d.ts +12 -0
- package/dist/types/src/extensions/command/menu.d.ts.map +1 -0
- 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.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +3 -3
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts +2 -1
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/{styles/stack-item-content-class-names.d.ts → fragments.d.ts} +1 -1
- package/dist/types/src/fragments.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextEditor.d.ts +3 -3
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +0 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/theme.d.ts.map +1 -1
- package/package.json +28 -28
- package/src/InputMode.stories.tsx +4 -4
- package/src/TextEditor.stories.tsx +173 -59
- package/src/components/EditorToolbar/EditorToolbar.tsx +4 -5
- package/src/defaults.ts +12 -0
- package/src/extensions/command/command.ts +21 -2
- package/src/extensions/command/hint.ts +3 -0
- package/src/extensions/command/menu.ts +100 -0
- package/src/extensions/command/preview.ts +79 -0
- package/src/extensions/command/state.ts +9 -4
- package/src/extensions/comments.ts +6 -10
- package/src/extensions/factories.ts +4 -3
- package/src/{styles/stack-item-content-class-names.ts → fragments.ts} +4 -2
- package/src/hooks/useTextEditor.ts +17 -8
- package/src/index.ts +0 -4
- package/src/styles/theme.ts +6 -1
- package/src/util/debug.ts +1 -1
- package/dist/types/src/styles/stack-item-content-class-names.d.ts.map +0 -1
@@ -7,11 +7,11 @@ import React, { useCallback } from 'react';
|
|
7
7
|
import { type NodeArg } from '@dxos/app-graph';
|
8
8
|
import { ElevationProvider } from '@dxos/react-ui';
|
9
9
|
import {
|
10
|
-
ToolbarMenu,
|
11
|
-
MenuProvider,
|
12
10
|
type MenuActionHandler,
|
13
|
-
|
11
|
+
MenuProvider,
|
12
|
+
ToolbarMenu,
|
14
13
|
createGapSeparator,
|
14
|
+
useMenuActions,
|
15
15
|
} from '@dxos/react-ui-menu';
|
16
16
|
import { textBlockWidth } from '@dxos/react-ui-theme';
|
17
17
|
|
@@ -27,7 +27,7 @@ import {
|
|
27
27
|
editorToolbarSearch,
|
28
28
|
} from './util';
|
29
29
|
import { createViewMode } from './viewMode';
|
30
|
-
import { stackItemContentToolbarClassNames } from '../../
|
30
|
+
import { stackItemContentToolbarClassNames } from '../../fragments';
|
31
31
|
|
32
32
|
const createToolbar = ({
|
33
33
|
state,
|
@@ -86,7 +86,6 @@ const createToolbar = ({
|
|
86
86
|
|
87
87
|
const useEditorToolbarActionGraph = ({ onAction, ...props }: EditorToolbarProps) => {
|
88
88
|
const menuCreator = useCallback(() => createToolbar(props), [props]);
|
89
|
-
|
90
89
|
const { resolveGroupItems } = useMenuActions(menuCreator);
|
91
90
|
|
92
91
|
return { resolveGroupItems, onAction: onAction as MenuActionHandler };
|
package/src/defaults.ts
CHANGED
@@ -41,3 +41,15 @@ export const editorMonospace = EditorView.theme({
|
|
41
41
|
|
42
42
|
export const editorWithToolbarLayout =
|
43
43
|
'grid grid-cols-1 grid-rows-[min-content_1fr] data-[toolbar=disabled]:grid-rows-[1fr] justify-center content-start overflow-hidden';
|
44
|
+
|
45
|
+
export const stackItemContentEditorClassNames = (role?: string) =>
|
46
|
+
mx(
|
47
|
+
'attention-surface dx-focus-ring-inset data-[toolbar=disabled]:pbs-2',
|
48
|
+
role === 'section' ? '[&_.cm-scroller]:overflow-hidden [&_.cm-scroller]:min-bs-24' : 'min-bs-0',
|
49
|
+
);
|
50
|
+
|
51
|
+
export const stackItemContentToolbarClassNames = (role?: string) =>
|
52
|
+
mx(
|
53
|
+
'attention-surface is-full border-be !border-separator',
|
54
|
+
role === 'section' && 'sticky block-start-0 z-[1] -mbe-px min-is-0',
|
55
|
+
);
|
@@ -6,6 +6,8 @@ import { type Extension } from '@codemirror/state';
|
|
6
6
|
import { EditorView, keymap } from '@codemirror/view';
|
7
7
|
|
8
8
|
import { hintViewPlugin } from './hint';
|
9
|
+
import { floatingMenu } from './menu';
|
10
|
+
import { preview, type PreviewOptions } from './preview';
|
9
11
|
import { closeEffect, commandConfig, commandKeyBindings, commandState } from './state';
|
10
12
|
|
11
13
|
// TODO(burdon): Create knowledge base for CM notes and ideas.
|
@@ -13,23 +15,40 @@ import { closeEffect, commandConfig, commandKeyBindings, commandState } from './
|
|
13
15
|
// https://github.com/saminzadeh/codemirror-extension-inline-suggestion
|
14
16
|
// https://github.com/ChromeDevTools/devtools-frontend/blob/main/front_end/ui/components/text_editor/config.ts#L370
|
15
17
|
|
18
|
+
// TODO(burdon): Discriminated union.
|
16
19
|
export type CommandAction = {
|
17
20
|
insert?: string;
|
18
21
|
};
|
19
22
|
|
20
23
|
export type CommandOptions = {
|
21
|
-
onRender: (el: HTMLElement, cb: (action?: CommandAction) => void) => void;
|
22
24
|
onHint: () => string | undefined;
|
23
|
-
|
25
|
+
onRenderDialog: (el: HTMLElement, cb: (action?: CommandAction) => void) => void;
|
26
|
+
onRenderMenu: (el: HTMLElement, cb: () => void) => void;
|
27
|
+
} & Pick<PreviewOptions, 'onRenderPreview'>;
|
24
28
|
|
25
29
|
export const command = (options: CommandOptions): Extension => {
|
26
30
|
return [
|
27
31
|
commandConfig.of(options),
|
28
32
|
commandState,
|
29
33
|
keymap.of(commandKeyBindings),
|
34
|
+
preview(options),
|
35
|
+
floatingMenu(options),
|
30
36
|
hintViewPlugin(options),
|
31
37
|
EditorView.focusChangeEffect.of((_, focusing) => {
|
32
38
|
return focusing ? closeEffect.of(null) : null;
|
33
39
|
}),
|
40
|
+
EditorView.theme({
|
41
|
+
'.cm-tooltip': {
|
42
|
+
background: 'transparent',
|
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
|
+
},
|
52
|
+
}),
|
34
53
|
];
|
35
54
|
};
|
@@ -24,6 +24,7 @@ class CommandHint extends WidgetType {
|
|
24
24
|
} else {
|
25
25
|
wrap.setAttribute('aria-hidden', 'true');
|
26
26
|
}
|
27
|
+
|
27
28
|
return wrap;
|
28
29
|
}
|
29
30
|
|
@@ -32,12 +33,14 @@ class CommandHint extends WidgetType {
|
|
32
33
|
if (!rects.length) {
|
33
34
|
return null;
|
34
35
|
}
|
36
|
+
|
35
37
|
const style = window.getComputedStyle(dom.parentNode as HTMLElement);
|
36
38
|
const rect = flattenRect(rects[0], style.direction !== 'rtl');
|
37
39
|
const lineHeight = parseInt(style.lineHeight);
|
38
40
|
if (rect.bottom - rect.top > lineHeight * 1.5) {
|
39
41
|
return { left: rect.left, right: rect.right, top: rect.top, bottom: rect.top + lineHeight };
|
40
42
|
}
|
43
|
+
|
41
44
|
return rect;
|
42
45
|
}
|
43
46
|
|
@@ -0,0 +1,100 @@
|
|
1
|
+
//
|
2
|
+
// Copyright 2024 DXOS.org
|
3
|
+
//
|
4
|
+
|
5
|
+
import { type BlockInfo, type EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view';
|
6
|
+
|
7
|
+
import { type CommandOptions } from './command';
|
8
|
+
import { closeEffect, openCommand, openEffect } from './state';
|
9
|
+
|
10
|
+
// TODO(burdon): Trigger completion on click.
|
11
|
+
// TODO(burdon): Hide when dialog is open.
|
12
|
+
export const floatingMenu = (options: CommandOptions) =>
|
13
|
+
ViewPlugin.fromClass(
|
14
|
+
class {
|
15
|
+
button: HTMLElement;
|
16
|
+
view: EditorView;
|
17
|
+
rafId: number | null = null;
|
18
|
+
|
19
|
+
constructor(view: EditorView) {
|
20
|
+
this.view = view;
|
21
|
+
|
22
|
+
// Position context: scrollDOM
|
23
|
+
const container = view.scrollDOM;
|
24
|
+
if (getComputedStyle(container).position === 'static') {
|
25
|
+
container.style.position = 'relative';
|
26
|
+
}
|
27
|
+
|
28
|
+
// Render menu externally.
|
29
|
+
this.button = document.createElement('div');
|
30
|
+
this.button.style.position = 'absolute';
|
31
|
+
this.button.style.zIndex = '10';
|
32
|
+
this.button.style.display = 'none';
|
33
|
+
|
34
|
+
options.onRenderMenu(this.button, () => {
|
35
|
+
openCommand(view);
|
36
|
+
});
|
37
|
+
container.appendChild(this.button);
|
38
|
+
|
39
|
+
// Listen for scroll events.
|
40
|
+
container.addEventListener('scroll', this.scheduleUpdate);
|
41
|
+
this.scheduleUpdate();
|
42
|
+
}
|
43
|
+
|
44
|
+
update(update: ViewUpdate) {
|
45
|
+
// TODO(burdon): Timer to fade in/out.
|
46
|
+
if (update.transactions.some((tr) => tr.effects.some((effect) => effect.is(openEffect)))) {
|
47
|
+
this.button.style.display = 'none';
|
48
|
+
} else if (update.transactions.some((tr) => tr.effects.some((effect) => effect.is(closeEffect)))) {
|
49
|
+
this.button.style.display = 'block';
|
50
|
+
} else if (update.selectionSet || update.viewportChanged || update.docChanged || update.geometryChanged) {
|
51
|
+
this.scheduleUpdate();
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
scheduleUpdate() {
|
56
|
+
if (this.rafId != null) {
|
57
|
+
cancelAnimationFrame(this.rafId);
|
58
|
+
}
|
59
|
+
this.rafId = requestAnimationFrame(() => this.updateButtonPosition());
|
60
|
+
}
|
61
|
+
|
62
|
+
updateButtonPosition() {
|
63
|
+
const pos = this.view.state.selection.main.head;
|
64
|
+
const lineBlock: BlockInfo = this.view.lineBlockAt(pos);
|
65
|
+
const domInfo = this.view.domAtPos(lineBlock.from);
|
66
|
+
|
67
|
+
// Find nearest HTMLElement for the line block
|
68
|
+
let node: Node | null = domInfo.node;
|
69
|
+
while (node && !(node instanceof HTMLElement)) {
|
70
|
+
node = node.parentNode;
|
71
|
+
}
|
72
|
+
|
73
|
+
if (!node) {
|
74
|
+
this.button.style.display = 'none';
|
75
|
+
return;
|
76
|
+
}
|
77
|
+
|
78
|
+
const lineRect = (node as HTMLElement).getBoundingClientRect();
|
79
|
+
const containerRect = this.view.scrollDOM.getBoundingClientRect();
|
80
|
+
|
81
|
+
// Account for scroll and padding/margin in scrollDOM.
|
82
|
+
const offsetTop = lineRect.top - containerRect.top + this.view.scrollDOM.scrollTop;
|
83
|
+
const offsetLeft = this.view.scrollDOM.clientWidth + this.view.scrollDOM.scrollLeft - lineRect.x;
|
84
|
+
|
85
|
+
// TODO(burdon): Position is incorrect if cursor is in fenced code block.
|
86
|
+
// console.log('offsetTop', lineRect, containerRect);
|
87
|
+
|
88
|
+
this.button.style.top = `${offsetTop}px`;
|
89
|
+
this.button.style.left = `${offsetLeft}px`;
|
90
|
+
this.button.style.display = 'block';
|
91
|
+
}
|
92
|
+
|
93
|
+
destroy() {
|
94
|
+
this.button.remove();
|
95
|
+
if (this.rafId != null) {
|
96
|
+
cancelAnimationFrame(this.rafId);
|
97
|
+
}
|
98
|
+
}
|
99
|
+
},
|
100
|
+
);
|
@@ -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
|
+
}
|
@@ -4,10 +4,10 @@
|
|
4
4
|
|
5
5
|
import { StateEffect, StateField } from '@codemirror/state';
|
6
6
|
import {
|
7
|
+
showTooltip,
|
7
8
|
type Command,
|
8
9
|
type EditorView,
|
9
10
|
type KeyBinding,
|
10
|
-
showTooltip,
|
11
11
|
type Tooltip,
|
12
12
|
type TooltipView,
|
13
13
|
} from '@codemirror/view';
|
@@ -50,14 +50,17 @@ export const commandState = StateField.define<CommandState>({
|
|
50
50
|
}
|
51
51
|
|
52
52
|
// Render react component.
|
53
|
-
options.
|
53
|
+
options.onRenderDialog(dom, (action) => {
|
54
54
|
view.dispatch({ effects: closeEffect.of(null) });
|
55
55
|
if (action?.insert?.length) {
|
56
|
+
// Insert into editor.
|
57
|
+
const text = action.insert + '\n';
|
56
58
|
view.dispatch({
|
57
|
-
changes: { from: pos, insert:
|
58
|
-
selection: { anchor: pos +
|
59
|
+
changes: { from: pos, insert: text },
|
60
|
+
selection: { anchor: pos + text.length },
|
59
61
|
});
|
60
62
|
}
|
63
|
+
|
61
64
|
// NOTE: Truncates text if set focus immediately.
|
62
65
|
requestAnimationFrame(() => view.focus());
|
63
66
|
});
|
@@ -88,6 +91,7 @@ export const openCommand: Command = (view: EditorView) => {
|
|
88
91
|
return true;
|
89
92
|
}
|
90
93
|
}
|
94
|
+
|
91
95
|
return false;
|
92
96
|
};
|
93
97
|
|
@@ -96,6 +100,7 @@ export const closeCommand: Command = (view: EditorView) => {
|
|
96
100
|
view.dispatch({ effects: closeEffect.of(null) });
|
97
101
|
return true;
|
98
102
|
}
|
103
|
+
|
99
104
|
return false;
|
100
105
|
};
|
101
106
|
|
@@ -4,12 +4,12 @@
|
|
4
4
|
|
5
5
|
import { invertedEffects } from '@codemirror/commands';
|
6
6
|
import {
|
7
|
+
type ChangeDesc,
|
8
|
+
type EditorState,
|
7
9
|
type Extension,
|
8
10
|
StateEffect,
|
9
11
|
StateField,
|
10
12
|
type Text,
|
11
|
-
type ChangeDesc,
|
12
|
-
type EditorState,
|
13
13
|
} from '@codemirror/state';
|
14
14
|
import {
|
15
15
|
hoverTooltip,
|
@@ -24,7 +24,7 @@ import {
|
|
24
24
|
import sortBy from 'lodash.sortby';
|
25
25
|
import { useEffect, useMemo } from 'react';
|
26
26
|
|
27
|
-
import { debounce, type
|
27
|
+
import { debounce, type CleanupFn } from '@dxos/async';
|
28
28
|
import { type ReactiveObject } from '@dxos/live-object';
|
29
29
|
import { log } from '@dxos/log';
|
30
30
|
import { isNonNullable } from '@dxos/util';
|
@@ -181,6 +181,7 @@ const handleCommentClick = EditorView.domEventHandlers({
|
|
181
181
|
return false;
|
182
182
|
},
|
183
183
|
});
|
184
|
+
|
184
185
|
//
|
185
186
|
// Cut-and-paste.
|
186
187
|
//
|
@@ -575,12 +576,7 @@ const hasActiveSelection = (state: EditorState): boolean => {
|
|
575
576
|
class ExternalCommentSync implements PluginValue {
|
576
577
|
private readonly unsubscribe: () => void;
|
577
578
|
|
578
|
-
constructor(
|
579
|
-
view: EditorView,
|
580
|
-
id: string,
|
581
|
-
subscribe: (sink: () => void) => UnsubscribeCallback,
|
582
|
-
getComments: () => Comment[],
|
583
|
-
) {
|
579
|
+
constructor(view: EditorView, id: string, subscribe: (sink: () => void) => CleanupFn, getComments: () => Comment[]) {
|
584
580
|
const updateComments = () => {
|
585
581
|
const comments = getComments();
|
586
582
|
if (id === view.state.facet(documentId)) {
|
@@ -599,7 +595,7 @@ class ExternalCommentSync implements PluginValue {
|
|
599
595
|
// TODO(burdon): Needs comment.
|
600
596
|
export const createExternalCommentSync = (
|
601
597
|
id: string,
|
602
|
-
subscribe: (sink: () => void) =>
|
598
|
+
subscribe: (sink: () => void) => CleanupFn,
|
603
599
|
getComments: () => Comment[],
|
604
600
|
): Extension =>
|
605
601
|
ViewPlugin.fromClass(
|
@@ -61,7 +61,8 @@ export type BasicExtensionsOptions = {
|
|
61
61
|
lineNumbers?: boolean;
|
62
62
|
lineWrapping?: boolean;
|
63
63
|
placeholder?: string;
|
64
|
-
|
64
|
+
/** If true user cannot edit the text, but they can still select and copy it. */
|
65
|
+
readOnly?: boolean;
|
65
66
|
search?: boolean;
|
66
67
|
scrollPastEnd?: boolean;
|
67
68
|
standardKeymap?: boolean;
|
@@ -73,7 +74,6 @@ const defaultBasicOptions: BasicExtensionsOptions = {
|
|
73
74
|
bracketMatching: true,
|
74
75
|
closeBrackets: true,
|
75
76
|
drawSelection: true,
|
76
|
-
editable: true,
|
77
77
|
focus: true,
|
78
78
|
history: true,
|
79
79
|
keymap: 'standard',
|
@@ -101,13 +101,14 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
|
|
101
101
|
props.closeBrackets && closeBrackets(),
|
102
102
|
props.dropCursor && dropCursor(),
|
103
103
|
props.drawSelection && drawSelection({ cursorBlinkRate: 1_200 }),
|
104
|
+
props.editable !== undefined && EditorView.editable.of(props.editable),
|
104
105
|
props.focus && focus,
|
105
106
|
props.highlightActiveLine && highlightActiveLine(),
|
106
107
|
props.history && history(),
|
107
108
|
props.lineNumbers && lineNumbers(),
|
108
109
|
props.lineWrapping && EditorView.lineWrapping,
|
109
110
|
props.placeholder && placeholder(props.placeholder),
|
110
|
-
props.
|
111
|
+
props.readOnly !== undefined && EditorState.readOnly.of(props.readOnly),
|
111
112
|
props.scrollPastEnd && scrollPastEnd(),
|
112
113
|
props.tabSize && EditorState.tabSize.of(props.tabSize),
|
113
114
|
|
@@ -4,10 +4,12 @@
|
|
4
4
|
|
5
5
|
import { mx } from '@dxos/react-ui-theme';
|
6
6
|
|
7
|
+
// TODO(burdon): Move this to a common plugin.
|
8
|
+
|
7
9
|
export const stackItemContentEditorClassNames = (role?: string) =>
|
8
10
|
mx(
|
9
|
-
'dx-focus-ring-inset data-[toolbar=disabled]:pbs-2
|
10
|
-
role === '
|
11
|
+
'attention-surface dx-focus-ring-inset data-[toolbar=disabled]:pbs-2',
|
12
|
+
role === 'section' ? '[&_.cm-scroller]:overflow-hidden [&_.cm-scroller]:min-bs-24' : 'min-bs-0',
|
11
13
|
);
|
12
14
|
|
13
15
|
export const stackItemContentToolbarClassNames = (role?: string) =>
|
@@ -4,7 +4,7 @@
|
|
4
4
|
|
5
5
|
import { EditorState, type EditorStateConfig } from '@codemirror/state';
|
6
6
|
import { EditorView } from '@codemirror/view';
|
7
|
-
import { useFocusableGroup } from '@fluentui/react-tabster';
|
7
|
+
import { useFocusableGroup, type TabsterTypes } from '@fluentui/react-tabster';
|
8
8
|
import {
|
9
9
|
type DependencyList,
|
10
10
|
type KeyboardEventHandler,
|
@@ -19,14 +19,14 @@ import {
|
|
19
19
|
import { log } from '@dxos/log';
|
20
20
|
import { getProviderValue, isNotFalsy, type MaybeProvider } from '@dxos/util';
|
21
21
|
|
22
|
-
import {
|
22
|
+
import { type EditorSelection, documentId, createEditorStateTransaction, editorInputMode } from '../extensions';
|
23
23
|
import { debugDispatcher } from '../util';
|
24
24
|
|
25
25
|
export type UseTextEditor = {
|
26
26
|
// TODO(burdon): Rename.
|
27
27
|
parentRef: RefObject<HTMLDivElement>;
|
28
28
|
view?: EditorView;
|
29
|
-
focusAttributes
|
29
|
+
focusAttributes?: TabsterTypes.TabsterDOMAttribute & {
|
30
30
|
tabIndex: 0;
|
31
31
|
onKeyUp: KeyboardEventHandler<HTMLDivElement>;
|
32
32
|
};
|
@@ -55,7 +55,7 @@ export type UseTextEditorProps = Pick<EditorStateConfig, 'extensions'> & {
|
|
55
55
|
let instanceCount = 0;
|
56
56
|
|
57
57
|
/**
|
58
|
-
*
|
58
|
+
* Creates codemirror text editor.
|
59
59
|
*/
|
60
60
|
export const useTextEditor = (
|
61
61
|
props: MaybeProvider<UseTextEditorProps> = {},
|
@@ -155,9 +155,11 @@ export const useTextEditor = (
|
|
155
155
|
}
|
156
156
|
}, [autoFocus, view]);
|
157
157
|
|
158
|
-
const
|
158
|
+
const focusableGroupAttrs = useFocusableGroup({
|
159
159
|
tabBehavior: 'limited',
|
160
|
-
ignoreDefaultKeydown: {
|
160
|
+
ignoreDefaultKeydown: {
|
161
|
+
Escape: view?.state.facet(editorInputMode).noTabster,
|
162
|
+
},
|
161
163
|
});
|
162
164
|
|
163
165
|
// Focus editor on Enter (e.g., when tabbing to this component).
|
@@ -176,6 +178,13 @@ export const useTextEditor = (
|
|
176
178
|
[view],
|
177
179
|
);
|
178
180
|
|
179
|
-
|
180
|
-
|
181
|
+
return {
|
182
|
+
parentRef,
|
183
|
+
view,
|
184
|
+
focusAttributes: {
|
185
|
+
tabIndex: 0 as const,
|
186
|
+
...focusableGroupAttrs,
|
187
|
+
onKeyUp: handleKeyUp,
|
188
|
+
},
|
189
|
+
};
|
181
190
|
};
|
package/src/index.ts
CHANGED
@@ -14,10 +14,6 @@ export * from './components';
|
|
14
14
|
export * from './defaults';
|
15
15
|
export * from './extensions';
|
16
16
|
export * from './hooks';
|
17
|
-
export {
|
18
|
-
stackItemContentEditorClassNames,
|
19
|
-
stackItemContentToolbarClassNames,
|
20
|
-
} from './styles/stack-item-content-class-names';
|
21
17
|
export * from './types';
|
22
18
|
export * from './util';
|
23
19
|
|
package/src/styles/theme.ts
CHANGED
@@ -77,6 +77,10 @@ export const defaultTheme: ThemeStyles = {
|
|
77
77
|
background: 'transparent',
|
78
78
|
},
|
79
79
|
'.cm-gutter': {},
|
80
|
+
'.cm-gutter.cm-lineNumbers': {
|
81
|
+
paddingRight: '4px',
|
82
|
+
borderRight: '1px solid var(--dx-separator)',
|
83
|
+
},
|
80
84
|
'.cm-gutter.cm-lineNumbers .cm-gutterElement': {
|
81
85
|
minWidth: '40px',
|
82
86
|
alignContent: 'center',
|
@@ -86,7 +90,7 @@ export const defaultTheme: ThemeStyles = {
|
|
86
90
|
*/
|
87
91
|
'.cm-gutterElement': {
|
88
92
|
alignItems: 'center',
|
89
|
-
fontSize: '
|
93
|
+
fontSize: '12px',
|
90
94
|
},
|
91
95
|
|
92
96
|
/**
|
@@ -137,6 +141,7 @@ export const defaultTheme: ThemeStyles = {
|
|
137
141
|
'.cm-link': {
|
138
142
|
textDecorationLine: 'underline',
|
139
143
|
textDecorationThickness: '1px',
|
144
|
+
textDecorationColor: 'var(--dx-separator)',
|
140
145
|
textUnderlineOffset: '2px',
|
141
146
|
borderRadius: '.125rem',
|
142
147
|
},
|
package/src/util/debug.ts
CHANGED
@@ -1 +0,0 @@
|
|
1
|
-
{"version":3,"file":"stack-item-content-class-names.d.ts","sourceRoot":"","sources":["../../../../src/styles/stack-item-content-class-names.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,gCAAgC,UAAW,MAAM,WAI3D,CAAC;AAEJ,eAAO,MAAM,iCAAiC,UAAW,MAAM,WAI5D,CAAC"}
|