@seorii/tiptap 0.0.9 → 0.1.0
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/i18n/en-us/index.d.ts +43 -0
- package/dist/i18n/en-us/index.js +42 -0
- package/dist/i18n/index.d.ts +7 -0
- package/dist/i18n/index.js +22 -0
- package/dist/i18n/ko-kr/index.d.ts +43 -0
- package/dist/i18n/ko-kr/index.js +42 -0
- package/dist/plugin/command/stores.d.ts +1 -0
- package/dist/plugin/command/stores.js +1 -0
- package/dist/plugin/command/suggest.d.ts +3 -3
- package/dist/plugin/command/suggest.js +49 -20
- package/dist/plugin/youtube.d.ts +15 -0
- package/dist/plugin/youtube.js +100 -0
- package/dist/tiptap/Bubble.svelte +73 -5
- package/dist/tiptap/Command.svelte +91 -21
- package/dist/tiptap/Floating.svelte +8 -8
- package/dist/tiptap/TipTap.svelte +17 -12
- package/dist/tiptap/TipTap.svelte.d.ts +1 -1
- package/dist/tiptap/tiptap.js +21 -2
- package/package.json +2 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
target: string[];
|
|
3
|
+
lang: string;
|
|
4
|
+
country: string;
|
|
5
|
+
text: string;
|
|
6
|
+
block: string;
|
|
7
|
+
loading: string;
|
|
8
|
+
delete: string;
|
|
9
|
+
close: string;
|
|
10
|
+
cancel: string;
|
|
11
|
+
insert: string;
|
|
12
|
+
noResult: string;
|
|
13
|
+
title: string;
|
|
14
|
+
paragraph: string;
|
|
15
|
+
link: string;
|
|
16
|
+
alignLeft: string;
|
|
17
|
+
alignCenter: string;
|
|
18
|
+
alignRight: string;
|
|
19
|
+
alignJustify: string;
|
|
20
|
+
unorderedList: string;
|
|
21
|
+
numberList: string;
|
|
22
|
+
codeBlock: string;
|
|
23
|
+
mathBlock: string;
|
|
24
|
+
table: string;
|
|
25
|
+
image: string;
|
|
26
|
+
iframe: string;
|
|
27
|
+
youtube: string;
|
|
28
|
+
blockquote: string;
|
|
29
|
+
title1Info: string;
|
|
30
|
+
title2Info: string;
|
|
31
|
+
title3Info: string;
|
|
32
|
+
unorderedListInfo: string;
|
|
33
|
+
numberListInfo: string;
|
|
34
|
+
codeBlockInfo: string;
|
|
35
|
+
mathBlockInfo: string;
|
|
36
|
+
tableInfo: string;
|
|
37
|
+
imageInfo: string;
|
|
38
|
+
iframeInfo: string;
|
|
39
|
+
youtubeInfo: string;
|
|
40
|
+
blockquoteInfo: string;
|
|
41
|
+
newLineInfo: string;
|
|
42
|
+
};
|
|
43
|
+
export default _default;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
target: ['en', 'en-US', 'en-GB'],
|
|
3
|
+
lang: 'en',
|
|
4
|
+
country: 'US',
|
|
5
|
+
text: 'Text',
|
|
6
|
+
block: 'Block',
|
|
7
|
+
loading: 'Loading...',
|
|
8
|
+
delete: 'Delete',
|
|
9
|
+
close: 'Close',
|
|
10
|
+
cancel: 'Cancel',
|
|
11
|
+
insert: 'Insert',
|
|
12
|
+
noResult: 'No result',
|
|
13
|
+
title: 'Title',
|
|
14
|
+
paragraph: 'Paragraph',
|
|
15
|
+
link: 'Link',
|
|
16
|
+
alignLeft: 'Align left',
|
|
17
|
+
alignCenter: 'Align center',
|
|
18
|
+
alignRight: 'Align right',
|
|
19
|
+
alignJustify: 'Align justify',
|
|
20
|
+
unorderedList: 'Unordered list',
|
|
21
|
+
numberList: 'Number list',
|
|
22
|
+
codeBlock: 'Code block',
|
|
23
|
+
mathBlock: 'Math block',
|
|
24
|
+
table: 'Table',
|
|
25
|
+
image: 'Image',
|
|
26
|
+
iframe: 'iframe',
|
|
27
|
+
youtube: 'Youtube',
|
|
28
|
+
blockquote: 'Blockquote',
|
|
29
|
+
title1Info: 'Big title',
|
|
30
|
+
title2Info: 'Smaller title',
|
|
31
|
+
title3Info: 'Medium title',
|
|
32
|
+
unorderedListInfo: 'Unordered list',
|
|
33
|
+
numberListInfo: '1, 2, 3, 4',
|
|
34
|
+
codeBlockInfo: 'Code block with syntax highlighting',
|
|
35
|
+
mathBlockInfo: 'Math block',
|
|
36
|
+
tableInfo: 'Table',
|
|
37
|
+
imageInfo: 'Image',
|
|
38
|
+
iframeInfo: 'Embed another website',
|
|
39
|
+
youtubeInfo: 'Embed Youtube video',
|
|
40
|
+
blockquoteInfo: 'Blockquote',
|
|
41
|
+
newLineInfo: 'Press / to enter commands. Or'
|
|
42
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import enUs from './en-us/index';
|
|
2
|
+
import koKr from './ko-kr/index';
|
|
3
|
+
import { browser } from "$app/environment";
|
|
4
|
+
const locales = [enUs, koKr];
|
|
5
|
+
export function getLocale(locales) {
|
|
6
|
+
if (typeof navigator === 'undefined')
|
|
7
|
+
return enUs;
|
|
8
|
+
const language = navigator.language;
|
|
9
|
+
const locale = locales.find(item => item.target.includes(language));
|
|
10
|
+
return locale || enUs;
|
|
11
|
+
}
|
|
12
|
+
const locale = getLocale(locales);
|
|
13
|
+
export default function i18n(...args) {
|
|
14
|
+
return args.reduce((prev, next) => {
|
|
15
|
+
return prev[next] || '';
|
|
16
|
+
}, locale);
|
|
17
|
+
}
|
|
18
|
+
i18n.locale = locale;
|
|
19
|
+
i18n.localeLangCountry = i18n('lang') + '-' + i18n('country');
|
|
20
|
+
//@ts-ignore
|
|
21
|
+
if (browser)
|
|
22
|
+
window.__me_i18n = i18n;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
target: string[];
|
|
3
|
+
lang: string;
|
|
4
|
+
country: string;
|
|
5
|
+
text: string;
|
|
6
|
+
block: string;
|
|
7
|
+
loading: string;
|
|
8
|
+
delete: string;
|
|
9
|
+
close: string;
|
|
10
|
+
cancel: string;
|
|
11
|
+
insert: string;
|
|
12
|
+
noResult: string;
|
|
13
|
+
title: string;
|
|
14
|
+
paragraph: string;
|
|
15
|
+
link: string;
|
|
16
|
+
alignLeft: string;
|
|
17
|
+
alignCenter: string;
|
|
18
|
+
alignRight: string;
|
|
19
|
+
alignJustify: string;
|
|
20
|
+
unorderedList: string;
|
|
21
|
+
numberList: string;
|
|
22
|
+
codeBlock: string;
|
|
23
|
+
mathBlock: string;
|
|
24
|
+
table: string;
|
|
25
|
+
image: string;
|
|
26
|
+
iframe: string;
|
|
27
|
+
youtube: string;
|
|
28
|
+
blockquote: string;
|
|
29
|
+
title1Info: string;
|
|
30
|
+
title2Info: string;
|
|
31
|
+
title3Info: string;
|
|
32
|
+
unorderedListInfo: string;
|
|
33
|
+
numberListInfo: string;
|
|
34
|
+
codeBlockInfo: string;
|
|
35
|
+
mathBlockInfo: string;
|
|
36
|
+
tableInfo: string;
|
|
37
|
+
imageInfo: string;
|
|
38
|
+
iframeInfo: string;
|
|
39
|
+
youtubeInfo: string;
|
|
40
|
+
blockquoteInfo: string;
|
|
41
|
+
newLineInfo: string;
|
|
42
|
+
};
|
|
43
|
+
export default _default;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
target: ['ko'],
|
|
3
|
+
lang: 'ko',
|
|
4
|
+
country: 'KR',
|
|
5
|
+
text: '텍스트',
|
|
6
|
+
block: '블록',
|
|
7
|
+
loading: '로딩 중...',
|
|
8
|
+
delete: '삭제',
|
|
9
|
+
close: '닫기',
|
|
10
|
+
cancel: '취소',
|
|
11
|
+
insert: '삽입',
|
|
12
|
+
noResult: '결과 없음',
|
|
13
|
+
title: '제목',
|
|
14
|
+
paragraph: '본문',
|
|
15
|
+
link: '링크',
|
|
16
|
+
alignLeft: '왼쪽 정렬',
|
|
17
|
+
alignCenter: '가운데 정렬',
|
|
18
|
+
alignRight: '오른쪽 정렬',
|
|
19
|
+
alignJustify: '양쪽 정렬',
|
|
20
|
+
unorderedList: '리스트',
|
|
21
|
+
numberList: '숫자 리스트',
|
|
22
|
+
codeBlock: '코드 블록',
|
|
23
|
+
mathBlock: '수식 블록',
|
|
24
|
+
table: '테이블',
|
|
25
|
+
image: '이미지',
|
|
26
|
+
iframe: 'iframe',
|
|
27
|
+
youtube: '유튜브',
|
|
28
|
+
blockquote: '인용구',
|
|
29
|
+
title1Info: '큰 제목',
|
|
30
|
+
title2Info: '좀 더 작은 제목',
|
|
31
|
+
title3Info: '적당히 큰 제목',
|
|
32
|
+
unorderedListInfo: '순서 없는 리스트',
|
|
33
|
+
numberListInfo: '1, 2, 3, 4',
|
|
34
|
+
codeBlockInfo: '하이라이팅되는 코드 블록',
|
|
35
|
+
mathBlockInfo: '가운데로 정렬되는 수식 블록',
|
|
36
|
+
tableInfo: '표 삽입',
|
|
37
|
+
imageInfo: '이미지',
|
|
38
|
+
iframeInfo: '다른 웹사이트 삽입',
|
|
39
|
+
youtubeInfo: '유튜브 동영상 삽입',
|
|
40
|
+
blockquoteInfo: '있어보이는 인용구 삽입',
|
|
41
|
+
newLineInfo: '/로 명령어 입력. 또는'
|
|
42
|
+
};
|
|
@@ -2,11 +2,11 @@ declare const _default: {
|
|
|
2
2
|
items: ({ query }: {
|
|
3
3
|
query: any;
|
|
4
4
|
}) => {
|
|
5
|
-
section:
|
|
5
|
+
section: any;
|
|
6
6
|
list: {
|
|
7
7
|
icon: string;
|
|
8
|
-
title:
|
|
9
|
-
subtitle:
|
|
8
|
+
title: any;
|
|
9
|
+
subtitle: any;
|
|
10
10
|
command: ({ editor, range }: {
|
|
11
11
|
editor: any;
|
|
12
12
|
range: any;
|
|
@@ -1,37 +1,38 @@
|
|
|
1
|
-
import { slashVisible, slashItems, slashLocaltion, slashProps } from './stores';
|
|
1
|
+
import { slashVisible, slashItems, slashLocaltion, slashProps, slashDetail } from './stores';
|
|
2
|
+
import i18n from "../../i18n";
|
|
2
3
|
export default {
|
|
3
4
|
items: ({ query }) => {
|
|
4
5
|
const raw = [
|
|
5
6
|
{
|
|
6
|
-
section: '
|
|
7
|
+
section: i18n('text'), list: [
|
|
7
8
|
{
|
|
8
9
|
icon: 'title',
|
|
9
|
-
title: '
|
|
10
|
-
subtitle: '
|
|
10
|
+
title: i18n('title') + ' 1',
|
|
11
|
+
subtitle: i18n('title1Info'),
|
|
11
12
|
command: ({ editor, range }) => {
|
|
12
13
|
editor.chain().focus().deleteRange(range).setNode('heading', { level: 1 }).run();
|
|
13
14
|
}
|
|
14
15
|
},
|
|
15
16
|
{
|
|
16
17
|
icon: 'title',
|
|
17
|
-
title: '
|
|
18
|
-
subtitle: '
|
|
18
|
+
title: i18n('title') + ' 2',
|
|
19
|
+
subtitle: i18n('title2Info'),
|
|
19
20
|
command: ({ editor, range }) => {
|
|
20
21
|
editor.chain().focus().deleteRange(range).setNode('heading', { level: 2 }).run();
|
|
21
22
|
}
|
|
22
23
|
},
|
|
23
24
|
{
|
|
24
25
|
icon: 'title',
|
|
25
|
-
title: '
|
|
26
|
-
subtitle: '
|
|
26
|
+
title: i18n('title') + ' 3',
|
|
27
|
+
subtitle: i18n('title3Info'),
|
|
27
28
|
command: ({ editor, range }) => {
|
|
28
29
|
editor.chain().focus().deleteRange(range).setNode('heading', { level: 3 }).run();
|
|
29
30
|
}
|
|
30
31
|
},
|
|
31
32
|
{
|
|
32
33
|
icon: 'format_list_bulleted',
|
|
33
|
-
title: '
|
|
34
|
-
subtitle: '
|
|
34
|
+
title: i18n('unorderedList'),
|
|
35
|
+
subtitle: i18n('unorderedListInfo'),
|
|
35
36
|
command: ({ editor, range }) => {
|
|
36
37
|
editor.commands.deleteRange(range);
|
|
37
38
|
editor.commands.toggleBulletList();
|
|
@@ -39,8 +40,8 @@ export default {
|
|
|
39
40
|
},
|
|
40
41
|
{
|
|
41
42
|
icon: 'format_list_numbered',
|
|
42
|
-
title: '
|
|
43
|
-
subtitle: '
|
|
43
|
+
title: i18n('numberList'),
|
|
44
|
+
subtitle: i18n('numberListInfo'),
|
|
44
45
|
command: ({ editor, range }) => {
|
|
45
46
|
editor.commands.deleteRange(range);
|
|
46
47
|
editor.commands.toggleOrderedList();
|
|
@@ -49,19 +50,19 @@ export default {
|
|
|
49
50
|
]
|
|
50
51
|
},
|
|
51
52
|
{
|
|
52
|
-
section: '
|
|
53
|
+
section: i18n('block'), list: [
|
|
53
54
|
{
|
|
54
55
|
icon: 'code',
|
|
55
|
-
title: '
|
|
56
|
-
subtitle: '
|
|
56
|
+
title: i18n('codeBlock'),
|
|
57
|
+
subtitle: i18n('codeBlockInfo'),
|
|
57
58
|
command: ({ editor, range }) => {
|
|
58
59
|
editor.chain().focus().deleteRange(range).setNode('codeBlock').run();
|
|
59
60
|
}
|
|
60
61
|
},
|
|
61
62
|
{
|
|
62
63
|
icon: 'functions',
|
|
63
|
-
title: '
|
|
64
|
-
subtitle: '
|
|
64
|
+
title: i18n('mathBlock'),
|
|
65
|
+
subtitle: i18n('mathBlockInfo'),
|
|
65
66
|
command: ({ editor, range }) => {
|
|
66
67
|
const { to } = range;
|
|
67
68
|
editor.chain().focus().deleteRange(range).setNode('math_display').focus().run();
|
|
@@ -69,16 +70,43 @@ export default {
|
|
|
69
70
|
},
|
|
70
71
|
{
|
|
71
72
|
icon: 'table_chart',
|
|
72
|
-
title: '
|
|
73
|
-
subtitle: '
|
|
73
|
+
title: i18n('table'),
|
|
74
|
+
subtitle: i18n('tableInfo'),
|
|
74
75
|
command: ({ editor, range }) => {
|
|
75
76
|
editor.chain().focus().insertTable({ rows: 2, cols: 3 }).run();
|
|
76
77
|
}
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
icon: 'format_quote',
|
|
81
|
+
title: i18n('blockquote'),
|
|
82
|
+
subtitle: i18n('blockquoteInfo'),
|
|
83
|
+
command: ({ editor, range }) => {
|
|
84
|
+
editor.chain().focus().deleteRange(range).setBlockquote().focus().run();
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
icon: 'iframe',
|
|
89
|
+
title: i18n('iframe'),
|
|
90
|
+
subtitle: i18n('iframeInfo'),
|
|
91
|
+
command: ({ editor, range }) => {
|
|
92
|
+
slashDetail.set('iframe');
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
icon: 'youtube_activity',
|
|
97
|
+
title: i18n('youtube'),
|
|
98
|
+
subtitle: i18n('youtubeInfo'),
|
|
99
|
+
command: ({ editor, range }) => {
|
|
100
|
+
slashDetail.set('youtube');
|
|
101
|
+
}
|
|
77
102
|
}
|
|
78
103
|
]
|
|
79
104
|
}
|
|
80
105
|
];
|
|
81
|
-
const filtered = raw.map(({ section, list }) => ({
|
|
106
|
+
const filtered = raw.map(({ section, list }) => ({
|
|
107
|
+
section, list: list.filter((item) => item.title.toLowerCase().includes(query.toLowerCase())
|
|
108
|
+
|| item.subtitle.toLowerCase().includes(query.toLowerCase()))
|
|
109
|
+
})).filter(({ list }) => list.length > 0);
|
|
82
110
|
return filtered;
|
|
83
111
|
},
|
|
84
112
|
render: () => {
|
|
@@ -91,6 +119,7 @@ export default {
|
|
|
91
119
|
slashVisible.set(true);
|
|
92
120
|
slashLocaltion.set({ x: location.x, y: location.y, height: location.height });
|
|
93
121
|
slashItems.set(props.items);
|
|
122
|
+
slashDetail.set(null);
|
|
94
123
|
},
|
|
95
124
|
onUpdate(props) {
|
|
96
125
|
slashItems.set(props.items);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Node } from "@tiptap/core";
|
|
2
|
+
export interface VideoPlayerOptions {
|
|
3
|
+
HTMLAttributes: Record<string, any>;
|
|
4
|
+
}
|
|
5
|
+
declare module "@tiptap/core" {
|
|
6
|
+
interface Commands<ReturnType> {
|
|
7
|
+
videoPlayer: {
|
|
8
|
+
insertVideoPlayer: (options: {
|
|
9
|
+
url: string;
|
|
10
|
+
}) => ReturnType;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
declare const _default: Node<VideoPlayerOptions, any>;
|
|
15
|
+
export default _default;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Node, mergeAttributes } from "@tiptap/core";
|
|
2
|
+
import { Plugin, PluginKey } from "prosemirror-state";
|
|
3
|
+
const youtubeRegExp = /^(?:(?:https?:)?\/\/)?(?:www\.)?(?:m\.)?(?:youtu(?:be)?\.com\/(?:v\/|embed\/|watch(?:\/|\?v=))|youtu\.be\/)((?:\w|-){11})(?:\S+)?$/;
|
|
4
|
+
const youtubeExtractId = (url) => {
|
|
5
|
+
const match = youtubeRegExp.exec(url.trim());
|
|
6
|
+
return match ? match[1] : false;
|
|
7
|
+
};
|
|
8
|
+
const videoPlayerStaticAttributes = { nocookie: true };
|
|
9
|
+
export default Node.create({
|
|
10
|
+
name: "lite-youtube",
|
|
11
|
+
content: "",
|
|
12
|
+
marks: "",
|
|
13
|
+
group: "block",
|
|
14
|
+
draggable: true,
|
|
15
|
+
addAttributes() {
|
|
16
|
+
return {
|
|
17
|
+
videoid: {
|
|
18
|
+
default: null,
|
|
19
|
+
},
|
|
20
|
+
provider: {
|
|
21
|
+
default: "youtube",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
parseHTML() {
|
|
26
|
+
return [
|
|
27
|
+
{
|
|
28
|
+
tag: "lite-youtube",
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
},
|
|
32
|
+
renderHTML({ HTMLAttributes }) {
|
|
33
|
+
return [
|
|
34
|
+
"lite-youtube",
|
|
35
|
+
mergeAttributes(videoPlayerStaticAttributes, this.options.HTMLAttributes, HTMLAttributes),
|
|
36
|
+
];
|
|
37
|
+
},
|
|
38
|
+
addCommands() {
|
|
39
|
+
return {
|
|
40
|
+
insertVideoPlayer: (options) => ({ chain, editor }) => {
|
|
41
|
+
const { url } = options;
|
|
42
|
+
const videoid = youtubeExtractId(url);
|
|
43
|
+
if (videoid) {
|
|
44
|
+
const { selection } = editor.state;
|
|
45
|
+
const pos = selection.$head;
|
|
46
|
+
return chain().insertContentAt(pos.before(), [{
|
|
47
|
+
type: this.name,
|
|
48
|
+
attrs: { videoid, provider: "youtube" },
|
|
49
|
+
}]).run();
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
addProseMirrorPlugins() {
|
|
56
|
+
return [
|
|
57
|
+
new Plugin({
|
|
58
|
+
key: new PluginKey("handlePasteVideoURL"),
|
|
59
|
+
props: {
|
|
60
|
+
handlePaste: (view, _event, slice) => {
|
|
61
|
+
if (slice.content.childCount !== 1)
|
|
62
|
+
return false;
|
|
63
|
+
const { state } = view;
|
|
64
|
+
const { selection } = state;
|
|
65
|
+
const { empty } = selection;
|
|
66
|
+
if (!empty)
|
|
67
|
+
return false;
|
|
68
|
+
const pos = selection.$head;
|
|
69
|
+
const node = pos.node();
|
|
70
|
+
if (node.content.size > 0)
|
|
71
|
+
return false;
|
|
72
|
+
let textContent = "", href = "";
|
|
73
|
+
slice.content.forEach((node) => {
|
|
74
|
+
textContent += node.textContent;
|
|
75
|
+
});
|
|
76
|
+
textContent = textContent.trim();
|
|
77
|
+
let videoid = youtubeExtractId(textContent);
|
|
78
|
+
if (!videoid)
|
|
79
|
+
for (const mark of slice?.content?.content?.[0]?.marks) {
|
|
80
|
+
if (mark.attrs.href) {
|
|
81
|
+
const id = youtubeExtractId(mark.attrs.href);
|
|
82
|
+
if (id) {
|
|
83
|
+
videoid = id;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (!videoid)
|
|
89
|
+
return false;
|
|
90
|
+
this.editor.chain().insertContentAt(pos.before(), [{
|
|
91
|
+
type: this.name,
|
|
92
|
+
attrs: { videoid, provider: "youtube" },
|
|
93
|
+
}]).run();
|
|
94
|
+
return true;
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
];
|
|
99
|
+
},
|
|
100
|
+
});
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
<script>import { BubbleMenu } from "svelte-tiptap";
|
|
2
|
-
import { getContext } from "svelte";
|
|
2
|
+
import { getContext, tick } from "svelte";
|
|
3
3
|
import 'tippy.js/animations/shift-away-subtle.css';
|
|
4
4
|
import ToolbarButton from "./ToolbarButton.svelte";
|
|
5
5
|
import { isTableAnySelected } from "../plugin/table/util";
|
|
6
6
|
import deleteTable from "../plugin/table/deleteTable";
|
|
7
|
-
//@ts-ignore
|
|
8
|
-
import { MathInline } from "@seorii/prosemirror-math/tiptap";
|
|
9
7
|
import setMath from "./setMath";
|
|
8
|
+
import { Button, Icon, IconButton, Input, List, OneLine, Tooltip } from "nunui";
|
|
9
|
+
import i18n from "../i18n";
|
|
10
10
|
const tiptap = getContext('editor');
|
|
11
|
+
let link = false, href = '', sel = '', _sel = '';
|
|
11
12
|
$: selection = $tiptap?.state?.selection;
|
|
12
13
|
$: table = isTableAnySelected(selection);
|
|
14
|
+
$: sel = selection?.from + '-' + selection?.to;
|
|
15
|
+
$: if ($tiptap && sel !== _sel) {
|
|
16
|
+
_sel = sel;
|
|
17
|
+
link = false;
|
|
18
|
+
href = $tiptap.getAttributes('link').href;
|
|
19
|
+
}
|
|
20
|
+
$: if ($tiptap) {
|
|
21
|
+
if (href)
|
|
22
|
+
$tiptap.chain().setLink({ href }).run();
|
|
23
|
+
else
|
|
24
|
+
$tiptap.chain().unsetLink().run();
|
|
25
|
+
}
|
|
13
26
|
</script>
|
|
14
27
|
|
|
15
28
|
|
|
@@ -20,7 +33,25 @@ $: table = isTableAnySelected(selection);
|
|
|
20
33
|
<slot/>
|
|
21
34
|
{:else}
|
|
22
35
|
<main>
|
|
23
|
-
{#if
|
|
36
|
+
{#if link}
|
|
37
|
+
<div class="link">
|
|
38
|
+
<p>
|
|
39
|
+
<Icon icon="link"/>
|
|
40
|
+
{i18n('link')}
|
|
41
|
+
</p>
|
|
42
|
+
<Input placeholder="url" fullWidth bind:value={href} autofocus/>
|
|
43
|
+
<div>
|
|
44
|
+
<Button tabindex="0" transparent small on:click={() => {
|
|
45
|
+
href = ''
|
|
46
|
+
$tiptap.chain().focus().unsetLink().run()
|
|
47
|
+
tick().then(() => link = false)
|
|
48
|
+
}}>{i18n('delete')}
|
|
49
|
+
</Button>
|
|
50
|
+
<Button tabindex="0" transparent small
|
|
51
|
+
on:click={() => link = false}>{i18n('close')}</Button>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
{:else if table}
|
|
24
55
|
{#if table.length > 1}
|
|
25
56
|
<ToolbarButton icon="cell_merge" handler={() => $tiptap.commands.mergeCells()}/>
|
|
26
57
|
{:else}
|
|
@@ -36,12 +67,34 @@ $: table = isTableAnySelected(selection);
|
|
|
36
67
|
handler={() => $tiptap.chain().focus().addRowAfter().run()}/>
|
|
37
68
|
<ToolbarButton icon="close" handler={() => deleteTable({editor: $tiptap})}/>
|
|
38
69
|
{:else}
|
|
70
|
+
<Tooltip bottom left xstack width="160px">
|
|
71
|
+
<IconButton size="1.2em" icon="format_align_left" slot="target"/>
|
|
72
|
+
<div style="margin: -6px;font-size: 0.6em">
|
|
73
|
+
<List>
|
|
74
|
+
<OneLine icon="format_align_left" title={i18n('alignLeft')}
|
|
75
|
+
on:click={() => $tiptap.chain().focus().setTextAlign('left').run()}
|
|
76
|
+
active={$tiptap.isActive({ textAlign: 'left' })}/>
|
|
77
|
+
<OneLine icon="format_align_center" title={i18n('alignCenter')}
|
|
78
|
+
on:click={() => $tiptap.chain().focus().setTextAlign('center').run()}
|
|
79
|
+
active={$tiptap.isActive({ textAlign: 'center' })}/>
|
|
80
|
+
<OneLine icon="format_align_right" title={i18n('alignRight')}
|
|
81
|
+
on:click={() => $tiptap.chain().focus().setTextAlign('right').run()}
|
|
82
|
+
active={$tiptap.isActive({ textAlign: 'right' })}/>
|
|
83
|
+
<OneLine icon="format_align_justify" title={i18n('alignJustify')}
|
|
84
|
+
on:click={() => $tiptap.chain().focus().setTextAlign('justify').run()}
|
|
85
|
+
active={$tiptap.isActive({ textAlign: 'justify' })}/>
|
|
86
|
+
</List>
|
|
87
|
+
</div>
|
|
88
|
+
</Tooltip>
|
|
39
89
|
<ToolbarButton icon="format_bold" prop="bold"/>
|
|
40
90
|
<ToolbarButton icon="format_italic" prop="italic"/>
|
|
41
91
|
<ToolbarButton icon="format_strikethrough" prop="strike"/>
|
|
42
92
|
<ToolbarButton icon="format_underlined" prop="underline"/>
|
|
93
|
+
<ToolbarButton icon="superscript" prop="superscript"/>
|
|
94
|
+
<ToolbarButton icon="subscript" prop="subscript"/>
|
|
43
95
|
<ToolbarButton icon="functions" handler={() => setMath($tiptap)}/>
|
|
44
96
|
<ToolbarButton icon="code" prop="code"/>
|
|
97
|
+
<ToolbarButton icon="link" prop="link" handler={() => link = true}/>
|
|
45
98
|
{/if}
|
|
46
99
|
</main>
|
|
47
100
|
{/if}
|
|
@@ -50,7 +103,8 @@ $: table = isTableAnySelected(selection);
|
|
|
50
103
|
|
|
51
104
|
<style>main {
|
|
52
105
|
box-shadow: var(--shadow);
|
|
53
|
-
background: var(--surface);
|
|
106
|
+
background: var(--surface, #fff);
|
|
107
|
+
color: var(--on-surface, #000);
|
|
54
108
|
padding: 8px;
|
|
55
109
|
border-radius: 4px;
|
|
56
110
|
display: flex;
|
|
@@ -66,4 +120,18 @@ main > :global(*):first-child {
|
|
|
66
120
|
}
|
|
67
121
|
main > :global(*):last-child {
|
|
68
122
|
margin-right: 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.link {
|
|
126
|
+
display: flex;
|
|
127
|
+
flex-direction: column;
|
|
128
|
+
font-size: 0.7em;
|
|
129
|
+
}
|
|
130
|
+
.link p {
|
|
131
|
+
margin: 0 0 0.6em 0;
|
|
132
|
+
}
|
|
133
|
+
.link div {
|
|
134
|
+
margin-top: 0.6em;
|
|
135
|
+
display: flex;
|
|
136
|
+
justify-content: flex-end;
|
|
69
137
|
}</style>
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
<script>import { List, TwoLine } from "nunui";
|
|
1
|
+
<script>import { Button, IconButton, Input, List, TwoLine } from "nunui";
|
|
2
2
|
import { getContext } from "svelte";
|
|
3
|
-
import { slashVisible, slashItems, slashLocaltion, slashProps } from '../plugin/command/stores';
|
|
3
|
+
import { slashVisible, slashItems, slashLocaltion, slashProps, slashDetail } from '../plugin/command/stores';
|
|
4
4
|
import { fly, slide } from "svelte/transition";
|
|
5
5
|
import { quartOut } from "svelte/easing";
|
|
6
|
+
import i18n from "../i18n";
|
|
6
7
|
const tiptap = getContext('editor');
|
|
7
8
|
export let selectedIndex = 0;
|
|
8
9
|
let height = 0, elements = [];
|
|
10
|
+
let iframe = '';
|
|
11
|
+
$: if ($slashVisible) {
|
|
12
|
+
iframe = '';
|
|
13
|
+
}
|
|
9
14
|
</script>
|
|
10
15
|
|
|
11
16
|
<svelte:window bind:innerHeight={height}/>
|
|
@@ -15,27 +20,67 @@ let height = 0, elements = [];
|
|
|
15
20
|
<main style="left: {$slashLocaltion.x}px; top: {$slashLocaltion.y + $slashLocaltion.height + 384 > height
|
|
16
21
|
? $slashLocaltion.y - $slashLocaltion.height - 384
|
|
17
22
|
: $slashLocaltion.y + $slashLocaltion.height}px;" transition:fly={{y: 10, duration: 200, easing: quartOut}}>
|
|
18
|
-
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
<
|
|
22
|
-
<div
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
{#if $slashDetail === 'iframe'}
|
|
24
|
+
<div class="detail">
|
|
25
|
+
<header>
|
|
26
|
+
<IconButton icon="arrow_back" on:click={() => $slashDetail = ''}/>
|
|
27
|
+
<div class="title">iframe</div>
|
|
28
|
+
</header>
|
|
29
|
+
<Input placeholder="url" fullWidth bind:value={iframe} autofocus
|
|
30
|
+
on:submit={() => $tiptap.commands.insertContent({type: 'iframe', attrs: {src: iframe}})}/>
|
|
31
|
+
<footer>
|
|
32
|
+
<Button tabindex="0" transparent small on:click={() => {
|
|
33
|
+
iframe = ''
|
|
34
|
+
$slashDetail = ''
|
|
35
|
+
}}>{i18n('cancel')}
|
|
36
|
+
</Button>
|
|
37
|
+
<Button tabindex="0" transparent small
|
|
38
|
+
on:click={() => $tiptap.commands.insertContent({type: 'iframe', attrs: {src: iframe}})}>{i18n('insert')}
|
|
39
|
+
</Button>
|
|
40
|
+
</footer>
|
|
41
|
+
</div>
|
|
42
|
+
{:else if $slashDetail === 'youtube'}
|
|
43
|
+
<div class="detail">
|
|
44
|
+
<header>
|
|
45
|
+
<IconButton icon="arrow_back" on:click={() => $slashDetail = ''}/>
|
|
46
|
+
<div class="title">Youtube</div>
|
|
47
|
+
</header>
|
|
48
|
+
<Input placeholder="url" fullWidth bind:value={iframe} autofocus
|
|
49
|
+
on:submit={() => $tiptap.commands.insertVideoPlayer({url: iframe})}/>
|
|
50
|
+
<footer>
|
|
51
|
+
<Button tabindex="0" transparent small on:click={() => {
|
|
52
|
+
iframe = ''
|
|
53
|
+
$slashDetail = ''
|
|
54
|
+
}}>{i18n('cancel')}
|
|
55
|
+
</Button>
|
|
56
|
+
<Button tabindex="0" transparent small
|
|
57
|
+
on:click={() => $tiptap.commands.insertVideoPlayer({url: iframe})}>{i18n('insert')}
|
|
58
|
+
</Button>
|
|
59
|
+
</footer>
|
|
60
|
+
</div>
|
|
61
|
+
{:else}
|
|
62
|
+
<div class="list">
|
|
63
|
+
<List>
|
|
64
|
+
{#each $slashItems as {section, list}(section)}
|
|
65
|
+
<div class="section" transition:slide={{duration: 400, easing: quartOut}}>{section}</div>
|
|
66
|
+
<div transition:slide={{duration: 400, easing: quartOut}}>
|
|
67
|
+
{#each list || [] as {title, subtitle, icon, command, section}, i(title)}
|
|
68
|
+
<div transition:slide={{duration: 400, easing: quartOut}}>
|
|
69
|
+
<TwoLine on:mouseenter={() => (selectedIndex = i)} on:click={() => {
|
|
27
70
|
command?.($slashProps);
|
|
28
71
|
setTimeout(() => $tiptap.commands.focus());
|
|
29
72
|
}} bind:this={elements[i]} {icon} {title} subtitle={subtitle || ''}/>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
73
|
+
</div>
|
|
74
|
+
{/each}
|
|
75
|
+
</div>
|
|
76
|
+
{/each}
|
|
77
|
+
{#if !$slashItems.length}
|
|
78
|
+
<div class="section"
|
|
79
|
+
transition:slide={{duration: 400, easing: quartOut}}>{i18n('noResult')}</div>
|
|
80
|
+
{/if}
|
|
81
|
+
</List>
|
|
82
|
+
</div>
|
|
83
|
+
{/if}
|
|
39
84
|
</main>
|
|
40
85
|
{/if}
|
|
41
86
|
|
|
@@ -50,7 +95,7 @@ let height = 0, elements = [];
|
|
|
50
95
|
}
|
|
51
96
|
|
|
52
97
|
main {
|
|
53
|
-
position:
|
|
98
|
+
position: fixed;
|
|
54
99
|
background: var(--surface, #fff);
|
|
55
100
|
width: 220px;
|
|
56
101
|
border-radius: 4px;
|
|
@@ -79,4 +124,29 @@ main {
|
|
|
79
124
|
font-size: 0.8em !important;
|
|
80
125
|
font-weight: 300 !important;
|
|
81
126
|
color: var(--primary-dark1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.detail {
|
|
130
|
+
font-size: 0.8em;
|
|
131
|
+
padding: 8px;
|
|
132
|
+
display: flex;
|
|
133
|
+
flex-direction: column;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
header {
|
|
137
|
+
display: flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
margin-bottom: 6px;
|
|
140
|
+
}
|
|
141
|
+
header > :global(*) {
|
|
142
|
+
margin-right: 8px;
|
|
143
|
+
}
|
|
144
|
+
header > :global(*):last-child {
|
|
145
|
+
margin-right: 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
footer {
|
|
149
|
+
margin-top: 0.6em;
|
|
150
|
+
display: flex;
|
|
151
|
+
justify-content: flex-end;
|
|
82
152
|
}</style>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
<script>import {
|
|
1
|
+
<script>import { FloatingMenu } from "svelte-tiptap";
|
|
2
2
|
import { getContext } from "svelte";
|
|
3
|
-
import {
|
|
3
|
+
import { IconButton, List, OneLine, Tooltip } from "nunui";
|
|
4
4
|
import ToolbarButton from "./ToolbarButton.svelte";
|
|
5
|
-
import
|
|
5
|
+
import i18n from "../i18n";
|
|
6
6
|
const tiptap = getContext('editor');
|
|
7
7
|
</script>
|
|
8
8
|
|
|
@@ -11,16 +11,16 @@ const tiptap = getContext('editor');
|
|
|
11
11
|
tippyOptions={{animation:'fade', duration: [200, 50]}}>
|
|
12
12
|
<main on:mousedown={() => setTimeout(() => $tiptap.commands.focus())}>
|
|
13
13
|
<span>
|
|
14
|
-
|
|
14
|
+
{i18n('newLineInfo')}
|
|
15
15
|
</span>
|
|
16
16
|
<Tooltip bottom left xstack width="160px">
|
|
17
17
|
<IconButton size="1.2em" icon="text_fields" slot="target"/>
|
|
18
18
|
<div style="margin: -6px">
|
|
19
19
|
<List>
|
|
20
|
-
<OneLine title="
|
|
21
|
-
<OneLine title="
|
|
22
|
-
<OneLine title="
|
|
23
|
-
<OneLine
|
|
20
|
+
<OneLine icon="counter_1" title="{i18n('title')} 1" on:click={() => $tiptap.commands.setHeading({level: 1})}/>
|
|
21
|
+
<OneLine icon="counter_2" title="{i18n('title')} 2" on:click={() => $tiptap.commands.setHeading({level: 2})}/>
|
|
22
|
+
<OneLine icon="counter_3" title="{i18n('title')} 3" on:click={() => $tiptap.commands.setHeading({level: 3})}/>
|
|
23
|
+
<OneLine icon="segment" title={i18n('paragraph')} on:click={() => $tiptap.commands.setParagraph()}/>
|
|
24
24
|
</List>
|
|
25
25
|
</div>
|
|
26
26
|
</Tooltip>
|
|
@@ -7,8 +7,9 @@ import Bubble from "./Bubble.svelte";
|
|
|
7
7
|
import Floating from "./Floating.svelte";
|
|
8
8
|
import Command from "./Command.svelte";
|
|
9
9
|
import { slashItems, slashProps, slashVisible } from "../plugin/command/stores";
|
|
10
|
+
import i18n from "../i18n";
|
|
10
11
|
const san = (body) => sanitizeHtml(body, {
|
|
11
|
-
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'math-inline', 'math-node', 'iframe', 'tiptap-file']),
|
|
12
|
+
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'math-inline', 'math-node', 'iframe', 'tiptap-file', 'lite-youtube', 'blockquote']),
|
|
12
13
|
allowedStyles: '*', allowedAttributes: {
|
|
13
14
|
'*': ['style', 'class'],
|
|
14
15
|
a: ['href', 'name', 'target'],
|
|
@@ -16,23 +17,24 @@ const san = (body) => sanitizeHtml(body, {
|
|
|
16
17
|
iframe: ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
|
|
17
18
|
th: ['colwidth', 'colspan', 'rowspan'],
|
|
18
19
|
td: ['colwidth', 'colspan', 'rowspan'],
|
|
19
|
-
'tiptap-file': ['id']
|
|
20
|
+
'tiptap-file': ['id'],
|
|
21
|
+
'lite-youtube': ['videoid', 'params', 'nocookie', 'title', 'provider'],
|
|
20
22
|
},
|
|
21
23
|
});
|
|
22
|
-
export let body = '',
|
|
24
|
+
export let body = '', editable = false, style = '', ref = null, options = {};
|
|
23
25
|
const tiptap = setContext('editor', writable(null));
|
|
24
26
|
let element, fullscreen = false, mounted = false, last = '';
|
|
25
27
|
$: ref = $tiptap;
|
|
26
|
-
$: $tiptap && $tiptap.setEditable(
|
|
28
|
+
$: $tiptap && $tiptap.setEditable(editable);
|
|
27
29
|
if (browser) {
|
|
28
30
|
onMount(() => {
|
|
29
31
|
body = last = san(body);
|
|
30
32
|
mounted = true;
|
|
31
|
-
import('./tiptap').then(({ default: tt }) => {
|
|
33
|
+
Promise.all([import('./tiptap'), import("@justinribeiro/lite-youtube")]).then(([{ default: tt }]) => {
|
|
32
34
|
if (!mounted)
|
|
33
35
|
return;
|
|
34
36
|
$tiptap = tt(element, body, {
|
|
35
|
-
editable:
|
|
37
|
+
editable: editable,
|
|
36
38
|
onTransaction: () => $tiptap = $tiptap,
|
|
37
39
|
...options,
|
|
38
40
|
});
|
|
@@ -81,19 +83,19 @@ function selectItem(index) {
|
|
|
81
83
|
const item = $slashItems[index];
|
|
82
84
|
if (item) {
|
|
83
85
|
let range = $slashProps.range;
|
|
84
|
-
item.command({ editor, range });
|
|
86
|
+
item.command({ editor: editable, range });
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
89
|
</script>
|
|
88
90
|
|
|
89
|
-
<main class:fullscreen class:
|
|
91
|
+
<main class:fullscreen class:editable>
|
|
90
92
|
<div class="wrapper">
|
|
91
93
|
<div bind:this={element} class="target" on:keydown|capture={handleKeydown}></div>
|
|
92
94
|
{#if !$tiptap}
|
|
93
|
-
|
|
95
|
+
{i18n('loading')}
|
|
94
96
|
{/if}
|
|
95
97
|
</div>
|
|
96
|
-
{#if
|
|
98
|
+
{#if editable}
|
|
97
99
|
<Command {selectedIndex}/>
|
|
98
100
|
<Floating/>
|
|
99
101
|
{#if $$slots.bubble}
|
|
@@ -138,12 +140,12 @@ main .wrapper {
|
|
|
138
140
|
margin-bottom: 0 !important;
|
|
139
141
|
}
|
|
140
142
|
|
|
141
|
-
.
|
|
143
|
+
.editable :global(.ProseMirror-selectednode img) {
|
|
142
144
|
transition: all 0.2s ease-in-out;
|
|
143
145
|
filter: drop-shadow(0 0 0.75rem var(--primary-light13));
|
|
144
146
|
}
|
|
145
147
|
|
|
146
|
-
.
|
|
148
|
+
.editable :global(.iframe-wrapper.ProseMirror-selectednode) {
|
|
147
149
|
outline: 3px solid var(--primary);
|
|
148
150
|
}
|
|
149
151
|
|
|
@@ -157,6 +159,9 @@ div > :global(div) :global(.ProseMirror) :global(p.is-editor-empty:first-child::
|
|
|
157
159
|
height: 0;
|
|
158
160
|
pointer-events: none;
|
|
159
161
|
}
|
|
162
|
+
div > :global(div) :global(a) {
|
|
163
|
+
cursor: pointer;
|
|
164
|
+
}
|
|
160
165
|
div > :global(div) :global(img) {
|
|
161
166
|
transition: all 0.2s ease-in-out;
|
|
162
167
|
max-width: 100%;
|
package/dist/tiptap/tiptap.js
CHANGED
|
@@ -22,16 +22,35 @@ import TextStyle from '@tiptap/extension-text-style';
|
|
|
22
22
|
import Iframe from "../plugin/iframe";
|
|
23
23
|
// @ts-ignore
|
|
24
24
|
import { MathInline, MathBlock } from "@seorii/prosemirror-math/tiptap";
|
|
25
|
+
import Youtube from "../plugin/youtube";
|
|
25
26
|
import command from "../plugin/command";
|
|
26
27
|
export default (element, content, { plugins = [], ...props } = {}) => new Editor({
|
|
27
28
|
element, content, ...props,
|
|
28
29
|
extensions: [
|
|
29
|
-
CodeBlockLowlight.
|
|
30
|
+
CodeBlockLowlight.extend({
|
|
31
|
+
addKeyboardShortcuts() {
|
|
32
|
+
return {
|
|
33
|
+
...this.parent?.(),
|
|
34
|
+
'Tab': () => {
|
|
35
|
+
if (this.editor.isActive('codeBlock')) {
|
|
36
|
+
return this.editor.commands.insertContent(' ');
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}).configure({ lowlight }),
|
|
30
43
|
Image,
|
|
44
|
+
Youtube,
|
|
31
45
|
StarterKit,
|
|
32
46
|
Underline,
|
|
33
47
|
Highlight.configure({ multicolor: true }),
|
|
34
|
-
Link.configure({
|
|
48
|
+
Link.configure({
|
|
49
|
+
openOnClick: true, protocols: ['ftp', 'mailto', {
|
|
50
|
+
scheme: 'tel',
|
|
51
|
+
optionalSlashes: true
|
|
52
|
+
}]
|
|
53
|
+
}),
|
|
35
54
|
TextAlign.configure({ types: ['heading', 'paragraph', 'image'] }),
|
|
36
55
|
DropCursor,
|
|
37
56
|
orderedlist,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seorii/tiptap",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "vite dev",
|
|
6
6
|
"build": "svelte-kit sync && svelte-package",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"type": "module",
|
|
38
38
|
"dependencies": {
|
|
39
|
+
"@justinribeiro/lite-youtube": "^1.5.0",
|
|
39
40
|
"@seorii/prosemirror-math": "^0.4.2",
|
|
40
41
|
"@tiptap/core": "^2.0.4",
|
|
41
42
|
"@tiptap/extension-code": "^2.0.4",
|