frappe-ui 0.0.78 → 0.0.80
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 +1 -1
- package/src/components/TextEditor/InsertImage.vue +8 -6
- package/src/components/TextEditor/InsertLink.vue +67 -0
- package/src/components/TextEditor/InsertVideo.vue +94 -0
- package/src/components/TextEditor/Menu.vue +28 -69
- package/src/components/TextEditor/TextEditor.vue +7 -0
- package/src/components/TextEditor/TextEditorBubbleMenu.vue +1 -0
- package/src/components/TextEditor/TextEditorFixedMenu.vue +1 -0
- package/src/components/TextEditor/commands.js +10 -0
- package/src/components/TextEditor/icons/video-add-line.vue +14 -0
- package/src/components/TextEditor/video-extension.js +60 -0
- package/src/resources/listResource.js +4 -4
- package/src/resources/resources.js +1 -1
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<slot v-bind="{ openDialog
|
|
2
|
+
<slot v-bind="{ onClick: openDialog }"></slot>
|
|
3
3
|
<Dialog
|
|
4
4
|
:options="{ title: 'Add Image' }"
|
|
5
5
|
v-model="addImageDialog.show"
|
|
6
|
-
@after-leave="
|
|
6
|
+
@after-leave="reset"
|
|
7
7
|
>
|
|
8
8
|
<template #body-content>
|
|
9
9
|
<label
|
|
@@ -29,12 +29,14 @@
|
|
|
29
29
|
<Button appearance="primary" @click="addImage(addImageDialog.url)">
|
|
30
30
|
Insert Image
|
|
31
31
|
</Button>
|
|
32
|
+
<Button @click="reset"> Cancel </Button>
|
|
32
33
|
</template>
|
|
33
34
|
</Dialog>
|
|
34
35
|
</template>
|
|
35
36
|
<script>
|
|
36
37
|
import fileToBase64 from '../../utils/file-to-base64'
|
|
37
38
|
import Dialog from '../Dialog.vue'
|
|
39
|
+
import Button from '../Button.vue'
|
|
38
40
|
|
|
39
41
|
export default {
|
|
40
42
|
name: 'InsertImage',
|
|
@@ -45,7 +47,7 @@ export default {
|
|
|
45
47
|
addImageDialog: { url: '', file: null, show: false },
|
|
46
48
|
}
|
|
47
49
|
},
|
|
48
|
-
components: { Dialog },
|
|
50
|
+
components: { Button, Dialog },
|
|
49
51
|
methods: {
|
|
50
52
|
openDialog() {
|
|
51
53
|
this.addImageDialog.show = true
|
|
@@ -62,10 +64,10 @@ export default {
|
|
|
62
64
|
},
|
|
63
65
|
addImage(src) {
|
|
64
66
|
this.editor.chain().focus().setImage({ src }).run()
|
|
65
|
-
this.
|
|
67
|
+
this.reset()
|
|
66
68
|
},
|
|
67
|
-
|
|
68
|
-
this.addImageDialog =
|
|
69
|
+
reset() {
|
|
70
|
+
this.addImageDialog = this.$options.data().addImageDialog
|
|
69
71
|
},
|
|
70
72
|
},
|
|
71
73
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<slot v-bind="{ onClick: openDialog }"></slot>
|
|
3
|
+
<Dialog
|
|
4
|
+
:options="{ title: 'Set Link' }"
|
|
5
|
+
v-model="setLinkDialog.show"
|
|
6
|
+
@after-leave="reset"
|
|
7
|
+
>
|
|
8
|
+
<template #body-content>
|
|
9
|
+
<Input
|
|
10
|
+
type="text"
|
|
11
|
+
label="URL"
|
|
12
|
+
v-model="setLinkDialog.url"
|
|
13
|
+
@keydown.enter="(e) => setLink(e.target.value)"
|
|
14
|
+
/>
|
|
15
|
+
</template>
|
|
16
|
+
<template #actions>
|
|
17
|
+
<Button appearance="primary" @click="setLink(setLinkDialog.url)">
|
|
18
|
+
Save
|
|
19
|
+
</Button>
|
|
20
|
+
</template>
|
|
21
|
+
</Dialog>
|
|
22
|
+
</template>
|
|
23
|
+
<script>
|
|
24
|
+
import Dialog from '../Dialog.vue'
|
|
25
|
+
import Button from '../Button.vue'
|
|
26
|
+
import Input from '../Input.vue'
|
|
27
|
+
|
|
28
|
+
export default {
|
|
29
|
+
name: 'InsertLink',
|
|
30
|
+
props: ['editor'],
|
|
31
|
+
components: { Button, Input, Dialog },
|
|
32
|
+
data() {
|
|
33
|
+
return {
|
|
34
|
+
setLinkDialog: { url: '', show: false },
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
methods: {
|
|
38
|
+
openDialog() {
|
|
39
|
+
let existingURL = this.editor.getAttributes('link').href
|
|
40
|
+
if (existingURL) {
|
|
41
|
+
this.setLinkDialog.url = existingURL
|
|
42
|
+
}
|
|
43
|
+
this.setLinkDialog.show = true
|
|
44
|
+
},
|
|
45
|
+
setLink(url) {
|
|
46
|
+
// empty
|
|
47
|
+
if (url === '') {
|
|
48
|
+
this.editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
|
49
|
+
} else {
|
|
50
|
+
// update link
|
|
51
|
+
this.editor
|
|
52
|
+
.chain()
|
|
53
|
+
.focus()
|
|
54
|
+
.extendMarkRange('link')
|
|
55
|
+
.setLink({ href: url })
|
|
56
|
+
.run()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.setLinkDialog.show = false
|
|
60
|
+
this.setLinkDialog.url = ''
|
|
61
|
+
},
|
|
62
|
+
reset() {
|
|
63
|
+
this.setLinkDialog = this.$options.data().setLinkDialog
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
</script>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<slot v-bind="{ onClick: openDialog }"></slot>
|
|
3
|
+
<Dialog
|
|
4
|
+
:options="{ title: 'Add Video' }"
|
|
5
|
+
v-model="addVideoDialog.show"
|
|
6
|
+
@after-leave="reset"
|
|
7
|
+
>
|
|
8
|
+
<template #body-content>
|
|
9
|
+
<FileUploader
|
|
10
|
+
file-types="video/*"
|
|
11
|
+
@success="(file) => (addVideoDialog.url = file.file_url)"
|
|
12
|
+
>
|
|
13
|
+
<template v-slot="{ file, progress, uploading, openFileSelector }">
|
|
14
|
+
<div class="flex items-center space-x-2">
|
|
15
|
+
<Button @click="openFileSelector">
|
|
16
|
+
{{
|
|
17
|
+
uploading
|
|
18
|
+
? `Uploading ${progress}%`
|
|
19
|
+
: addVideoDialog.url
|
|
20
|
+
? 'Change Video'
|
|
21
|
+
: 'Upload Video'
|
|
22
|
+
}}
|
|
23
|
+
</Button>
|
|
24
|
+
<Button
|
|
25
|
+
v-if="addVideoDialog.url"
|
|
26
|
+
@click="
|
|
27
|
+
() => {
|
|
28
|
+
addVideoDialog.url = null
|
|
29
|
+
addVideoDialog.file = null
|
|
30
|
+
}
|
|
31
|
+
"
|
|
32
|
+
>
|
|
33
|
+
Remove
|
|
34
|
+
</Button>
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
37
|
+
</FileUploader>
|
|
38
|
+
<video
|
|
39
|
+
v-if="addVideoDialog.url"
|
|
40
|
+
:src="addVideoDialog.url"
|
|
41
|
+
class="mt-2 w-full rounded-lg"
|
|
42
|
+
type="video/mp4"
|
|
43
|
+
controls
|
|
44
|
+
/>
|
|
45
|
+
</template>
|
|
46
|
+
<template #actions>
|
|
47
|
+
<Button appearance="primary" @click="addVideo(addVideoDialog.url)">
|
|
48
|
+
Insert Video
|
|
49
|
+
</Button>
|
|
50
|
+
<Button @click="reset">Cancel</Button>
|
|
51
|
+
</template>
|
|
52
|
+
</Dialog>
|
|
53
|
+
</template>
|
|
54
|
+
<script>
|
|
55
|
+
import Button from '../Button.vue'
|
|
56
|
+
import Dialog from '../Dialog.vue'
|
|
57
|
+
import FileUploader from '../FileUploader.vue'
|
|
58
|
+
|
|
59
|
+
export default {
|
|
60
|
+
name: 'InsertImage',
|
|
61
|
+
props: ['editor'],
|
|
62
|
+
expose: ['openDialog'],
|
|
63
|
+
data() {
|
|
64
|
+
return {
|
|
65
|
+
addVideoDialog: { url: '', file: null, show: false },
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
components: { Button, Dialog, FileUploader },
|
|
69
|
+
methods: {
|
|
70
|
+
openDialog() {
|
|
71
|
+
this.addVideoDialog.show = true
|
|
72
|
+
},
|
|
73
|
+
onVideoSelect(e) {
|
|
74
|
+
let file = e.target.files[0]
|
|
75
|
+
if (!file) {
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
this.addVideoDialog.file = file
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
addVideo(src) {
|
|
82
|
+
this.editor
|
|
83
|
+
.chain()
|
|
84
|
+
.focus()
|
|
85
|
+
.insertContent(`<video src="${src}"></video>`)
|
|
86
|
+
.run()
|
|
87
|
+
this.reset()
|
|
88
|
+
},
|
|
89
|
+
reset() {
|
|
90
|
+
this.addVideoDialog = this.$options.data().addVideoDialog
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
</script>
|
|
@@ -50,88 +50,47 @@
|
|
|
50
50
|
</template>
|
|
51
51
|
</Popover>
|
|
52
52
|
</div>
|
|
53
|
-
<button
|
|
54
|
-
v-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
53
|
+
<component v-else :is="button.component || 'div'" v-bind="{ editor }">
|
|
54
|
+
<template v-slot="componentSlotProps">
|
|
55
|
+
<button
|
|
56
|
+
class="flex rounded p-1 text-gray-800 transition-colors"
|
|
57
|
+
:class="
|
|
58
|
+
button.isActive(editor) ? 'bg-gray-100' : 'hover:bg-gray-100'
|
|
59
|
+
"
|
|
60
|
+
@click="
|
|
61
|
+
componentSlotProps?.onClick
|
|
62
|
+
? componentSlotProps.onClick(button)
|
|
63
|
+
: onButtonClick(button)
|
|
64
|
+
"
|
|
65
|
+
:title="button.label"
|
|
66
|
+
>
|
|
67
|
+
<component v-if="button.icon" :is="button.icon" class="h-4 w-4" />
|
|
68
|
+
<span
|
|
69
|
+
class="inline-block h-4 min-w-[1rem] text-sm leading-4"
|
|
70
|
+
v-else
|
|
71
|
+
>
|
|
72
|
+
{{ button.text }}
|
|
73
|
+
</span>
|
|
74
|
+
</button>
|
|
75
|
+
</template>
|
|
76
|
+
</component>
|
|
65
77
|
</template>
|
|
66
78
|
</div>
|
|
67
|
-
|
|
68
|
-
<Dialog :options="{ title: 'Set Link' }" v-model="setLinkDialog.show">
|
|
69
|
-
<template #body-content>
|
|
70
|
-
<Input
|
|
71
|
-
type="text"
|
|
72
|
-
label="URL"
|
|
73
|
-
v-model="setLinkDialog.url"
|
|
74
|
-
@keydown.enter="(e) => setLink(e.target.value)"
|
|
75
|
-
/>
|
|
76
|
-
</template>
|
|
77
|
-
<template #actions>
|
|
78
|
-
<Button appearance="primary" @click="setLink(setLinkDialog.url)">
|
|
79
|
-
Save
|
|
80
|
-
</Button>
|
|
81
|
-
</template>
|
|
82
|
-
</Dialog>
|
|
83
|
-
<InsertImage ref="insertImage" :editor="editor" />
|
|
84
79
|
</div>
|
|
85
80
|
</template>
|
|
86
81
|
<script>
|
|
87
|
-
import { Popover
|
|
88
|
-
|
|
82
|
+
import { Popover } from 'frappe-ui'
|
|
83
|
+
|
|
89
84
|
export default {
|
|
90
85
|
name: 'TipTapMenu',
|
|
91
86
|
props: ['buttons'],
|
|
92
87
|
inject: ['editor'],
|
|
93
88
|
components: {
|
|
94
89
|
Popover,
|
|
95
|
-
Dialog,
|
|
96
|
-
Input,
|
|
97
|
-
Button,
|
|
98
|
-
InsertImage,
|
|
99
|
-
},
|
|
100
|
-
data() {
|
|
101
|
-
return {
|
|
102
|
-
setLinkDialog: { url: '', show: false },
|
|
103
|
-
}
|
|
104
90
|
},
|
|
105
91
|
methods: {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
this.setLinkDialog.show = true
|
|
109
|
-
let existingURL = this.editor.getAttributes('link').href
|
|
110
|
-
if (existingURL) {
|
|
111
|
-
this.setLinkDialog.url = existingURL
|
|
112
|
-
}
|
|
113
|
-
} else if (button.label === 'Image') {
|
|
114
|
-
this.$refs.insertImage.openDialog()
|
|
115
|
-
} else {
|
|
116
|
-
button.action(this.editor)
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
setLink(url) {
|
|
120
|
-
// empty
|
|
121
|
-
if (url === '') {
|
|
122
|
-
this.editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
|
123
|
-
} else {
|
|
124
|
-
// update link
|
|
125
|
-
this.editor
|
|
126
|
-
.chain()
|
|
127
|
-
.focus()
|
|
128
|
-
.extendMarkRange('link')
|
|
129
|
-
.setLink({ href: url })
|
|
130
|
-
.run()
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
this.setLinkDialog.show = false
|
|
134
|
-
this.setLinkDialog.url = ''
|
|
92
|
+
onButtonClick(button) {
|
|
93
|
+
button.action(this.editor)
|
|
135
94
|
},
|
|
136
95
|
},
|
|
137
96
|
}
|
|
@@ -24,6 +24,7 @@ import TableCell from '@tiptap/extension-table-cell'
|
|
|
24
24
|
import TableHeader from '@tiptap/extension-table-header'
|
|
25
25
|
import TableRow from '@tiptap/extension-table-row'
|
|
26
26
|
import Image from './image-extension'
|
|
27
|
+
import Video from './video-extension'
|
|
27
28
|
import Link from '@tiptap/extension-link'
|
|
28
29
|
import configureMention from './mention'
|
|
29
30
|
import TextEditorFixedMenu from './TextEditorFixedMenu.vue'
|
|
@@ -135,6 +136,7 @@ export default {
|
|
|
135
136
|
types: ['heading', 'paragraph'],
|
|
136
137
|
}),
|
|
137
138
|
Image,
|
|
139
|
+
Video,
|
|
138
140
|
Link,
|
|
139
141
|
Placeholder.configure({
|
|
140
142
|
showOnlyWhenEditable: false,
|
|
@@ -209,6 +211,11 @@ export default {
|
|
|
209
211
|
height: 0;
|
|
210
212
|
}
|
|
211
213
|
|
|
214
|
+
.ProseMirror-selectednode video,
|
|
215
|
+
img.ProseMirror-selectednode {
|
|
216
|
+
border: 2px solid theme('colors.blue.300');
|
|
217
|
+
}
|
|
218
|
+
|
|
212
219
|
/* Mentions */
|
|
213
220
|
.mention {
|
|
214
221
|
font-weight: 600;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { defineAsyncComponent } from 'vue'
|
|
1
2
|
import H1 from './icons/h-1.vue'
|
|
2
3
|
import H2 from './icons/h-2.vue'
|
|
3
4
|
import H3 from './icons/h-3.vue'
|
|
@@ -17,6 +18,7 @@ import DoubleQuotes from './icons/double-quotes-r.vue'
|
|
|
17
18
|
import CodeView from './icons/code-view.vue'
|
|
18
19
|
import Link from './icons/link.vue'
|
|
19
20
|
import Image from './icons/image-add-line.vue'
|
|
21
|
+
import Video from './icons/video-add-line.vue'
|
|
20
22
|
import ArrowGoBack from './icons/arrow-go-back-line.vue'
|
|
21
23
|
import ArrowGoForward from './icons/arrow-go-forward-line.vue'
|
|
22
24
|
import Separator from './icons/separator.vue'
|
|
@@ -147,11 +149,19 @@ export default {
|
|
|
147
149
|
label: 'Link',
|
|
148
150
|
icon: Link,
|
|
149
151
|
isActive: (editor) => editor.isActive('link'),
|
|
152
|
+
component: defineAsyncComponent(() => import('./InsertLink.vue')),
|
|
150
153
|
},
|
|
151
154
|
Image: {
|
|
152
155
|
label: 'Image',
|
|
153
156
|
icon: Image,
|
|
154
157
|
isActive: (editor) => false,
|
|
158
|
+
component: defineAsyncComponent(() => import('./InsertImage.vue')),
|
|
159
|
+
},
|
|
160
|
+
Video: {
|
|
161
|
+
label: 'Video',
|
|
162
|
+
icon: Video,
|
|
163
|
+
isActive: (editor) => false,
|
|
164
|
+
component: defineAsyncComponent(() => import('./InsertVideo.vue')),
|
|
155
165
|
},
|
|
156
166
|
Undo: {
|
|
157
167
|
label: 'Undo',
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg
|
|
3
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
4
|
+
viewBox="0 0 24 24"
|
|
5
|
+
width="24"
|
|
6
|
+
height="24"
|
|
7
|
+
>
|
|
8
|
+
<path fill="none" d="M0 0H24V24H0z" />
|
|
9
|
+
<path
|
|
10
|
+
d="M16 4c.552 0 1 .448 1 1v4.2l5.213-3.65c.226-.158.538-.103.697.124.058.084.09.184.09.286v12.08c0 .276-.224.5-.5.5-.103 0-.203-.032-.287-.09L17 14.8V19c0 .552-.448 1-1 1H2c-.552 0-1-.448-1-1V5c0-.552.448-1 1-1h14zm-1 2H3v12h12V6zM8 8h2v3h3v2H9.999L10 16H8l-.001-3H5v-2h3V8zm13 .841l-4 2.8v.718l4 2.8V8.84z"
|
|
11
|
+
fill="currentColor"
|
|
12
|
+
/>
|
|
13
|
+
</svg>
|
|
14
|
+
</template>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Node, mergeAttributes } from '@tiptap/core'
|
|
2
|
+
// Inspired by this blog: https://www.codemzy.com/blog/tiptap-video-embed-extension
|
|
3
|
+
|
|
4
|
+
const Video = Node.create({
|
|
5
|
+
name: 'video',
|
|
6
|
+
group: 'block',
|
|
7
|
+
selectable: true,
|
|
8
|
+
draggable: true,
|
|
9
|
+
atom: true,
|
|
10
|
+
|
|
11
|
+
addAttributes() {
|
|
12
|
+
return {
|
|
13
|
+
src: {
|
|
14
|
+
default: null,
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
parseHTML() {
|
|
20
|
+
return [
|
|
21
|
+
{
|
|
22
|
+
tag: 'video',
|
|
23
|
+
},
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
renderHTML({ HTMLAttributes }) {
|
|
28
|
+
return ['video', mergeAttributes(HTMLAttributes)]
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
addNodeView() {
|
|
32
|
+
return ({ editor, node }) => {
|
|
33
|
+
const div = document.createElement('div')
|
|
34
|
+
div.className =
|
|
35
|
+
'relative aspect-w-16 aspect-h-9' +
|
|
36
|
+
(editor.isEditable ? ' cursor-pointer' : '')
|
|
37
|
+
|
|
38
|
+
const video = document.createElement('video')
|
|
39
|
+
if (editor.isEditable) {
|
|
40
|
+
video.className = 'pointer-events-none'
|
|
41
|
+
}
|
|
42
|
+
video.src = node.attrs.src
|
|
43
|
+
if (!editor.isEditable) {
|
|
44
|
+
video.setAttribute('controls', '')
|
|
45
|
+
} else {
|
|
46
|
+
let videoPill = document.createElement('div')
|
|
47
|
+
videoPill.className =
|
|
48
|
+
'absolute top-0 right-0 text-xs m-2 bg-gray-800 text-white px-2 py-1 rounded-md'
|
|
49
|
+
videoPill.innerHTML = 'Video'
|
|
50
|
+
div.append(videoPill)
|
|
51
|
+
}
|
|
52
|
+
div.append(video)
|
|
53
|
+
return {
|
|
54
|
+
dom: div,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
export default Video
|
|
@@ -118,9 +118,9 @@ export function createListResource(options, vm, getResource) {
|
|
|
118
118
|
fieldname: values,
|
|
119
119
|
}
|
|
120
120
|
},
|
|
121
|
-
onSuccess(
|
|
122
|
-
out.
|
|
123
|
-
options.setValue?.onSuccess?.call(vm,
|
|
121
|
+
onSuccess(doc) {
|
|
122
|
+
updateRowInListResource(out.doctype, doc)
|
|
123
|
+
options.setValue?.onSuccess?.call(vm, doc)
|
|
124
124
|
},
|
|
125
125
|
onError: options.setValue?.onError,
|
|
126
126
|
},
|
|
@@ -182,7 +182,7 @@ export function createListResource(options, vm, getResource) {
|
|
|
182
182
|
function transform(data) {
|
|
183
183
|
if (options.transform) {
|
|
184
184
|
let returnValue = options.transform.call(vm, data)
|
|
185
|
-
if (
|
|
185
|
+
if (returnValue != null) {
|
|
186
186
|
return returnValue
|
|
187
187
|
}
|
|
188
188
|
}
|
|
@@ -155,7 +155,7 @@ export function createResource(options, vm, getResource) {
|
|
|
155
155
|
function transform(data) {
|
|
156
156
|
if (options.transform) {
|
|
157
157
|
let returnValue = options.transform.call(vm, data)
|
|
158
|
-
if (
|
|
158
|
+
if (returnValue != null) {
|
|
159
159
|
return returnValue
|
|
160
160
|
}
|
|
161
161
|
}
|