pimelon-ui 0.0.98

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.
Files changed (93) hide show
  1. package/license.md +0 -0
  2. package/package.json +75 -0
  3. package/readme.md +0 -0
  4. package/src/components/Alert.vue +57 -0
  5. package/src/components/Autocomplete.vue +176 -0
  6. package/src/components/Avatar.vue +62 -0
  7. package/src/components/Badge.vue +42 -0
  8. package/src/components/Button.vue +155 -0
  9. package/src/components/Card.vue +37 -0
  10. package/src/components/DatePicker.vue +252 -0
  11. package/src/components/Dialog.vue +216 -0
  12. package/src/components/Dropdown.vue +145 -0
  13. package/src/components/ErrorMessage.vue +24 -0
  14. package/src/components/FeatherIcon.vue +59 -0
  15. package/src/components/FileUploader.vue +220 -0
  16. package/src/components/GreenCheckIcon.vue +16 -0
  17. package/src/components/Input.vue +214 -0
  18. package/src/components/Link.vue +28 -0
  19. package/src/components/ListItem.vue +28 -0
  20. package/src/components/LoadingIndicator.vue +27 -0
  21. package/src/components/LoadingText.vue +21 -0
  22. package/src/components/Popover.vue +253 -0
  23. package/src/components/Resource.vue +21 -0
  24. package/src/components/TextEditor/FontColor.vue +108 -0
  25. package/src/components/TextEditor/InsertImage.vue +74 -0
  26. package/src/components/TextEditor/InsertLink.vue +67 -0
  27. package/src/components/TextEditor/InsertVideo.vue +94 -0
  28. package/src/components/TextEditor/MentionList.vue +95 -0
  29. package/src/components/TextEditor/Menu.vue +99 -0
  30. package/src/components/TextEditor/TextEditor.vue +275 -0
  31. package/src/components/TextEditor/TextEditorBubbleMenu.vue +69 -0
  32. package/src/components/TextEditor/TextEditorFixedMenu.vue +72 -0
  33. package/src/components/TextEditor/TextEditorFloatingMenu.vue +55 -0
  34. package/src/components/TextEditor/commands.js +272 -0
  35. package/src/components/TextEditor/icons/align-center.vue +14 -0
  36. package/src/components/TextEditor/icons/align-justify.vue +14 -0
  37. package/src/components/TextEditor/icons/align-left.vue +14 -0
  38. package/src/components/TextEditor/icons/align-right.vue +14 -0
  39. package/src/components/TextEditor/icons/arrow-go-back-line.vue +14 -0
  40. package/src/components/TextEditor/icons/arrow-go-forward-line.vue +14 -0
  41. package/src/components/TextEditor/icons/bold.vue +14 -0
  42. package/src/components/TextEditor/icons/code-view.vue +14 -0
  43. package/src/components/TextEditor/icons/double-quotes-r.vue +14 -0
  44. package/src/components/TextEditor/icons/font-color.vue +14 -0
  45. package/src/components/TextEditor/icons/format-clear.vue +14 -0
  46. package/src/components/TextEditor/icons/h-1.vue +14 -0
  47. package/src/components/TextEditor/icons/h-2.vue +14 -0
  48. package/src/components/TextEditor/icons/h-3.vue +14 -0
  49. package/src/components/TextEditor/icons/h-4.vue +14 -0
  50. package/src/components/TextEditor/icons/h-5.vue +14 -0
  51. package/src/components/TextEditor/icons/h-6.vue +14 -0
  52. package/src/components/TextEditor/icons/image-add-line.vue +14 -0
  53. package/src/components/TextEditor/icons/italic.vue +14 -0
  54. package/src/components/TextEditor/icons/link.vue +14 -0
  55. package/src/components/TextEditor/icons/list-ordered.vue +14 -0
  56. package/src/components/TextEditor/icons/list-unordered.vue +14 -0
  57. package/src/components/TextEditor/icons/readme.md +1 -0
  58. package/src/components/TextEditor/icons/separator.vue +14 -0
  59. package/src/components/TextEditor/icons/strikethrough.vue +14 -0
  60. package/src/components/TextEditor/icons/table-2.vue +14 -0
  61. package/src/components/TextEditor/icons/text.vue +11 -0
  62. package/src/components/TextEditor/icons/underline.vue +14 -0
  63. package/src/components/TextEditor/icons/video-add-line.vue +14 -0
  64. package/src/components/TextEditor/image-extension.js +152 -0
  65. package/src/components/TextEditor/index.js +6 -0
  66. package/src/components/TextEditor/mention.js +72 -0
  67. package/src/components/TextEditor/utils.js +11 -0
  68. package/src/components/TextEditor/video-extension.js +60 -0
  69. package/src/components/Toast.vue +83 -0
  70. package/src/components/Tooltip.vue +36 -0
  71. package/src/components/toast.js +98 -0
  72. package/src/directives/onOutsideClick.js +34 -0
  73. package/src/directives/visibility.js +24 -0
  74. package/src/index.js +56 -0
  75. package/src/resources/documentResource.js +194 -0
  76. package/src/resources/index.js +4 -0
  77. package/src/resources/listResource.js +312 -0
  78. package/src/resources/local.js +16 -0
  79. package/src/resources/plugin.js +105 -0
  80. package/src/resources/resources.js +215 -0
  81. package/src/style.css +15 -0
  82. package/src/utils/call.js +98 -0
  83. package/src/utils/config.js +9 -0
  84. package/src/utils/debounce.js +15 -0
  85. package/src/utils/file-to-base64.js +9 -0
  86. package/src/utils/markdown.js +29 -0
  87. package/src/utils/melonRequest.js +105 -0
  88. package/src/utils/pageMeta.js +52 -0
  89. package/src/utils/plugin.js +24 -0
  90. package/src/utils/request.js +49 -0
  91. package/src/utils/socketio.js +11 -0
  92. package/src/utils/tailwind.config.js +117 -0
  93. package/src/utils/vite-dev-server.js +14 -0
