@dxos/react-ui-editor 0.4.4 → 0.4.5-main.17763ce
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 +421 -540
- 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 +5 -5
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/cursor-line-margin.d.ts +8 -0
- package/dist/types/src/extensions/cursor-line-margin.d.ts.map +1 -0
- package/dist/types/src/extensions/index.d.ts +1 -0
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts +5 -0
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/index.d.ts +1 -4
- package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/link.d.ts +1 -11
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
- package/dist/types/src/styles/layout.d.ts +2 -2
- package/dist/types/src/styles/layout.d.ts.map +1 -1
- package/package.json +31 -28
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +29 -37
- package/src/components/TextEditor/hooks.stories.tsx +5 -17
- package/src/components/Toolbar/Toolbar.stories.tsx +4 -7
- package/src/components/Toolbar/Toolbar.tsx +0 -1
- package/src/extensions/cursor-line-margin.ts +92 -0
- package/src/extensions/index.ts +1 -0
- package/src/extensions/markdown/decorate.ts +301 -0
- package/src/extensions/markdown/formatting.test.ts +17 -0
- package/src/extensions/markdown/formatting.ts +67 -76
- package/src/extensions/markdown/index.ts +1 -4
- package/src/extensions/markdown/link.ts +23 -185
- package/src/styles/layout.ts +3 -2
- package/dist/types/src/extensions/markdown/code.d.ts +0 -2
- package/dist/types/src/extensions/markdown/code.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/heading.d.ts +0 -6
- package/dist/types/src/extensions/markdown/heading.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/hr.d.ts +0 -2
- package/dist/types/src/extensions/markdown/hr.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/tasklist.d.ts +0 -3
- package/dist/types/src/extensions/markdown/tasklist.d.ts.map +0 -1
- package/src/extensions/markdown/code.ts +0 -162
- package/src/extensions/markdown/heading.ts +0 -59
- package/src/extensions/markdown/hr.ts +0 -71
- package/src/extensions/markdown/tasklist.ts +0 -110
|
@@ -101,6 +101,8 @@ describe('setHeading', () => {
|
|
|
101
101
|
testCommand('can add a heading inside block markup', '> - {one\n> - two}\n', setHeading(1), '> - # one\n> - # two\n');
|
|
102
102
|
|
|
103
103
|
testCommand('can remove a heading inside block markup', '1. # one{}', setHeading(0), '1. one');
|
|
104
|
+
|
|
105
|
+
testCommand('can add a heading to a blank line', 'one\n\n{}', setHeading(1), 'one\n\n# {}');
|
|
104
106
|
});
|
|
105
107
|
|
|
106
108
|
describe('addStyle', () => {
|
|
@@ -158,6 +160,8 @@ describe('addStyle', () => {
|
|
|
158
160
|
em,
|
|
159
161
|
'*[one {two](x) * five',
|
|
160
162
|
);
|
|
163
|
+
|
|
164
|
+
testCommand('can add a style to an empty line', '{}', em, '*{}*');
|
|
161
165
|
});
|
|
162
166
|
|
|
163
167
|
describe('removeStyle', () => {
|
|
@@ -188,6 +192,15 @@ describe('removeStyle', () => {
|
|
|
188
192
|
);
|
|
189
193
|
|
|
190
194
|
testCommand('can shrink existing styles', '*one {two three} four*', em, '*one* {two three} *four*');
|
|
195
|
+
|
|
196
|
+
testCommand('can remove strong from a partially emphasized text', '**{one*two*}**', str, '{one*two*}');
|
|
197
|
+
|
|
198
|
+
testCommand(
|
|
199
|
+
'can remove strong from a partially emphasized text with markers outside',
|
|
200
|
+
'***{o*ne*two}***',
|
|
201
|
+
str,
|
|
202
|
+
'*{o*ne*two}*',
|
|
203
|
+
);
|
|
191
204
|
});
|
|
192
205
|
|
|
193
206
|
describe('addLink', () => {
|
|
@@ -284,6 +297,8 @@ describe('addList', () => {
|
|
|
284
297
|
);
|
|
285
298
|
|
|
286
299
|
testCommand("doesn't renumber lists with a different parent", '> one{}\n\n1. two', ordered, '> 1. one\n\n1. two');
|
|
300
|
+
|
|
301
|
+
testCommand('can add a list to an empty line', '{}', ordered, '1. {}');
|
|
287
302
|
});
|
|
288
303
|
|
|
289
304
|
describe('removeList', () => {
|
|
@@ -345,6 +360,8 @@ describe('addBlockquote', () => {
|
|
|
345
360
|
addBlockquote,
|
|
346
361
|
'> one\n> two\n>\n> # three\n\nfour',
|
|
347
362
|
);
|
|
363
|
+
|
|
364
|
+
testCommand('can add a blockquote to an empty line', '{}', addBlockquote, '>{}');
|
|
348
365
|
});
|
|
349
366
|
|
|
350
367
|
describe('removeBlockquote', () => {
|
|
@@ -7,14 +7,13 @@ import { syntaxTree } from '@codemirror/language';
|
|
|
7
7
|
import {
|
|
8
8
|
type Extension,
|
|
9
9
|
type StateCommand,
|
|
10
|
-
RangeSetBuilder,
|
|
11
10
|
type EditorState,
|
|
12
11
|
type ChangeSpec,
|
|
13
12
|
type Text,
|
|
14
13
|
EditorSelection,
|
|
15
14
|
type Line,
|
|
16
15
|
} from '@codemirror/state';
|
|
17
|
-
import {
|
|
16
|
+
import { EditorView, keymap } from '@codemirror/view';
|
|
18
17
|
import { type SyntaxNodeRef, type SyntaxNode } from '@lezer/common';
|
|
19
18
|
import { useState, useMemo } from 'react';
|
|
20
19
|
|
|
@@ -94,6 +93,7 @@ export const setHeading =
|
|
|
94
93
|
const changes: ChangeSpec[] = [];
|
|
95
94
|
let prevBlock = -1;
|
|
96
95
|
for (const range of ranges) {
|
|
96
|
+
let sawBlock = false;
|
|
97
97
|
syntaxTree(state).iterate({
|
|
98
98
|
from: range.from,
|
|
99
99
|
to: range.to,
|
|
@@ -101,6 +101,7 @@ export const setHeading =
|
|
|
101
101
|
if (!Object.hasOwn(Textblocks, node.name) || prevBlock === node.from) {
|
|
102
102
|
return;
|
|
103
103
|
}
|
|
104
|
+
sawBlock = true;
|
|
104
105
|
prevBlock = node.from;
|
|
105
106
|
const blockType = Textblocks[node.name];
|
|
106
107
|
const isHeading = /heading(\d)/.exec(blockType);
|
|
@@ -129,13 +130,25 @@ export const setHeading =
|
|
|
129
130
|
}
|
|
130
131
|
},
|
|
131
132
|
});
|
|
133
|
+
let line;
|
|
134
|
+
if (!sawBlock && range.empty && level > 0 && !/\S/.test((line = state.doc.lineAt(range.from)).text)) {
|
|
135
|
+
changes.push({ from: line.from, to: line.to, insert: '#'.repeat(level) + ' ' });
|
|
136
|
+
}
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
if (!changes.length) {
|
|
135
140
|
return false;
|
|
136
141
|
}
|
|
137
142
|
|
|
138
|
-
|
|
143
|
+
const changeSet = state.changes(changes);
|
|
144
|
+
dispatch(
|
|
145
|
+
state.update({
|
|
146
|
+
changes: changeSet,
|
|
147
|
+
selection: state.selection.map(changeSet, 1),
|
|
148
|
+
userEvent: 'format.setHeading',
|
|
149
|
+
scrollIntoView: true,
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
139
152
|
return true;
|
|
140
153
|
};
|
|
141
154
|
|
|
@@ -174,8 +187,8 @@ export const setStyle =
|
|
|
174
187
|
const changesAtEnd: ChangeSpec[] = [];
|
|
175
188
|
let blockStart = -1;
|
|
176
189
|
let blockEnd = -1;
|
|
177
|
-
let startCovered: boolean |
|
|
178
|
-
let endCovered: boolean |
|
|
190
|
+
let startCovered: boolean | { from: number; to: number } = false;
|
|
191
|
+
let endCovered: boolean | { from: number; to: number } = false;
|
|
179
192
|
let { from, to } = range;
|
|
180
193
|
// Iterate the selected range. For each textblock, determine a
|
|
181
194
|
// start and end position, the overlap of the selected range and
|
|
@@ -218,10 +231,16 @@ export const setStyle =
|
|
|
218
231
|
// by this.
|
|
219
232
|
if (markType === type) {
|
|
220
233
|
if (openEnd <= from && closeStart >= from) {
|
|
221
|
-
startCovered =
|
|
234
|
+
startCovered =
|
|
235
|
+
!enable && openEnd === skipMarkers(from, node.node, -1, openEnd)
|
|
236
|
+
? { from: node.from, to: openEnd }
|
|
237
|
+
: true;
|
|
222
238
|
}
|
|
223
239
|
if (openEnd <= to && closeStart >= to) {
|
|
224
|
-
endCovered =
|
|
240
|
+
endCovered =
|
|
241
|
+
!enable && closeStart === skipMarkers(to, node.node, 1, closeStart)
|
|
242
|
+
? { from: closeStart, to: node.to }
|
|
243
|
+
: true;
|
|
225
244
|
}
|
|
226
245
|
}
|
|
227
246
|
// Marks of the same type in range, or any mark if we're
|
|
@@ -263,13 +282,13 @@ export const setStyle =
|
|
|
263
282
|
changes.push({ from: rangeEnd, insert: marker });
|
|
264
283
|
}
|
|
265
284
|
} else {
|
|
266
|
-
if (startCovered === '
|
|
267
|
-
changes.push(
|
|
285
|
+
if (typeof startCovered === 'object') {
|
|
286
|
+
changes.push(startCovered);
|
|
268
287
|
} else if (startCovered) {
|
|
269
288
|
changes.push({ from: skipSpaces(rangeStart, state.doc, -1, blockStart), insert: marker });
|
|
270
289
|
}
|
|
271
|
-
if (endCovered === '
|
|
272
|
-
changes.push(
|
|
290
|
+
if (typeof endCovered === 'object') {
|
|
291
|
+
changes.push(endCovered);
|
|
273
292
|
} else if (endCovered) {
|
|
274
293
|
changes.push({ from: skipSpaces(rangeEnd, state.doc, 1, blockEnd), insert: marker });
|
|
275
294
|
}
|
|
@@ -277,6 +296,12 @@ export const setStyle =
|
|
|
277
296
|
}
|
|
278
297
|
},
|
|
279
298
|
});
|
|
299
|
+
if (blockStart < 0 && range.empty && enable && !/\S/.test(state.doc.lineAt(range.from).text)) {
|
|
300
|
+
return {
|
|
301
|
+
changes: { from: range.head, insert: marker + marker },
|
|
302
|
+
range: EditorSelection.cursor(range.head + marker.length),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
280
305
|
const changeSet = state.changes(changes.concat(changesAtEnd));
|
|
281
306
|
return {
|
|
282
307
|
changes: changeSet,
|
|
@@ -350,6 +375,20 @@ const skipSpaces = (pos: number, doc: Text, dir: -1 | 1, limit?: number) => {
|
|
|
350
375
|
return pos;
|
|
351
376
|
};
|
|
352
377
|
|
|
378
|
+
const skipMarkers = (pos: number, tree: SyntaxNode, dir: -1 | 1, limit?: number) => {
|
|
379
|
+
for (;;) {
|
|
380
|
+
const next = tree.resolve(pos, dir);
|
|
381
|
+
if (!IgnoreInline.has(next.name)) {
|
|
382
|
+
return pos;
|
|
383
|
+
}
|
|
384
|
+
const moveTo = dir < 0 ? next.from : next.to;
|
|
385
|
+
if (limit != null && (dir < 0 ? moveTo < limit : moveTo > limit)) {
|
|
386
|
+
return pos;
|
|
387
|
+
}
|
|
388
|
+
pos = moveTo;
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
353
392
|
// TODO(burdon): Define and trigger snippets for codeblock, table, etc.
|
|
354
393
|
const snippets = {
|
|
355
394
|
codeblock: snippet(
|
|
@@ -515,7 +554,6 @@ export const addLink: StateCommand = ({ state, dispatch }) => {
|
|
|
515
554
|
// Lists
|
|
516
555
|
//
|
|
517
556
|
|
|
518
|
-
// TODO(burdon): Cursor is positioned incorrectly.
|
|
519
557
|
export const addList =
|
|
520
558
|
(type: List): StateCommand =>
|
|
521
559
|
({ state, dispatch }) => {
|
|
@@ -654,7 +692,15 @@ export const addList =
|
|
|
654
692
|
}
|
|
655
693
|
}
|
|
656
694
|
|
|
657
|
-
|
|
695
|
+
const changeSet = state.changes(changes);
|
|
696
|
+
dispatch(
|
|
697
|
+
state.update({
|
|
698
|
+
changes: changeSet,
|
|
699
|
+
selection: state.selection.map(changeSet, 1),
|
|
700
|
+
userEvent: 'format.list.add',
|
|
701
|
+
scrollIntoView: true,
|
|
702
|
+
}),
|
|
703
|
+
);
|
|
658
704
|
return true;
|
|
659
705
|
};
|
|
660
706
|
|
|
@@ -751,6 +797,7 @@ export const setBlockquote =
|
|
|
751
797
|
const lines: Line[] = [];
|
|
752
798
|
let lastBlock = -1;
|
|
753
799
|
for (const { from, to } of state.selection.ranges) {
|
|
800
|
+
const sawBlock = false;
|
|
754
801
|
syntaxTree(state).iterate({
|
|
755
802
|
from,
|
|
756
803
|
to,
|
|
@@ -786,6 +833,10 @@ export const setBlockquote =
|
|
|
786
833
|
}
|
|
787
834
|
},
|
|
788
835
|
});
|
|
836
|
+
let line;
|
|
837
|
+
if (!sawBlock && enable && from === to && !/\S/.test((line = state.doc.lineAt(from)).text)) {
|
|
838
|
+
lines.push(line);
|
|
839
|
+
}
|
|
789
840
|
}
|
|
790
841
|
|
|
791
842
|
const changes: ChangeSpec[] = [];
|
|
@@ -803,9 +854,11 @@ export const setBlockquote =
|
|
|
803
854
|
return false;
|
|
804
855
|
}
|
|
805
856
|
|
|
857
|
+
const changeSet = state.changes(changes);
|
|
806
858
|
dispatch(
|
|
807
859
|
state.update({
|
|
808
|
-
changes,
|
|
860
|
+
changes: changeSet,
|
|
861
|
+
selection: state.selection.map(changeSet, 1),
|
|
809
862
|
userEvent: enable ? 'format.blockquote.add' : 'format.blockquote.remove',
|
|
810
863
|
scrollIntoView: true,
|
|
811
864
|
}),
|
|
@@ -942,68 +995,6 @@ export const formatting = (options: FormattingOptions = {}): Extension => {
|
|
|
942
995
|
run: toggleStrong,
|
|
943
996
|
},
|
|
944
997
|
]),
|
|
945
|
-
styling(),
|
|
946
|
-
];
|
|
947
|
-
};
|
|
948
|
-
|
|
949
|
-
const styling = (): Extension => {
|
|
950
|
-
const buildDecorations = (view: EditorView): DecorationSet => {
|
|
951
|
-
const builder = new RangeSetBuilder<Decoration>();
|
|
952
|
-
const { state } = view;
|
|
953
|
-
const cursor = state.selection.main.head;
|
|
954
|
-
|
|
955
|
-
// TODO(burdon): Bug if '***foo***' (since StrongEmphasis is nested inside EmphasisMark).
|
|
956
|
-
// Ranges must be added sorted by `from` position and `startSide`.
|
|
957
|
-
const replace = (node: SyntaxNodeRef, marks: SyntaxNode[]) => {
|
|
958
|
-
if (cursor <= node.from || cursor >= node.to) {
|
|
959
|
-
for (const mark of marks) {
|
|
960
|
-
builder.add(mark.from, mark.to, Decoration.replace({}));
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
};
|
|
964
|
-
|
|
965
|
-
for (const { from, to } of view.visibleRanges) {
|
|
966
|
-
syntaxTree(state).iterate({
|
|
967
|
-
enter: (node) => {
|
|
968
|
-
switch (node.name) {
|
|
969
|
-
case 'Emphasis':
|
|
970
|
-
case 'StrongEmphasis': {
|
|
971
|
-
const marks = node.node.getChildren('EmphasisMark');
|
|
972
|
-
replace(node, marks);
|
|
973
|
-
break;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
case 'Strikethrough': {
|
|
977
|
-
const marks = node.node.getChildren('StrikethroughMark');
|
|
978
|
-
replace(node, marks);
|
|
979
|
-
break;
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
},
|
|
983
|
-
from,
|
|
984
|
-
to,
|
|
985
|
-
});
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
return builder.finish();
|
|
989
|
-
};
|
|
990
|
-
|
|
991
|
-
return [
|
|
992
|
-
ViewPlugin.fromClass(
|
|
993
|
-
class {
|
|
994
|
-
decorations: DecorationSet;
|
|
995
|
-
constructor(view: EditorView) {
|
|
996
|
-
this.decorations = buildDecorations(view);
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
update(update: ViewUpdate) {
|
|
1000
|
-
this.decorations = buildDecorations(update.view);
|
|
1001
|
-
}
|
|
1002
|
-
},
|
|
1003
|
-
{
|
|
1004
|
-
decorations: (value) => value.decorations,
|
|
1005
|
-
},
|
|
1006
|
-
),
|
|
1007
998
|
];
|
|
1008
999
|
};
|
|
1009
1000
|
|
|
@@ -3,12 +3,9 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
export * from './bundle';
|
|
6
|
-
export * from './
|
|
6
|
+
export * from './decorate';
|
|
7
7
|
export * from './formatting';
|
|
8
|
-
export * from './heading';
|
|
9
8
|
export * from './highlight';
|
|
10
|
-
export * from './hr';
|
|
11
9
|
export * from './image';
|
|
12
10
|
export * from './link';
|
|
13
11
|
export * from './table';
|
|
14
|
-
export * from './tasklist';
|
|
@@ -4,196 +4,34 @@
|
|
|
4
4
|
//
|
|
5
5
|
|
|
6
6
|
import { syntaxTree } from '@codemirror/language';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
hoverTooltip,
|
|
10
|
-
Decoration,
|
|
11
|
-
type DecorationSet,
|
|
12
|
-
type EditorView,
|
|
13
|
-
ViewPlugin,
|
|
14
|
-
type ViewUpdate,
|
|
15
|
-
WidgetType,
|
|
16
|
-
} from '@codemirror/view';
|
|
7
|
+
import { hoverTooltip } from '@codemirror/view';
|
|
17
8
|
import { type SyntaxNode } from '@lezer/common';
|
|
18
9
|
|
|
19
10
|
import { tooltipContent } from '@dxos/react-ui-theme';
|
|
20
11
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
link?: boolean;
|
|
28
|
-
onHover?: (el: Element, url: string) => void;
|
|
29
|
-
onRender?: (el: Element, url: string) => void;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Creates a state field to replace AST elements with a hyperlink widget.
|
|
34
|
-
* https://codemirror.net/docs/ref/#state.StateField
|
|
35
|
-
*/
|
|
36
|
-
export const link = (options: LinkOptions = {}): Extension => {
|
|
37
|
-
const extensions: Extension[] = [
|
|
38
|
-
ViewPlugin.fromClass(
|
|
39
|
-
class {
|
|
40
|
-
decorations: DecorationSet;
|
|
41
|
-
constructor(view: EditorView) {
|
|
42
|
-
this.decorations = buildDecorations(view, options);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
update(update: ViewUpdate) {
|
|
46
|
-
if (update.docChanged || update.viewportChanged || update.selectionSet) {
|
|
47
|
-
this.decorations = buildDecorations(update.view, options);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
decorations: (value) => value.decorations,
|
|
53
|
-
},
|
|
54
|
-
),
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
if (options.onHover) {
|
|
58
|
-
extensions.push(
|
|
59
|
-
// https://codemirror.net/examples/tooltip
|
|
60
|
-
// https://codemirror.net/docs/ref/#view.hoverTooltip
|
|
61
|
-
// https://github.com/codemirror/view/blob/main/src/tooltip.ts
|
|
62
|
-
hoverTooltip((view, pos, side) => {
|
|
63
|
-
const syntax = syntaxTree(view.state).resolveInner(pos, side);
|
|
64
|
-
let link = null;
|
|
65
|
-
for (let i = 0, node: SyntaxNode | null = syntax; !link && node && i < 5; node = node.parent, i++) {
|
|
66
|
-
link = node.name === 'Link' ? node : null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const url = link && link.getChild('URL');
|
|
70
|
-
if (!url || !link) {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const urlText = view.state.sliceDoc(url.from, url.to);
|
|
75
|
-
return {
|
|
76
|
-
pos: link.from,
|
|
77
|
-
end: link.to,
|
|
78
|
-
above: true,
|
|
79
|
-
create: () => {
|
|
80
|
-
const el = document.createElement('div');
|
|
81
|
-
el.className = tooltipContent({}, 'pli-2 plb-1');
|
|
82
|
-
options.onHover!(el, urlText);
|
|
83
|
-
return { dom: el, offset: { x: 0, y: 4 } };
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
}),
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return extensions;
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
class LinkButton extends WidgetType {
|
|
94
|
-
constructor(
|
|
95
|
-
private readonly _url: string,
|
|
96
|
-
private readonly _onAttach: LinkOptions['onRender'],
|
|
97
|
-
) {
|
|
98
|
-
super();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
override eq(other: this) {
|
|
102
|
-
return this._url === other._url;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
override toDOM(view: EditorView) {
|
|
106
|
-
const el = document.createElement('span');
|
|
107
|
-
this._onAttach!(el, this._url);
|
|
108
|
-
return el;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
class LinkText extends WidgetType {
|
|
113
|
-
constructor(
|
|
114
|
-
private readonly _text: string,
|
|
115
|
-
private readonly _url?: string,
|
|
116
|
-
) {
|
|
117
|
-
super();
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
override eq(other: this) {
|
|
121
|
-
return this._url === other._url;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
override toDOM(view: EditorView) {
|
|
125
|
-
const link = document.createElement('a');
|
|
126
|
-
link.setAttribute('class', 'cm-link');
|
|
127
|
-
link.textContent = this._text;
|
|
128
|
-
if (this._url) {
|
|
129
|
-
link.setAttribute('rel', 'noreferrer');
|
|
130
|
-
link.setAttribute('target', '_blank');
|
|
131
|
-
link.href = this._url;
|
|
132
|
-
} else {
|
|
133
|
-
link.onclick = () => {
|
|
134
|
-
view.dispatch({
|
|
135
|
-
selection: {
|
|
136
|
-
anchor: view.posAtDOM(link),
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
};
|
|
12
|
+
export const linkTooltip = (render: (el: Element, url: string) => void) =>
|
|
13
|
+
hoverTooltip((view, pos, side) => {
|
|
14
|
+
const syntax = syntaxTree(view.state).resolveInner(pos, side);
|
|
15
|
+
let link = null;
|
|
16
|
+
for (let i = 0, node: SyntaxNode | null = syntax; !link && node && i < 5; node = node.parent, i++) {
|
|
17
|
+
link = node.name === 'Link' ? node : null;
|
|
140
18
|
}
|
|
141
19
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Range sets provide a data structure that can hold a collection of tagged,
|
|
148
|
-
* possibly overlapping ranges in such a way that they can efficiently be mapped though document changes.
|
|
149
|
-
* https://codemirror.net/docs/ref/#state
|
|
150
|
-
*/
|
|
151
|
-
const buildDecorations = (view: EditorView, options: LinkOptions): DecorationSet => {
|
|
152
|
-
const builder = new RangeSetBuilder<Decoration>();
|
|
153
|
-
const { state } = view;
|
|
154
|
-
const cursor = state.selection.main.head;
|
|
155
|
-
|
|
156
|
-
for (const { from, to } of view.visibleRanges) {
|
|
157
|
-
syntaxTree(state).iterate({
|
|
158
|
-
enter: (node) => {
|
|
159
|
-
// Check if cursor is inside text.
|
|
160
|
-
if (node.name === 'Link') {
|
|
161
|
-
const marks = node.node.getChildren('LinkMark');
|
|
162
|
-
const text = marks.length >= 2 ? state.sliceDoc(marks[0].to, marks[1].from) : '';
|
|
163
|
-
const urlNode = node.node.getChild('URL');
|
|
164
|
-
const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
|
|
165
|
-
if (!url) {
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// TODO(burdon): Don't expand if moving cursor up/down.
|
|
170
|
-
if (!view.hasFocus || state.readOnly || cursor < node.from || cursor > node.to) {
|
|
171
|
-
builder.add(
|
|
172
|
-
node.from,
|
|
173
|
-
node.to,
|
|
174
|
-
Decoration.replace({
|
|
175
|
-
widget: new LinkText(text, options.link ? url : undefined),
|
|
176
|
-
}),
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (options.onRender) {
|
|
181
|
-
builder.add(
|
|
182
|
-
node.to,
|
|
183
|
-
node.to,
|
|
184
|
-
Decoration.replace({
|
|
185
|
-
widget: new LinkButton(url, options.onRender),
|
|
186
|
-
}),
|
|
187
|
-
);
|
|
188
|
-
}
|
|
20
|
+
const url = link && link.getChild('URL');
|
|
21
|
+
if (!url || !link) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
189
24
|
|
|
190
|
-
|
|
191
|
-
|
|
25
|
+
const urlText = view.state.sliceDoc(url.from, url.to);
|
|
26
|
+
return {
|
|
27
|
+
pos: link.from,
|
|
28
|
+
end: link.to,
|
|
29
|
+
above: true,
|
|
30
|
+
create: () => {
|
|
31
|
+
const el = document.createElement('div');
|
|
32
|
+
el.className = tooltipContent({}, 'pli-2 plb-1');
|
|
33
|
+
render(el, urlText);
|
|
34
|
+
return { dom: el, offset: { x: 0, y: 4 } };
|
|
192
35
|
},
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return builder.finish();
|
|
199
|
-
};
|
|
36
|
+
};
|
|
37
|
+
});
|
package/src/styles/layout.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
export const editorWithToolbarLayout =
|
|
6
6
|
'grid grid-cols-1 grid-rows-[min-content_1fr] data-[toolbar=disabled]:grid-rows-[1fr] justify-center content-start overflow-hidden';
|
|
7
7
|
|
|
8
|
-
export const editorFillLayoutRoot = '
|
|
9
|
-
export const editorFillLayoutEditor =
|
|
8
|
+
export const editorFillLayoutRoot = 'bs-full overflow-hidden grid';
|
|
9
|
+
export const editorFillLayoutEditor =
|
|
10
|
+
'bs-full overflow-hidden [&>.cm-scroller]:bs-full [&>.cm-scroller]:overflow-y-auto';
|
|
10
11
|
|
|
11
12
|
export const editorHalfViewportOverscrollContent = 'after:block after:min-bs-[50dvh] after:pointer-events-none';
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"code.d.ts","sourceRoot":"","sources":["../../../../../src/extensions/markdown/code.ts"],"names":[],"mappings":"AA8IA,eAAO,MAAM,IAAI,+CAmBhB,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"heading.d.ts","sourceRoot":"","sources":["../../../../../src/extensions/markdown/heading.ts"],"names":[],"mappings":"AAMA,OAAO,EAAc,KAAK,aAAa,EAAmB,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAkChH,eAAO,MAAM,OAAO;;mBASG,UAAU;IAShC,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"hr.d.ts","sourceRoot":"","sources":["../../../../../src/extensions/markdown/hr.ts"],"names":[],"mappings":"AAmDA,eAAO,MAAM,EAAE,+CAmBd,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tasklist.d.ts","sourceRoot":"","sources":["../../../../../src/extensions/markdown/tasklist.ts"],"names":[],"mappings":"AAsFA,MAAM,MAAM,eAAe,GAAG,EAAE,CAAC;AAEjC,eAAO,MAAM,QAAQ,aAAa,eAAe,4CAqBhD,CAAC"}
|