pimelon-ui 0.0.73 → 0.0.84
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 +2 -1
- package/src/components/Button.vue +4 -0
- package/src/components/Dialog.vue +29 -4
- package/src/components/Dropdown.vue +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 +29 -70
- package/src/components/TextEditor/TextEditor.vue +29 -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/index.js +4 -0
- package/src/components/TextEditor/video-extension.js +60 -0
- package/src/components/Toast.vue +1 -1
- package/src/resources/listResource.js +8 -14
- package/src/resources/resources.js +4 -2
- package/src/utils/markdown.js +29 -0
- package/src/utils/tailwind.config.js +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pimelon-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.84",
|
|
4
4
|
"description": "A set of components and utilities for rapid UI development",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"feather-icons": "^4.28.0",
|
|
40
40
|
"idb-keyval": "^6.2.0",
|
|
41
41
|
"postcss": "^8.4.5",
|
|
42
|
+
"showdown": "^2.1.0",
|
|
42
43
|
"socket.io-client": "^4.5.1",
|
|
43
44
|
"tailwindcss": "^3.0.12",
|
|
44
45
|
"tippy.js": "^6.3.7"
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
:class="buttonClasses"
|
|
5
5
|
@click="handleClick"
|
|
6
6
|
:disabled="isDisabled"
|
|
7
|
+
:aria-label="ariaLabel"
|
|
7
8
|
>
|
|
8
9
|
<LoadingIndicator
|
|
9
10
|
v-if="loading"
|
|
@@ -136,6 +137,9 @@ export default {
|
|
|
136
137
|
isDisabled() {
|
|
137
138
|
return this.disabled || this.loading
|
|
138
139
|
},
|
|
140
|
+
ariaLabel() {
|
|
141
|
+
return this.icon ? this.label : null
|
|
142
|
+
},
|
|
139
143
|
},
|
|
140
144
|
methods: {
|
|
141
145
|
handleClick() {
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
@close="open = false"
|
|
11
11
|
>
|
|
12
12
|
<div
|
|
13
|
-
class="flex min-h-screen flex-col items-center
|
|
13
|
+
class="flex min-h-screen flex-col items-center px-4 pt-4 pb-20 text-center"
|
|
14
|
+
:class="dialogPositionClasses"
|
|
14
15
|
>
|
|
15
16
|
<TransitionChild
|
|
16
17
|
as="template"
|
|
@@ -36,7 +37,20 @@
|
|
|
36
37
|
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
37
38
|
>
|
|
38
39
|
<div
|
|
39
|
-
class="my-8 inline-block w-full
|
|
40
|
+
class="my-8 inline-block w-full transform overflow-hidden rounded-lg bg-white text-left align-middle shadow-xl transition-all"
|
|
41
|
+
:class="{
|
|
42
|
+
'max-w-7xl': options.size === '7xl',
|
|
43
|
+
'max-w-6xl': options.size === '6xl',
|
|
44
|
+
'max-w-5xl': options.size === '5xl',
|
|
45
|
+
'max-w-4xl': options.size === '4xl',
|
|
46
|
+
'max-w-3xl': options.size === '3xl',
|
|
47
|
+
'max-w-2xl': options.size === '2xl',
|
|
48
|
+
'max-w-xl': options.size === 'xl',
|
|
49
|
+
'max-w-md': options.size === 'md',
|
|
50
|
+
'max-w-lg': options.size === 'lg' || !options.size,
|
|
51
|
+
'max-w-sm': options.size === 'sm',
|
|
52
|
+
'max-w-xs': options.size === 'xs',
|
|
53
|
+
}"
|
|
40
54
|
>
|
|
41
55
|
<slot name="body">
|
|
42
56
|
<slot name="body-main">
|
|
@@ -76,7 +90,10 @@
|
|
|
76
90
|
</DialogTitle>
|
|
77
91
|
|
|
78
92
|
<slot name="body-content">
|
|
79
|
-
<p
|
|
93
|
+
<p
|
|
94
|
+
class="text-base text-gray-600"
|
|
95
|
+
v-if="options.message"
|
|
96
|
+
>
|
|
80
97
|
{{ options.message }}
|
|
81
98
|
</p>
|
|
82
99
|
</slot>
|
|
@@ -118,7 +135,8 @@ import {
|
|
|
118
135
|
TransitionChild,
|
|
119
136
|
TransitionRoot,
|
|
120
137
|
} from '@headlessui/vue'
|
|
121
|
-
import
|
|
138
|
+
import Button from './Button.vue'
|
|
139
|
+
import FeatherIcon from './FeatherIcon.vue'
|
|
122
140
|
|
|
123
141
|
export default {
|
|
124
142
|
name: 'Dialog',
|
|
@@ -187,6 +205,13 @@ export default {
|
|
|
187
205
|
}
|
|
188
206
|
return icon
|
|
189
207
|
},
|
|
208
|
+
dialogPositionClasses() {
|
|
209
|
+
let position = this.options?.position || 'center'
|
|
210
|
+
return {
|
|
211
|
+
'justify-center': position === 'center',
|
|
212
|
+
'pt-[20vh]': position === 'top',
|
|
213
|
+
}
|
|
214
|
+
},
|
|
190
215
|
},
|
|
191
216
|
}
|
|
192
217
|
</script>
|
|
@@ -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>
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
class="w-full rounded px-2 py-1 text-left text-base hover:bg-gray-50"
|
|
39
39
|
@click="
|
|
40
40
|
() => {
|
|
41
|
-
|
|
41
|
+
onButtonClick(option)
|
|
42
42
|
close()
|
|
43
43
|
}
|
|
44
44
|
"
|
|
@@ -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
|
|
88
|
-
|
|
82
|
+
import Popover from '../Popover.vue'
|
|
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,11 +24,14 @@ 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'
|
|
30
31
|
import TextEditorBubbleMenu from './TextEditorBubbleMenu.vue'
|
|
31
32
|
import TextEditorFloatingMenu from './TextEditorFloatingMenu.vue'
|
|
33
|
+
import { detectMarkdown, markdownToHTML } from '../../utils/markdown'
|
|
34
|
+
import { DOMParser } from 'prosemirror-model'
|
|
32
35
|
|
|
33
36
|
export default {
|
|
34
37
|
name: 'TextEditor',
|
|
@@ -133,6 +136,7 @@ export default {
|
|
|
133
136
|
types: ['heading', 'paragraph'],
|
|
134
137
|
}),
|
|
135
138
|
Image,
|
|
139
|
+
Video,
|
|
136
140
|
Link,
|
|
137
141
|
Placeholder.configure({
|
|
138
142
|
showOnlyWhenEditable: false,
|
|
@@ -161,6 +165,26 @@ export default {
|
|
|
161
165
|
this.editorClass,
|
|
162
166
|
]),
|
|
163
167
|
},
|
|
168
|
+
clipboardTextParser: (text, $context) => {
|
|
169
|
+
if (!detectMarkdown(text)) return
|
|
170
|
+
if (
|
|
171
|
+
!confirm(
|
|
172
|
+
'Do you want to convert markdown content to HTML before pasting?'
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
let dom = document.createElement('div')
|
|
178
|
+
dom.innerHTML = markdownToHTML(text)
|
|
179
|
+
let parser =
|
|
180
|
+
this.editor.view.someProp('clipboardParser') ||
|
|
181
|
+
this.editor.view.someProp('domParser') ||
|
|
182
|
+
DOMParser.fromSchema(this.editor.schema)
|
|
183
|
+
return parser.parseSlice(dom, {
|
|
184
|
+
preserveWhitespace: true,
|
|
185
|
+
context: $context,
|
|
186
|
+
})
|
|
187
|
+
},
|
|
164
188
|
}
|
|
165
189
|
},
|
|
166
190
|
},
|
|
@@ -187,6 +211,11 @@ export default {
|
|
|
187
211
|
height: 0;
|
|
188
212
|
}
|
|
189
213
|
|
|
214
|
+
.ProseMirror-selectednode video,
|
|
215
|
+
img.ProseMirror-selectednode {
|
|
216
|
+
border: 2px solid theme('colors.blue.300');
|
|
217
|
+
}
|
|
218
|
+
|
|
190
219
|
/* Mentions */
|
|
191
220
|
.mention {
|
|
192
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>
|
|
@@ -1 +1,5 @@
|
|
|
1
1
|
export { default } from './TextEditor.vue'
|
|
2
|
+
export { default as TextEditor } from './TextEditor.vue'
|
|
3
|
+
export { default as TextEditorBubbleMenu } from './TextEditorBubbleMenu.vue'
|
|
4
|
+
export { default as TextEditorFixedMenu } from './TextEditorFixedMenu.vue'
|
|
5
|
+
export { default as TextEditorFloatingMenu } from './TextEditorFloatingMenu.vue'
|
|
@@ -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
|
package/src/components/Toast.vue
CHANGED
|
@@ -40,6 +40,8 @@ export function createListResource(options, vm, getResource) {
|
|
|
40
40
|
fields: out.fields,
|
|
41
41
|
filters: out.filters,
|
|
42
42
|
order_by: out.order_by,
|
|
43
|
+
start: out.start,
|
|
44
|
+
limit: out.limit,
|
|
43
45
|
limit_start: out.start,
|
|
44
46
|
limit_page_length: out.limit,
|
|
45
47
|
parent: out.parent,
|
|
@@ -77,18 +79,9 @@ export function createListResource(options, vm, getResource) {
|
|
|
77
79
|
onSuccess(data) {
|
|
78
80
|
if (data.length > 0 && out.originalData) {
|
|
79
81
|
let doc = data[0]
|
|
80
|
-
|
|
81
|
-
out.originalData = out.originalData.filter(
|
|
82
|
-
(d) => d.name !== doc.name
|
|
83
|
-
)
|
|
84
|
-
out.originalData = [
|
|
85
|
-
out.originalData.slice(0, index),
|
|
86
|
-
data,
|
|
87
|
-
out.originalData.slice(index),
|
|
88
|
-
].flat()
|
|
82
|
+
updateRowInListResource(out.doctype, doc)
|
|
89
83
|
}
|
|
90
84
|
|
|
91
|
-
out.data = transform(out.originalData)
|
|
92
85
|
options.fetchOne?.onSuccess?.call(vm, out.data)
|
|
93
86
|
},
|
|
94
87
|
onError: options.fetchOne?.onError,
|
|
@@ -125,9 +118,9 @@ export function createListResource(options, vm, getResource) {
|
|
|
125
118
|
fieldname: values,
|
|
126
119
|
}
|
|
127
120
|
},
|
|
128
|
-
onSuccess(
|
|
129
|
-
out.
|
|
130
|
-
options.setValue?.onSuccess?.call(vm,
|
|
121
|
+
onSuccess(doc) {
|
|
122
|
+
updateRowInListResource(out.doctype, doc)
|
|
123
|
+
options.setValue?.onSuccess?.call(vm, doc)
|
|
131
124
|
},
|
|
132
125
|
onError: options.setValue?.onError,
|
|
133
126
|
},
|
|
@@ -189,7 +182,7 @@ export function createListResource(options, vm, getResource) {
|
|
|
189
182
|
function transform(data) {
|
|
190
183
|
if (options.transform) {
|
|
191
184
|
let returnValue = options.transform.call(vm, data)
|
|
192
|
-
if (
|
|
185
|
+
if (returnValue != null) {
|
|
193
186
|
return returnValue
|
|
194
187
|
}
|
|
195
188
|
}
|
|
@@ -256,6 +249,7 @@ export function getCachedListResource(cacheKey) {
|
|
|
256
249
|
}
|
|
257
250
|
|
|
258
251
|
export function updateRowInListResource(doctype, doc) {
|
|
252
|
+
if (!doc.name) return
|
|
259
253
|
let resources = resourcesByDocType[doctype] || []
|
|
260
254
|
for (let resource of resources) {
|
|
261
255
|
if (resource.originalData) {
|
|
@@ -31,6 +31,7 @@ export function createResource(options, vm, getResource) {
|
|
|
31
31
|
loading: false,
|
|
32
32
|
fetched: false,
|
|
33
33
|
error: null,
|
|
34
|
+
promise: null,
|
|
34
35
|
auto: options.auto,
|
|
35
36
|
params: null,
|
|
36
37
|
fetch: fetchFunction,
|
|
@@ -85,7 +86,8 @@ export function createResource(options, vm, getResource) {
|
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
try {
|
|
88
|
-
|
|
89
|
+
out.promise = resourceFetcher(options.method, params || options.params)
|
|
90
|
+
let data = await out.promise
|
|
89
91
|
saveLocal(cacheKey, data)
|
|
90
92
|
out.data = transform(data)
|
|
91
93
|
out.fetched = true
|
|
@@ -155,7 +157,7 @@ export function createResource(options, vm, getResource) {
|
|
|
155
157
|
function transform(data) {
|
|
156
158
|
if (options.transform) {
|
|
157
159
|
let returnValue = options.transform.call(vm, data)
|
|
158
|
-
if (
|
|
160
|
+
if (returnValue != null) {
|
|
159
161
|
return returnValue
|
|
160
162
|
}
|
|
161
163
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import showdown from 'showdown'
|
|
2
|
+
|
|
3
|
+
export function markdownToHTML(text) {
|
|
4
|
+
const converter = new showdown.Converter()
|
|
5
|
+
return converter.makeHtml(text)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function htmlToMarkdown(text) {
|
|
9
|
+
const converter = new showdown.Converter()
|
|
10
|
+
return converter.makeMarkdown(text)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function detectMarkdown(text) {
|
|
14
|
+
const lines = text.split('\n')
|
|
15
|
+
const markdown = lines.filter(
|
|
16
|
+
(line) =>
|
|
17
|
+
line.startsWith('![') ||
|
|
18
|
+
line.startsWith('#') ||
|
|
19
|
+
line.startsWith('> ') ||
|
|
20
|
+
line.startsWith('*') ||
|
|
21
|
+
line.startsWith('- ') ||
|
|
22
|
+
line.startsWith('1. ') ||
|
|
23
|
+
line.startsWith('```') ||
|
|
24
|
+
line.startsWith('`') ||
|
|
25
|
+
line.startsWith('[') ||
|
|
26
|
+
line.startsWith('---')
|
|
27
|
+
)
|
|
28
|
+
return markdown.length > 0
|
|
29
|
+
}
|