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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frappe-ui",
3
- "version": "0.1.180",
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.479.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",
@@ -9,6 +9,7 @@ const state = reactive({
9
9
  loadingText: null,
10
10
  disabled: false,
11
11
  link: null,
12
+ tooltip: 'Hover for more!',
12
13
  })
13
14
  const variants = ['solid', 'subtle', 'outline', 'ghost']
14
15
  const themes = ['gray', 'blue', 'green', 'red']
@@ -1,55 +1,57 @@
1
1
  <template>
2
- <button
3
- v-bind="$attrs"
4
- :class="buttonClasses"
5
- @click="handleClick"
6
- :disabled="isDisabled"
7
- :ariaLabel="ariaLabel"
8
- >
9
- <LoadingIndicator
10
- v-if="loading"
11
- :class="{
12
- 'h-3 w-3': size == 'sm',
13
- 'h-[13.5px] w-[13.5px]': size == 'md',
14
- 'h-[15px] w-[15px]': size == 'lg',
15
- 'h-4.5 w-4.5': size == 'xl' || size == '2xl',
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
- <component v-else-if="iconLeft" :is="iconLeft" :class="slotClasses" />
26
- </slot>
27
-
28
- <template v-if="loading && loadingText">{{ loadingText }}</template>
29
- <template v-else-if="isIconButton && !loading">
30
- <FeatherIcon
31
- v-if="icon && typeof icon === 'string'"
32
- :name="icon"
33
- :class="slotClasses"
34
- :aria-label="label"
35
- />
36
- <component v-else-if="icon" :is="icon" :class="slotClasses" />
37
- <slot name="icon" v-else-if="$slots.icon" />
38
- </template>
39
- <span v-else :class="{ 'sr-only': isIconButton }" class="truncate">
40
- <slot>{{ label }}</slot>
41
- </span>
42
-
43
- <slot name="suffix">
44
- <FeatherIcon
45
- v-if="iconRight && typeof iconRight === 'string'"
46
- :name="iconRight"
47
- :class="slotClasses"
48
- aria-hidden="true"
49
- />
50
- <component v-else-if="iconRight" :is="iconRight" :class="slotClasses" />
51
- </slot>
52
- </button>
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',
@@ -13,6 +13,7 @@ export interface ButtonProps {
13
13
  icon?: string | Component
14
14
  iconLeft?: string | Component
15
15
  iconRight?: string | Component
16
+ tooltip?: string
16
17
  loading?: boolean
17
18
  loadingText?: string
18
19
  disabled?: boolean
@@ -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
- <div
25
- v-for="group in groups"
26
- :key="group.key"
27
- :class="cssClasses.groupContainer"
28
- >
29
- <DropdownMenuLabel
30
- v-if="group.group && !group.hideLabel"
31
- :class="cssClasses.groupLabel"
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
- <DropdownMenuItem
37
- v-for="item in group.items"
38
- :key="item.label"
39
- as-child
40
- @select="item.onClick"
41
- >
42
- <component
43
- v-if="item.component"
44
- :is="item.component"
45
- :active="false"
46
- />
47
- <DropdownMenuSub v-else-if="item.submenu">
48
- <DropdownMenuSubTrigger as-child>
49
- <button :class="cssClasses.submenuTrigger">
50
- <FeatherIcon
51
- v-if="item.icon && typeof item.icon === 'string'"
52
- :name="item.icon"
53
- :class="cssClasses.itemIcon"
54
- aria-hidden="true"
55
- />
56
- <component
57
- :class="cssClasses.itemIcon"
58
- v-else-if="item.icon"
59
- :is="item.icon"
60
- />
61
- <span :class="cssClasses.itemLabel">
62
- {{ item.label }}
63
- </span>
64
- <FeatherIcon
65
- name="chevron-right"
66
- :class="cssClasses.chevronIcon"
67
- aria-hidden="true"
68
- />
69
- </button>
70
- </DropdownMenuSubTrigger>
71
- <DropdownMenuPortal>
72
- <DropdownMenuSubContent
73
- :class="cssClasses.dropdownContent"
74
- :side-offset="4"
75
- >
76
- <div
77
- v-for="submenuGroup in getSubmenuGroups(item.submenu)"
78
- :key="submenuGroup.key"
79
- :class="cssClasses.groupContainer"
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
- <DropdownMenuLabel
82
- v-if="submenuGroup.group && !submenuGroup.hideLabel"
83
- :class="cssClasses.groupLabel"
78
+ <div
79
+ v-for="submenuGroup in getSubmenuGroups(item.submenu)"
80
+ :key="submenuGroup.key"
81
+ :class="cssClasses.groupContainer"
84
82
  >
85
- {{ submenuGroup.group }}
86
- </DropdownMenuLabel>
83
+ <DropdownMenuLabel
84
+ v-if="submenuGroup.group && !submenuGroup.hideLabel"
85
+ :class="cssClasses.groupLabel"
86
+ >
87
+ {{ submenuGroup.group }}
88
+ </DropdownMenuLabel>
87
89
 
88
- <DropdownMenuItem
89
- v-for="subItem in submenuGroup.items"
90
- :key="subItem.label"
91
- as-child
92
- @select="() => handleItemClick(subItem)"
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
- :class="cssClasses.itemIcon"
110
- v-else-if="subItem.icon"
111
- :is="subItem.icon"
97
+ v-if="subItem.component"
98
+ :is="subItem.component"
99
+ :active="false"
112
100
  />
113
- <span :class="cssClasses.itemLabel">
114
- {{ subItem.label }}
115
- </span>
116
- </button>
117
- </DropdownMenuItem>
118
- </div>
119
- </DropdownMenuSubContent>
120
- </DropdownMenuPortal>
121
- </DropdownMenuSub>
122
- <button v-else :class="cssClasses.itemButton">
123
- <FeatherIcon
124
- v-if="item.icon && typeof item.icon === 'string'"
125
- :name="item.icon"
126
- :class="cssClasses.itemIcon"
127
- aria-hidden="true"
128
- />
129
- <component
130
- :class="cssClasses.itemIcon"
131
- v-else-if="item.icon"
132
- :is="item.icon"
133
- />
134
- <span :class="cssClasses.itemLabel">
135
- {{ item.label }}
136
- </span>
137
- </button>
138
- </DropdownMenuItem>
139
- </div>
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 text-ink-gray-6',
264
- itemLabel: 'whitespace-nowrap text-ink-gray-7',
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 text-ink-gray-6',
268
- chevronIcon: 'ml-auto h-4 w-4 flex-shrink-0 text-ink-gray-6',
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 text-ink-gray-6 focus:bg-surface-gray-3 focus:outline-none data-[highlighted]:bg-surface-gray-3',
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:bg-surface-gray-3 focus:outline-none data-[highlighted]:bg-surface-gray-3 data-[state=open]:bg-surface-gray-3',
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<DropdownOption | DropdownGroupOption>
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 configureMention from './mention'
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
- configureMention(props.mentions),
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
+ })
@@ -0,0 +1,7 @@
1
+ .mention {
2
+ font-weight: 600;
3
+ box-decoration-break: clone;
4
+ padding: 1px 3px;
5
+ border-radius: 4px;
6
+ display: inline-block;
7
+ }
@@ -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?: any[]
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
 
@@ -9,6 +9,7 @@ export type TreeOptions = {
9
9
  rowHeight?: string
10
10
  indentWidth?: string
11
11
  showIndentationGuides?: boolean
12
+ defaultCollapsed?: boolean
12
13
  }
13
14
 
14
15
  export interface TreeProps {
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'