@@ -0,0 +1,253 @@
1
+ <template>
2
+ <div ref="reference">
3
+ <div
4
+ ref="target"
5
+ :class="['inline-block', $attrs.class]"
6
+ @click="updatePosition"
7
+ @focusin="updatePosition"
8
+ @keydown="updatePosition"
9
+ @mouseover="onMouseover"
10
+ @mouseleave="onMouseleave"
11
+ >
12
+ <slot
13
+ name="target"
14
+ v-bind="{ togglePopover, updatePosition, open, close, isOpen }"
15
+ />
16
+ </div>
17
+ <teleport to="#melonui-popper-root">
18
+ <div
19
+ ref="popover"
20
+ :class="popoverClass"
21
+ class="popover-container relative z-[100]"
22
+ :style="{ minWidth: targetWidth ? targetWidth + 'px' : null }"
23
+ @mouseover="pointerOverTargetOrPopup = true"
24
+ @mouseleave="onMouseleave"
25
+ >
26
+ <transition v-bind="popupTransition">
27
+ <div v-show="isOpen">
28
+ <slot
29
+ name="body"
30
+ v-bind="{ togglePopover, updatePosition, open, close, isOpen }"
31
+ >
32
+ <div class="rounded-lg border border-gray-100 bg-white shadow-xl">
33
+ <slot
34
+ name="body-main"
35
+ v-bind="{
36
+ togglePopover,
37
+ updatePosition,
38
+ open,
39
+ close,
40
+ isOpen,
41
+ }"
42
+ />
43
+ </div>
44
+ </slot>
45
+ </div>
46
+ </transition>
47
+ </div>
48
+ </teleport>
49
+ </div>
50
+ </template>
51
+
52
+ <script>
53
+ import { createPopper } from '@popperjs/core'
54
+
55
+ export default {
56
+ name: 'Popover',
57
+ inheritAttrs: false,
58
+ props: {
59
+ show: {
60
+ default: undefined,
61
+ },
62
+ trigger: {
63
+ type: String,
64
+ default: 'click', // click, hover
65
+ },
66
+ hoverDelay: {
67
+ type: Number,
68
+ default: 0,
69
+ },
70
+ leaveDelay: {
71
+ type: Number,
72
+ default: 0,
73
+ },
74
+ placement: {
75
+ type: String,
76
+ default: 'bottom-start',
77
+ },
78
+ popoverClass: [String, Object, Array],
79
+ transition: {
80
+ default: null,
81
+ },
82
+ hideOnBlur: {
83
+ default: true,
84
+ },
85
+ },
86
+ emits: ['open', 'close', 'update:show'],
87
+ expose: ['open', 'close'],
88
+ data() {
89
+ return {
90
+ showPopup: false,
91
+ targetWidth: null,
92
+ pointerOverTargetOrPopup: false,
93
+ }
94
+ },
95
+ watch: {
96
+ show(val) {
97
+ if (val) {
98
+ this.open()
99
+ } else {
100
+ this.close()
101
+ }
102
+ },
103
+ },
104
+ created() {
105
+ if (typeof window === 'undefined') return
106
+ if (!document.getElementById('melonui-popper-root')) {
107
+ const root = document.createElement('div')
108
+ root.id = 'melonui-popper-root'
109
+ document.body.appendChild(root)
110
+ }
111
+ },
112
+ mounted() {
113
+ this.listener = (e) => {
114
+ let $els = [this.$refs.reference, this.$refs.popover]
115
+ let insideClick = $els.some(
116
+ ($el) => $el && (e.target === $el || $el.contains(e.target))
117
+ )
118
+ if (insideClick) {
119
+ return
120
+ }
121
+ this.close()
122
+ }
123
+ if (this.hideOnBlur) {
124
+ document.addEventListener('click', this.listener)
125
+ }
126
+ this.$nextTick(() => {
127
+ this.targetWidth = this.$refs['target'].clientWidth
128
+ })
129
+ },
130
+ beforeDestroy() {
131
+ this.popper && this.popper.destroy()
132
+ document.removeEventListener('click', this.listener)
133
+ },
134
+ computed: {
135
+ showPropPassed() {
136
+ return this.show != null
137
+ },
138
+ isOpen: {
139
+ get() {
140
+ if (this.showPropPassed) {
141
+ return this.show
142
+ }
143
+ return this.showPopup
144
+ },
145
+ set(val) {
146
+ val = Boolean(val)
147
+ if (this.showPropPassed) {
148
+ this.$emit('update:show', val)
149
+ } else {
150
+ this.showPopup = val
151
+ }
152
+ if (val === false) {
153
+ this.$emit('close')
154
+ } else if (val === true) {
155
+ this.$emit('open')
156
+ }
157
+ },
158
+ },
159
+ popupTransition() {
160
+ let templates = {
161
+ default: {
162
+ enterActiveClass: 'transition duration-200 ease-out',
163
+ enterFromClass: 'translate-y-1 opacity-0',
164
+ enterToClass: 'translate-y-0 opacity-100',
165
+ leaveActiveClass: 'transition duration-150 ease-in',
166
+ leaveFromClass: 'translate-y-0 opacity-100',
167
+ leaveToClass: 'translate-y-1 opacity-0',
168
+ },
169
+ }
170
+ if (typeof this.transition === 'string') {
171
+ return templates[this.transition]
172
+ }
173
+ return this.transition
174
+ },
175
+ },
176
+ methods: {
177
+ setupPopper() {
178
+ if (!this.popper) {
179
+ this.popper = createPopper(this.$refs.reference, this.$refs.popover, {
180
+ placement: this.placement,
181
+ })
182
+ } else {
183
+ this.updatePosition()
184
+ }
185
+ },
186
+ updatePosition() {
187
+ this.popper && this.popper.update()
188
+ },
189
+ togglePopover(flag) {
190
+ if (flag instanceof Event) {
191
+ flag = null
192
+ }
193
+ if (flag == null) {
194
+ flag = !this.isOpen
195
+ }
196
+ flag = Boolean(flag)
197
+ if (flag) {
198
+ this.open()
199
+ } else {
200
+ this.close()
201
+ }
202
+ },
203
+ open() {
204
+ this.isOpen = true
205
+ this.$nextTick(() => this.setupPopper())
206
+ },
207
+ close() {
208
+ this.isOpen = false
209
+ },
210
+ onMouseover() {
211
+ this.pointerOverTargetOrPopup = true
212
+ if (this.leaveTimer) {
213
+ clearTimeout(this.leaveTimer)
214
+ this.leaveTimer = null
215
+ }
216
+ if (this.trigger === 'hover') {
217
+ if (this.hoverDelay) {
218
+ this.hoverTimer = setTimeout(() => {
219
+ if (this.pointerOverTargetOrPopup) {
220
+ this.open()
221
+ }
222
+ }, Number(this.hoverDelay) * 1000)
223
+ } else {
224
+ this.open()
225
+ }
226
+ }
227
+ },
228
+ onMouseleave(e) {
229
+ this.pointerOverTargetOrPopup = false
230
+ if (this.hoverTimer) {
231
+ clearTimeout(this.hoverTimer)
232
+ this.hoverTimer = null
233
+ }
234
+ if (this.trigger === 'hover') {
235
+ if (this.leaveTimer) {
236
+ clearTimeout(this.leaveTimer)
237
+ }
238
+ if (this.leaveDelay) {
239
+ this.leaveTimer = setTimeout(() => {
240
+ if (!this.pointerOverTargetOrPopup) {
241
+ this.close()
242
+ }
243
+ }, Number(this.leaveDelay) * 1000)
244
+ } else {
245
+ if (!this.pointerOverTargetOrPopup) {
246
+ this.close()
247
+ }
248
+ }
249
+ }
250
+ },
251
+ },
252
+ }
253
+ </script>
@@ -0,0 +1,21 @@
1
+ <script>
2
+ export default {
3
+ name: 'Resource',
4
+ props: ['options'],
5
+ resources: {
6
+ resource() {
7
+ return this.options
8
+ },
9
+ },
10
+ render() {
11
+ return this.$slots.default({
12
+ resource: this.$resources.resource,
13
+ data: this.$resources.resource.data,
14
+ error: this.$resources.resource.error,
15
+ loading: this.$resources.resource.loading,
16
+ fetch: (params) => this.$resources.resource.fetch(params),
17
+ submit: (params) => this.$resources.resource.submit(params),
18
+ })
19
+ },
20
+ }
21
+ </script>
@@ -0,0 +1,108 @@
1
+ <template>
2
+ <Popover transition="default">
3
+ <template #target="{ togglePopover, isOpen }">
4
+ <slot
5
+ v-bind="{ onClick: () => togglePopover(), isActive: isOpen }"
6
+ ></slot>
7
+ </template>
8
+ <template #body-main>
9
+ <div class="p-2">
10
+ <div class="text-sm text-gray-700">Text Color</div>
11
+ <div class="mt-1 grid grid-cols-8 gap-1">
12
+ <Tooltip
13
+ class="flex"
14
+ v-for="color in foregroundColors"
15
+ :key="color.name"
16
+ :text="color.name"
17
+ >
18
+ <button
19
+ :aria-label="color.name"
20
+ class="flex h-5 w-5 items-center justify-center rounded border text-base"
21
+ :style="{
22
+ color: color.hex,
23
+ }"
24
+ @click="setForegroundColor(color)"
25
+ >
26
+ A
27
+ </button>
28
+ </Tooltip>
29
+ </div>
30
+ <div class="mt-2 text-sm text-gray-700">Background Color</div>
31
+ <div class="mt-1 grid grid-cols-8 gap-1">
32
+ <Tooltip
33
+ class="flex"
34
+ v-for="color in backgroundColors"
35
+ :key="color.name"
36
+ :text="color.name"
37
+ >
38
+ <button
39
+ :aria-label="color.name"
40
+ class="flex h-5 w-5 items-center justify-center rounded border text-base text-gray-900"
41
+ :class="!color.hex ? 'border-gray-200' : 'border-transparent'"
42
+ :style="{
43
+ backgroundColor: color.hex,
44
+ }"
45
+ @click="setBackgroundColor(color)"
46
+ >
47
+ A
48
+ </button>
49
+ </Tooltip>
50
+ </div>
51
+ </div>
52
+ </template>
53
+ </Popover>
54
+ </template>
55
+ <script>
56
+ import Popover from '../Popover.vue'
57
+ import Tooltip from '../Tooltip.vue'
58
+
59
+ export default {
60
+ name: 'FontColor',
61
+ props: ['editor'],
62
+ components: { Popover, Tooltip },
63
+ methods: {
64
+ setBackgroundColor(color) {
65
+ if (color.name != 'Default') {
66
+ this.editor.chain().focus().toggleHighlight({ color: color.hex }).run()
67
+ } else {
68
+ this.editor.chain().focus().unsetHighlight().run()
69
+ }
70
+ },
71
+ setForegroundColor(color) {
72
+ if (color.name != 'Default') {
73
+ this.editor.chain().focus().setColor(color.hex).run()
74
+ } else {
75
+ this.editor.chain().focus().unsetColor().run()
76
+ }
77
+ },
78
+ },
79
+ computed: {
80
+ foregroundColors() {
81
+ // tailwind css colors, scale 600
82
+ return [
83
+ { name: 'Default', hex: '#1F272E' },
84
+ { name: 'Yellow', hex: '#ca8a04' },
85
+ { name: 'Orange', hex: '#ea580c' },
86
+ { name: 'Red', hex: '#dc2626' },
87
+ { name: 'Green', hex: '#16a34a' },
88
+ { name: 'Blue', hex: '#1579D0' },
89
+ { name: 'Purple', hex: '#9333ea' },
90
+ { name: 'Pink', hex: '#db2777' },
91
+ ]
92
+ },
93
+ backgroundColors() {
94
+ // tailwind css colors, scale 100
95
+ return [
96
+ { name: 'Default', hex: null },
97
+ { name: 'Yellow', hex: '#fef9c3' },
98
+ { name: 'Orange', hex: '#ffedd5' },
99
+ { name: 'Red', hex: '#fee2e2' },
100
+ { name: 'Green', hex: '#dcfce7' },
101
+ { name: 'Blue', hex: '#D3E9FC' },
102
+ { name: 'Purple', hex: '#f3e8ff' },
103
+ { name: 'Pink', hex: '#fce7f3' },
104
+ ]
105
+ },
106
+ },
107
+ }
108
+ </script>
@@ -0,0 +1,74 @@
1
+ <template>
2
+ <slot v-bind="{ onClick: openDialog }"></slot>
3
+ <Dialog
4
+ :options="{ title: 'Add Image' }"
5
+ v-model="addImageDialog.show"
6
+ @after-leave="reset"
7
+ >
8
+ <template #body-content>
9
+ <label
10
+ class="relative cursor-pointer rounded-lg bg-gray-100 py-1 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 select-none px-2 py-1 text-base">
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="mt-2 w-full rounded-lg"
26
+ />
27
+ </template>
28
+ <template #actions>
29
+ <Button appearance="primary" @click="addImage(addImageDialog.url)">
30
+ Insert Image
31
+ </Button>
32
+ <Button @click="reset"> Cancel </Button>
33
+ </template>
34
+ </Dialog>
35
+ </template>
36
+ <script>
37
+ import fileToBase64 from '../../utils/file-to-base64'
38
+ import Dialog from '../Dialog.vue'
39
+ import Button from '../Button.vue'
40
+
41
+ export default {
42
+ name: 'InsertImage',
43
+ props: ['editor'],
44
+ expose: ['openDialog'],
45
+ data() {
46
+ return {
47
+ addImageDialog: { url: '', file: null, show: false },
48
+ }
49
+ },
50
+ components: { Button, Dialog },
51
+ methods: {
52
+ openDialog() {
53
+ this.addImageDialog.show = true
54
+ },
55
+ onImageSelect(e) {
56
+ let file = e.target.files[0]
57
+ if (!file) {
58
+ return
59
+ }
60
+ this.addImageDialog.file = file
61
+ fileToBase64(file).then((base64) => {
62
+ this.addImageDialog.url = base64
63
+ })
64
+ },
65
+ addImage(src) {
66
+ this.editor.chain().focus().setImage({ src }).run()
67
+ this.reset()
68
+ },
69
+ reset() {
70
+ this.addImageDialog = this.$options.data().addImageDialog
71
+ },
72
+ },
73
+ }
74
+ </script>
@@ -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>
@@ -0,0 +1,95 @@
1
+ <template>
2
+ <div>
3
+ <div
4
+ v-if="items.length"
5
+ class="min-w-40 rounded-lg border bg-white p-1 text-base shadow-lg"
6
+ >
7
+ <button
8
+ :class="[
9
+ index === selectedIndex ? 'bg-gray-100' : 'text-gray-900',
10
+ 'flex w-full items-center whitespace-nowrap rounded-md px-2 py-2 text-sm',
11
+ ]"
12
+ v-for="(item, index) in items"
13
+ :key="index"
14
+ @click="selectItem(index)"
15
+ @mouseover="selectedIndex = index"
16
+ >
17
+ {{ item.label }}
18
+ </button>
19
+ </div>
20
+ </div>
21
+ </template>
22
+
23
+ <script>
24
+ export default {
25
+ props: {
26
+ items: {
27
+ type: Array,
28
+ required: true,
29
+ },
30
+ command: {
31
+ type: Function,
32
+ required: true,
33
+ },
34
+ },
35
+ data() {
36
+ return {
37
+ selectedIndex: 0,
38
+ }
39
+ },
40
+ watch: {
41
+ items() {
42
+ this.selectedIndex = 0
43
+ },
44
+ },
45
+ methods: {
46
+ onKeyDown({ event }) {
47
+ if (event.key === 'ArrowUp') {
48
+ this.upHandler()
49
+ return true
50
+ }
51
+ if (event.key === 'ArrowDown') {
52
+ this.downHandler()
53
+ return true
54
+ }
55
+ if (event.key === 'Enter') {
56
+ this.enterHandler()
57
+ return true
58
+ }
59
+ return false
60
+ },
61
+ upHandler() {
62
+ this.selectedIndex =
63
+ (this.selectedIndex + this.items.length - 1) % this.items.length
64
+ },
65
+ downHandler() {
66
+ this.selectedIndex = (this.selectedIndex + 1) % this.items.length
67
+ },
68
+ enterHandler() {
69
+ this.selectItem(this.selectedIndex)
70
+ },
71
+ selectItem(index) {
72
+ const item = this.items[index]
73
+ if (item) {
74
+ this.command({ id: item.value, label: item.label })
75
+ }
76
+ },
77
+ },
78
+ }
79
+ </script>
80
+
81
+ <style>
82
+ .item {
83
+ display: block;
84
+ margin: 0;
85
+ width: 100%;
86
+ text-align: left;
87
+ background: transparent;
88
+ border-radius: 0.4rem;
89
+ border: 1px solid transparent;
90
+ padding: 0.2rem 0.4rem;
91
+ }
92
+ .item.is-selected {
93
+ border-color: #000;
94
+ }
95
+ </style>