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.
- package/license.md +0 -0
- package/package.json +75 -0
- package/readme.md +0 -0
- package/src/components/Alert.vue +57 -0
- package/src/components/Autocomplete.vue +176 -0
- package/src/components/Avatar.vue +62 -0
- package/src/components/Badge.vue +42 -0
- package/src/components/Button.vue +155 -0
- package/src/components/Card.vue +37 -0
- package/src/components/DatePicker.vue +252 -0
- package/src/components/Dialog.vue +216 -0
- package/src/components/Dropdown.vue +145 -0
- package/src/components/ErrorMessage.vue +24 -0
- package/src/components/FeatherIcon.vue +59 -0
- package/src/components/FileUploader.vue +220 -0
- package/src/components/GreenCheckIcon.vue +16 -0
- package/src/components/Input.vue +214 -0
- package/src/components/Link.vue +28 -0
- package/src/components/ListItem.vue +28 -0
- package/src/components/LoadingIndicator.vue +27 -0
- package/src/components/LoadingText.vue +21 -0
- package/src/components/Popover.vue +253 -0
- package/src/components/Resource.vue +21 -0
- package/src/components/TextEditor/FontColor.vue +108 -0
- package/src/components/TextEditor/InsertImage.vue +74 -0
- package/src/components/TextEditor/InsertLink.vue +67 -0
- package/src/components/TextEditor/InsertVideo.vue +94 -0
- package/src/components/TextEditor/MentionList.vue +95 -0
- package/src/components/TextEditor/Menu.vue +99 -0
- package/src/components/TextEditor/TextEditor.vue +275 -0
- package/src/components/TextEditor/TextEditorBubbleMenu.vue +69 -0
- package/src/components/TextEditor/TextEditorFixedMenu.vue +72 -0
- package/src/components/TextEditor/TextEditorFloatingMenu.vue +55 -0
- package/src/components/TextEditor/commands.js +272 -0
- package/src/components/TextEditor/icons/align-center.vue +14 -0
- package/src/components/TextEditor/icons/align-justify.vue +14 -0
- package/src/components/TextEditor/icons/align-left.vue +14 -0
- package/src/components/TextEditor/icons/align-right.vue +14 -0
- package/src/components/TextEditor/icons/arrow-go-back-line.vue +14 -0
- package/src/components/TextEditor/icons/arrow-go-forward-line.vue +14 -0
- package/src/components/TextEditor/icons/bold.vue +14 -0
- package/src/components/TextEditor/icons/code-view.vue +14 -0
- package/src/components/TextEditor/icons/double-quotes-r.vue +14 -0
- package/src/components/TextEditor/icons/font-color.vue +14 -0
- package/src/components/TextEditor/icons/format-clear.vue +14 -0
- package/src/components/TextEditor/icons/h-1.vue +14 -0
- package/src/components/TextEditor/icons/h-2.vue +14 -0
- package/src/components/TextEditor/icons/h-3.vue +14 -0
- package/src/components/TextEditor/icons/h-4.vue +14 -0
- package/src/components/TextEditor/icons/h-5.vue +14 -0
- package/src/components/TextEditor/icons/h-6.vue +14 -0
- package/src/components/TextEditor/icons/image-add-line.vue +14 -0
- package/src/components/TextEditor/icons/italic.vue +14 -0
- package/src/components/TextEditor/icons/link.vue +14 -0
- package/src/components/TextEditor/icons/list-ordered.vue +14 -0
- package/src/components/TextEditor/icons/list-unordered.vue +14 -0
- package/src/components/TextEditor/icons/readme.md +1 -0
- package/src/components/TextEditor/icons/separator.vue +14 -0
- package/src/components/TextEditor/icons/strikethrough.vue +14 -0
- package/src/components/TextEditor/icons/table-2.vue +14 -0
- package/src/components/TextEditor/icons/text.vue +11 -0
- package/src/components/TextEditor/icons/underline.vue +14 -0
- package/src/components/TextEditor/icons/video-add-line.vue +14 -0
- package/src/components/TextEditor/image-extension.js +152 -0
- package/src/components/TextEditor/index.js +6 -0
- package/src/components/TextEditor/mention.js +72 -0
- package/src/components/TextEditor/utils.js +11 -0
- package/src/components/TextEditor/video-extension.js +60 -0
- package/src/components/Toast.vue +83 -0
- package/src/components/Tooltip.vue +36 -0
- package/src/components/toast.js +98 -0
- package/src/directives/onOutsideClick.js +34 -0
- package/src/directives/visibility.js +24 -0
- package/src/index.js +56 -0
- package/src/resources/documentResource.js +194 -0
- package/src/resources/index.js +4 -0
- package/src/resources/listResource.js +312 -0
- package/src/resources/local.js +16 -0
- package/src/resources/plugin.js +105 -0
- package/src/resources/resources.js +215 -0
- package/src/style.css +15 -0
- package/src/utils/call.js +98 -0
- package/src/utils/config.js +9 -0
- package/src/utils/debounce.js +15 -0
- package/src/utils/file-to-base64.js +9 -0
- package/src/utils/markdown.js +29 -0
- package/src/utils/melonRequest.js +105 -0
- package/src/utils/pageMeta.js +52 -0
- package/src/utils/plugin.js +24 -0
- package/src/utils/request.js +49 -0
- package/src/utils/socketio.js +11 -0
- package/src/utils/tailwind.config.js +117 -0
- package/src/utils/vite-dev-server.js +14 -0
|
@@ -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="M8 3v9a4 4 0 1 0 8 0V3h2v9a6 6 0 1 1-12 0V3h2zM4 20h16v2H4v-2z"
|
|
11
|
+
fill="currentColor"
|
|
12
|
+
/>
|
|
13
|
+
</svg>
|
|
14
|
+
</template>
|
|
@@ -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,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,6 @@
|
|
|
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'
|
|
6
|
+
export { EditorContent as TextEditorContent } from '@tiptap/vue-3'
|
|
@@ -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
|
+
}
|
|
@@ -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
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="my-2 min-w-[15rem] max-w-[40rem] rounded-lg border bg-white p-4 shadow-md"
|
|
4
|
+
>
|
|
5
|
+
<div class="flex items-start">
|
|
6
|
+
<div v-if="icon" class="mr-3 grid h-5 w-5 place-items-center">
|
|
7
|
+
<FeatherIcon :name="icon" :class="['h-5 w-5', iconClasses]" />
|
|
8
|
+
</div>
|
|
9
|
+
<div>
|
|
10
|
+
<slot>
|
|
11
|
+
<p
|
|
12
|
+
v-if="title"
|
|
13
|
+
class="text-base font-medium text-gray-900"
|
|
14
|
+
:class="{ 'mb-1': text }"
|
|
15
|
+
>
|
|
16
|
+
{{ title }}
|
|
17
|
+
</p>
|
|
18
|
+
<p v-if="text" class="text-base text-gray-600">
|
|
19
|
+
{{ text }}
|
|
20
|
+
</p>
|
|
21
|
+
</slot>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="ml-auto pl-2">
|
|
24
|
+
<slot name="actions">
|
|
25
|
+
<button
|
|
26
|
+
class="grid h-5 w-5 place-items-center rounded hover:bg-gray-100"
|
|
27
|
+
@click="$emit('close')"
|
|
28
|
+
>
|
|
29
|
+
<FeatherIcon name="x" class="h-4 w-4 text-gray-700" />
|
|
30
|
+
</button>
|
|
31
|
+
</slot>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</template>
|
|
36
|
+
<script>
|
|
37
|
+
import FeatherIcon from './FeatherIcon.vue'
|
|
38
|
+
const positions = [
|
|
39
|
+
'top-right',
|
|
40
|
+
'top-center',
|
|
41
|
+
'top-left',
|
|
42
|
+
'bottom-right',
|
|
43
|
+
'bottom-center',
|
|
44
|
+
'bottom-left',
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
export default {
|
|
48
|
+
name: 'Toast',
|
|
49
|
+
props: {
|
|
50
|
+
position: {
|
|
51
|
+
type: String,
|
|
52
|
+
default: 'top-center',
|
|
53
|
+
},
|
|
54
|
+
icon: {
|
|
55
|
+
type: String,
|
|
56
|
+
},
|
|
57
|
+
iconClasses: {
|
|
58
|
+
type: String,
|
|
59
|
+
},
|
|
60
|
+
title: {
|
|
61
|
+
type: String,
|
|
62
|
+
},
|
|
63
|
+
text: {
|
|
64
|
+
type: String,
|
|
65
|
+
},
|
|
66
|
+
timeout: {
|
|
67
|
+
type: Number,
|
|
68
|
+
default: 5,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
emits: ['close'],
|
|
72
|
+
components: {
|
|
73
|
+
FeatherIcon,
|
|
74
|
+
},
|
|
75
|
+
mounted() {
|
|
76
|
+
if (this.timeout > 0) {
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
this.$emit('close')
|
|
79
|
+
}, this.timeout * 1000)
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
</script>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Popover trigger="hover" :hoverDelay="hoverDelay" :placement="placement">
|
|
3
|
+
<template #target>
|
|
4
|
+
<slot />
|
|
5
|
+
</template>
|
|
6
|
+
<template #body>
|
|
7
|
+
<slot name="body">
|
|
8
|
+
<div
|
|
9
|
+
v-if="text"
|
|
10
|
+
class="rounded-lg border border-gray-100 bg-gray-800 px-2 py-1 text-xs text-white shadow-xl"
|
|
11
|
+
>
|
|
12
|
+
{{ text }}
|
|
13
|
+
</div>
|
|
14
|
+
</slot>
|
|
15
|
+
</template>
|
|
16
|
+
</Popover>
|
|
17
|
+
</template>
|
|
18
|
+
<script>
|
|
19
|
+
import Popover from './Popover.vue'
|
|
20
|
+
export default {
|
|
21
|
+
name: 'Tooltip',
|
|
22
|
+
components: { Popover },
|
|
23
|
+
props: {
|
|
24
|
+
hoverDelay: {
|
|
25
|
+
default: 0.5,
|
|
26
|
+
},
|
|
27
|
+
placement: {
|
|
28
|
+
default: 'top',
|
|
29
|
+
},
|
|
30
|
+
text: {
|
|
31
|
+
type: String,
|
|
32
|
+
default: null,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { h, reactive, TransitionGroup, ref, Teleport } from 'vue'
|
|
2
|
+
import Toast from './Toast.vue'
|
|
3
|
+
|
|
4
|
+
let toasts = ref([])
|
|
5
|
+
|
|
6
|
+
export let Toasts = {
|
|
7
|
+
name: 'Toasts',
|
|
8
|
+
created() {
|
|
9
|
+
if (typeof window === 'undefined') return
|
|
10
|
+
if (!document.getElementById('melonui-toast-root')) {
|
|
11
|
+
const root = document.createElement('div')
|
|
12
|
+
root.id = 'melonui-toast-root'
|
|
13
|
+
root.style.position = 'fixed'
|
|
14
|
+
root.style.top = '16px'
|
|
15
|
+
root.style.right = '16px'
|
|
16
|
+
root.style.bottom = '16px'
|
|
17
|
+
root.style.left = '16px'
|
|
18
|
+
root.style.zIndex = '9999'
|
|
19
|
+
root.style.pointerEvents = 'none'
|
|
20
|
+
document.body.appendChild(root)
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
render() {
|
|
24
|
+
return h(Teleport, { to: '#melonui-toast-root' }, [
|
|
25
|
+
getToastsGroup('top-left'),
|
|
26
|
+
getToastsGroup('top-center'),
|
|
27
|
+
getToastsGroup('top-right'),
|
|
28
|
+
getToastsGroup('bottom-left'),
|
|
29
|
+
getToastsGroup('bottom-center'),
|
|
30
|
+
getToastsGroup('bottom-right'),
|
|
31
|
+
])
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getToastsGroup(position) {
|
|
36
|
+
let transition =
|
|
37
|
+
'transition duration-[230ms] ease-[cubic-bezier(.21,1.02,.73,1)]'
|
|
38
|
+
let classes = ['absolute']
|
|
39
|
+
if (position === 'top-left') {
|
|
40
|
+
classes.push('top-0 left-0')
|
|
41
|
+
}
|
|
42
|
+
if (position === 'top-right') {
|
|
43
|
+
classes.push('top-0 right-0')
|
|
44
|
+
}
|
|
45
|
+
if (position === 'top-center') {
|
|
46
|
+
classes.push('top-0 left-1/2 -translate-x-1/2')
|
|
47
|
+
}
|
|
48
|
+
if (position === 'bottom-left') {
|
|
49
|
+
classes.push('bottom-0 left-0')
|
|
50
|
+
}
|
|
51
|
+
if (position === 'bottom-right') {
|
|
52
|
+
classes.push('bottom-0 right-0')
|
|
53
|
+
}
|
|
54
|
+
if (position === 'bottom-center') {
|
|
55
|
+
classes.push('bottom-0 left-1/2 -translate-x-1/2')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return h(
|
|
59
|
+
TransitionGroup,
|
|
60
|
+
{
|
|
61
|
+
tag: 'div',
|
|
62
|
+
class: classes,
|
|
63
|
+
moveClass: transition,
|
|
64
|
+
enterActiveClass: transition,
|
|
65
|
+
enterFromClass: 'translate-y-1 opacity-0',
|
|
66
|
+
enterToClass: 'translate-y-0 opacity-100',
|
|
67
|
+
leaveActiveClass: `${transition} absolute`,
|
|
68
|
+
leaveFromClass: 'translate-y-0 opacity-100',
|
|
69
|
+
leaveToClass: 'translate-y-1 opacity-0',
|
|
70
|
+
},
|
|
71
|
+
() =>
|
|
72
|
+
toasts.value
|
|
73
|
+
.filter((toast) => toast.position === position)
|
|
74
|
+
.map((toast) => {
|
|
75
|
+
return h(
|
|
76
|
+
'div',
|
|
77
|
+
{ key: toast.key, class: 'pointer-events-auto flex' },
|
|
78
|
+
h(Toast, {
|
|
79
|
+
...toast,
|
|
80
|
+
onClose: () => {
|
|
81
|
+
toasts.value = toasts.value.filter((t) => t !== toast)
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
)
|
|
85
|
+
})
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function toast(options) {
|
|
90
|
+
let id = `toast-${Math.random().toString(36).slice(2, 9)}`
|
|
91
|
+
let toast = reactive({
|
|
92
|
+
key: id,
|
|
93
|
+
position: 'top-center',
|
|
94
|
+
...options,
|
|
95
|
+
})
|
|
96
|
+
toasts.value.push(toast)
|
|
97
|
+
return id
|
|
98
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const instanceMap = new Map()
|
|
2
|
+
|
|
3
|
+
function onDocumentClick(e, el, fn) {
|
|
4
|
+
let target = e.target
|
|
5
|
+
if (el !== target && !el.contains(target)) {
|
|
6
|
+
fn?.(e)
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
beforeMount(el, binding) {
|
|
12
|
+
const fn = binding.value
|
|
13
|
+
const clickHandler = function (e) {
|
|
14
|
+
onDocumentClick(e, el, fn)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
removeHandlerIfPresent(el)
|
|
18
|
+
instanceMap.set(el, clickHandler)
|
|
19
|
+
document.addEventListener('click', clickHandler)
|
|
20
|
+
},
|
|
21
|
+
unmounted(el) {
|
|
22
|
+
removeHandlerIfPresent(el)
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function removeHandlerIfPresent(el) {
|
|
27
|
+
const clickHandler = instanceMap.get(el)
|
|
28
|
+
if (!clickHandler) {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
instanceMap.delete(el)
|
|
33
|
+
document.removeEventListener('click', clickHandler)
|
|
34
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { nextTick } from 'vue'
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
beforeMount(el, binding, vnode) {
|
|
5
|
+
let fn = binding.value
|
|
6
|
+
if (!fn) return
|
|
7
|
+
|
|
8
|
+
let observer = new IntersectionObserver((entries) => {
|
|
9
|
+
let entry = entries[0]
|
|
10
|
+
let visible = entry.isIntersecting && entry.intersectionRatio > 0
|
|
11
|
+
fn(visible, entry)
|
|
12
|
+
})
|
|
13
|
+
nextTick(() => {
|
|
14
|
+
observer.observe(el)
|
|
15
|
+
})
|
|
16
|
+
el._visibility_observer = observer
|
|
17
|
+
},
|
|
18
|
+
unmounted(el) {
|
|
19
|
+
if (el._visibility_observer) {
|
|
20
|
+
el._visibility_observer.disconnect()
|
|
21
|
+
delete el._visibility_observer
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// components
|
|
2
|
+
export { default as Alert } from './components/Alert.vue'
|
|
3
|
+
export { default as Autocomplete } from './components/Autocomplete.vue'
|
|
4
|
+
export { default as Avatar } from './components/Avatar.vue'
|
|
5
|
+
export { default as Badge } from './components/Badge.vue'
|
|
6
|
+
export { default as Button } from './components/Button.vue'
|
|
7
|
+
export { default as Card } from './components/Card.vue'
|
|
8
|
+
export { default as DatePicker } from './components/DatePicker.vue'
|
|
9
|
+
export { default as Dialog } from './components/Dialog.vue'
|
|
10
|
+
export { default as Dropdown } from './components/Dropdown.vue'
|
|
11
|
+
export { default as ErrorMessage } from './components/ErrorMessage.vue'
|
|
12
|
+
export { default as FeatherIcon } from './components/FeatherIcon.vue'
|
|
13
|
+
export { default as FileUploader } from './components/FileUploader.vue'
|
|
14
|
+
export { default as GreenCheckIcon } from './components/GreenCheckIcon.vue'
|
|
15
|
+
export { default as Input } from './components/Input.vue'
|
|
16
|
+
export { default as Link } from './components/Link.vue'
|
|
17
|
+
export { default as ListItem } from './components/ListItem.vue'
|
|
18
|
+
export { default as LoadingIndicator } from './components/LoadingIndicator.vue'
|
|
19
|
+
export { default as LoadingText } from './components/LoadingText.vue'
|
|
20
|
+
export { default as Popover } from './components/Popover.vue'
|
|
21
|
+
export { default as Resource } from './components/Resource.vue'
|
|
22
|
+
export {
|
|
23
|
+
TextEditor,
|
|
24
|
+
TextEditorFixedMenu,
|
|
25
|
+
TextEditorBubbleMenu,
|
|
26
|
+
TextEditorFloatingMenu,
|
|
27
|
+
TextEditorContent,
|
|
28
|
+
} from './components/TextEditor'
|
|
29
|
+
export { default as Toast } from './components/Toast.vue'
|
|
30
|
+
export { toast, Toasts } from './components/toast.js'
|
|
31
|
+
export { default as Tooltip } from './components/Tooltip.vue'
|
|
32
|
+
|
|
33
|
+
// directives
|
|
34
|
+
export { default as onOutsideClickDirective } from './directives/onOutsideClick.js'
|
|
35
|
+
export { default as visibilityDirective } from './directives/visibility.js'
|
|
36
|
+
|
|
37
|
+
// utilities
|
|
38
|
+
export { default as call, createCall } from './utils/call.js'
|
|
39
|
+
export { default as debounce } from './utils/debounce.js'
|
|
40
|
+
export { default as fileToBase64 } from './utils/file-to-base64.js'
|
|
41
|
+
|
|
42
|
+
// data-fetching, resources
|
|
43
|
+
export {
|
|
44
|
+
createResource,
|
|
45
|
+
createDocumentResource,
|
|
46
|
+
createListResource,
|
|
47
|
+
resourcesPlugin,
|
|
48
|
+
} from './resources'
|
|
49
|
+
export { request } from './utils/request.js'
|
|
50
|
+
export { melonRequest } from './utils/melonRequest.js'
|
|
51
|
+
export { default as initSocket } from './utils/socketio.js'
|
|
52
|
+
export { setConfig, getConfig } from './utils/config.js'
|
|
53
|
+
|
|
54
|
+
// plugin
|
|
55
|
+
export { default as pageMetaPlugin } from './utils/pageMeta.js'
|
|
56
|
+
export { default as MelonUI } from './utils/plugin.js'
|