frappe-ui 0.1.180 → 0.1.182
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 -2
- package/src/components/Button/Button.story.vue +1 -0
- package/src/components/Button/Button.vue +53 -50
- package/src/components/Button/types.ts +1 -0
- package/src/components/Dropdown/Dropdown.story.vue +8 -0
- package/src/components/Dropdown/Dropdown.vue +154 -116
- package/src/components/Dropdown/types.ts +4 -1
- package/src/components/TextEditor/TextEditor.vue +15 -13
- package/src/components/TextEditor/extensions/mention/index.ts +1 -0
- package/src/components/TextEditor/extensions/mention/mention-extension.ts +180 -0
- package/src/components/TextEditor/extensions/mention/style.css +7 -0
- package/src/components/TextEditor/types.ts +11 -1
- package/src/components/Tree/Tree.vue +2 -1
- package/src/components/Tree/types.ts +1 -0
- package/src/index.ts +1 -7
- /package/src/components/TextEditor/{index.js → index.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frappe-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.182",
|
|
4
4
|
"description": "A set of components and utilities for rapid UI development",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"highlight.js": "^11.11.1",
|
|
68
68
|
"idb-keyval": "^6.2.0",
|
|
69
69
|
"lowlight": "^3.3.0",
|
|
70
|
-
"lucide-static": "^0.
|
|
70
|
+
"lucide-static": "^0.535.0",
|
|
71
71
|
"marked": "^15.0.12",
|
|
72
72
|
"ora": "5.4.1",
|
|
73
73
|
"prettier": "^3.3.2",
|
|
@@ -1,55 +1,57 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
<slot name="prefix" v-else-if="$slots['prefix'] || iconLeft">
|
|
19
|
-
<FeatherIcon
|
|
20
|
-
v-if="iconLeft && typeof iconLeft === 'string'"
|
|
21
|
-
:name="iconLeft"
|
|
22
|
-
:class="slotClasses"
|
|
23
|
-
aria-hidden="true"
|
|
2
|
+
<Tooltip :text="tooltip" :disabled="!tooltip?.length">
|
|
3
|
+
<button
|
|
4
|
+
v-bind="$attrs"
|
|
5
|
+
:class="buttonClasses"
|
|
6
|
+
@click="handleClick"
|
|
7
|
+
:disabled="isDisabled"
|
|
8
|
+
:ariaLabel="ariaLabel"
|
|
9
|
+
>
|
|
10
|
+
<LoadingIndicator
|
|
11
|
+
v-if="loading"
|
|
12
|
+
:class="{
|
|
13
|
+
'h-3 w-3': size == 'sm',
|
|
14
|
+
'h-[13.5px] w-[13.5px]': size == 'md',
|
|
15
|
+
'h-[15px] w-[15px]': size == 'lg',
|
|
16
|
+
'h-4.5 w-4.5': size == 'xl' || size == '2xl',
|
|
17
|
+
}"
|
|
24
18
|
/>
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
19
|
+
<slot name="prefix" v-else-if="$slots['prefix'] || iconLeft">
|
|
20
|
+
<FeatherIcon
|
|
21
|
+
v-if="iconLeft && typeof iconLeft === 'string'"
|
|
22
|
+
:name="iconLeft"
|
|
23
|
+
:class="slotClasses"
|
|
24
|
+
aria-hidden="true"
|
|
25
|
+
/>
|
|
26
|
+
<component v-else-if="iconLeft" :is="iconLeft" :class="slotClasses" />
|
|
27
|
+
</slot>
|
|
28
|
+
|
|
29
|
+
<template v-if="loading && loadingText">{{ loadingText }}</template>
|
|
30
|
+
<template v-else-if="isIconButton && !loading">
|
|
31
|
+
<FeatherIcon
|
|
32
|
+
v-if="icon && typeof icon === 'string'"
|
|
33
|
+
:name="icon"
|
|
34
|
+
:class="slotClasses"
|
|
35
|
+
:aria-label="label"
|
|
36
|
+
/>
|
|
37
|
+
<component v-else-if="icon" :is="icon" :class="slotClasses" />
|
|
38
|
+
<slot name="icon" v-else-if="$slots.icon" />
|
|
39
|
+
</template>
|
|
40
|
+
<span v-else :class="{ 'sr-only': isIconButton }" class="truncate">
|
|
41
|
+
<slot>{{ label }}</slot>
|
|
42
|
+
</span>
|
|
43
|
+
|
|
44
|
+
<slot name="suffix">
|
|
45
|
+
<FeatherIcon
|
|
46
|
+
v-if="iconRight && typeof iconRight === 'string'"
|
|
47
|
+
:name="iconRight"
|
|
48
|
+
:class="slotClasses"
|
|
49
|
+
aria-hidden="true"
|
|
50
|
+
/>
|
|
51
|
+
<component v-else-if="iconRight" :is="iconRight" :class="slotClasses" />
|
|
52
|
+
</slot>
|
|
53
|
+
</button>
|
|
54
|
+
</Tooltip>
|
|
53
55
|
</template>
|
|
54
56
|
<script lang="ts" setup>
|
|
55
57
|
import { computed, useSlots } from 'vue'
|
|
@@ -57,6 +59,7 @@ import FeatherIcon from '../FeatherIcon.vue'
|
|
|
57
59
|
import LoadingIndicator from '../LoadingIndicator.vue'
|
|
58
60
|
import { useRouter } from 'vue-router'
|
|
59
61
|
import type { ButtonProps, ThemeVariant } from './types'
|
|
62
|
+
import Tooltip from '../Tooltip/Tooltip.vue'
|
|
60
63
|
|
|
61
64
|
const props = withDefaults(defineProps<ButtonProps>(), {
|
|
62
65
|
theme: 'gray',
|
|
@@ -11,6 +11,7 @@ const actions = [
|
|
|
11
11
|
{
|
|
12
12
|
label: 'Delete',
|
|
13
13
|
icon: 'trash-2',
|
|
14
|
+
theme: 'red',
|
|
14
15
|
onClick: () => console.log('Delete clicked'),
|
|
15
16
|
},
|
|
16
17
|
]
|
|
@@ -69,6 +70,7 @@ const groupedActions = [
|
|
|
69
70
|
{
|
|
70
71
|
label: 'Delete',
|
|
71
72
|
icon: 'trash-2',
|
|
73
|
+
theme: 'red',
|
|
72
74
|
onClick: () => console.log('Delete clicked'),
|
|
73
75
|
},
|
|
74
76
|
],
|
|
@@ -93,6 +95,12 @@ const submenuActions = [
|
|
|
93
95
|
icon: 'file-text',
|
|
94
96
|
onClick: () => console.log('New Template clicked'),
|
|
95
97
|
},
|
|
98
|
+
{
|
|
99
|
+
label: 'Delete',
|
|
100
|
+
icon: 'trash-2',
|
|
101
|
+
theme: 'red',
|
|
102
|
+
onClick: () => console.log('Delete clicked'),
|
|
103
|
+
},
|
|
96
104
|
],
|
|
97
105
|
},
|
|
98
106
|
{
|
|
@@ -21,122 +21,145 @@
|
|
|
21
21
|
:align="contentAlign"
|
|
22
22
|
:side-offset="4"
|
|
23
23
|
>
|
|
24
|
-
<
|
|
25
|
-
v-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
>
|
|
33
|
-
{{ group.group }}
|
|
34
|
-
</DropdownMenuLabel>
|
|
24
|
+
<template v-for="group in groups" :key="group.key">
|
|
25
|
+
<div v-if="group.items.length" :class="cssClasses.groupContainer">
|
|
26
|
+
<DropdownMenuLabel
|
|
27
|
+
v-if="group.group && !group.hideLabel"
|
|
28
|
+
:class="[cssClasses.groupLabel, getTextColor(group)]"
|
|
29
|
+
>
|
|
30
|
+
{{ group.group }}
|
|
31
|
+
</DropdownMenuLabel>
|
|
35
32
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
:class="cssClasses.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
>
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
:
|
|
79
|
-
:
|
|
33
|
+
<DropdownMenuItem
|
|
34
|
+
v-for="item in group.items"
|
|
35
|
+
:key="item.label"
|
|
36
|
+
as-child
|
|
37
|
+
@select="item.onClick"
|
|
38
|
+
>
|
|
39
|
+
<component
|
|
40
|
+
v-if="item.component"
|
|
41
|
+
:is="item.component"
|
|
42
|
+
:active="false"
|
|
43
|
+
/>
|
|
44
|
+
<DropdownMenuSub v-else-if="item.submenu">
|
|
45
|
+
<DropdownMenuSubTrigger as-child>
|
|
46
|
+
<button
|
|
47
|
+
:class="[
|
|
48
|
+
cssClasses.submenuTrigger,
|
|
49
|
+
getSubmenuBackgroundColor(item),
|
|
50
|
+
]"
|
|
51
|
+
>
|
|
52
|
+
<FeatherIcon
|
|
53
|
+
v-if="item.icon && typeof item.icon === 'string'"
|
|
54
|
+
:name="item.icon"
|
|
55
|
+
:class="[cssClasses.itemIcon, getIconColor(item)]"
|
|
56
|
+
aria-hidden="true"
|
|
57
|
+
/>
|
|
58
|
+
<component
|
|
59
|
+
:class="[cssClasses.itemIcon, getIconColor(item)]"
|
|
60
|
+
v-else-if="item.icon"
|
|
61
|
+
:is="item.icon"
|
|
62
|
+
/>
|
|
63
|
+
<span :class="[cssClasses.itemLabel, getTextColor(item)]">
|
|
64
|
+
{{ item.label }}
|
|
65
|
+
</span>
|
|
66
|
+
<FeatherIcon
|
|
67
|
+
name="chevron-right"
|
|
68
|
+
:class="[cssClasses.chevronIcon, getIconColor(item)]"
|
|
69
|
+
aria-hidden="true"
|
|
70
|
+
/>
|
|
71
|
+
</button>
|
|
72
|
+
</DropdownMenuSubTrigger>
|
|
73
|
+
<DropdownMenuPortal>
|
|
74
|
+
<DropdownMenuSubContent
|
|
75
|
+
:class="cssClasses.dropdownContent"
|
|
76
|
+
:side-offset="4"
|
|
80
77
|
>
|
|
81
|
-
<
|
|
82
|
-
v-
|
|
83
|
-
:
|
|
78
|
+
<div
|
|
79
|
+
v-for="submenuGroup in getSubmenuGroups(item.submenu)"
|
|
80
|
+
:key="submenuGroup.key"
|
|
81
|
+
:class="cssClasses.groupContainer"
|
|
84
82
|
>
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
<DropdownMenuLabel
|
|
84
|
+
v-if="submenuGroup.group && !submenuGroup.hideLabel"
|
|
85
|
+
:class="cssClasses.groupLabel"
|
|
86
|
+
>
|
|
87
|
+
{{ submenuGroup.group }}
|
|
88
|
+
</DropdownMenuLabel>
|
|
87
89
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
<component
|
|
95
|
-
v-if="subItem.component"
|
|
96
|
-
:is="subItem.component"
|
|
97
|
-
:active="false"
|
|
98
|
-
/>
|
|
99
|
-
<button v-else :class="cssClasses.itemButton">
|
|
100
|
-
<FeatherIcon
|
|
101
|
-
v-if="
|
|
102
|
-
subItem.icon && typeof subItem.icon === 'string'
|
|
103
|
-
"
|
|
104
|
-
:name="subItem.icon"
|
|
105
|
-
:class="cssClasses.itemIcon"
|
|
106
|
-
aria-hidden="true"
|
|
107
|
-
/>
|
|
90
|
+
<DropdownMenuItem
|
|
91
|
+
v-for="subItem in submenuGroup.items"
|
|
92
|
+
:key="subItem.label"
|
|
93
|
+
as-child
|
|
94
|
+
@select="() => handleItemClick(subItem)"
|
|
95
|
+
>
|
|
108
96
|
<component
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
:
|
|
97
|
+
v-if="subItem.component"
|
|
98
|
+
:is="subItem.component"
|
|
99
|
+
:active="false"
|
|
112
100
|
/>
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
101
|
+
<button
|
|
102
|
+
v-else
|
|
103
|
+
:class="[
|
|
104
|
+
cssClasses.itemButton,
|
|
105
|
+
getBackgroundColor(subItem),
|
|
106
|
+
]"
|
|
107
|
+
>
|
|
108
|
+
<FeatherIcon
|
|
109
|
+
v-if="
|
|
110
|
+
subItem.icon && typeof subItem.icon === 'string'
|
|
111
|
+
"
|
|
112
|
+
:name="subItem.icon"
|
|
113
|
+
:class="[
|
|
114
|
+
cssClasses.itemIcon,
|
|
115
|
+
getIconColor(subItem),
|
|
116
|
+
]"
|
|
117
|
+
aria-hidden="true"
|
|
118
|
+
/>
|
|
119
|
+
<component
|
|
120
|
+
:class="[
|
|
121
|
+
cssClasses.itemIcon,
|
|
122
|
+
getIconColor(subItem),
|
|
123
|
+
]"
|
|
124
|
+
v-else-if="subItem.icon"
|
|
125
|
+
:is="subItem.icon"
|
|
126
|
+
/>
|
|
127
|
+
<span
|
|
128
|
+
:class="[
|
|
129
|
+
cssClasses.itemLabel,
|
|
130
|
+
getTextColor(subItem),
|
|
131
|
+
]"
|
|
132
|
+
>
|
|
133
|
+
{{ subItem.label }}
|
|
134
|
+
</span>
|
|
135
|
+
</button>
|
|
136
|
+
</DropdownMenuItem>
|
|
137
|
+
</div>
|
|
138
|
+
</DropdownMenuSubContent>
|
|
139
|
+
</DropdownMenuPortal>
|
|
140
|
+
</DropdownMenuSub>
|
|
141
|
+
<button
|
|
142
|
+
v-else
|
|
143
|
+
:class="[cssClasses.itemButton, getBackgroundColor(item)]"
|
|
144
|
+
>
|
|
145
|
+
<FeatherIcon
|
|
146
|
+
v-if="item.icon && typeof item.icon === 'string'"
|
|
147
|
+
:name="item.icon"
|
|
148
|
+
:class="[cssClasses.itemIcon, getIconColor(item)]"
|
|
149
|
+
aria-hidden="true"
|
|
150
|
+
/>
|
|
151
|
+
<component
|
|
152
|
+
:class="[cssClasses.itemIcon, getIconColor(item)]"
|
|
153
|
+
v-else-if="item.icon"
|
|
154
|
+
:is="item.icon"
|
|
155
|
+
/>
|
|
156
|
+
<span :class="[cssClasses.itemLabel, getTextColor(item)]"
|
|
157
|
+
>{{ item.label }}
|
|
158
|
+
</span>
|
|
159
|
+
</button>
|
|
160
|
+
</DropdownMenuItem>
|
|
161
|
+
</div>
|
|
162
|
+
</template>
|
|
140
163
|
</DropdownMenuContent>
|
|
141
164
|
</DropdownMenuPortal>
|
|
142
165
|
</DropdownMenuRoot>
|
|
@@ -163,6 +186,7 @@ import type {
|
|
|
163
186
|
DropdownOption,
|
|
164
187
|
DropdownGroupOption,
|
|
165
188
|
DropdownOptions,
|
|
189
|
+
DropdownItem,
|
|
166
190
|
} from './types'
|
|
167
191
|
|
|
168
192
|
defineOptions({
|
|
@@ -189,6 +213,7 @@ const handleItemClick = (item: DropdownOption) => {
|
|
|
189
213
|
const normalizeDropdownItem = (option: DropdownOption) => {
|
|
190
214
|
return {
|
|
191
215
|
label: option.label,
|
|
216
|
+
theme: option.theme || 'gray',
|
|
192
217
|
icon: option.icon,
|
|
193
218
|
component: option.component,
|
|
194
219
|
onClick: () => handleItemClick(option),
|
|
@@ -196,6 +221,19 @@ const normalizeDropdownItem = (option: DropdownOption) => {
|
|
|
196
221
|
}
|
|
197
222
|
}
|
|
198
223
|
|
|
224
|
+
const getIconColor = (item: DropdownItem) =>
|
|
225
|
+
item.theme === 'red' ? 'text-ink-red-3' : 'text-ink-gray-6'
|
|
226
|
+
const getTextColor = (item: DropdownItem) =>
|
|
227
|
+
item.theme === 'red' ? 'text-ink-red-3' : 'text-ink-gray-7'
|
|
228
|
+
const getBackgroundColor = (item: DropdownItem) =>
|
|
229
|
+
item.theme === 'red'
|
|
230
|
+
? 'focus:bg-surface-red-3 data-[highlighted]:bg-surface-red-3 data-[state=open]:bg-surface-red-3'
|
|
231
|
+
: 'focus:bg-surface-gray-3 data-[highlighted]:bg-surface-gray-3 data-[state=open]:bg-surface-gray-3'
|
|
232
|
+
const getSubmenuBackgroundColor = (item: DropdownItem) =>
|
|
233
|
+
getBackgroundColor(item) +
|
|
234
|
+
' data-[state=open]:bg-surface-' +
|
|
235
|
+
(item.theme === 'red' ? 'red-3' : 'gray-3')
|
|
236
|
+
|
|
199
237
|
// Unified group processing for both main options and submenu options
|
|
200
238
|
const processOptionsIntoGroups = (
|
|
201
239
|
options: DropdownOptions,
|
|
@@ -260,18 +298,18 @@ const cssClasses = {
|
|
|
260
298
|
groupContainer: 'p-1.5',
|
|
261
299
|
|
|
262
300
|
// Label classes
|
|
263
|
-
groupLabel: 'flex h-7 items-center px-2 text-sm font-medium
|
|
264
|
-
itemLabel: 'whitespace-nowrap
|
|
301
|
+
groupLabel: 'flex h-7 items-center px-2 text-sm font-medium',
|
|
302
|
+
itemLabel: 'whitespace-nowrap',
|
|
265
303
|
|
|
266
304
|
// Icon classes
|
|
267
|
-
itemIcon: 'mr-2 h-4 w-4 flex-shrink-0
|
|
268
|
-
chevronIcon: 'ml-auto h-4 w-4 flex-shrink-0
|
|
305
|
+
itemIcon: 'mr-2 h-4 w-4 flex-shrink-0',
|
|
306
|
+
chevronIcon: 'ml-auto h-4 w-4 flex-shrink-0',
|
|
269
307
|
|
|
270
308
|
// Button classes
|
|
271
309
|
itemButton:
|
|
272
|
-
'group flex h-7 w-full items-center rounded px-2 text-base
|
|
310
|
+
'group flex h-7 w-full items-center rounded px-2 text-base focus:outline-none',
|
|
273
311
|
submenuTrigger:
|
|
274
|
-
'group flex h-7 w-full items-center rounded px-2 text-base text-ink-gray-6 focus:
|
|
312
|
+
'group flex h-7 w-full items-center rounded px-2 text-base text-ink-gray-6 focus:outline-none',
|
|
275
313
|
}
|
|
276
314
|
|
|
277
315
|
const groups = computed(() => {
|
|
@@ -4,6 +4,7 @@ import { ButtonProps } from '../Button'
|
|
|
4
4
|
export type DropdownOption = {
|
|
5
5
|
label: string
|
|
6
6
|
icon?: string | null
|
|
7
|
+
theme?: 'gray' | 'red'
|
|
7
8
|
component?: any
|
|
8
9
|
onClick?: () => void
|
|
9
10
|
route?: RouterLinkProps['to']
|
|
@@ -16,9 +17,11 @@ export type DropdownGroupOption = {
|
|
|
16
17
|
group: string
|
|
17
18
|
items: DropdownOption[]
|
|
18
19
|
hideLabel?: boolean
|
|
20
|
+
theme?: 'gray' | 'red'
|
|
19
21
|
}
|
|
22
|
+
export type DropdownItem = DropdownOption | DropdownGroupOption
|
|
20
23
|
|
|
21
|
-
export type DropdownOptions = Array<
|
|
24
|
+
export type DropdownOptions = Array<DropdownItem>
|
|
22
25
|
|
|
23
26
|
export interface DropdownProps {
|
|
24
27
|
button?: ButtonProps
|
|
@@ -55,7 +55,7 @@ import NamedHighlightExtension from './extensions/highlight'
|
|
|
55
55
|
import { common, createLowlight } from 'lowlight'
|
|
56
56
|
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
|
|
57
57
|
import CodeBlockComponent from './CodeBlockComponent.vue'
|
|
58
|
-
import
|
|
58
|
+
import { MentionExtension } from './extensions/mention'
|
|
59
59
|
import TextEditorFixedMenu from './TextEditorFixedMenu.vue'
|
|
60
60
|
import TextEditorBubbleMenu from './TextEditorBubbleMenu.vue'
|
|
61
61
|
import TextEditorFloatingMenu from './TextEditorFloatingMenu.vue'
|
|
@@ -88,7 +88,7 @@ const props = withDefaults(defineProps<TextEditorProps>(), {
|
|
|
88
88
|
floatingMenu: false,
|
|
89
89
|
extensions: () => [],
|
|
90
90
|
starterkitOptions: () => ({}),
|
|
91
|
-
mentions:
|
|
91
|
+
mentions: null,
|
|
92
92
|
tags: () => [],
|
|
93
93
|
})
|
|
94
94
|
|
|
@@ -120,7 +120,7 @@ watch(
|
|
|
120
120
|
editor.value.commands.setContent(val)
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
-
}
|
|
123
|
+
},
|
|
124
124
|
)
|
|
125
125
|
|
|
126
126
|
watch(
|
|
@@ -129,7 +129,7 @@ watch(
|
|
|
129
129
|
if (editor.value) {
|
|
130
130
|
editor.value.setEditable(value)
|
|
131
131
|
}
|
|
132
|
-
}
|
|
132
|
+
},
|
|
133
133
|
)
|
|
134
134
|
|
|
135
135
|
watch(
|
|
@@ -141,7 +141,7 @@ watch(
|
|
|
141
141
|
})
|
|
142
142
|
}
|
|
143
143
|
},
|
|
144
|
-
{ deep: true }
|
|
144
|
+
{ deep: true },
|
|
145
145
|
)
|
|
146
146
|
|
|
147
147
|
onMounted(() => {
|
|
@@ -203,7 +203,15 @@ onMounted(() => {
|
|
|
203
203
|
? props.placeholder
|
|
204
204
|
: () => props.placeholder as string,
|
|
205
205
|
}),
|
|
206
|
-
|
|
206
|
+
props.mentions &&
|
|
207
|
+
MentionExtension.configure(
|
|
208
|
+
Array.isArray(props.mentions)
|
|
209
|
+
? { mentions: props.mentions }
|
|
210
|
+
: {
|
|
211
|
+
mentions: props.mentions.mentions,
|
|
212
|
+
component: props.mentions.component,
|
|
213
|
+
},
|
|
214
|
+
),
|
|
207
215
|
EmojiExtension,
|
|
208
216
|
SlashCommands,
|
|
209
217
|
TagNode,
|
|
@@ -238,7 +246,7 @@ onBeforeUnmount(() => {
|
|
|
238
246
|
|
|
239
247
|
provide(
|
|
240
248
|
'editor',
|
|
241
|
-
computed(() => editor.value)
|
|
249
|
+
computed(() => editor.value),
|
|
242
250
|
)
|
|
243
251
|
|
|
244
252
|
defineExpose({
|
|
@@ -275,12 +283,6 @@ img.ProseMirror-selectednode {
|
|
|
275
283
|
outline: 2px solid var(--outline-gray-2);
|
|
276
284
|
}
|
|
277
285
|
|
|
278
|
-
/* Mentions */
|
|
279
|
-
.mention {
|
|
280
|
-
font-weight: 600;
|
|
281
|
-
box-decoration-break: clone;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
286
|
/* Table styles */
|
|
285
287
|
.prose table p {
|
|
286
288
|
margin: 0;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MentionExtension } from './mention-extension'
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { MaybeRefOrGetter, toValue, type Component } from 'vue'
|
|
2
|
+
import { Extension, Node, mergeAttributes } from '@tiptap/core'
|
|
3
|
+
import { VueNodeViewRenderer } from '@tiptap/vue-3'
|
|
4
|
+
import { PluginKey } from '@tiptap/pm/state'
|
|
5
|
+
import {
|
|
6
|
+
createSuggestionExtension,
|
|
7
|
+
BaseSuggestionItem,
|
|
8
|
+
} from '../suggestion/createSuggestionExtension'
|
|
9
|
+
import SuggestionList from '../suggestion/SuggestionList.vue'
|
|
10
|
+
import './style.css'
|
|
11
|
+
|
|
12
|
+
export interface MentionSuggestionItem extends BaseSuggestionItem {
|
|
13
|
+
id: string
|
|
14
|
+
label: string
|
|
15
|
+
value?: string
|
|
16
|
+
email?: string
|
|
17
|
+
full_name?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createMentionNode(component?: Component) {
|
|
21
|
+
const config: any = {
|
|
22
|
+
name: 'mention',
|
|
23
|
+
group: 'inline',
|
|
24
|
+
inline: true,
|
|
25
|
+
selectable: true,
|
|
26
|
+
atom: true,
|
|
27
|
+
|
|
28
|
+
addOptions() {
|
|
29
|
+
return {
|
|
30
|
+
component: undefined,
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
addAttributes() {
|
|
35
|
+
return {
|
|
36
|
+
id: {
|
|
37
|
+
default: null,
|
|
38
|
+
parseHTML: (element: HTMLElement) => element.getAttribute('data-id'),
|
|
39
|
+
renderHTML: (attributes: any) => {
|
|
40
|
+
if (!attributes.id) {
|
|
41
|
+
return {}
|
|
42
|
+
}
|
|
43
|
+
return { 'data-id': attributes.id }
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
label: {
|
|
47
|
+
default: null,
|
|
48
|
+
parseHTML: (element: HTMLElement) =>
|
|
49
|
+
element.getAttribute('data-label'),
|
|
50
|
+
renderHTML: (attributes: any) => {
|
|
51
|
+
if (!attributes.label) {
|
|
52
|
+
return {}
|
|
53
|
+
}
|
|
54
|
+
return { 'data-label': attributes.label }
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
parseHTML() {
|
|
61
|
+
return [
|
|
62
|
+
{
|
|
63
|
+
tag: 'span.mention[data-type="mention"]',
|
|
64
|
+
getAttrs: (dom: any) => {
|
|
65
|
+
const element = dom as HTMLElement
|
|
66
|
+
return {
|
|
67
|
+
id: element.getAttribute('data-id'),
|
|
68
|
+
label: element.getAttribute('data-label'),
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
renderHTML({ HTMLAttributes }: any) {
|
|
76
|
+
return [
|
|
77
|
+
'span',
|
|
78
|
+
mergeAttributes(HTMLAttributes, {
|
|
79
|
+
class: 'mention',
|
|
80
|
+
'data-type': 'mention',
|
|
81
|
+
}),
|
|
82
|
+
`@${HTMLAttributes['data-label'] || HTMLAttributes.id || ''}`,
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (component) {
|
|
88
|
+
config.addNodeView = () => {
|
|
89
|
+
return VueNodeViewRenderer(component)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return Node.create(config)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const MentionSuggestionExtension =
|
|
97
|
+
createSuggestionExtension<MentionSuggestionItem>({
|
|
98
|
+
name: 'mentionSuggestion',
|
|
99
|
+
char: '@',
|
|
100
|
+
pluginKey: new PluginKey('mentionSuggestion'),
|
|
101
|
+
component: SuggestionList,
|
|
102
|
+
|
|
103
|
+
addOptions() {
|
|
104
|
+
return {
|
|
105
|
+
mentions: [],
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
items: ({ query, editor }) => {
|
|
110
|
+
const { mentions: _mentions } = editor.extensionManager.extensions.find(
|
|
111
|
+
(ext) => ext.name === 'mentionSuggestion',
|
|
112
|
+
)!.options
|
|
113
|
+
const mentions = toValue(_mentions)
|
|
114
|
+
|
|
115
|
+
const filtered = mentions
|
|
116
|
+
.filter((mention: MentionSuggestionItem) =>
|
|
117
|
+
mention.label.toLowerCase().startsWith(query.toLowerCase()),
|
|
118
|
+
)
|
|
119
|
+
.slice(0, 10)
|
|
120
|
+
.map((mention: MentionSuggestionItem) => ({
|
|
121
|
+
...mention,
|
|
122
|
+
display: mention.label,
|
|
123
|
+
}))
|
|
124
|
+
|
|
125
|
+
return filtered
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
command: ({ editor, range, props }) => {
|
|
129
|
+
const attributes = {
|
|
130
|
+
id: props.id || props.value,
|
|
131
|
+
label: props.label,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
editor
|
|
135
|
+
.chain()
|
|
136
|
+
.focus()
|
|
137
|
+
.insertContentAt(range, [
|
|
138
|
+
{
|
|
139
|
+
type: 'mention',
|
|
140
|
+
attrs: attributes,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
type: 'text',
|
|
144
|
+
text: ' ',
|
|
145
|
+
},
|
|
146
|
+
])
|
|
147
|
+
.run()
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
tippyOptions: {
|
|
151
|
+
placement: 'bottom-start',
|
|
152
|
+
offset: [0, 8],
|
|
153
|
+
},
|
|
154
|
+
allowSpaces: false,
|
|
155
|
+
decorationTag: 'span',
|
|
156
|
+
decorationClass: 'mention-suggestion-active',
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
export const MentionExtension = Extension.create<{
|
|
160
|
+
mentions: MaybeRefOrGetter<MentionSuggestionItem[]>
|
|
161
|
+
component?: Component
|
|
162
|
+
}>({
|
|
163
|
+
name: 'mentionExtension',
|
|
164
|
+
|
|
165
|
+
addOptions() {
|
|
166
|
+
return {
|
|
167
|
+
mentions: [],
|
|
168
|
+
component: undefined,
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
addExtensions() {
|
|
173
|
+
return [
|
|
174
|
+
createMentionNode(this.options.component),
|
|
175
|
+
MentionSuggestionExtension.configure({
|
|
176
|
+
mentions: this.options.mentions,
|
|
177
|
+
}),
|
|
178
|
+
]
|
|
179
|
+
},
|
|
180
|
+
})
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
+
import { type Component } from 'vue'
|
|
1
2
|
import { type UploadedFile } from '../../utils/useFileUpload'
|
|
3
|
+
import { type MentionSuggestionItem } from './extensions/mention/mention-extension'
|
|
4
|
+
|
|
5
|
+
type ConfigureMentionOptions =
|
|
6
|
+
| {
|
|
7
|
+
mentions: MentionSuggestionItem[]
|
|
8
|
+
component?: Component
|
|
9
|
+
}
|
|
10
|
+
| MentionSuggestionItem[]
|
|
11
|
+
| null
|
|
2
12
|
|
|
3
13
|
export interface TextEditorProps {
|
|
4
14
|
content?: string | null
|
|
@@ -11,7 +21,7 @@ export interface TextEditorProps {
|
|
|
11
21
|
floatingMenu?: boolean | any[]
|
|
12
22
|
extensions?: any[]
|
|
13
23
|
starterkitOptions?: any
|
|
14
|
-
mentions?:
|
|
24
|
+
mentions?: ConfigureMentionOptions
|
|
15
25
|
tags?: any[]
|
|
16
26
|
uploadFunction?: (file: File) => Promise<UploadedFile>
|
|
17
27
|
}
|
|
@@ -75,6 +75,7 @@ const props = withDefaults(defineProps<TreeProps>(), {
|
|
|
75
75
|
rowHeight: '25px',
|
|
76
76
|
indentWidth: '20px',
|
|
77
77
|
showIndentationGuides: true,
|
|
78
|
+
defaultCollapsed: true,
|
|
78
79
|
}),
|
|
79
80
|
})
|
|
80
81
|
|
|
@@ -96,7 +97,7 @@ const slots = defineSlots<{
|
|
|
96
97
|
}
|
|
97
98
|
}>()
|
|
98
99
|
|
|
99
|
-
const isCollapsed = ref(true)
|
|
100
|
+
const isCollapsed = ref(props.options.defaultCollapsed ?? true)
|
|
100
101
|
|
|
101
102
|
const linePadding = ref('')
|
|
102
103
|
|
package/src/index.ts
CHANGED
|
@@ -40,13 +40,7 @@ export { default as TabList } from './components/Tabs/TabList.vue'
|
|
|
40
40
|
export { default as TabPanel } from './components/Tabs/TabPanel.vue'
|
|
41
41
|
export * from './components/TextInput'
|
|
42
42
|
export * from './components/Textarea'
|
|
43
|
-
export
|
|
44
|
-
TextEditor,
|
|
45
|
-
TextEditorFixedMenu,
|
|
46
|
-
TextEditorBubbleMenu,
|
|
47
|
-
TextEditorFloatingMenu,
|
|
48
|
-
TextEditorContent,
|
|
49
|
-
} from './components/TextEditor'
|
|
43
|
+
export * from './components/TextEditor'
|
|
50
44
|
export { default as ListView } from './components/ListView/ListView.vue'
|
|
51
45
|
export { default as List } from './components/ListView/ListView.vue'
|
|
52
46
|
export { default as ListHeader } from './components/ListView/ListHeader.vue'
|
|
File without changes
|