@dxos/react-ui-editor 0.3.10-main.cb1d9e2 → 0.3.10-main.cc91b2d
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 +1185 -374
- 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 +23 -2
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +2 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +3 -0
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/basic.d.ts +2 -1
- package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/blast.d.ts +25 -0
- package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/code.d.ts +2 -0
- package/dist/types/src/components/TextEditor/extensions/code.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/comments.d.ts +2 -2
- package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/demo.d.ts +2 -1
- package/dist/types/src/components/TextEditor/extensions/demo.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/experimental.d.ts +1 -1
- package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/highlight.d.ts +24 -0
- package/dist/types/src/components/TextEditor/extensions/highlight.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/image.d.ts +4 -0
- package/dist/types/src/components/TextEditor/extensions/image.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/index.d.ts +6 -0
- package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts +10 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +9 -3
- package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/mention.d.ts +6 -0
- package/dist/types/src/components/TextEditor/extensions/mention.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/table.d.ts +8 -0
- package/dist/types/src/components/TextEditor/extensions/table.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts +2 -1
- package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/themes/default.d.ts +5 -0
- package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -1
- package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts +3 -0
- package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts.map +1 -1
- package/dist/types/src/hooks/automerge/automerge.stories.d.ts +8 -1
- package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/automerge/index.d.ts +1 -1
- package/dist/types/src/hooks/automerge/index.d.ts.map +1 -1
- package/dist/types/src/hooks/automerge/plugin.d.ts.map +1 -1
- package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts +5 -0
- package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts +6 -0
- package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/translation/index.d.ts +3 -0
- package/dist/types/src/hooks/automerge/translation/index.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextModel.d.ts +6 -0
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/{components/TextEditor → hooks/yjs}/yjs.stories.d.ts +2 -2
- package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +1 -0
- package/dist/types/src/styles/markdown.d.ts +3 -2
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/testing/replicator.d.ts.map +1 -1
- package/package.json +22 -20
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +124 -32
- package/src/components/TextEditor/TextEditor.stories.tsx +12 -1
- package/src/components/TextEditor/TextEditor.tsx +31 -24
- package/src/components/TextEditor/extensions/autocomplete.ts +5 -20
- package/src/components/TextEditor/extensions/basic.ts +6 -4
- package/src/components/TextEditor/extensions/blast.ts +373 -0
- package/src/components/TextEditor/extensions/code.ts +99 -0
- package/src/components/TextEditor/extensions/comments.ts +29 -46
- package/src/components/TextEditor/extensions/demo.ts +4 -1
- package/src/components/TextEditor/extensions/experimental.tsx +15 -36
- package/src/components/TextEditor/extensions/highlight.ts +128 -0
- package/src/components/TextEditor/extensions/image.ts +67 -0
- package/src/components/TextEditor/extensions/index.ts +6 -0
- package/src/components/TextEditor/extensions/link.ts +17 -25
- package/src/components/TextEditor/extensions/listener.ts +3 -3
- package/src/components/TextEditor/extensions/markdown/bundle.ts +45 -34
- package/src/components/TextEditor/extensions/markdown/highlight.ts +148 -108
- package/src/components/TextEditor/extensions/mention.ts +31 -0
- package/src/components/TextEditor/extensions/table.ts +119 -0
- package/src/components/TextEditor/extensions/tasklist.ts +42 -28
- package/src/components/TextEditor/themes/default.ts +97 -4
- package/src/hooks/automerge/PatchSemaphore.ts +12 -9
- package/src/hooks/automerge/automerge.stories.tsx +4 -2
- package/src/hooks/automerge/index.ts +1 -1
- package/src/hooks/automerge/plugin.ts +1 -0
- package/src/hooks/automerge/{amToCodemirror.ts → translation/automerge-to-codemirror.ts} +10 -2
- package/src/hooks/automerge/{codeMirrorToAm.ts → translation/codemirror-to-automerge.ts} +7 -4
- package/src/hooks/automerge/translation/index.ts +6 -0
- package/src/hooks/useTextModel.ts +9 -0
- package/src/{components/TextEditor → hooks/yjs}/yjs.stories.tsx +2 -2
- package/src/styles/markdown.ts +15 -8
- package/src/testing/replicator.ts +1 -0
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/amToCodemirror.d.ts +0 -6
- package/dist/types/src/hooks/automerge/amToCodemirror.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/codeMirrorToAm.d.ts +0 -7
- package/dist/types/src/hooks/automerge/codeMirrorToAm.d.ts.map +0 -1
|
@@ -18,25 +18,21 @@ import {
|
|
|
18
18
|
import { debounce } from '@dxos/async';
|
|
19
19
|
import { invariant } from '@dxos/invariant';
|
|
20
20
|
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// TODO(burdon): Import note (performance):
|
|
35
|
-
// Easily track & remove content with decorations
|
|
36
|
-
// https://discuss.codemirror.net/t/easily-track-remove-content-with-decorations/4606
|
|
21
|
+
// TODO(burdon): Reconcile with theme.
|
|
22
|
+
const styles = EditorView.baseTheme({
|
|
23
|
+
'& .cm-bookmark': {
|
|
24
|
+
cursor: 'pointer',
|
|
25
|
+
margin: '4px',
|
|
26
|
+
padding: '4px',
|
|
27
|
+
backgroundColor: 'yellow',
|
|
28
|
+
},
|
|
29
|
+
'& .cm-bookmark-selected': {
|
|
30
|
+
backgroundColor: 'orange',
|
|
31
|
+
},
|
|
32
|
+
});
|
|
37
33
|
|
|
38
34
|
class BookmarkWidget extends WidgetType {
|
|
39
|
-
constructor(private readonly _pos: number, private readonly _id: string) {
|
|
35
|
+
constructor(private readonly _pos: number, private readonly _id: string, private readonly _handleClick: () => void) {
|
|
40
36
|
super();
|
|
41
37
|
invariant(this._id);
|
|
42
38
|
}
|
|
@@ -48,34 +44,20 @@ class BookmarkWidget extends WidgetType {
|
|
|
48
44
|
override toDOM() {
|
|
49
45
|
const span = document.createElement('span');
|
|
50
46
|
span.className = 'cm-bookmark';
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
span.textContent = '💬';
|
|
47
|
+
span.textContent = '§';
|
|
48
|
+
span.onclick = () => this._handleClick();
|
|
54
49
|
return span;
|
|
55
50
|
}
|
|
56
51
|
}
|
|
57
52
|
|
|
58
|
-
// TODO(burdon): Reconcile with theme.
|
|
59
|
-
const styles = EditorView.baseTheme({
|
|
60
|
-
'& .cm-bookmark': {
|
|
61
|
-
cursor: 'pointer',
|
|
62
|
-
margin: '4px',
|
|
63
|
-
padding: '4px',
|
|
64
|
-
backgroundColor: 'yellow',
|
|
65
|
-
},
|
|
66
|
-
'& .cm-bookmark-selected': {
|
|
67
|
-
backgroundColor: 'orange',
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
|
|
71
53
|
type CommentsInfo = {
|
|
72
54
|
active?: string;
|
|
73
|
-
items
|
|
55
|
+
items?: { id: string; pos: number; location: Rect | null }[];
|
|
74
56
|
};
|
|
75
57
|
|
|
76
58
|
export type CommentsOptions = {
|
|
77
59
|
key?: string;
|
|
78
|
-
onCreate?: () => string | void;
|
|
60
|
+
onCreate?: (from: number, to: number) => string | void;
|
|
79
61
|
onUpdate?: (info: CommentsInfo) => void;
|
|
80
62
|
};
|
|
81
63
|
|
|
@@ -91,7 +73,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
91
73
|
const id = match[1];
|
|
92
74
|
return Decoration.replace({
|
|
93
75
|
id,
|
|
94
|
-
widget: new BookmarkWidget(pos, id),
|
|
76
|
+
widget: new BookmarkWidget(pos, id, () => handleUpdate({ active: id })),
|
|
95
77
|
});
|
|
96
78
|
},
|
|
97
79
|
});
|
|
@@ -116,7 +98,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
116
98
|
},
|
|
117
99
|
);
|
|
118
100
|
|
|
119
|
-
const
|
|
101
|
+
const handleUpdate = debounce((data: CommentsInfo) => {
|
|
120
102
|
options.onUpdate?.(data);
|
|
121
103
|
}, 200);
|
|
122
104
|
|
|
@@ -126,13 +108,14 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
126
108
|
key: options?.key ?? 'shift-meta-c',
|
|
127
109
|
run: (view) => {
|
|
128
110
|
// Insert footnote.
|
|
129
|
-
const
|
|
111
|
+
const { from, to, head } = view.state.selection.main;
|
|
112
|
+
const id = options.onCreate?.(from, to);
|
|
130
113
|
if (id) {
|
|
131
|
-
const pos = view.state.selection.main.head;
|
|
132
114
|
const tag = `[^${id}]`;
|
|
115
|
+
// TODO(burdon): Add selection.
|
|
133
116
|
view.dispatch({
|
|
134
|
-
changes: { from:
|
|
135
|
-
selection: { anchor:
|
|
117
|
+
changes: { from: head, insert: tag },
|
|
118
|
+
selection: { anchor: head + tag.length },
|
|
136
119
|
});
|
|
137
120
|
|
|
138
121
|
return true;
|
|
@@ -152,14 +135,14 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
152
135
|
return;
|
|
153
136
|
}
|
|
154
137
|
|
|
155
|
-
const view = update
|
|
156
|
-
const pos =
|
|
138
|
+
const { view, state } = update;
|
|
139
|
+
const pos = state.selection.main.head;
|
|
157
140
|
|
|
158
141
|
const decorations: { from: number; to: number; value: Decoration }[] = [];
|
|
159
142
|
const rangeSet = view.plugin(bookmarks)?.bookmarks;
|
|
160
143
|
let closest = Infinity;
|
|
161
|
-
// TODO(burdon):
|
|
162
|
-
const { from, to } = view.visibleRanges?.[0] ?? { from: 0, to:
|
|
144
|
+
// TODO(burdon): Handle multiple visible ranges in large documents.
|
|
145
|
+
const { from, to } = /* view.visibleRanges?.[0] ?? */ { from: 0, to: state.doc.length };
|
|
163
146
|
rangeSet?.between(from, to, (from, to, value) => {
|
|
164
147
|
decorations.push({ from, to, value });
|
|
165
148
|
const d = Math.min(Math.abs(pos - from), Math.abs(pos - to));
|
|
@@ -171,7 +154,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
171
154
|
});
|
|
172
155
|
|
|
173
156
|
if (decorations.length) {
|
|
174
|
-
|
|
157
|
+
handleUpdate({
|
|
175
158
|
active,
|
|
176
159
|
items: decorations.map(
|
|
177
160
|
({
|
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
5
6
|
import { keymap } from '@codemirror/view';
|
|
6
7
|
|
|
8
|
+
// TODO(burdon): Review https://github.com/sergeche/codemirror-movie?tab=readme-ov-file
|
|
9
|
+
|
|
7
10
|
export type DemoOptions = {
|
|
8
11
|
delay?: number;
|
|
9
12
|
items?: string[];
|
|
@@ -15,7 +18,7 @@ const defaultItems = ['hello world!', 'this is a test.', 'this is [DXOS](https:/
|
|
|
15
18
|
* Demo script.
|
|
16
19
|
* Configurable plugin that let's user cycle through pre-configured input script.
|
|
17
20
|
*/
|
|
18
|
-
export const demo = ({ delay = 75, items = defaultItems }: DemoOptions = {}) => {
|
|
21
|
+
export const demo = ({ delay = 75, items = defaultItems }: DemoOptions = {}): Extension => {
|
|
19
22
|
let t: any;
|
|
20
23
|
let idx = 0;
|
|
21
24
|
|
|
@@ -13,28 +13,21 @@ import {
|
|
|
13
13
|
type ViewUpdate,
|
|
14
14
|
} from '@codemirror/view';
|
|
15
15
|
|
|
16
|
-
// TODO(burdon):
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// Notes: Can't edit widget content (or cursor nav through them).
|
|
32
|
-
// - https://discuss.codemirror.net/t/focusing-inputs-within-widgets/5178/7
|
|
33
|
-
// Widgets intentionally always get set to contenteditable=false,
|
|
34
|
-
// or they would become part of CodeMirror’s editable content element.
|
|
35
|
-
// You should be able to introduce new contenteditable=true child elements inside of them.
|
|
16
|
+
// TODO(burdon): Reconcile with theme.
|
|
17
|
+
export const styles = EditorView.baseTheme({
|
|
18
|
+
'.cm-hyperlink-label': {
|
|
19
|
+
cursor: 'pointer',
|
|
20
|
+
textDecoration: 'underline',
|
|
21
|
+
},
|
|
22
|
+
'.cm-hyperlink-bracket': {
|
|
23
|
+
opacity: 0.2,
|
|
24
|
+
},
|
|
25
|
+
'.cm-hyperlink-url': {
|
|
26
|
+
opacity: 0.5,
|
|
27
|
+
fontFamily: 'monospace',
|
|
28
|
+
},
|
|
29
|
+
});
|
|
36
30
|
|
|
37
|
-
// TODO(burdon): URL match?
|
|
38
31
|
const markdownLinkRegexp = /\[([^\]]+)]\(([^)]+)\)(!?)/gi;
|
|
39
32
|
|
|
40
33
|
/**
|
|
@@ -99,7 +92,7 @@ export function hyperLinkExtension() {
|
|
|
99
92
|
}
|
|
100
93
|
},
|
|
101
94
|
{
|
|
102
|
-
decorations: (
|
|
95
|
+
decorations: (value) => value.decorations,
|
|
103
96
|
eventHandlers: {
|
|
104
97
|
// TODO(burdon): CLick to expand link.
|
|
105
98
|
// https://discuss.codemirror.net/t/decorator-iteration-on-click-event/4226/2
|
|
@@ -118,18 +111,4 @@ export function hyperLinkExtension() {
|
|
|
118
111
|
);
|
|
119
112
|
}
|
|
120
113
|
|
|
121
|
-
export const styles = EditorView.baseTheme({
|
|
122
|
-
'.cm-hyperlink-label': {
|
|
123
|
-
cursor: 'pointer',
|
|
124
|
-
textDecoration: 'underline',
|
|
125
|
-
},
|
|
126
|
-
'.cm-hyperlink-bracket': {
|
|
127
|
-
opacity: 0.2,
|
|
128
|
-
},
|
|
129
|
-
'.cm-hyperlink-url': {
|
|
130
|
-
opacity: 0.5,
|
|
131
|
-
fontFamily: 'monospace',
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
|
|
135
114
|
export const hyperlinkWidget: Extension = [hyperLinkExtension(), styles];
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension, StateEffect, StateField } from '@codemirror/state';
|
|
6
|
+
import { Decoration, EditorView } from '@codemirror/view';
|
|
7
|
+
import sortBy from 'lodash.sortby';
|
|
8
|
+
import { useEffect } from 'react';
|
|
9
|
+
|
|
10
|
+
import { type DocumentRange } from '../../../hooks';
|
|
11
|
+
|
|
12
|
+
// TODO(burdon): Detect click, or cursor proximity.
|
|
13
|
+
|
|
14
|
+
// TODO(burdon): Reconcile with theme.
|
|
15
|
+
const styles = EditorView.baseTheme({
|
|
16
|
+
'& .cm-highlight': {
|
|
17
|
+
backgroundColor: 'yellow', // TODO(burdon): Reconcile with tailwind theme.
|
|
18
|
+
},
|
|
19
|
+
'& .cm-highlight::before': {
|
|
20
|
+
content: ']',
|
|
21
|
+
},
|
|
22
|
+
'& .cm-highlight-active': {
|
|
23
|
+
backgroundColor: 'lime',
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const marks = {
|
|
28
|
+
highlight: Decoration.mark({ class: 'cm-highlight' }),
|
|
29
|
+
HighlightActive: Decoration.mark({ class: 'cm-highlight-active' }),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type HighlightRange = DocumentRange & { active?: boolean };
|
|
33
|
+
|
|
34
|
+
// TODO(burdon): Make configurable to support multiple layers.
|
|
35
|
+
const setHighlights = StateEffect.define<HighlightRange[]>();
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Update field set.
|
|
39
|
+
*/
|
|
40
|
+
// TODO(burdon): Reconcile range from data model, vs. in-markdown characters.
|
|
41
|
+
export const useHighlights = (view: EditorView | undefined, ranges: HighlightRange[] = []) => {
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
view?.dispatch({
|
|
44
|
+
effects: setHighlights.of(ranges),
|
|
45
|
+
});
|
|
46
|
+
}, [view, ranges]);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* State field that tracks highlight ranges.
|
|
51
|
+
*/
|
|
52
|
+
export const highlightStateField = StateField.define<HighlightRange[]>({
|
|
53
|
+
create: () => [],
|
|
54
|
+
update: (threads, tr) => {
|
|
55
|
+
for (const effect of tr.effects) {
|
|
56
|
+
if (effect.is(setHighlights)) {
|
|
57
|
+
return effect.value;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return threads;
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Decorate ranges.
|
|
67
|
+
*/
|
|
68
|
+
// TODO(burdon): Pass in field to make reusable (e.g., comments, search).
|
|
69
|
+
export const highlightDecorations = EditorView.decorations.compute([highlightStateField], (state) => {
|
|
70
|
+
const selectionRanges = state.field(highlightStateField);
|
|
71
|
+
const decorations =
|
|
72
|
+
sortBy(selectionRanges ?? [], (selection) => selection.from)?.flatMap((selection) => {
|
|
73
|
+
const range = { from: selection.from, to: selection.to + 1 };
|
|
74
|
+
if (selection.to > selection.from) {
|
|
75
|
+
if (selection.active) {
|
|
76
|
+
return marks.HighlightActive.range(range.from, range.to);
|
|
77
|
+
} else {
|
|
78
|
+
return marks.highlight.range(range.from, range.to);
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}) ?? [];
|
|
84
|
+
|
|
85
|
+
return Decoration.set(decorations);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// TODO(burdon): Use current range to create comment.
|
|
89
|
+
// TODO(burdon): Allow/prevent overlapping?
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Track cursor entering and existing ranges.
|
|
93
|
+
*/
|
|
94
|
+
const listener = (options: HighlightOptions) => {
|
|
95
|
+
return EditorView.updateListener.of((update) => {
|
|
96
|
+
const { view, state } = update;
|
|
97
|
+
const { /* from, to, */ head } = state.selection.main;
|
|
98
|
+
const ranges = state.field(highlightStateField);
|
|
99
|
+
|
|
100
|
+
// TODO(burdon): Modify range if editing.
|
|
101
|
+
// console.log('::::', update);
|
|
102
|
+
|
|
103
|
+
// Set active.
|
|
104
|
+
let mod = false;
|
|
105
|
+
const newRanges = ranges.map((range) => {
|
|
106
|
+
const active = head >= range.from && head <= range.to;
|
|
107
|
+
if (active !== range.active) {
|
|
108
|
+
mod = true;
|
|
109
|
+
return { ...range, active };
|
|
110
|
+
} else {
|
|
111
|
+
return range;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (mod) {
|
|
116
|
+
view.dispatch({ effects: setHighlights.of(newRanges) });
|
|
117
|
+
options?.onChange?.(newRanges.find((range) => range.active)?.id);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export type HighlightOptions = {
|
|
123
|
+
onChange?: (active?: string) => {};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const highlight = (options: HighlightOptions = {}): Extension => {
|
|
127
|
+
return [highlightStateField, highlightDecorations, listener(options), styles];
|
|
128
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
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, EditorView, WidgetType } from '@codemirror/view';
|
|
15
|
+
|
|
16
|
+
export type ImageOptions = {};
|
|
17
|
+
|
|
18
|
+
export const image = (options: ImageOptions = {}): Extension => {
|
|
19
|
+
return StateField.define<RangeSet<any>>({
|
|
20
|
+
create: (state) => update(state, options),
|
|
21
|
+
update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
|
|
22
|
+
provide: (field) => EditorView.decorations.from(field),
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const update = (state: EditorState, options: ImageOptions) => {
|
|
27
|
+
const builder = new RangeSetBuilder();
|
|
28
|
+
const cursor = state.selection.main.head;
|
|
29
|
+
|
|
30
|
+
syntaxTree(state).iterate({
|
|
31
|
+
enter: (node) => {
|
|
32
|
+
if (node.name === 'Image') {
|
|
33
|
+
const urlNode = node.node.getChild('URL');
|
|
34
|
+
if (urlNode) {
|
|
35
|
+
const hide = state.readOnly || cursor < node.from || cursor > node.to;
|
|
36
|
+
const url = state.sliceDoc(urlNode.from, urlNode.to);
|
|
37
|
+
builder.add(
|
|
38
|
+
hide ? node.from : node.to,
|
|
39
|
+
node.to,
|
|
40
|
+
Decoration.replace({
|
|
41
|
+
widget: new ImageWidget(url),
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return builder.finish();
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
class ImageWidget extends WidgetType {
|
|
53
|
+
constructor(readonly _url: string) {
|
|
54
|
+
super();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override eq(other: WidgetType) {
|
|
58
|
+
return this._url === (other as any as ImageWidget)?._url;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
toDOM(view: EditorView) {
|
|
62
|
+
const img = document.createElement('img');
|
|
63
|
+
img.setAttribute('src', this._url);
|
|
64
|
+
img.setAttribute('class', 'cm-image');
|
|
65
|
+
return img;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -4,10 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
export * from './autocomplete';
|
|
6
6
|
export * from './basic';
|
|
7
|
+
export * from './blast';
|
|
8
|
+
export * from './code';
|
|
7
9
|
export * from './demo';
|
|
8
10
|
export * from './comments';
|
|
11
|
+
export * from './highlight';
|
|
12
|
+
export * from './image';
|
|
9
13
|
export * from './link';
|
|
10
14
|
export * from './listener';
|
|
11
15
|
export * from './markdown';
|
|
16
|
+
export * from './mention';
|
|
17
|
+
export * from './table';
|
|
12
18
|
export * from './tasklist';
|
|
13
19
|
export * from './tooltip';
|
|
@@ -18,13 +18,22 @@ import { Decoration, EditorView, WidgetType } from '@codemirror/view';
|
|
|
18
18
|
// https://codemirror.net/try/?c=aW1wb3J0IHsgYmFzaWNTZXR1cCwgRWRpdG9yVmlldyB9IGZyb20gImNvZGVtaXJyb3IiCmltcG9ydCB7IG1hcmtkb3duIH0gZnJvbSAiQGNvZGVtaXJyb3IvbGFuZy1tYXJrZG93biIKaW1wb3J0IHsgc3ludGF4VHJlZSB9IGZyb20gIkBjb2RlbWlycm9yL2xhbmd1YWdlIjsKaW1wb3J0IHsgRWRpdG9yU3RhdGUsIFJhbmdlU2V0QnVpbGRlciwgU3RhdGVGaWVsZCB9IGZyb20gIkBjb2RlbWlycm9yL3N0YXRlIjsKaW1wb3J0IHsgRGVjb3JhdGlvbiwgV2lkZ2V0VHlwZSB9IGZyb20gIkBjb2RlbWlycm9yL3ZpZXciOwoKY2xhc3MgTGluayBleHRlbmRzIFdpZGdldFR5cGUgewogIGNvbnN0cnVjdG9yKHRleHQsIHVybCkgewogICAgc3VwZXIoKQogICAgdGhpcy50ZXh0ID0gdGV4dAogICAgdGhpcy51cmwgPSB1cmwKICB9CiAgZXEob3RoZXIpIHsgcmV0dXJuIHRoaXMudGV4dCA9PSBvdGhlci50ZXh0ICYmIHRoaXMudXJsID09IG90aGVyLnVybCB9CiAgdG9ET00oKSB7CiAgICBsZXQgbGluayA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImEiKQogICAgbGluay50ZXh0Q29udGVudCA9IHRoaXMudGV4dAogICAgbGluay5ocmVmID0gdGhpcy51cmwKICAgIHJldHVybiBsaW5rOwogIH0KfQoKbGV0IGRlY29yYXRpb25zRmllbGQgPSBTdGF0ZUZpZWxkLmRlZmluZSh7CiAgY3JlYXRlKCkgewogICAgcmV0dXJuIERlY29yYXRpb24ubm9uZTsKICB9LAogIHVwZGF0ZShfLCB0cikgewogICAgY29uc3QgYnVpbGRlciA9IG5ldyBSYW5nZVNldEJ1aWxkZXIoKTsKICAgIGxldCBjdXJzb3IgPSB0ci5zdGF0ZS5zZWxlY3Rpb24ubWFpbi5oZWFkOwogICAgc3ludGF4VHJlZSh0ci5zdGF0ZSkuaXRlcmF0ZSh7CiAgICAgIGVudGVyOiAobm9kZSkgPT4gewogICAgICAgIGlmICgoY3Vyc29yIDwgbm9kZS5mcm9tIHx8IGN1cnNvciA+IG5vZGUudG8pICYmIG5vZGUubmFtZSA9PSAiTGluayIpIHsKICAgICAgICAgIGxldCBtYXJrcyA9IG5vZGUubm9kZS5nZXRDaGlsZHJlbigiTGlua01hcmsiKTsKICAgICAgICAgIGxldCB0ZXh0ID0gbWFya3MubGVuZ3RoID49IDIgPyB0ci5zdGF0ZS5zbGljZURvYyhtYXJrc1swXS50bywgbWFya3NbMV0uZnJvbSkgOiAiIgoKICAgICAgICAgIGxldCB1cmxOb2RlID0gbm9kZS5ub2RlLmdldENoaWxkKCJVUkwiKTsKICAgICAgICAgIGxldCB1cmwgPSB1cmxOb2RlID8gdHIuc3RhdGUuc2xpY2VEb2ModXJsTm9kZS5mcm9tLCB1cmxOb2RlLnRvKSA6ICIiCiAgICAgICAgICBidWlsZGVyLmFkZChub2RlLmZyb20sIG5vZGUudG8sIERlY29yYXRpb24ucmVwbGFjZSh7CiAgICAgICAgICAgIHdpZGdldDogbmV3IExpbmsodGV4dCwgdXJsKSwKICAgICAgICAgIH0pLAogICAgICAgICAgICAgICAgICAgICApOwogICAgICAgICAgcmV0dXJuIGZhbHNlOwogICAgICAgIH0KICAgICAgICByZXR1cm4gdHJ1ZTsKICAgICAgfSwKICAgIH0pOwogICAgcmV0dXJuIGJ1aWxkZXIuZmluaXNoKCk7CiAgfSwKICBwcm92aWRlOiAoZikgPT4gRWRpdG9yVmlldy5kZWNvcmF0aW9ucy5mcm9tKGYpLAp9KTsKCmxldCB2aWV3ID0gbmV3IEVkaXRvclZpZXcoewogIGRvYzogIiMgSGVsbG8gV29ybGRcblxuW1Rlc3RdKGh0dHBzOi8vZXhhbXBsZS5jb20pXG5cblxuIiwKICBleHRlbnNpb25zOiBbZGVjb3JhdGlvbnNGaWVsZCwgbWFya2Rvd24oKV0sCiAgcGFyZW50OiBkb2N1bWVudC5ib2R5LAp9KTsKCnZpZXcuZm9jdXMoKTs=
|
|
19
19
|
// https://discuss.codemirror.net/t/link-widget-is-clicked-only-if-editor-is-out-of-focus/7433
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
export type LinkOptions = {
|
|
22
|
+
link?: boolean;
|
|
23
|
+
onRender?: (el: Element, url: string) => void;
|
|
24
|
+
};
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Creates a state field to replace AST elements with a hyperlink widget.
|
|
28
|
+
* https://codemirror.net/docs/ref/#state.StateField
|
|
29
|
+
*/
|
|
30
|
+
export const link = (options: LinkOptions = {}): Extension => {
|
|
31
|
+
return StateField.define<RangeSet<any>>({
|
|
32
|
+
create: (state) => update(state, options),
|
|
33
|
+
update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
|
|
34
|
+
provide: (field) => EditorView.decorations.from(field),
|
|
35
|
+
});
|
|
36
|
+
};
|
|
28
37
|
|
|
29
38
|
class LinkButton extends WidgetType {
|
|
30
39
|
constructor(
|
|
@@ -85,20 +94,20 @@ class LinkText extends WidgetType {
|
|
|
85
94
|
const update = (state: EditorState, options: LinkOptions) => {
|
|
86
95
|
const builder = new RangeSetBuilder();
|
|
87
96
|
const cursor = state.selection.main.head;
|
|
97
|
+
|
|
88
98
|
syntaxTree(state).iterate({
|
|
89
99
|
enter: (node) => {
|
|
90
100
|
// Check if cursor is inside text.
|
|
91
101
|
if (node.name === 'Link') {
|
|
92
102
|
const marks = node.node.getChildren('LinkMark');
|
|
93
103
|
const text = marks.length >= 2 ? state.sliceDoc(marks[0].to, marks[1].from) : '';
|
|
94
|
-
|
|
95
104
|
const urlNode = node.node.getChild('URL');
|
|
96
105
|
const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
|
|
97
106
|
if (!url) {
|
|
98
107
|
return false;
|
|
99
108
|
}
|
|
100
109
|
|
|
101
|
-
if (cursor < node.from || cursor > node.to) {
|
|
110
|
+
if (state.readOnly || cursor < node.from || cursor > node.to) {
|
|
102
111
|
builder.add(
|
|
103
112
|
node.from,
|
|
104
113
|
node.to,
|
|
@@ -125,20 +134,3 @@ const update = (state: EditorState, options: LinkOptions) => {
|
|
|
125
134
|
|
|
126
135
|
return builder.finish();
|
|
127
136
|
};
|
|
128
|
-
|
|
129
|
-
export type LinkOptions = {
|
|
130
|
-
link?: boolean;
|
|
131
|
-
onRender?: (el: Element, url: string) => void;
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Creates a state field to replace AST elements with a hyperlink widget.
|
|
136
|
-
* https://codemirror.net/docs/ref/#state.StateField
|
|
137
|
-
*/
|
|
138
|
-
export const link = (options: LinkOptions = {}): Extension => {
|
|
139
|
-
return StateField.define<RangeSet<any>>({
|
|
140
|
-
create: (state) => update(state, options),
|
|
141
|
-
update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
|
|
142
|
-
provide: (field) => EditorView.decorations.from(field),
|
|
143
|
-
});
|
|
144
|
-
};
|
|
@@ -9,9 +9,8 @@ export type ListenerOptions = { onChange: (text: string) => void };
|
|
|
9
9
|
/**
|
|
10
10
|
* Based on https://github.com/codemirror/dev/issues/44#issuecomment-789093799
|
|
11
11
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
StateField.define({
|
|
12
|
+
export const listener = ({ onChange }: ListenerOptions) => {
|
|
13
|
+
return StateField.define({
|
|
15
14
|
create: () => null,
|
|
16
15
|
update: (_value, transaction) => {
|
|
17
16
|
if (transaction.docChanged && onChange) {
|
|
@@ -21,3 +20,4 @@ export const listener = ({ onChange }: ListenerOptions) =>
|
|
|
21
20
|
return null;
|
|
22
21
|
},
|
|
23
22
|
});
|
|
23
|
+
};
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
|
|
6
|
-
import {
|
|
6
|
+
import { history, historyKeymap, indentWithTab, standardKeymap } from '@codemirror/commands';
|
|
7
7
|
import { markdownLanguage, markdown } from '@codemirror/lang-markdown';
|
|
8
8
|
import { bracketMatching, defaultHighlightStyle, indentOnInput, syntaxHighlighting } from '@codemirror/language';
|
|
9
9
|
import { languages } from '@codemirror/language-data';
|
|
10
|
-
import {
|
|
10
|
+
import { searchKeymap } from '@codemirror/search';
|
|
11
11
|
import { EditorState, type Extension } from '@codemirror/state';
|
|
12
12
|
import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
|
|
13
13
|
import {
|
|
@@ -16,54 +16,50 @@ import {
|
|
|
16
16
|
dropCursor,
|
|
17
17
|
EditorView,
|
|
18
18
|
highlightActiveLine,
|
|
19
|
-
highlightActiveLineGutter,
|
|
20
|
-
highlightSpecialChars,
|
|
21
19
|
keymap,
|
|
22
20
|
placeholder,
|
|
23
|
-
rectangularSelection,
|
|
24
21
|
} from '@codemirror/view';
|
|
25
22
|
|
|
26
23
|
import type { ThemeMode } from '@dxos/react-ui';
|
|
27
24
|
|
|
28
|
-
import { markdownHighlightStyle,
|
|
25
|
+
import { markdownHighlightStyle, markdownTagsExtensions } from './highlight';
|
|
29
26
|
|
|
30
27
|
export type MarkdownBundleOptions = {
|
|
28
|
+
readonly?: boolean;
|
|
31
29
|
themeMode?: ThemeMode;
|
|
32
30
|
placeholder?: string;
|
|
33
31
|
};
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Markdown bundle.
|
|
35
|
+
* Refs:
|
|
36
|
+
* https://github.com/codemirror/basic-setup
|
|
37
|
+
* https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts#L50
|
|
38
|
+
* https://codemirror.net/docs/community
|
|
39
|
+
* https://codemirror.net/docs/ref/#codemirror.basicSetup
|
|
40
|
+
*/
|
|
41
|
+
// TODO(burdon): Add Composer here: https://codemirror.net/docs/community
|
|
42
|
+
export const markdownBundle = ({
|
|
43
|
+
readonly,
|
|
44
|
+
themeMode,
|
|
45
|
+
placeholder: _placeholder,
|
|
46
|
+
}: MarkdownBundleOptions): Extension[] => {
|
|
38
47
|
return [
|
|
39
|
-
bracketMatching(),
|
|
40
|
-
closeBrackets(),
|
|
41
|
-
_placeholder && placeholder(_placeholder),
|
|
42
|
-
|
|
43
48
|
EditorState.allowMultipleSelections.of(true),
|
|
49
|
+
EditorState.tabSize.of(2),
|
|
44
50
|
EditorView.lineWrapping,
|
|
45
51
|
|
|
52
|
+
customKeymap(),
|
|
53
|
+
|
|
54
|
+
bracketMatching(),
|
|
55
|
+
closeBrackets(),
|
|
46
56
|
crosshairCursor(),
|
|
47
57
|
dropCursor(),
|
|
48
58
|
drawSelection(),
|
|
49
59
|
highlightActiveLine(),
|
|
50
|
-
highlightActiveLineGutter(),
|
|
51
|
-
highlightSelectionMatches(),
|
|
52
|
-
highlightSpecialChars(),
|
|
53
60
|
history(),
|
|
54
61
|
indentOnInput(),
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// TODO(burdon): Review.
|
|
58
|
-
keymap.of([
|
|
59
|
-
...closeBracketsKeymap,
|
|
60
|
-
...defaultKeymap,
|
|
61
|
-
// ...foldKeymap,
|
|
62
|
-
// ...historyKeymap,
|
|
63
|
-
// ...lintKeymap,
|
|
64
|
-
...searchKeymap,
|
|
65
|
-
indentWithTab,
|
|
66
|
-
]),
|
|
62
|
+
_placeholder && placeholder(_placeholder),
|
|
67
63
|
|
|
68
64
|
// Main extension.
|
|
69
65
|
// https://github.com/codemirror/lang-markdown
|
|
@@ -81,15 +77,30 @@ export const markdownBundle = ({ themeMode, placeholder: _placeholder }: Markdow
|
|
|
81
77
|
|
|
82
78
|
// Parser extensions.
|
|
83
79
|
extensions: [
|
|
84
|
-
// GFM
|
|
85
|
-
|
|
80
|
+
// GFM provided by default.
|
|
81
|
+
markdownTagsExtensions,
|
|
86
82
|
],
|
|
87
83
|
}),
|
|
88
84
|
|
|
89
|
-
//
|
|
90
|
-
syntaxHighlighting(markdownHighlightStyle),
|
|
91
|
-
|
|
92
|
-
// TODO(thure): All but one rule here apply to both themes; rename or refactor.
|
|
85
|
+
// https://github.com/codemirror/theme-one-dark
|
|
93
86
|
themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
|
|
87
|
+
|
|
88
|
+
// Custom styles.
|
|
89
|
+
syntaxHighlighting(markdownHighlightStyle(readonly)),
|
|
94
90
|
].filter(Boolean) as Extension[];
|
|
95
91
|
};
|
|
92
|
+
|
|
93
|
+
// https://codemirror.net/docs/ref/#view.keymap
|
|
94
|
+
const customKeymap = () =>
|
|
95
|
+
keymap.of([
|
|
96
|
+
// https://codemirror.net/docs/ref/#commands.indentWithTab
|
|
97
|
+
indentWithTab,
|
|
98
|
+
// https://codemirror.net/docs/ref/#commands.standardKeymap
|
|
99
|
+
...standardKeymap,
|
|
100
|
+
// https://codemirror.net/docs/ref/#commands.historyKeymap
|
|
101
|
+
...historyKeymap,
|
|
102
|
+
// https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
|
|
103
|
+
...closeBracketsKeymap,
|
|
104
|
+
// https://codemirror.net/docs/ref/#search.searchKeymap
|
|
105
|
+
...searchKeymap,
|
|
106
|
+
]);
|