frappe-ui 0.1.181 → 0.1.183

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.181",
3
+ "version": "0.1.183",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "main": "./src/index.ts",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div
3
- class="flex h-full flex-col flex-shrink-0 overflow-y-auto border-r border-outline-gray-1 bg-surface-menu-bar transition-all duration-300 ease-in-out p-2"
3
+ class="flex h-full flex-col flex-shrink-0 overflow-y-auto overflow-x-hidden border-r border-outline-gray-1 bg-surface-menu-bar transition-all duration-300 ease-in-out p-2"
4
4
  :class="isCollapsed ? 'w-12' : 'w-60'"
5
5
  >
6
6
  <SidebarHeader
@@ -11,8 +11,8 @@
11
11
  :logo="props.header.logo"
12
12
  :menu-items="props.header.menuItems"
13
13
  >
14
- <template #header-logo>
15
- <slot name="logo"></slot>
14
+ <template #logo>
15
+ <slot name="header-logo"></slot>
16
16
  </template>
17
17
  </SidebarHeader>
18
18
 
@@ -25,7 +25,7 @@
25
25
  />
26
26
 
27
27
  <div class="mt-auto flex flex-col gap-2">
28
- <slot name="footer-items" />
28
+ <slot name="footer-items" v-bind="{ isCollapsed, isMobile }"/>
29
29
  <SidebarItem
30
30
  :label="isCollapsed ? 'Expand' : 'Collapse'"
31
31
  :isCollapsed="isCollapsed"
@@ -14,17 +14,18 @@
14
14
  <div class="w-8 h-8 rounded overflow-hidden">
15
15
  <slot name="logo">
16
16
  <img
17
- v-if="props.logo"
17
+ v-if="typeof props.logo === 'string'"
18
18
  :src="props.logo"
19
19
  class="w-full h-full object-cover"
20
20
  alt="Logo"
21
21
  />
22
22
  <div
23
- v-else
23
+ v-else-if="!props.logo"
24
24
  class="w-full h-full bg-surface-gray-4 flex items-center justify-center text-ink-gray-7"
25
25
  >
26
26
  {{ props.title.charAt(0).toUpperCase() }}
27
27
  </div>
28
+ <component v-else :is="props.logo" class="w-full h-full" />
28
29
  </slot>
29
30
  </div>
30
31
  <div
@@ -4,7 +4,7 @@
4
4
  v-if="props.label"
5
5
  class="relative flex items-center gap-1 px-2 py-1.5"
6
6
  :class="props.collapsible ? 'cursor-pointer' : ''"
7
- @click="isCollapsed = !isCollapsed"
7
+ @click="props.collapsible ? (isCollapsed = !isCollapsed) : null"
8
8
  >
9
9
  <h3
10
10
  class="h-4 text-sm text-ink-gray-5 transition-all duration-300 ease-in-out"
@@ -58,9 +58,9 @@
58
58
  </template>
59
59
 
60
60
  <script setup lang="ts">
61
- import { inject, ref } from 'vue';
62
- import SidebarItem from './SidebarItem.vue';
63
- import { SidebarSectionProps } from './types';
61
+ import { inject, ref } from 'vue'
62
+ import SidebarItem from './SidebarItem.vue'
63
+ import { SidebarSectionProps } from './types'
64
64
 
65
65
  const props = defineProps<SidebarSectionProps>()
66
66
 
@@ -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
  }
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'