@dxos/react-ui-editor 0.6.13-staging.1e988a3 → 0.6.14-main.1366248
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 +772 -712
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +5667 -0
- package/dist/lib/node/index.cjs.map +7 -0
- package/dist/lib/node/meta.json +1 -0
- package/dist/lib/node-esm/index.mjs +5650 -0
- package/dist/lib/node-esm/index.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -0
- package/dist/types/src/InputMode.stories.d.ts +11 -11
- package/dist/types/src/InputMode.stories.d.ts.map +1 -1
- package/dist/types/src/TextEditor.stories.d.ts +4 -1
- package/dist/types/src/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/defaults.d.ts.map +1 -1
- package/dist/types/src/extensions/autocomplete.d.ts +2 -1
- package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness/awareness.d.ts +2 -2
- package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -1
- package/dist/types/src/extensions/command/state.d.ts +2 -2
- package/dist/types/src/extensions/command/state.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +1 -1
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/debug.d.ts +2 -2
- package/dist/types/src/extensions/debug.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts +1 -0
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/extensions/focus.d.ts +7 -0
- package/dist/types/src/extensions/focus.d.ts.map +1 -0
- package/dist/types/src/extensions/folding.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +2 -4
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/listener.d.ts +2 -1
- package/dist/types/src/extensions/listener.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.test.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/image.d.ts +3 -6
- package/dist/types/src/extensions/markdown/image.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/link.d.ts +1 -1
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/styles.d.ts.map +1 -1
- package/dist/types/src/extensions/modes.d.ts +3 -4
- package/dist/types/src/extensions/modes.d.ts.map +1 -1
- package/dist/types/src/extensions/{state.d.ts → selection.d.ts} +8 -4
- package/dist/types/src/extensions/selection.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextEditor.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/markdown.d.ts +1 -2
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/styles/theme.d.ts.map +1 -1
- package/dist/types/src/types.d.ts.map +1 -0
- package/dist/types/src/{extensions → util}/cursor.d.ts +9 -3
- package/dist/types/src/util/cursor.d.ts.map +1 -0
- package/dist/types/src/util/debug.d.ts +17 -0
- package/dist/types/src/util/debug.d.ts.map +1 -0
- package/dist/types/src/util/dom.d.ts.map +1 -0
- package/dist/types/src/util/facet.d.ts +3 -0
- package/dist/types/src/util/facet.d.ts.map +1 -0
- package/dist/types/src/util/index.d.ts +6 -0
- package/dist/types/src/util/index.d.ts.map +1 -0
- package/dist/types/src/{extensions/util → util}/react.d.ts +1 -1
- package/dist/types/src/util/react.d.ts.map +1 -0
- package/package.json +46 -41
- package/src/InputMode.stories.tsx +8 -8
- package/src/TextEditor.stories.tsx +100 -75
- package/src/components/Toolbar/Toolbar.tsx +8 -11
- package/src/defaults.ts +0 -2
- package/src/extensions/annotations.ts +1 -1
- package/src/extensions/autocomplete.ts +9 -8
- package/src/extensions/automerge/automerge.stories.tsx +2 -2
- package/src/extensions/automerge/{automerge.spec.tsx → automerge.test.tsx} +1 -0
- package/src/extensions/automerge/automerge.ts +2 -2
- package/src/extensions/automerge/cursor.ts +1 -1
- package/src/extensions/awareness/awareness.ts +3 -5
- package/src/extensions/command/hint.ts +1 -1
- package/src/extensions/command/state.ts +3 -4
- package/src/extensions/comments.ts +45 -47
- package/src/extensions/debug.ts +2 -2
- package/src/extensions/factories.ts +5 -1
- package/src/extensions/focus.ts +35 -0
- package/src/extensions/folding.tsx +7 -5
- package/src/extensions/index.ts +2 -4
- package/src/extensions/listener.ts +5 -2
- package/src/extensions/markdown/changes.test.ts +1 -3
- package/src/extensions/markdown/decorate.ts +50 -7
- package/src/extensions/markdown/formatting.test.ts +1 -3
- package/src/extensions/markdown/highlight.ts +0 -5
- package/src/extensions/markdown/image.ts +53 -42
- package/src/extensions/markdown/link.ts +3 -2
- package/src/extensions/markdown/parser.test.ts +1 -2
- package/src/extensions/markdown/styles.ts +10 -0
- package/src/extensions/markdown/table.ts +3 -3
- package/src/extensions/modes.ts +6 -7
- package/src/extensions/{state.ts → selection.ts} +20 -16
- package/src/hooks/useTextEditor.ts +36 -35
- package/src/index.ts +1 -0
- package/src/styles/markdown.ts +1 -3
- package/src/styles/theme.ts +3 -1
- package/src/{extensions → util}/cursor.ts +11 -8
- package/src/{util.ts → util/debug.ts} +25 -2
- package/src/util/facet.ts +13 -0
- package/src/{extensions/util → util}/index.ts +3 -2
- package/src/{extensions/util → util}/react.tsx +6 -1
- package/dist/types/src/extensions/automerge/automerge.spec.d.ts +0 -2
- package/dist/types/src/extensions/automerge/automerge.spec.d.ts.map +0 -1
- package/dist/types/src/extensions/cursor.d.ts.map +0 -1
- package/dist/types/src/extensions/doc.d.ts +0 -6
- package/dist/types/src/extensions/doc.d.ts.map +0 -1
- package/dist/types/src/extensions/state.d.ts.map +0 -1
- package/dist/types/src/extensions/types.d.ts.map +0 -1
- package/dist/types/src/extensions/util/dom.d.ts.map +0 -1
- package/dist/types/src/extensions/util/error.d.ts +0 -2
- package/dist/types/src/extensions/util/error.d.ts.map +0 -1
- package/dist/types/src/extensions/util/index.d.ts +0 -5
- package/dist/types/src/extensions/util/index.d.ts.map +0 -1
- package/dist/types/src/extensions/util/overlap.d.ts +0 -8
- package/dist/types/src/extensions/util/overlap.d.ts.map +0 -1
- package/dist/types/src/extensions/util/react.d.ts.map +0 -1
- package/dist/types/src/util.d.ts +0 -7
- package/dist/types/src/util.d.ts.map +0 -1
- package/src/extensions/automerge/automerge.test.ts +0 -13
- package/src/extensions/doc.ts +0 -17
- package/src/extensions/util/error.ts +0 -15
- package/src/extensions/util/overlap.ts +0 -12
- /package/dist/types/src/{extensions/types.d.ts → types.d.ts} +0 -0
- /package/dist/types/src/{extensions/util → util}/dom.d.ts +0 -0
- /package/src/{extensions/types.ts → types.ts} +0 -0
- /package/src/{extensions/util → util}/dom.ts +0 -0
@@ -5,7 +5,6 @@
|
|
5
5
|
import { invertedEffects } from '@codemirror/commands';
|
6
6
|
import {
|
7
7
|
type Extension,
|
8
|
-
Facet,
|
9
8
|
StateEffect,
|
10
9
|
StateField,
|
11
10
|
type Text,
|
@@ -29,18 +28,14 @@ import { debounce, type UnsubscribeCallback } from '@dxos/async';
|
|
29
28
|
import { log } from '@dxos/log';
|
30
29
|
import { nonNullable } from '@dxos/util';
|
31
30
|
|
32
|
-
import {
|
33
|
-
import { type Comment, type Range } from '
|
34
|
-
import { overlap } from '
|
35
|
-
import { callbackWrapper } from '../util';
|
31
|
+
import { documentId } from './selection';
|
32
|
+
import { type Comment, type Range } from '../types';
|
33
|
+
import { Cursor, overlap, singleValueFacet, callbackWrapper } from '../util';
|
36
34
|
|
37
35
|
//
|
38
36
|
// State management.
|
39
37
|
//
|
40
38
|
|
41
|
-
// TODO(wittjosiah): Factor out, not comments-specific.
|
42
|
-
const documentId = Facet.define<string | undefined, string | undefined>({ combine: (values) => values[0] });
|
43
|
-
|
44
39
|
type CommentState = {
|
45
40
|
comment: Comment;
|
46
41
|
range: Range;
|
@@ -369,7 +364,7 @@ export type CommentsOptions = {
|
|
369
364
|
onHover?: (el: Element, shortcut: string) => void;
|
370
365
|
};
|
371
366
|
|
372
|
-
const optionsFacet =
|
367
|
+
const optionsFacet = singleValueFacet<CommentsOptions>();
|
373
368
|
|
374
369
|
/**
|
375
370
|
* Comment threads.
|
@@ -389,7 +384,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
389
384
|
|
390
385
|
return [
|
391
386
|
optionsFacet.of(options),
|
392
|
-
documentId.of(options.id),
|
387
|
+
options.id ? documentId.of(options.id) : undefined,
|
393
388
|
commentsState,
|
394
389
|
commentsDecorations,
|
395
390
|
handleCommentClick,
|
@@ -398,45 +393,43 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
398
393
|
//
|
399
394
|
// Keymap.
|
400
395
|
//
|
401
|
-
options.onCreate
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
: [],
|
396
|
+
options.onCreate &&
|
397
|
+
keymap.of([
|
398
|
+
{
|
399
|
+
key: shortcut,
|
400
|
+
run: callbackWrapper(createComment),
|
401
|
+
},
|
402
|
+
]),
|
409
403
|
|
410
404
|
//
|
411
405
|
// Hover tooltip (for key shortcut hints, etc.)
|
412
406
|
// TODO(burdon): Factor out to generic hints extension for current selection/line.
|
413
407
|
//
|
414
|
-
options.onHover
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
408
|
+
options.onHover &&
|
409
|
+
hoverTooltip(
|
410
|
+
(view, pos) => {
|
411
|
+
const selection = view.state.selection.main;
|
412
|
+
if (selection && pos >= selection.from && pos <= selection.to) {
|
413
|
+
return {
|
414
|
+
pos: selection.from,
|
415
|
+
end: selection.to,
|
416
|
+
above: true,
|
417
|
+
create: () => {
|
418
|
+
const el = document.createElement('div');
|
419
|
+
options.onHover!(el, shortcut);
|
420
|
+
return { dom: el, offset: { x: 0, y: 8 } };
|
421
|
+
},
|
422
|
+
};
|
423
|
+
}
|
430
424
|
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
: [],
|
425
|
+
return null;
|
426
|
+
},
|
427
|
+
{
|
428
|
+
// TODO(burdon): Hide on change triggered immediately?
|
429
|
+
// hideOnChange: true,
|
430
|
+
hoverTime: 1_000,
|
431
|
+
},
|
432
|
+
),
|
440
433
|
|
441
434
|
//
|
442
435
|
// Track deleted ranges and update ranges for decorations.
|
@@ -511,8 +504,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
511
504
|
}
|
512
505
|
}),
|
513
506
|
|
514
|
-
options.onUpdate
|
515
|
-
];
|
507
|
+
options.onUpdate && trackPastedComments(options.onUpdate),
|
508
|
+
].filter(nonNullable);
|
516
509
|
};
|
517
510
|
|
518
511
|
//
|
@@ -553,9 +546,13 @@ export const scrollThreadIntoView = (view: EditorView, id: string, center = true
|
|
553
546
|
* Query the editor state for the active formatting at the selection.
|
554
547
|
*/
|
555
548
|
export const selectionOverlapsComment = (state: EditorState): boolean => {
|
556
|
-
|
557
|
-
const commentState = state.field(commentsState);
|
549
|
+
// May not be defined if thread plugin not installed.
|
550
|
+
const commentState = state.field(commentsState, false);
|
551
|
+
if (commentState === undefined) {
|
552
|
+
return false;
|
553
|
+
}
|
558
554
|
|
555
|
+
const { selection } = state;
|
559
556
|
for (const range of selection.ranges) {
|
560
557
|
if (commentState.comments.some(({ range: commentRange }) => overlap(commentRange, range))) {
|
561
558
|
return true;
|
@@ -597,6 +594,7 @@ class ExternalCommentSync implements PluginValue {
|
|
597
594
|
};
|
598
595
|
}
|
599
596
|
|
597
|
+
// TODO(burdon): Needs comment.
|
600
598
|
export const createExternalCommentSync = (
|
601
599
|
id: string,
|
602
600
|
subscribe: (sink: () => void) => UnsubscribeCallback,
|
package/src/extensions/debug.ts
CHANGED
@@ -3,10 +3,10 @@
|
|
3
3
|
//
|
4
4
|
|
5
5
|
import { syntaxTree } from '@codemirror/language';
|
6
|
-
import { type EditorState, type RangeSet, StateField, type Transaction } from '@codemirror/state';
|
6
|
+
import { type EditorState, type Extension, type RangeSet, StateField, type Transaction } from '@codemirror/state';
|
7
7
|
|
8
8
|
// eslint-disable-next-line no-console
|
9
|
-
export const debugNodeLogger = (log: (...args: any[]) => void = console.log) => {
|
9
|
+
export const debugNodeLogger = (log: (...args: any[]) => void = console.log): Extension => {
|
10
10
|
const logTokens = (state: EditorState) => syntaxTree(state).iterate({ enter: (node) => log(node.type) });
|
11
11
|
return StateField.define<any>({
|
12
12
|
create: (state) => logTokens(state),
|
@@ -31,7 +31,8 @@ import { type HuePalette, hueTokens } from '@dxos/react-ui-theme';
|
|
31
31
|
import { hexToHue, isNotFalsy } from '@dxos/util';
|
32
32
|
|
33
33
|
import { automerge } from './automerge';
|
34
|
-
import {
|
34
|
+
import { SpaceAwarenessProvider, awareness } from './awareness';
|
35
|
+
import { focus } from './focus';
|
35
36
|
import { type ThemeStyles, defaultTheme } from '../styles';
|
36
37
|
|
37
38
|
//
|
@@ -52,6 +53,7 @@ export type BasicExtensionsOptions = {
|
|
52
53
|
dropCursor?: boolean;
|
53
54
|
drawSelection?: boolean;
|
54
55
|
editable?: boolean;
|
56
|
+
focus?: boolean;
|
55
57
|
highlightActiveLine?: boolean;
|
56
58
|
history?: boolean;
|
57
59
|
indentWithTab?: boolean;
|
@@ -72,6 +74,7 @@ const defaultBasicOptions: BasicExtensionsOptions = {
|
|
72
74
|
closeBrackets: true,
|
73
75
|
drawSelection: true,
|
74
76
|
editable: true,
|
77
|
+
focus: true,
|
75
78
|
history: true,
|
76
79
|
keymap: 'standard',
|
77
80
|
lineWrapping: true,
|
@@ -98,6 +101,7 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
|
|
98
101
|
props.closeBrackets && closeBrackets(),
|
99
102
|
props.dropCursor && dropCursor(),
|
100
103
|
props.drawSelection && drawSelection({ cursorBlinkRate: 1_200 }),
|
104
|
+
props.focus && focus,
|
101
105
|
props.highlightActiveLine && highlightActiveLine(),
|
102
106
|
props.history && history(),
|
103
107
|
props.lineNumbers && lineNumbers(),
|
@@ -0,0 +1,35 @@
|
|
1
|
+
//
|
2
|
+
// Copyright 2024 DXOS.org
|
3
|
+
//
|
4
|
+
|
5
|
+
import { StateEffect, StateField } from '@codemirror/state';
|
6
|
+
import { EditorView } from '@codemirror/view';
|
7
|
+
|
8
|
+
const focusEffect = StateEffect.define<boolean>();
|
9
|
+
|
10
|
+
export const focusField = StateField.define<boolean>({
|
11
|
+
create: () => false,
|
12
|
+
update: (value, tr) => {
|
13
|
+
for (const effect of tr.effects) {
|
14
|
+
if (effect.is(focusEffect)) {
|
15
|
+
return effect.value;
|
16
|
+
}
|
17
|
+
}
|
18
|
+
return value;
|
19
|
+
},
|
20
|
+
});
|
21
|
+
|
22
|
+
/**
|
23
|
+
* Manage focus.
|
24
|
+
*/
|
25
|
+
export const focus = [
|
26
|
+
focusField,
|
27
|
+
EditorView.domEventHandlers({
|
28
|
+
focus: (event, view) => {
|
29
|
+
setTimeout(() => view.dispatch({ effects: focusEffect.of(true) }));
|
30
|
+
},
|
31
|
+
blur: (event, view) => {
|
32
|
+
setTimeout(() => view.dispatch({ effects: focusEffect.of(false) }));
|
33
|
+
},
|
34
|
+
}),
|
35
|
+
];
|
@@ -8,16 +8,15 @@ import { EditorView } from '@codemirror/view';
|
|
8
8
|
import React from 'react';
|
9
9
|
|
10
10
|
import { Icon } from '@dxos/react-ui';
|
11
|
-
import { getSize } from '@dxos/react-ui-theme';
|
12
11
|
|
13
|
-
import { createElement, renderRoot } from '
|
12
|
+
import { createElement, renderRoot } from '../util';
|
14
13
|
|
15
14
|
export type FoldingOptions = {};
|
16
15
|
|
17
16
|
/**
|
18
17
|
* https://codemirror.net/examples/gutter
|
19
18
|
*/
|
20
|
-
// TODO(burdon): Remember folding state.
|
19
|
+
// TODO(burdon): Remember folding state (to state).
|
21
20
|
export const folding = (_props: FoldingOptions = {}): Extension => [
|
22
21
|
codeFolding({
|
23
22
|
placeholderDOM: () => {
|
@@ -26,9 +25,11 @@ export const folding = (_props: FoldingOptions = {}): Extension => [
|
|
26
25
|
}),
|
27
26
|
foldGutter({
|
28
27
|
markerDOM: (open) => {
|
28
|
+
// TODO(burdon): Use sprite directly.
|
29
|
+
const el = createElement('div', { className: 'flex h-full items-center' });
|
29
30
|
return renderRoot(
|
30
|
-
|
31
|
-
<Icon icon='ph--caret-right--regular' classNames={[
|
31
|
+
el,
|
32
|
+
<Icon icon='ph--caret-right--regular' size={3} classNames={['mx-3 cursor-pointer', open && 'rotate-90']} />,
|
32
33
|
);
|
33
34
|
},
|
34
35
|
}),
|
@@ -36,6 +37,7 @@ export const folding = (_props: FoldingOptions = {}): Extension => [
|
|
36
37
|
'.cm-foldGutter': {
|
37
38
|
opacity: 0.3,
|
38
39
|
transition: 'opacity 0.3s',
|
40
|
+
width: '32px',
|
39
41
|
},
|
40
42
|
'.cm-foldGutter:hover': {
|
41
43
|
opacity: 1,
|
package/src/extensions/index.ts
CHANGED
@@ -9,16 +9,14 @@ export * from './awareness';
|
|
9
9
|
export * from './blast';
|
10
10
|
export * from './command';
|
11
11
|
export * from './comments';
|
12
|
-
export * from './cursor';
|
13
12
|
export * from './debug';
|
14
|
-
export * from './doc';
|
15
13
|
export * from './dnd';
|
16
14
|
export * from './factories';
|
15
|
+
export * from './focus';
|
17
16
|
export * from './folding';
|
18
17
|
export * from './listener';
|
19
18
|
export * from './markdown';
|
20
19
|
export * from './mention';
|
21
20
|
export * from './modes';
|
22
|
-
export * from './
|
23
|
-
export * from './types';
|
21
|
+
export * from './selection';
|
24
22
|
export * from './typewriter';
|
@@ -5,13 +5,16 @@
|
|
5
5
|
import { type Extension } from '@codemirror/state';
|
6
6
|
import { EditorView } from '@codemirror/view';
|
7
7
|
|
8
|
+
import { documentId } from './selection';
|
9
|
+
|
8
10
|
export type ListenerOptions = {
|
9
11
|
onFocus?: (focusing: boolean) => void;
|
10
|
-
onChange?: (text: string) => void;
|
12
|
+
onChange?: (text: string, id: string) => void;
|
11
13
|
};
|
12
14
|
|
13
15
|
/**
|
14
16
|
* Event listener.
|
17
|
+
* @deprecated Use EditorView.updateListener and listen for specific update events.
|
15
18
|
*/
|
16
19
|
export const listener = ({ onFocus, onChange }: ListenerOptions): Extension => {
|
17
20
|
const extensions: Extension[] = [];
|
@@ -27,7 +30,7 @@ export const listener = ({ onFocus, onChange }: ListenerOptions): Extension => {
|
|
27
30
|
onChange &&
|
28
31
|
extensions.push(
|
29
32
|
EditorView.updateListener.of((update) => {
|
30
|
-
onChange(update.state.doc.toString());
|
33
|
+
onChange(update.state.doc.toString(), update.state.facet(documentId));
|
31
34
|
}),
|
32
35
|
);
|
33
36
|
|
@@ -15,7 +15,7 @@ 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 { wrapWithCatch } from '
|
18
|
+
import { wrapWithCatch } from '../../util';
|
19
19
|
|
20
20
|
/**
|
21
21
|
* Unicode characters.
|
@@ -45,7 +45,7 @@ class HorizontalRuleWidget extends WidgetType {
|
|
45
45
|
class LinkButton extends WidgetType {
|
46
46
|
constructor(
|
47
47
|
private readonly url: string,
|
48
|
-
private readonly render: (el:
|
48
|
+
private readonly render: (el: HTMLElement, url: string) => void,
|
49
49
|
) {
|
50
50
|
super();
|
51
51
|
}
|
@@ -54,6 +54,7 @@ class LinkButton extends WidgetType {
|
|
54
54
|
return this.url === other.url;
|
55
55
|
}
|
56
56
|
|
57
|
+
// TODO(burdon): Create icon and link directly without react?
|
57
58
|
override toDOM(view: EditorView) {
|
58
59
|
const el = document.createElement('span');
|
59
60
|
this.render(el, this.url);
|
@@ -127,6 +128,7 @@ class TextWidget extends WidgetType {
|
|
127
128
|
}
|
128
129
|
|
129
130
|
const hide = Decoration.replace({});
|
131
|
+
const blockQuote = Decoration.line({ class: mx('cm-blockquote') });
|
130
132
|
const fencedCodeLine = Decoration.line({ class: mx('cm-code cm-codeblock-line') });
|
131
133
|
const fencedCodeLineFirst = Decoration.line({ class: mx('cm-code cm-codeblock-line', 'cm-codeblock-first') });
|
132
134
|
const fencedCodeLineLast = Decoration.line({ class: mx('cm-code cm-codeblock-line', 'cm-codeblock-last') });
|
@@ -199,7 +201,7 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
|
|
199
201
|
return listLevels[listLevels.length - 1];
|
200
202
|
};
|
201
203
|
|
202
|
-
//
|
204
|
+
// let count = 0;
|
203
205
|
const enterNode = (node: SyntaxNodeRef) => {
|
204
206
|
// console.log(`[${count++}]`, { node: node.name, from: node.from, to: node.to });
|
205
207
|
switch (node.name) {
|
@@ -329,7 +331,36 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
|
|
329
331
|
break;
|
330
332
|
}
|
331
333
|
|
334
|
+
//
|
335
|
+
// Blockquote > QuoteMark > Paragraph
|
336
|
+
//
|
337
|
+
|
338
|
+
case 'Blockquote': {
|
339
|
+
const editing = editingRange(state, node, focus);
|
340
|
+
const quoteMark = node.node.getChild('QuoteMark');
|
341
|
+
const paragraph = node.node.getChild('Paragraph');
|
342
|
+
if (!editing && quoteMark && paragraph) {
|
343
|
+
atomicDeco.add(quoteMark.from, paragraph.from, hide);
|
344
|
+
}
|
345
|
+
|
346
|
+
for (const block of view.viewportLineBlocks) {
|
347
|
+
if (block.to < node.from) {
|
348
|
+
continue;
|
349
|
+
}
|
350
|
+
if (block.from > node.to) {
|
351
|
+
break;
|
352
|
+
}
|
353
|
+
|
354
|
+
deco.add(block.from, block.from, blockQuote);
|
355
|
+
}
|
356
|
+
|
357
|
+
break;
|
358
|
+
}
|
359
|
+
|
360
|
+
//
|
332
361
|
// CommentBlock
|
362
|
+
//
|
363
|
+
|
333
364
|
case 'CommentBlock': {
|
334
365
|
const editing = editingRange(state, node, focus);
|
335
366
|
for (const block of view.viewportLineBlocks) {
|
@@ -339,21 +370,27 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
|
|
339
370
|
if (block.from > node.to) {
|
340
371
|
break;
|
341
372
|
}
|
342
|
-
|
343
|
-
const
|
373
|
+
|
374
|
+
const isFirst = block.from <= node.from;
|
375
|
+
const isLast = block.to >= node.to && /^(\s>)*-->$/.test(state.doc.sliceString(block.from, block.to));
|
376
|
+
|
344
377
|
deco.add(
|
345
378
|
block.from,
|
346
379
|
block.from,
|
347
|
-
|
380
|
+
isFirst ? commentBlockLineFirst : isLast ? commentBlockLineLast : commentBlockLine,
|
348
381
|
);
|
349
|
-
|
382
|
+
|
383
|
+
if (!editing && (isFirst || isLast)) {
|
350
384
|
atomicDeco.add(block.from, block.to, hide);
|
351
385
|
}
|
352
386
|
}
|
353
387
|
break;
|
354
388
|
}
|
355
389
|
|
390
|
+
//
|
356
391
|
// FencedCode > CodeMark > [CodeInfo] > CodeText > CodeMark
|
392
|
+
//
|
393
|
+
|
357
394
|
case 'FencedCode': {
|
358
395
|
for (const block of view.viewportLineBlocks) {
|
359
396
|
if (block.to < node.from) {
|
@@ -375,7 +412,10 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
|
|
375
412
|
return false;
|
376
413
|
}
|
377
414
|
|
415
|
+
//
|
378
416
|
// Link > [LinkMark, URL]
|
417
|
+
//
|
418
|
+
|
379
419
|
case 'Link': {
|
380
420
|
const marks = node.node.getChildren('LinkMark');
|
381
421
|
const urlNode = node.node.getChild('URL');
|
@@ -412,7 +452,10 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
|
|
412
452
|
break;
|
413
453
|
}
|
414
454
|
|
455
|
+
//
|
415
456
|
// HR
|
457
|
+
//
|
458
|
+
|
416
459
|
case 'HorizontalRule': {
|
417
460
|
if (!editingRange(state, node, focus)) {
|
418
461
|
deco.add(node.from, node.to, horizontalRule);
|
@@ -4,9 +4,7 @@
|
|
4
4
|
|
5
5
|
import { markdownLanguage } from '@codemirror/lang-markdown';
|
6
6
|
import { EditorState, type StateCommand } from '@codemirror/state';
|
7
|
-
import { expect } from '
|
8
|
-
|
9
|
-
import { describe, test } from '@dxos/test';
|
7
|
+
import { describe, expect, test } from 'vitest';
|
10
8
|
|
11
9
|
import {
|
12
10
|
addBlockquote,
|
@@ -168,11 +168,6 @@ export const markdownHighlightStyle = (_options: HighlightOptions = {}) => {
|
|
168
168
|
class: theme.code,
|
169
169
|
},
|
170
170
|
|
171
|
-
{
|
172
|
-
tag: [markdownTags.QuoteMark],
|
173
|
-
class: theme.blockquote,
|
174
|
-
},
|
175
|
-
|
176
171
|
{
|
177
172
|
tag: [markdownTags.TableCell],
|
178
173
|
class: 'font-mono',
|
@@ -3,43 +3,52 @@
|
|
3
3
|
//
|
4
4
|
|
5
5
|
import { syntaxTree } from '@codemirror/language';
|
6
|
-
import { type EditorState, type Extension, StateField, type Transaction
|
6
|
+
import { type EditorState, type Extension, type Range, StateField, type Transaction } from '@codemirror/state';
|
7
7
|
import { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemirror/view';
|
8
8
|
|
9
|
+
import { focusField } from '../focus';
|
10
|
+
|
9
11
|
export type ImageOptions = {};
|
10
12
|
|
13
|
+
/**
|
14
|
+
* Create image decorations.
|
15
|
+
*/
|
11
16
|
export const image = (_options: ImageOptions = {}): Extension => {
|
12
|
-
return
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
return [
|
18
|
+
StateField.define<DecorationSet>({
|
19
|
+
create: (state) => {
|
20
|
+
// Process all images.
|
21
|
+
return Decoration.set(buildDecorations(0, state.doc.length, state));
|
22
|
+
},
|
23
|
+
update: (value: DecorationSet, tr: Transaction) => {
|
24
|
+
if (!tr.docChanged && !tr.selection) {
|
25
|
+
return value;
|
26
|
+
}
|
20
27
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
28
|
+
// Find range of changes and cursor changes.
|
29
|
+
const cursor = tr.state.selection.main.head;
|
30
|
+
const oldCursor = tr.changes.mapPos(tr.startState.selection.main.head);
|
31
|
+
let from = Math.min(cursor, oldCursor);
|
32
|
+
let to = Math.max(cursor, oldCursor);
|
33
|
+
tr.changes.iterChangedRanges((fromA, toA, fromB, toB) => {
|
34
|
+
from = Math.min(from, fromB);
|
35
|
+
to = Math.max(to, toB);
|
36
|
+
});
|
37
|
+
|
38
|
+
// Expand to cover lines.
|
39
|
+
from = tr.state.doc.lineAt(from).from;
|
40
|
+
to = tr.state.doc.lineAt(to).to;
|
41
|
+
|
42
|
+
return value.map(tr.changes).update({
|
43
|
+
filterFrom: from,
|
44
|
+
filterTo: to,
|
45
|
+
filter: () => false,
|
46
|
+
add: buildDecorations(from, to, tr.state),
|
47
|
+
});
|
48
|
+
},
|
49
|
+
provide: (field) => EditorView.decorations.from(field),
|
50
|
+
}),
|
51
|
+
];
|
43
52
|
};
|
44
53
|
|
45
54
|
const preloaded = new Set<string>();
|
@@ -55,15 +64,19 @@ const preloadImage = (url: string) => {
|
|
55
64
|
const buildDecorations = (from: number, to: number, state: EditorState) => {
|
56
65
|
const decorations: Range<Decoration>[] = [];
|
57
66
|
const cursor = state.selection.main.head;
|
58
|
-
|
59
67
|
syntaxTree(state).iterate({
|
60
68
|
enter: (node) => {
|
61
69
|
if (node.name === 'Image') {
|
62
70
|
const urlNode = node.node.getChild('URL');
|
63
71
|
if (urlNode) {
|
64
|
-
const hide = state.readOnly || cursor < node.from || cursor > node.to;
|
72
|
+
const hide = state.readOnly || cursor < node.from || cursor > node.to || !state.field(focusField);
|
73
|
+
|
65
74
|
const url = state.sliceDoc(urlNode.from, urlNode.to);
|
66
|
-
//
|
75
|
+
// Some plugins might be using custom URLs; avoid attempts to render those URLs.
|
76
|
+
if (url.match(/^https?:\/\//) === null && url.match(/^file?:\/\//) === null) {
|
77
|
+
return;
|
78
|
+
}
|
79
|
+
|
67
80
|
preloadImage(url);
|
68
81
|
decorations.push(
|
69
82
|
Decoration.replace({
|
@@ -94,14 +107,12 @@ class ImageWidget extends WidgetType {
|
|
94
107
|
const img = document.createElement('img');
|
95
108
|
img.setAttribute('src', this._url);
|
96
109
|
img.setAttribute('class', 'cm-image');
|
97
|
-
//
|
98
|
-
|
110
|
+
// If focused, hide image until successfully loaded to avoid flickering effects.
|
111
|
+
if (view.state.field(focusField)) {
|
112
|
+
img.onload = () => img.classList.add('cm-loaded-image');
|
113
|
+
} else {
|
114
|
+
img.classList.add('cm-loaded-image');
|
115
|
+
}
|
99
116
|
return img;
|
100
117
|
}
|
101
118
|
}
|
102
|
-
|
103
|
-
export type ImageUploadOptions = {
|
104
|
-
onSelect: () => { url: string };
|
105
|
-
};
|
106
|
-
|
107
|
-
export const imageUpload = (options: ImageOptions = {}) => {};
|
@@ -9,8 +9,8 @@ import { type SyntaxNode } from '@lezer/common';
|
|
9
9
|
|
10
10
|
import { tooltipContent } from '@dxos/react-ui-theme';
|
11
11
|
|
12
|
-
export const linkTooltip = (render: (el:
|
13
|
-
hoverTooltip((view, pos, side) => {
|
12
|
+
export const linkTooltip = (render: (el: HTMLElement, url: string) => void) => {
|
13
|
+
return hoverTooltip((view, pos, side) => {
|
14
14
|
const syntax = syntaxTree(view.state).resolveInner(pos, side);
|
15
15
|
let link = null;
|
16
16
|
for (let i = 0, node: SyntaxNode | null = syntax; !link && node && i < 5; node = node.parent, i++) {
|
@@ -35,3 +35,4 @@ export const linkTooltip = (render: (el: Element, url: string) => void) =>
|
|
35
35
|
},
|
36
36
|
};
|
37
37
|
});
|
38
|
+
};
|
@@ -5,8 +5,7 @@
|
|
5
5
|
// @ts-ignore
|
6
6
|
import { testTree } from '@lezer/generator/test';
|
7
7
|
import { parser } from '@lezer/markdown';
|
8
|
-
|
9
|
-
import { describe, test } from '@dxos/test';
|
8
|
+
import { describe, test } from 'vitest';
|
10
9
|
|
11
10
|
describe('parser', () => {
|
12
11
|
// test.only('list-mark', () => {
|
@@ -39,6 +39,16 @@ export const formattingStyles = EditorView.theme({
|
|
39
39
|
width: `${orderedListIndentationWidth}px`,
|
40
40
|
},
|
41
41
|
|
42
|
+
/**
|
43
|
+
* Blockquote.
|
44
|
+
*/
|
45
|
+
'& .cm-blockquote': {
|
46
|
+
background: 'var(--dx-cmCodeblock)',
|
47
|
+
borderLeft: '2px solid var(--dx-cmSeparator)',
|
48
|
+
paddingLeft: '1rem',
|
49
|
+
margin: '0',
|
50
|
+
},
|
51
|
+
|
42
52
|
/**
|
43
53
|
* Code and codeblocks.
|
44
54
|
*/
|