frappe-ui 0.0.46 → 0.0.49
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/package.json +9 -7
- package/src/components/Input.vue +1 -0
- package/src/components/TextEditor/InsertImage.vue +72 -0
- package/src/components/TextEditor/MentionList.vue +93 -0
- package/src/components/TextEditor/Menu.vue +4 -55
- package/src/components/TextEditor/TextEditor.vue +54 -22
- package/src/components/TextEditor/image-extension.js +152 -0
- package/src/components/TextEditor/mention.js +72 -0
- package/src/components/Toast.vue +6 -2
- package/src/components/Tooltip.vue +1 -1
- package/src/utils/file-to-base64.js +9 -0
- package/src/utils/vite-dev-server.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frappe-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.49",
|
|
4
4
|
"description": "A set of components and utilities for rapid UI development",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -22,17 +22,19 @@
|
|
|
22
22
|
"@popperjs/core": "^2.11.2",
|
|
23
23
|
"@tailwindcss/forms": "^0.4.0",
|
|
24
24
|
"@tailwindcss/typography": "^0.5.0",
|
|
25
|
-
"@tiptap/extension-image": "^2.0.0-beta.
|
|
26
|
-
"@tiptap/extension-link": "^2.0.0-beta.
|
|
27
|
-
"@tiptap/extension-
|
|
25
|
+
"@tiptap/extension-image": "^2.0.0-beta.30",
|
|
26
|
+
"@tiptap/extension-link": "^2.0.0-beta.43",
|
|
27
|
+
"@tiptap/extension-mention": "^2.0.0-beta.102",
|
|
28
|
+
"@tiptap/extension-placeholder": "^2.0.0-beta.53",
|
|
28
29
|
"@tiptap/extension-text-align": "^2.0.0-beta.31",
|
|
29
|
-
"@tiptap/starter-kit": "^2.0.0-beta.
|
|
30
|
-
"@tiptap/vue-3": "^2.0.0-beta.
|
|
30
|
+
"@tiptap/starter-kit": "^2.0.0-beta.191",
|
|
31
|
+
"@tiptap/vue-3": "^2.0.0-beta.96",
|
|
31
32
|
"autoprefixer": "^10.4.2",
|
|
32
33
|
"feather-icons": "^4.28.0",
|
|
33
34
|
"postcss": "^8.4.5",
|
|
34
35
|
"socket.io-client": "^4.5.1",
|
|
35
|
-
"tailwindcss": "^3.0.12"
|
|
36
|
+
"tailwindcss": "^3.0.12",
|
|
37
|
+
"tippy.js": "^6.3.7"
|
|
36
38
|
},
|
|
37
39
|
"devDependencies": {
|
|
38
40
|
"husky": ">=6",
|
package/src/components/Input.vue
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<slot v-bind="{ openDialog, resetAddImage }"></slot>
|
|
3
|
+
<Dialog
|
|
4
|
+
:options="{ title: 'Add Image' }"
|
|
5
|
+
v-model="addImageDialog.show"
|
|
6
|
+
@after-leave="resetAddImage"
|
|
7
|
+
>
|
|
8
|
+
<template #body-content>
|
|
9
|
+
<label
|
|
10
|
+
class="relative py-1 bg-gray-100 rounded-lg cursor-pointer focus-within:bg-gray-200 hover:bg-gray-200"
|
|
11
|
+
>
|
|
12
|
+
<input
|
|
13
|
+
type="file"
|
|
14
|
+
class="w-full opacity-0"
|
|
15
|
+
@change="onImageSelect"
|
|
16
|
+
accept="image/*"
|
|
17
|
+
/>
|
|
18
|
+
<span class="absolute inset-0 px-2 py-1 text-base select-none">
|
|
19
|
+
{{ addImageDialog.file ? 'Select another image' : 'Select an image' }}
|
|
20
|
+
</span>
|
|
21
|
+
</label>
|
|
22
|
+
<img
|
|
23
|
+
v-if="addImageDialog.url"
|
|
24
|
+
:src="addImageDialog.url"
|
|
25
|
+
class="w-full mt-2 rounded-lg"
|
|
26
|
+
/>
|
|
27
|
+
</template>
|
|
28
|
+
<template #actions>
|
|
29
|
+
<Button appearance="primary" @click="addImage(addImageDialog.url)">
|
|
30
|
+
Insert Image
|
|
31
|
+
</Button>
|
|
32
|
+
</template>
|
|
33
|
+
</Dialog>
|
|
34
|
+
</template>
|
|
35
|
+
<script>
|
|
36
|
+
import fileToBase64 from '../../utils/file-to-base64'
|
|
37
|
+
import Dialog from '../Dialog.vue'
|
|
38
|
+
|
|
39
|
+
export default {
|
|
40
|
+
name: 'InsertImage',
|
|
41
|
+
props: ['editor'],
|
|
42
|
+
expose: ['openDialog'],
|
|
43
|
+
data() {
|
|
44
|
+
return {
|
|
45
|
+
addImageDialog: { url: '', file: null, show: false },
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
components: { Dialog },
|
|
49
|
+
methods: {
|
|
50
|
+
openDialog() {
|
|
51
|
+
this.addImageDialog.show = true
|
|
52
|
+
},
|
|
53
|
+
onImageSelect(e) {
|
|
54
|
+
let file = e.target.files[0]
|
|
55
|
+
if (!file) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
this.addImageDialog.file = file
|
|
59
|
+
fileToBase64(file).then((base64) => {
|
|
60
|
+
this.addImageDialog.url = base64
|
|
61
|
+
})
|
|
62
|
+
},
|
|
63
|
+
addImage(src) {
|
|
64
|
+
this.editor.chain().focus().setImage({ src }).run()
|
|
65
|
+
this.resetAddImage()
|
|
66
|
+
},
|
|
67
|
+
resetAddImage() {
|
|
68
|
+
this.addImageDialog = { show: false, url: null, file: null }
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
</script>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
v-if="items.length"
|
|
4
|
+
class="p-1 text-base bg-white border rounded-lg shadow-lg min-w-40"
|
|
5
|
+
>
|
|
6
|
+
<button
|
|
7
|
+
:class="[
|
|
8
|
+
index === selectedIndex ? 'bg-gray-100' : 'text-gray-900',
|
|
9
|
+
'whitespace-nowrap flex rounded-md items-center w-full px-2 py-2 text-sm',
|
|
10
|
+
]"
|
|
11
|
+
v-for="(item, index) in items"
|
|
12
|
+
:key="index"
|
|
13
|
+
@click="selectItem(index)"
|
|
14
|
+
@mouseover="selectedIndex = index"
|
|
15
|
+
>
|
|
16
|
+
{{ item.label }}
|
|
17
|
+
</button>
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script>
|
|
22
|
+
export default {
|
|
23
|
+
props: {
|
|
24
|
+
items: {
|
|
25
|
+
type: Array,
|
|
26
|
+
required: true,
|
|
27
|
+
},
|
|
28
|
+
command: {
|
|
29
|
+
type: Function,
|
|
30
|
+
required: true,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
data() {
|
|
34
|
+
return {
|
|
35
|
+
selectedIndex: 0,
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
watch: {
|
|
39
|
+
items() {
|
|
40
|
+
this.selectedIndex = 0
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
methods: {
|
|
44
|
+
onKeyDown({ event }) {
|
|
45
|
+
if (event.key === 'ArrowUp') {
|
|
46
|
+
this.upHandler()
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
if (event.key === 'ArrowDown') {
|
|
50
|
+
this.downHandler()
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
if (event.key === 'Enter') {
|
|
54
|
+
this.enterHandler()
|
|
55
|
+
return true
|
|
56
|
+
}
|
|
57
|
+
return false
|
|
58
|
+
},
|
|
59
|
+
upHandler() {
|
|
60
|
+
this.selectedIndex =
|
|
61
|
+
(this.selectedIndex + this.items.length - 1) % this.items.length
|
|
62
|
+
},
|
|
63
|
+
downHandler() {
|
|
64
|
+
this.selectedIndex = (this.selectedIndex + 1) % this.items.length
|
|
65
|
+
},
|
|
66
|
+
enterHandler() {
|
|
67
|
+
this.selectItem(this.selectedIndex)
|
|
68
|
+
},
|
|
69
|
+
selectItem(index) {
|
|
70
|
+
const item = this.items[index]
|
|
71
|
+
if (item) {
|
|
72
|
+
this.command({ id: item.value, label: item.label })
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<style>
|
|
80
|
+
.item {
|
|
81
|
+
display: block;
|
|
82
|
+
margin: 0;
|
|
83
|
+
width: 100%;
|
|
84
|
+
text-align: left;
|
|
85
|
+
background: transparent;
|
|
86
|
+
border-radius: 0.4rem;
|
|
87
|
+
border: 1px solid transparent;
|
|
88
|
+
padding: 0.2rem 0.4rem;
|
|
89
|
+
}
|
|
90
|
+
.item.is-selected {
|
|
91
|
+
border-color: #000;
|
|
92
|
+
}
|
|
93
|
+
</style>
|
|
@@ -67,43 +67,12 @@
|
|
|
67
67
|
</Button>
|
|
68
68
|
</template>
|
|
69
69
|
</Dialog>
|
|
70
|
-
<
|
|
71
|
-
:options="{ title: 'Add Image' }"
|
|
72
|
-
v-model="addImageDialog.show"
|
|
73
|
-
@after-leave="resetAddImage"
|
|
74
|
-
>
|
|
75
|
-
<template #body-content>
|
|
76
|
-
<label
|
|
77
|
-
class="relative py-1 bg-gray-100 rounded-lg cursor-pointer focus-within:bg-gray-200 hover:bg-gray-200"
|
|
78
|
-
>
|
|
79
|
-
<input
|
|
80
|
-
type="file"
|
|
81
|
-
class="w-full opacity-0"
|
|
82
|
-
@change="onImageSelect"
|
|
83
|
-
accept="image/*"
|
|
84
|
-
/>
|
|
85
|
-
<span class="absolute inset-0 px-2 py-1 text-base select-none">
|
|
86
|
-
{{
|
|
87
|
-
addImageDialog.file ? 'Select another image' : 'Select an image'
|
|
88
|
-
}}
|
|
89
|
-
</span>
|
|
90
|
-
</label>
|
|
91
|
-
<img
|
|
92
|
-
v-if="addImageDialog.url"
|
|
93
|
-
:src="addImageDialog.url"
|
|
94
|
-
class="w-full mt-2 rounded-lg"
|
|
95
|
-
/>
|
|
96
|
-
</template>
|
|
97
|
-
<template #actions>
|
|
98
|
-
<Button appearance="primary" @click="addImage(addImageDialog.url)">
|
|
99
|
-
Insert Image
|
|
100
|
-
</Button>
|
|
101
|
-
</template>
|
|
102
|
-
</Dialog>
|
|
70
|
+
<InsertImage ref="insertImage" :editor="editor" />
|
|
103
71
|
</div>
|
|
104
72
|
</template>
|
|
105
73
|
<script>
|
|
106
74
|
import { Popover, Dialog, Input, Button } from 'frappe-ui'
|
|
75
|
+
import InsertImage from './InsertImage.vue'
|
|
107
76
|
export default {
|
|
108
77
|
name: 'TipTapMenu',
|
|
109
78
|
props: ['editor', 'buttons'],
|
|
@@ -112,11 +81,11 @@ export default {
|
|
|
112
81
|
Dialog,
|
|
113
82
|
Input,
|
|
114
83
|
Button,
|
|
84
|
+
InsertImage,
|
|
115
85
|
},
|
|
116
86
|
data() {
|
|
117
87
|
return {
|
|
118
88
|
setLinkDialog: { url: '', show: false },
|
|
119
|
-
addImageDialog: { url: '', file: null, show: false },
|
|
120
89
|
}
|
|
121
90
|
},
|
|
122
91
|
methods: {
|
|
@@ -128,7 +97,7 @@ export default {
|
|
|
128
97
|
this.setLinkDialog.url = existingURL
|
|
129
98
|
}
|
|
130
99
|
} else if (button.label === 'Image') {
|
|
131
|
-
this.
|
|
100
|
+
this.$refs.insertImage.openDialog()
|
|
132
101
|
} else {
|
|
133
102
|
button.action(this.editor)
|
|
134
103
|
}
|
|
@@ -150,26 +119,6 @@ export default {
|
|
|
150
119
|
this.setLinkDialog.show = false
|
|
151
120
|
this.setLinkDialog.url = ''
|
|
152
121
|
},
|
|
153
|
-
onImageSelect(e) {
|
|
154
|
-
let file = e.target.files[0]
|
|
155
|
-
if (!file) {
|
|
156
|
-
return
|
|
157
|
-
}
|
|
158
|
-
this.addImageDialog.file = file
|
|
159
|
-
let reader = new FileReader()
|
|
160
|
-
reader.onloadend = () => {
|
|
161
|
-
let base64string = reader.result
|
|
162
|
-
this.addImageDialog.url = base64string
|
|
163
|
-
}
|
|
164
|
-
reader.readAsDataURL(file)
|
|
165
|
-
},
|
|
166
|
-
addImage(src) {
|
|
167
|
-
this.editor.chain().focus().setImage({ src }).run()
|
|
168
|
-
this.resetAddImage()
|
|
169
|
-
},
|
|
170
|
-
resetAddImage() {
|
|
171
|
-
this.addImageDialog = { show: false, url: null, file: null }
|
|
172
|
-
},
|
|
173
122
|
},
|
|
174
123
|
}
|
|
175
124
|
</script>
|
|
@@ -41,12 +41,6 @@
|
|
|
41
41
|
</button>
|
|
42
42
|
</FloatingMenu>
|
|
43
43
|
<editor-content :editor="editor" />
|
|
44
|
-
<span
|
|
45
|
-
v-if="!content"
|
|
46
|
-
class="absolute top-0 text-base text-gray-500 pointer-events-none"
|
|
47
|
-
>
|
|
48
|
-
{{ placeholder }}
|
|
49
|
-
</span>
|
|
50
44
|
</div>
|
|
51
45
|
</template>
|
|
52
46
|
|
|
@@ -55,8 +49,9 @@ import { Editor, EditorContent, BubbleMenu, FloatingMenu } from '@tiptap/vue-3'
|
|
|
55
49
|
import StarterKit from '@tiptap/starter-kit'
|
|
56
50
|
import Placeholder from '@tiptap/extension-placeholder'
|
|
57
51
|
import TextAlign from '@tiptap/extension-text-align'
|
|
58
|
-
import Image from '
|
|
52
|
+
import Image from './image-extension'
|
|
59
53
|
import Link from '@tiptap/extension-link'
|
|
54
|
+
import configureMention from './mention'
|
|
60
55
|
import Menu from './Menu.vue'
|
|
61
56
|
import commands from './commands'
|
|
62
57
|
import { normalizeClass } from 'vue'
|
|
@@ -70,17 +65,48 @@ export default {
|
|
|
70
65
|
FloatingMenu,
|
|
71
66
|
Menu,
|
|
72
67
|
},
|
|
73
|
-
props:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
68
|
+
props: {
|
|
69
|
+
content: {
|
|
70
|
+
type: String,
|
|
71
|
+
default: null,
|
|
72
|
+
},
|
|
73
|
+
placeholder: {
|
|
74
|
+
type: String,
|
|
75
|
+
default: '',
|
|
76
|
+
},
|
|
77
|
+
editorClass: {
|
|
78
|
+
type: [String, Array, Object],
|
|
79
|
+
default: '',
|
|
80
|
+
},
|
|
81
|
+
editable: {
|
|
82
|
+
type: Boolean,
|
|
83
|
+
default: true,
|
|
84
|
+
},
|
|
85
|
+
bubbleMenu: {
|
|
86
|
+
type: [Boolean, Array],
|
|
87
|
+
default: false,
|
|
88
|
+
},
|
|
89
|
+
fixedMenu: {
|
|
90
|
+
type: [Boolean, Array],
|
|
91
|
+
default: false,
|
|
92
|
+
},
|
|
93
|
+
floatingMenu: {
|
|
94
|
+
type: [Boolean, Array],
|
|
95
|
+
default: false,
|
|
96
|
+
},
|
|
97
|
+
extensions: {
|
|
98
|
+
type: Array,
|
|
99
|
+
default: () => [],
|
|
100
|
+
},
|
|
101
|
+
starterkitOptions: {
|
|
102
|
+
type: Object,
|
|
103
|
+
default: () => ({}),
|
|
104
|
+
},
|
|
105
|
+
mentions: {
|
|
106
|
+
type: Array,
|
|
107
|
+
default: () => [],
|
|
108
|
+
},
|
|
109
|
+
},
|
|
84
110
|
emits: ['change'],
|
|
85
111
|
expose: ['editor'],
|
|
86
112
|
data() {
|
|
@@ -119,13 +145,15 @@ export default {
|
|
|
119
145
|
TextAlign.configure({
|
|
120
146
|
types: ['heading', 'paragraph'],
|
|
121
147
|
}),
|
|
122
|
-
Image
|
|
123
|
-
allowBase64: true,
|
|
124
|
-
}),
|
|
148
|
+
Image,
|
|
125
149
|
Link,
|
|
126
150
|
Placeholder.configure({
|
|
127
|
-
|
|
151
|
+
showOnlyWhenEditable: false,
|
|
152
|
+
placeholder: () => {
|
|
153
|
+
return this.placeholder
|
|
154
|
+
},
|
|
128
155
|
}),
|
|
156
|
+
configureMention(this.mentions),
|
|
129
157
|
...(this.extensions || []),
|
|
130
158
|
],
|
|
131
159
|
onUpdate: ({ editor }) => {
|
|
@@ -248,4 +276,8 @@ function createEditorButton(option) {
|
|
|
248
276
|
pointer-events: none;
|
|
249
277
|
height: 0;
|
|
250
278
|
}
|
|
279
|
+
.mention {
|
|
280
|
+
font-weight: 600;
|
|
281
|
+
box-decoration-break: clone;
|
|
282
|
+
}
|
|
251
283
|
</style>
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Plugin adapted from the following examples:
|
|
2
|
+
// - https://github.com/ueberdosis/tiptap/blob/main/packages/extension-image/src/image.ts
|
|
3
|
+
// - https://gist.github.com/slava-vishnyakov/16076dff1a77ddaca93c4bccd4ec4521
|
|
4
|
+
|
|
5
|
+
import { mergeAttributes, Node, nodeInputRule } from '@tiptap/core'
|
|
6
|
+
import { Plugin } from 'prosemirror-state'
|
|
7
|
+
import fileToBase64 from '../../utils/file-to-base64'
|
|
8
|
+
|
|
9
|
+
export const inputRegex =
|
|
10
|
+
/(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/
|
|
11
|
+
|
|
12
|
+
export default Node.create({
|
|
13
|
+
name: 'image',
|
|
14
|
+
addOptions() {
|
|
15
|
+
return {
|
|
16
|
+
inline: false,
|
|
17
|
+
HTMLAttributes: {},
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
inline() {
|
|
21
|
+
return this.options.inline
|
|
22
|
+
},
|
|
23
|
+
group() {
|
|
24
|
+
return this.options.inline ? 'inline' : 'block'
|
|
25
|
+
},
|
|
26
|
+
draggable: true,
|
|
27
|
+
addAttributes() {
|
|
28
|
+
return {
|
|
29
|
+
src: {
|
|
30
|
+
default: null,
|
|
31
|
+
},
|
|
32
|
+
alt: {
|
|
33
|
+
default: null,
|
|
34
|
+
},
|
|
35
|
+
title: {
|
|
36
|
+
default: null,
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
parseHTML() {
|
|
41
|
+
return [
|
|
42
|
+
{
|
|
43
|
+
tag: 'img[src]',
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
renderHTML({ HTMLAttributes }) {
|
|
48
|
+
return ['img', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
addCommands() {
|
|
52
|
+
return {
|
|
53
|
+
setImage:
|
|
54
|
+
(options) =>
|
|
55
|
+
({ commands }) => {
|
|
56
|
+
return commands.insertContent({
|
|
57
|
+
type: this.name,
|
|
58
|
+
attrs: options,
|
|
59
|
+
})
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
addInputRules() {
|
|
65
|
+
return [
|
|
66
|
+
nodeInputRule({
|
|
67
|
+
find: inputRegex,
|
|
68
|
+
type: this.type,
|
|
69
|
+
getAttributes: (match) => {
|
|
70
|
+
const [, , alt, src, title] = match
|
|
71
|
+
|
|
72
|
+
return { src, alt, title }
|
|
73
|
+
},
|
|
74
|
+
}),
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
addProseMirrorPlugins() {
|
|
79
|
+
return [dropImagePlugin()]
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const dropImagePlugin = () => {
|
|
84
|
+
return new Plugin({
|
|
85
|
+
props: {
|
|
86
|
+
handlePaste(view, event, slice) {
|
|
87
|
+
const items = Array.from(event.clipboardData?.items || [])
|
|
88
|
+
const { schema } = view.state
|
|
89
|
+
|
|
90
|
+
items.forEach((item) => {
|
|
91
|
+
const image = item.getAsFile()
|
|
92
|
+
if (!image) return
|
|
93
|
+
|
|
94
|
+
if (item.type.indexOf('image') === 0) {
|
|
95
|
+
event.preventDefault()
|
|
96
|
+
|
|
97
|
+
fileToBase64(image).then((base64) => {
|
|
98
|
+
const node = schema.nodes.image.create({
|
|
99
|
+
src: base64,
|
|
100
|
+
})
|
|
101
|
+
const transaction = view.state.tr.replaceSelectionWith(node)
|
|
102
|
+
view.dispatch(transaction)
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
return false
|
|
108
|
+
},
|
|
109
|
+
handleDOMEvents: {
|
|
110
|
+
drop: (view, event) => {
|
|
111
|
+
const hasFiles =
|
|
112
|
+
event.dataTransfer &&
|
|
113
|
+
event.dataTransfer.files &&
|
|
114
|
+
event.dataTransfer.files.length
|
|
115
|
+
|
|
116
|
+
if (!hasFiles) {
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const images = Array.from(event.dataTransfer?.files ?? []).filter(
|
|
121
|
+
(file) => /image/i.test(file.type)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if (images.length === 0) {
|
|
125
|
+
return false
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
event.preventDefault()
|
|
129
|
+
|
|
130
|
+
const { schema } = view.state
|
|
131
|
+
const coordinates = view.posAtCoords({
|
|
132
|
+
left: event.clientX,
|
|
133
|
+
top: event.clientY,
|
|
134
|
+
})
|
|
135
|
+
if (!coordinates) return false
|
|
136
|
+
|
|
137
|
+
images.forEach(async (image) => {
|
|
138
|
+
fileToBase64(image).then((base64) => {
|
|
139
|
+
const node = schema.nodes.image.create({
|
|
140
|
+
src: base64,
|
|
141
|
+
})
|
|
142
|
+
const transaction = view.state.tr.insert(coordinates.pos, node)
|
|
143
|
+
view.dispatch(transaction)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
return true
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import tippy from 'tippy.js'
|
|
2
|
+
import { VueRenderer } from '@tiptap/vue-3'
|
|
3
|
+
import Mention from '@tiptap/extension-mention'
|
|
4
|
+
import MentionList from './MentionList.vue'
|
|
5
|
+
|
|
6
|
+
export default function configureMention(options) {
|
|
7
|
+
return Mention.configure({
|
|
8
|
+
HTMLAttributes: {
|
|
9
|
+
class: 'mention',
|
|
10
|
+
},
|
|
11
|
+
suggestion: getSuggestionOptions(options),
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getSuggestionOptions(options) {
|
|
16
|
+
return {
|
|
17
|
+
items: ({ query }) => {
|
|
18
|
+
return options
|
|
19
|
+
.filter((item) =>
|
|
20
|
+
item.label.toLowerCase().startsWith(query.toLowerCase())
|
|
21
|
+
)
|
|
22
|
+
.slice(0, 5)
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
render: () => {
|
|
26
|
+
let component
|
|
27
|
+
let popup
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
onStart: (props) => {
|
|
31
|
+
component = new VueRenderer(MentionList, {
|
|
32
|
+
props,
|
|
33
|
+
editor: props.editor,
|
|
34
|
+
})
|
|
35
|
+
if (!props.clientRect) {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
popup = tippy('body', {
|
|
39
|
+
getReferenceClientRect: props.clientRect,
|
|
40
|
+
appendTo: () => document.body,
|
|
41
|
+
content: component.element,
|
|
42
|
+
showOnCreate: true,
|
|
43
|
+
interactive: true,
|
|
44
|
+
trigger: 'manual',
|
|
45
|
+
placement: 'bottom-start',
|
|
46
|
+
})
|
|
47
|
+
},
|
|
48
|
+
onUpdate(props) {
|
|
49
|
+
component.updateProps(props)
|
|
50
|
+
if (!props.clientRect) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
popup[0].setProps({
|
|
54
|
+
getReferenceClientRect: props.clientRect,
|
|
55
|
+
})
|
|
56
|
+
},
|
|
57
|
+
onKeyDown(props) {
|
|
58
|
+
if (props.event.key === 'Escape') {
|
|
59
|
+
popup[0].hide()
|
|
60
|
+
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
63
|
+
return component.ref?.onKeyDown(props)
|
|
64
|
+
},
|
|
65
|
+
onExit() {
|
|
66
|
+
popup[0].destroy()
|
|
67
|
+
component.destroy()
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/components/Toast.vue
CHANGED
|
@@ -18,10 +18,14 @@
|
|
|
18
18
|
</div>
|
|
19
19
|
<div>
|
|
20
20
|
<slot>
|
|
21
|
-
<p
|
|
21
|
+
<p
|
|
22
|
+
v-if="title"
|
|
23
|
+
class="text-base font-medium text-gray-900"
|
|
24
|
+
:class="{ 'mb-1': text }"
|
|
25
|
+
>
|
|
22
26
|
{{ title }}
|
|
23
27
|
</p>
|
|
24
|
-
<p class="
|
|
28
|
+
<p v-if="text" class="text-base text-gray-600">
|
|
25
29
|
{{ text }}
|
|
26
30
|
</p>
|
|
27
31
|
</slot>
|