@seorii/tiptap 0.1.1 → 0.2.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/plugin/command/emoji.d.ts +20 -0
- package/dist/plugin/command/emoji.js +81 -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 +6 -1
- package/dist/plugin/command/suggest.js +68 -11
- package/dist/plugin/image/dragdrop.d.ts +4 -0
- package/dist/plugin/image/dragdrop.js +86 -0
- package/dist/plugin/{image.js → image/index.js} +5 -1
- package/dist/plugin/table/style/grip.scss +1 -1
- package/dist/plugin/table/style/resize.scss +2 -2
- package/dist/plugin/table/style/table.scss +1 -1
- package/dist/plugin/table/style.scss +0 -1
- package/dist/tiptap/Command.svelte +43 -17
- package/dist/tiptap/TipTap.svelte +13 -8
- package/dist/tiptap/TipTap.svelte.d.ts +6 -2
- package/dist/tiptap/tiptap.js +56 -51
- package/package.json +3 -1
- package/dist/plugin/command/index.d.ts +0 -4
- package/dist/plugin/command/index.js +0 -27
- package/dist/plugin/table/style/theme.scss +0 -6
- /package/dist/plugin/{image.d.ts → image/index.d.ts} +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { PluginKey } from "prosemirror-state";
|
|
2
|
+
import type { Editor } from "@tiptap/core";
|
|
3
|
+
export declare const emoji: {
|
|
4
|
+
pluginKey: PluginKey<any>;
|
|
5
|
+
char: string;
|
|
6
|
+
items: ({ query }: {
|
|
7
|
+
query: any;
|
|
8
|
+
}) => {
|
|
9
|
+
title: string;
|
|
10
|
+
command: {};
|
|
11
|
+
}[];
|
|
12
|
+
render: () => {
|
|
13
|
+
onStart: (props: any) => void;
|
|
14
|
+
onUpdate(props: any): void;
|
|
15
|
+
onKeyDown(props: any): true | undefined;
|
|
16
|
+
onExit(): void;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
declare const _default: (editor: Editor) => import("prosemirror-state").Plugin<any>;
|
|
20
|
+
export default _default;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { slashVisible, slashItems, slashLocaltion, slashProps, slashDetail } from './stores';
|
|
2
|
+
import { PluginKey } from "prosemirror-state";
|
|
3
|
+
import Suggestion from "@tiptap/suggestion";
|
|
4
|
+
//@ts-ignore
|
|
5
|
+
import emojis from 'emojis-list';
|
|
6
|
+
//@ts-ignore
|
|
7
|
+
import tags from 'emojis-keywords';
|
|
8
|
+
const max = 10;
|
|
9
|
+
function fixRange(editor, range, split = '/') {
|
|
10
|
+
const { state } = editor.view, { selection, doc } = state;
|
|
11
|
+
if (selection.$to.nodeBefore?.text?.includes?.(split)) {
|
|
12
|
+
range.from = range.to;
|
|
13
|
+
while (range.from > 0 && doc.textBetween(range.from - 1, range.from) !== split) {
|
|
14
|
+
try {
|
|
15
|
+
range.from -= 1;
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
range.from += 2;
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
range.from -= 1;
|
|
23
|
+
}
|
|
24
|
+
while (range.to < selection.to && doc.textBetween(range.to, range.to + 1) !== ' ') {
|
|
25
|
+
try {
|
|
26
|
+
range.to += 1;
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
range.to -= 1;
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return range;
|
|
34
|
+
}
|
|
35
|
+
export const emoji = {
|
|
36
|
+
pluginKey: new PluginKey('slash-emoji'),
|
|
37
|
+
char: ':',
|
|
38
|
+
items: ({ query }) => {
|
|
39
|
+
query = ':' + query.toLowerCase();
|
|
40
|
+
const filtered = [];
|
|
41
|
+
for (let i = 0; i < emojis.length; i++) {
|
|
42
|
+
if (tags[i]?.includes?.(query))
|
|
43
|
+
filtered.push({
|
|
44
|
+
title: emojis[i] + ' ' + tags[i],
|
|
45
|
+
command: ({ editor, range }) => {
|
|
46
|
+
editor.chain().deleteRange(fixRange(editor, range, ':')).insertContent(emojis[i]).run();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
if (filtered.length >= max)
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
return filtered;
|
|
53
|
+
},
|
|
54
|
+
render: () => {
|
|
55
|
+
return {
|
|
56
|
+
onStart: (props) => {
|
|
57
|
+
let editor = props.editor;
|
|
58
|
+
let range = props.range;
|
|
59
|
+
let location = props.clientRect();
|
|
60
|
+
slashProps.set({ editor, range });
|
|
61
|
+
slashVisible.set(true);
|
|
62
|
+
slashLocaltion.set({ x: location.x, y: location.y, height: location.height });
|
|
63
|
+
slashItems.set(props.items);
|
|
64
|
+
slashDetail.set('emoji');
|
|
65
|
+
},
|
|
66
|
+
onUpdate(props) {
|
|
67
|
+
slashItems.set(props.items);
|
|
68
|
+
},
|
|
69
|
+
onKeyDown(props) {
|
|
70
|
+
if (props.event.key === 'Escape') {
|
|
71
|
+
slashVisible.set(false);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
onExit() {
|
|
76
|
+
slashVisible.set(false);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
export default (editor) => Suggestion({ ...emoji, editor });
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
import { PluginKey } from "prosemirror-state";
|
|
2
|
+
import { Editor } from "@tiptap/core";
|
|
3
|
+
export declare const suggest: {
|
|
4
|
+
pluginKey: PluginKey<any>;
|
|
5
|
+
char: string;
|
|
2
6
|
items: ({ query }: {
|
|
3
7
|
query: any;
|
|
4
8
|
}) => {
|
|
@@ -20,4 +24,5 @@ declare const _default: {
|
|
|
20
24
|
onExit(): void;
|
|
21
25
|
};
|
|
22
26
|
};
|
|
27
|
+
declare const _default: (editor: Editor) => import("prosemirror-state").Plugin<any>;
|
|
23
28
|
export default _default;
|
|
@@ -1,6 +1,38 @@
|
|
|
1
|
-
import { slashVisible, slashItems, slashLocaltion, slashProps, slashDetail } from './stores';
|
|
1
|
+
import { slashVisible, slashItems, slashLocaltion, slashProps, slashDetail, slashSelection } from './stores';
|
|
2
2
|
import i18n from "../../i18n";
|
|
3
|
-
|
|
3
|
+
import { fallbackUpload } from "../image/dragdrop";
|
|
4
|
+
import { PluginKey } from "prosemirror-state";
|
|
5
|
+
import { Editor } from "@tiptap/core";
|
|
6
|
+
import Suggestion from "@tiptap/suggestion";
|
|
7
|
+
function fixRange(editor, range, split = '/') {
|
|
8
|
+
const { state } = editor.view, { selection, doc } = state;
|
|
9
|
+
if (selection.$to.nodeBefore?.text?.includes?.(split)) {
|
|
10
|
+
range.from = range.to;
|
|
11
|
+
while (range.from > 0 && doc.textBetween(range.from - 1, range.from) !== split) {
|
|
12
|
+
try {
|
|
13
|
+
range.from -= 1;
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
range.from += 2;
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
range.from -= 1;
|
|
21
|
+
}
|
|
22
|
+
while (range.to < selection.to && doc.textBetween(range.to, range.to + 1) !== ' ') {
|
|
23
|
+
try {
|
|
24
|
+
range.to += 1;
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
range.to -= 1;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return range;
|
|
32
|
+
}
|
|
33
|
+
export const suggest = {
|
|
34
|
+
pluginKey: new PluginKey('slash-suggest'),
|
|
35
|
+
char: '/',
|
|
4
36
|
items: ({ query }) => {
|
|
5
37
|
const raw = [
|
|
6
38
|
{
|
|
@@ -10,7 +42,7 @@ export default {
|
|
|
10
42
|
title: i18n('title') + ' 1',
|
|
11
43
|
subtitle: i18n('title1Info'),
|
|
12
44
|
command: ({ editor, range }) => {
|
|
13
|
-
editor.chain().focus().deleteRange(range).setNode('heading', { level: 1 }).run();
|
|
45
|
+
editor.chain().focus().deleteRange(fixRange(editor, range)).setNode('heading', { level: 1 }).run();
|
|
14
46
|
}
|
|
15
47
|
},
|
|
16
48
|
{
|
|
@@ -18,7 +50,7 @@ export default {
|
|
|
18
50
|
title: i18n('title') + ' 2',
|
|
19
51
|
subtitle: i18n('title2Info'),
|
|
20
52
|
command: ({ editor, range }) => {
|
|
21
|
-
editor.chain().focus().deleteRange(range).setNode('heading', { level: 2 }).run();
|
|
53
|
+
editor.chain().focus().deleteRange(fixRange(editor, range)).setNode('heading', { level: 2 }).run();
|
|
22
54
|
}
|
|
23
55
|
},
|
|
24
56
|
{
|
|
@@ -26,7 +58,7 @@ export default {
|
|
|
26
58
|
title: i18n('title') + ' 3',
|
|
27
59
|
subtitle: i18n('title3Info'),
|
|
28
60
|
command: ({ editor, range }) => {
|
|
29
|
-
editor.chain().focus().deleteRange(range).setNode('heading', { level: 3 }).run();
|
|
61
|
+
editor.chain().focus().deleteRange(fixRange(editor, range)).setNode('heading', { level: 3 }).run();
|
|
30
62
|
}
|
|
31
63
|
},
|
|
32
64
|
{
|
|
@@ -34,7 +66,7 @@ export default {
|
|
|
34
66
|
title: i18n('unorderedList'),
|
|
35
67
|
subtitle: i18n('unorderedListInfo'),
|
|
36
68
|
command: ({ editor, range }) => {
|
|
37
|
-
editor.commands.deleteRange(range);
|
|
69
|
+
editor.commands.deleteRange(fixRange(editor, range));
|
|
38
70
|
editor.commands.toggleBulletList();
|
|
39
71
|
}
|
|
40
72
|
},
|
|
@@ -43,7 +75,7 @@ export default {
|
|
|
43
75
|
title: i18n('numberList'),
|
|
44
76
|
subtitle: i18n('numberListInfo'),
|
|
45
77
|
command: ({ editor, range }) => {
|
|
46
|
-
editor.commands.deleteRange(range);
|
|
78
|
+
editor.commands.deleteRange(fixRange(editor, range));
|
|
47
79
|
editor.commands.toggleOrderedList();
|
|
48
80
|
}
|
|
49
81
|
}
|
|
@@ -51,12 +83,34 @@ export default {
|
|
|
51
83
|
},
|
|
52
84
|
{
|
|
53
85
|
section: i18n('block'), list: [
|
|
86
|
+
{
|
|
87
|
+
icon: 'image',
|
|
88
|
+
title: i18n('image'),
|
|
89
|
+
subtitle: i18n('imageInfo'),
|
|
90
|
+
command: ({ editor, range }) => {
|
|
91
|
+
editor.chain().focus().deleteRange(fixRange(editor, range)).run();
|
|
92
|
+
const input = document.createElement('input');
|
|
93
|
+
input.type = 'file';
|
|
94
|
+
input.accept = 'image/*';
|
|
95
|
+
input.onchange = async () => {
|
|
96
|
+
if (input.files) {
|
|
97
|
+
const file = input.files[0];
|
|
98
|
+
if (file) {
|
|
99
|
+
const upload = window.__image_uploader || fallbackUpload;
|
|
100
|
+
const src = await upload(file);
|
|
101
|
+
editor.chain().focus().deleteRange(range).setImage({ src }).run();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
input.click();
|
|
106
|
+
}
|
|
107
|
+
},
|
|
54
108
|
{
|
|
55
109
|
icon: 'code',
|
|
56
110
|
title: i18n('codeBlock'),
|
|
57
111
|
subtitle: i18n('codeBlockInfo'),
|
|
58
112
|
command: ({ editor, range }) => {
|
|
59
|
-
editor.chain().focus().deleteRange(range).setNode('codeBlock').run();
|
|
113
|
+
editor.chain().focus().deleteRange(fixRange(editor, range)).setNode('codeBlock').run();
|
|
60
114
|
}
|
|
61
115
|
},
|
|
62
116
|
{
|
|
@@ -65,7 +119,7 @@ export default {
|
|
|
65
119
|
subtitle: i18n('mathBlockInfo'),
|
|
66
120
|
command: ({ editor, range }) => {
|
|
67
121
|
const { to } = range;
|
|
68
|
-
editor.chain().focus().deleteRange(range).setNode('math_display').focus().run();
|
|
122
|
+
editor.chain().focus().deleteRange(fixRange(editor, range)).setNode('math_display').focus().run();
|
|
69
123
|
}
|
|
70
124
|
},
|
|
71
125
|
{
|
|
@@ -73,7 +127,7 @@ export default {
|
|
|
73
127
|
title: i18n('table'),
|
|
74
128
|
subtitle: i18n('tableInfo'),
|
|
75
129
|
command: ({ editor, range }) => {
|
|
76
|
-
editor.chain().focus().insertTable({ rows: 2, cols: 3 }).run();
|
|
130
|
+
editor.chain().focus().deleteRange(fixRange(editor, range)).insertTable({ rows: 2, cols: 3 }).run();
|
|
77
131
|
}
|
|
78
132
|
},
|
|
79
133
|
{
|
|
@@ -81,7 +135,7 @@ export default {
|
|
|
81
135
|
title: i18n('blockquote'),
|
|
82
136
|
subtitle: i18n('blockquoteInfo'),
|
|
83
137
|
command: ({ editor, range }) => {
|
|
84
|
-
editor.chain().focus().deleteRange(range).setBlockquote().focus().run();
|
|
138
|
+
editor.chain().focus().deleteRange(fixRange(editor, range)).setBlockquote().focus().run();
|
|
85
139
|
}
|
|
86
140
|
},
|
|
87
141
|
{
|
|
@@ -89,6 +143,7 @@ export default {
|
|
|
89
143
|
title: i18n('iframe'),
|
|
90
144
|
subtitle: i18n('iframeInfo'),
|
|
91
145
|
command: ({ editor, range }) => {
|
|
146
|
+
slashSelection.set(() => editor.chain().focus().deleteRange(fixRange(editor, range)).run());
|
|
92
147
|
slashDetail.set('iframe');
|
|
93
148
|
}
|
|
94
149
|
},
|
|
@@ -97,6 +152,7 @@ export default {
|
|
|
97
152
|
title: i18n('youtube'),
|
|
98
153
|
subtitle: i18n('youtubeInfo'),
|
|
99
154
|
command: ({ editor, range }) => {
|
|
155
|
+
slashSelection.set(() => editor.chain().focus().deleteRange(fixRange(editor, range)).run());
|
|
100
156
|
slashDetail.set('youtube');
|
|
101
157
|
}
|
|
102
158
|
}
|
|
@@ -136,3 +192,4 @@ export default {
|
|
|
136
192
|
};
|
|
137
193
|
}
|
|
138
194
|
};
|
|
195
|
+
export default (editor) => Suggestion({ ...suggest, editor });
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Plugin, PluginKey } from 'prosemirror-state';
|
|
2
|
+
export const fallbackUpload = (async (image) => URL.createObjectURL(image));
|
|
3
|
+
export const dropImagePlugin = () => {
|
|
4
|
+
return new Plugin({
|
|
5
|
+
props: {
|
|
6
|
+
handleDOMEvents: {
|
|
7
|
+
paste(view, event) {
|
|
8
|
+
const upload = window.__image_uploader || fallbackUpload;
|
|
9
|
+
const items = Array.from(event.clipboardData?.items || []);
|
|
10
|
+
const { schema } = view.state;
|
|
11
|
+
items.forEach((item) => {
|
|
12
|
+
const image = item.getAsFile();
|
|
13
|
+
if (item.type.indexOf('image') === 0) {
|
|
14
|
+
event.preventDefault();
|
|
15
|
+
if (upload && image) {
|
|
16
|
+
upload(image).then((src) => {
|
|
17
|
+
const node = schema.nodes.image.create({
|
|
18
|
+
src: src,
|
|
19
|
+
});
|
|
20
|
+
const transaction = view.state.tr.replaceSelectionWith(node);
|
|
21
|
+
view.dispatch(transaction);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
const reader = new FileReader();
|
|
27
|
+
reader.onload = (readerEvent) => {
|
|
28
|
+
const node = schema.nodes.image.create({
|
|
29
|
+
src: readerEvent.target?.result,
|
|
30
|
+
});
|
|
31
|
+
const transaction = view.state.tr.replaceSelectionWith(node);
|
|
32
|
+
view.dispatch(transaction);
|
|
33
|
+
};
|
|
34
|
+
if (!image)
|
|
35
|
+
return;
|
|
36
|
+
reader.readAsDataURL(image);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
return false;
|
|
40
|
+
},
|
|
41
|
+
drop: (view, event) => {
|
|
42
|
+
const upload = window.__image_uploader || fallbackUpload;
|
|
43
|
+
const hasFiles = event.dataTransfer &&
|
|
44
|
+
event.dataTransfer.files &&
|
|
45
|
+
event.dataTransfer.files.length;
|
|
46
|
+
if (!hasFiles) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const images = Array.from(event.dataTransfer?.files ?? []).filter((file) => /image/i.test(file.type));
|
|
50
|
+
if (images.length === 0) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
const { schema } = view.state;
|
|
55
|
+
const coordinates = view.posAtCoords({
|
|
56
|
+
left: event.clientX,
|
|
57
|
+
top: event.clientY,
|
|
58
|
+
});
|
|
59
|
+
if (!coordinates)
|
|
60
|
+
return false;
|
|
61
|
+
images.forEach(async (image) => {
|
|
62
|
+
const reader = new FileReader();
|
|
63
|
+
if (upload) {
|
|
64
|
+
const node = schema.nodes.image.create({
|
|
65
|
+
src: await upload(image),
|
|
66
|
+
});
|
|
67
|
+
const transaction = view.state.tr.insert(coordinates.pos, node);
|
|
68
|
+
view.dispatch(transaction);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
reader.onload = (readerEvent) => {
|
|
72
|
+
const node = schema.nodes.image.create({
|
|
73
|
+
src: readerEvent.target?.result,
|
|
74
|
+
});
|
|
75
|
+
const transaction = view.state.tr.insert(coordinates.pos, node);
|
|
76
|
+
view.dispatch(transaction);
|
|
77
|
+
};
|
|
78
|
+
reader.readAsDataURL(image);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
return true;
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Image from "@tiptap/extension-image";
|
|
2
2
|
import { mergeAttributes } from "@tiptap/core";
|
|
3
|
+
import { dropImagePlugin } from "./dragdrop";
|
|
3
4
|
export default Image.extend({
|
|
4
5
|
addOptions() {
|
|
5
6
|
return {
|
|
@@ -11,5 +12,8 @@ export default Image.extend({
|
|
|
11
12
|
renderHTML({ HTMLAttributes }) {
|
|
12
13
|
const { style } = HTMLAttributes;
|
|
13
14
|
return ["figure", { style }, ["img", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]];
|
|
14
|
-
}
|
|
15
|
+
},
|
|
16
|
+
addProseMirrorPlugins() {
|
|
17
|
+
return [dropImagePlugin()];
|
|
18
|
+
},
|
|
15
19
|
}).configure({ HTMLAttributes: { crossorigin: 'anonymous' } });
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
background-color: var(--primary-light6);
|
|
10
10
|
opacity: 0;
|
|
11
11
|
|
|
12
|
-
.
|
|
12
|
+
.editable & {
|
|
13
13
|
opacity: 1;
|
|
14
14
|
}
|
|
15
15
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
&.resize-cursor {
|
|
18
18
|
pointer-events: none;
|
|
19
19
|
|
|
20
|
-
.
|
|
20
|
+
.editable & {
|
|
21
21
|
pointer-events: initial;
|
|
22
22
|
cursor: ew-resize;
|
|
23
23
|
cursor: col-resize; /* stylelint-disable declaration-block-no-duplicate-properties */
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
<script>import { Button, IconButton, Input, List, TwoLine } from "nunui";
|
|
1
|
+
<script>import { Button, IconButton, Input, List, OneLine, TwoLine } from "nunui";
|
|
2
2
|
import { getContext } from "svelte";
|
|
3
|
-
import { slashVisible, slashItems, slashLocaltion, slashProps, slashDetail } from '../plugin/command/stores';
|
|
3
|
+
import { slashVisible, slashItems, slashLocaltion, slashProps, slashDetail, slashSelection } from '../plugin/command/stores';
|
|
4
4
|
import { fly, slide } from "svelte/transition";
|
|
5
5
|
import { quartOut } from "svelte/easing";
|
|
6
6
|
import i18n from "../i18n";
|
|
7
7
|
const tiptap = getContext('editor');
|
|
8
8
|
export let selectedIndex = 0;
|
|
9
9
|
let height = 0, elements = [];
|
|
10
|
-
let iframe = '';
|
|
10
|
+
let iframe = '', focus;
|
|
11
11
|
$: if ($slashVisible) {
|
|
12
12
|
iframe = '';
|
|
13
13
|
}
|
|
14
|
+
$: setTimeout(() => focus?.focus?.(), 100);
|
|
14
15
|
</script>
|
|
15
16
|
|
|
16
17
|
<svelte:window bind:innerHeight={height}/>
|
|
@@ -26,16 +27,19 @@ $: if ($slashVisible) {
|
|
|
26
27
|
<IconButton icon="arrow_back" on:click={() => $slashDetail = ''}/>
|
|
27
28
|
<div class="title">iframe</div>
|
|
28
29
|
</header>
|
|
29
|
-
<Input placeholder="url" fullWidth bind:value={iframe}
|
|
30
|
+
<Input placeholder="url" fullWidth bind:value={iframe} bind:input={focus}
|
|
30
31
|
on:submit={() => $tiptap.commands.insertContent({type: 'iframe', attrs: {src: iframe}})}/>
|
|
31
32
|
<footer>
|
|
32
33
|
<Button tabindex="0" transparent small on:click={() => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
iframe = ''
|
|
35
|
+
$slashDetail = ''
|
|
36
|
+
}}>{i18n('cancel')}
|
|
36
37
|
</Button>
|
|
37
38
|
<Button tabindex="0" transparent small
|
|
38
|
-
on:click={() =>
|
|
39
|
+
on:click={() => {
|
|
40
|
+
$slashSelection?.();
|
|
41
|
+
$tiptap.commands.insertContent({type: 'iframe', attrs: {src: iframe}})}
|
|
42
|
+
}>{i18n('insert')}
|
|
39
43
|
</Button>
|
|
40
44
|
</footer>
|
|
41
45
|
</div>
|
|
@@ -45,31 +49,52 @@ $: if ($slashVisible) {
|
|
|
45
49
|
<IconButton icon="arrow_back" on:click={() => $slashDetail = ''}/>
|
|
46
50
|
<div class="title">Youtube</div>
|
|
47
51
|
</header>
|
|
48
|
-
<Input placeholder="url" fullWidth bind:value={iframe}
|
|
52
|
+
<Input placeholder="url" fullWidth bind:value={iframe} bind:input={focus}
|
|
49
53
|
on:submit={() => $tiptap.commands.insertVideoPlayer({url: iframe})}/>
|
|
50
54
|
<footer>
|
|
51
55
|
<Button tabindex="0" transparent small on:click={() => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
iframe = ''
|
|
57
|
+
$slashDetail = ''
|
|
58
|
+
}}>{i18n('cancel')}
|
|
55
59
|
</Button>
|
|
56
|
-
<Button tabindex="0" transparent small
|
|
57
|
-
|
|
60
|
+
<Button tabindex="0" transparent small on:click={() => {
|
|
61
|
+
$slashSelection?.();
|
|
62
|
+
$tiptap.commands.insertVideoPlayer({url: iframe});
|
|
63
|
+
}}>{i18n('insert')}
|
|
58
64
|
</Button>
|
|
59
65
|
</footer>
|
|
60
66
|
</div>
|
|
67
|
+
{:else if $slashDetail === 'emoji'}
|
|
68
|
+
<div class="list">
|
|
69
|
+
<List>
|
|
70
|
+
{#each $slashItems as {title, command}, i(title)}
|
|
71
|
+
<div transition:slide={{duration: 400, easing: quartOut}}>
|
|
72
|
+
<OneLine on:click={() => {
|
|
73
|
+
command?.($slashProps);
|
|
74
|
+
setTimeout(() => $tiptap.commands.focus());
|
|
75
|
+
}} bind:this={elements[i]} {title} active={selectedIndex === i}/>
|
|
76
|
+
</div>
|
|
77
|
+
{/each}
|
|
78
|
+
{#if !$slashItems.length}
|
|
79
|
+
<div class="section"
|
|
80
|
+
transition:slide={{duration: 400, easing: quartOut}}>{i18n('noResult')}</div>
|
|
81
|
+
{/if}
|
|
82
|
+
</List>
|
|
83
|
+
</div>
|
|
61
84
|
{:else}
|
|
62
85
|
<div class="list">
|
|
63
86
|
<List>
|
|
64
|
-
{#each $slashItems as {section, list}(section)}
|
|
87
|
+
{#each $slashItems as {section, list}, j(section)}
|
|
88
|
+
{@const lastCount = $slashItems.slice(0, j).reduce((acc, cur) => acc + cur.list.length, 0)}
|
|
65
89
|
<div class="section" transition:slide={{duration: 400, easing: quartOut}}>{section}</div>
|
|
66
90
|
<div transition:slide={{duration: 400, easing: quartOut}}>
|
|
67
91
|
{#each list || [] as {title, subtitle, icon, command, section}, i(title)}
|
|
68
92
|
<div transition:slide={{duration: 400, easing: quartOut}}>
|
|
69
|
-
<TwoLine on:mouseenter={() => (selectedIndex = i)} on:click={() => {
|
|
93
|
+
<TwoLine on:mouseenter={() => (selectedIndex = i + lastCount)} on:click={() => {
|
|
70
94
|
command?.($slashProps);
|
|
71
95
|
setTimeout(() => $tiptap.commands.focus());
|
|
72
|
-
}} bind:this={elements[i]} {icon} {title} subtitle={subtitle || ''}
|
|
96
|
+
}} bind:this={elements[i + lastCount]} {icon} {title} subtitle={subtitle || ''}
|
|
97
|
+
active={selectedIndex === i + lastCount}/>
|
|
73
98
|
</div>
|
|
74
99
|
{/each}
|
|
75
100
|
</div>
|
|
@@ -98,6 +123,7 @@ main {
|
|
|
98
123
|
position: fixed;
|
|
99
124
|
background: var(--surface, #fff);
|
|
100
125
|
width: 220px;
|
|
126
|
+
max-height: 384px;
|
|
101
127
|
border-radius: 4px;
|
|
102
128
|
overflow-y: scroll;
|
|
103
129
|
z-index: 10;
|
|
@@ -8,6 +8,7 @@ import Floating from "./Floating.svelte";
|
|
|
8
8
|
import Command from "./Command.svelte";
|
|
9
9
|
import { slashItems, slashProps, slashVisible } from "../plugin/command/stores";
|
|
10
10
|
import i18n from "../i18n";
|
|
11
|
+
import { fallbackUpload } from "../plugin/image/dragdrop";
|
|
11
12
|
const san = (body) => sanitizeHtml(body, {
|
|
12
13
|
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'math-inline', 'math-node', 'iframe', 'tiptap-file', 'lite-youtube', 'blockquote']),
|
|
13
14
|
allowedStyles: '*', allowedAttributes: {
|
|
@@ -21,11 +22,12 @@ const san = (body) => sanitizeHtml(body, {
|
|
|
21
22
|
'lite-youtube': ['videoid', 'params', 'nocookie', 'title', 'provider'],
|
|
22
23
|
},
|
|
23
24
|
});
|
|
24
|
-
export let body = '', editable = false,
|
|
25
|
+
export let body = '', editable = false, ref = null, options = {};
|
|
26
|
+
export const imageUpload = fallbackUpload, style = '';
|
|
25
27
|
const tiptap = setContext('editor', writable(null));
|
|
26
28
|
let element, fullscreen = false, mounted = false, last = '';
|
|
27
|
-
$: ref = $tiptap;
|
|
28
29
|
$: $tiptap && $tiptap.setEditable(editable);
|
|
30
|
+
$: browser && (window.__image_uploader = imageUpload);
|
|
29
31
|
if (browser) {
|
|
30
32
|
onMount(() => {
|
|
31
33
|
body = last = san(body);
|
|
@@ -33,9 +35,9 @@ if (browser) {
|
|
|
33
35
|
Promise.all([import('./tiptap'), import("@justinribeiro/lite-youtube")]).then(([{ default: tt }]) => {
|
|
34
36
|
if (!mounted)
|
|
35
37
|
return;
|
|
36
|
-
$tiptap = tt(element, body, {
|
|
38
|
+
ref = $tiptap = tt(element, body, {
|
|
37
39
|
editable: editable,
|
|
38
|
-
onTransaction: () => $tiptap = $tiptap,
|
|
40
|
+
onTransaction: () => ref = $tiptap = $tiptap,
|
|
39
41
|
...options,
|
|
40
42
|
});
|
|
41
43
|
$tiptap.on('update', ({ editor: tiptap }) => {
|
|
@@ -62,14 +64,17 @@ $: selectedIndex = $slashVisible ? selectedIndex : 0;
|
|
|
62
64
|
function handleKeydown(event) {
|
|
63
65
|
if (!$slashVisible)
|
|
64
66
|
return;
|
|
67
|
+
let count = $slashItems.length;
|
|
68
|
+
if ($slashItems[0]?.list)
|
|
69
|
+
count = $slashItems.reduce((acc, item) => acc + item.list.length, 0);
|
|
65
70
|
if (event.key === 'ArrowUp') {
|
|
66
71
|
event.preventDefault();
|
|
67
|
-
selectedIndex = (selectedIndex +
|
|
72
|
+
selectedIndex = (selectedIndex + count - 1) % count;
|
|
68
73
|
return true;
|
|
69
74
|
}
|
|
70
75
|
if (event.key === 'ArrowDown') {
|
|
71
76
|
event.preventDefault();
|
|
72
|
-
selectedIndex = (selectedIndex + 1) %
|
|
77
|
+
selectedIndex = (selectedIndex + 1) % count;
|
|
73
78
|
return true;
|
|
74
79
|
}
|
|
75
80
|
if (event.key === 'Enter') {
|
|
@@ -80,10 +85,10 @@ function handleKeydown(event) {
|
|
|
80
85
|
return false;
|
|
81
86
|
}
|
|
82
87
|
function selectItem(index) {
|
|
83
|
-
const item = $slashItems[index];
|
|
88
|
+
const item = $slashItems[0]?.list ? $slashItems.map(i => i.list).flat()[index] : $slashItems[index];
|
|
84
89
|
if (item) {
|
|
85
90
|
let range = $slashProps.range;
|
|
86
|
-
item.command({ editor:
|
|
91
|
+
item.command({ editor: $tiptap, range });
|
|
87
92
|
}
|
|
88
93
|
}
|
|
89
94
|
</script>
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { SvelteComponentTyped } from "svelte";
|
|
2
2
|
import "@seorii/prosemirror-math/style.css";
|
|
3
|
+
import type { UploadFn } from "../plugin/image/dragdrop";
|
|
3
4
|
declare const __propDef: {
|
|
4
5
|
props: {
|
|
5
6
|
body?: string | undefined;
|
|
6
7
|
editable?: boolean | undefined;
|
|
7
|
-
|
|
8
|
-
ref?: any;
|
|
8
|
+
ref?: null | undefined;
|
|
9
9
|
options?: {} | undefined;
|
|
10
|
+
imageUpload?: UploadFn | undefined;
|
|
11
|
+
style?: "" | undefined;
|
|
10
12
|
};
|
|
11
13
|
events: {
|
|
12
14
|
[evt: string]: CustomEvent<any>;
|
|
@@ -19,5 +21,7 @@ export type TipTapProps = typeof __propDef.props;
|
|
|
19
21
|
export type TipTapEvents = typeof __propDef.events;
|
|
20
22
|
export type TipTapSlots = typeof __propDef.slots;
|
|
21
23
|
export default class TipTap extends SvelteComponentTyped<TipTapProps, TipTapEvents, TipTapSlots> {
|
|
24
|
+
get imageUpload(): UploadFn;
|
|
25
|
+
get style(): "";
|
|
22
26
|
}
|
|
23
27
|
export {};
|
package/dist/tiptap/tiptap.js
CHANGED
|
@@ -23,55 +23,60 @@ import Iframe from "../plugin/iframe";
|
|
|
23
23
|
// @ts-ignore
|
|
24
24
|
import { MathInline, MathBlock } from "@seorii/prosemirror-math/tiptap";
|
|
25
25
|
import Youtube from "../plugin/youtube";
|
|
26
|
-
import command from "../plugin/command";
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
import command from "../plugin/command/suggest";
|
|
27
|
+
import emoji from "../plugin/command/emoji";
|
|
28
|
+
export default (element, content, { plugins = [], ...props } = {}) => {
|
|
29
|
+
const tt = new Editor({
|
|
30
|
+
element, content, ...props,
|
|
31
|
+
extensions: [
|
|
32
|
+
CodeBlockLowlight.extend({
|
|
33
|
+
addKeyboardShortcuts() {
|
|
34
|
+
return {
|
|
35
|
+
...this.parent?.(),
|
|
36
|
+
'Tab': () => {
|
|
37
|
+
if (this.editor.isActive('codeBlock')) {
|
|
38
|
+
return this.editor.commands.insertContent(' ');
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
37
41
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}).configure({ lowlight }),
|
|
45
|
+
Image,
|
|
46
|
+
Youtube,
|
|
47
|
+
StarterKit,
|
|
48
|
+
Underline,
|
|
49
|
+
Highlight.configure({ multicolor: true }),
|
|
50
|
+
Link.configure({
|
|
51
|
+
openOnClick: true, protocols: ['ftp', 'mailto', {
|
|
52
|
+
scheme: 'tel',
|
|
53
|
+
optionalSlashes: true
|
|
54
|
+
}]
|
|
55
|
+
}),
|
|
56
|
+
TextAlign.configure({ types: ['heading', 'paragraph', 'image'] }),
|
|
57
|
+
DropCursor,
|
|
58
|
+
orderedlist,
|
|
59
|
+
MathInline,
|
|
60
|
+
MathBlock,
|
|
61
|
+
table,
|
|
62
|
+
tableHeader,
|
|
63
|
+
tableRow,
|
|
64
|
+
tableCell,
|
|
65
|
+
Superscript,
|
|
66
|
+
Subscript,
|
|
67
|
+
Indent,
|
|
68
|
+
Color,
|
|
69
|
+
TextStyle,
|
|
70
|
+
Iframe,
|
|
71
|
+
Code.extend({
|
|
72
|
+
renderHTML({ HTMLAttributes }) {
|
|
73
|
+
return ['code', mergeAttributes(HTMLAttributes, { class: 'inline' })];
|
|
74
|
+
}
|
|
75
|
+
}),
|
|
76
|
+
...plugins,
|
|
77
|
+
],
|
|
78
|
+
});
|
|
79
|
+
tt.registerPlugin(emoji(tt));
|
|
80
|
+
tt.registerPlugin(command(tt));
|
|
81
|
+
return tt;
|
|
82
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seorii/tiptap",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "vite dev",
|
|
6
6
|
"build": "svelte-kit sync && svelte-package",
|
|
@@ -60,6 +60,8 @@
|
|
|
60
60
|
"@tiptap/pm": "^2.0.4",
|
|
61
61
|
"@tiptap/starter-kit": "^2.0.4",
|
|
62
62
|
"@tiptap/suggestion": "^2.0.4",
|
|
63
|
+
"emojis-keywords": "2.0.0",
|
|
64
|
+
"emojis-list": "2.0.0",
|
|
63
65
|
"lowlight": "^2.9.0",
|
|
64
66
|
"nunui": "^0.0.101",
|
|
65
67
|
"prosemirror-commands": "^1.5.2",
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { Extension } from '@tiptap/core';
|
|
2
|
-
import Suggestion from '@tiptap/suggestion';
|
|
3
|
-
import suggestion from "./suggest";
|
|
4
|
-
export const Command = Extension.create({
|
|
5
|
-
name: 'slash',
|
|
6
|
-
addOptions() {
|
|
7
|
-
return {
|
|
8
|
-
suggestion: {
|
|
9
|
-
char: '/',
|
|
10
|
-
command: ({ editor, range, props }) => {
|
|
11
|
-
props.command({ editor, range });
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
};
|
|
15
|
-
},
|
|
16
|
-
addProseMirrorPlugins() {
|
|
17
|
-
return [
|
|
18
|
-
Suggestion({
|
|
19
|
-
editor: this.editor,
|
|
20
|
-
...this.options.suggestion
|
|
21
|
-
})
|
|
22
|
-
];
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
export default Command.configure({
|
|
26
|
-
suggestion
|
|
27
|
-
});
|
|
File without changes
|