frappe-ui 0.0.45 → 0.0.48

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,11 +1,12 @@
1
1
  {
2
2
  "name": "frappe-ui",
3
- "version": "0.0.45",
3
+ "version": "0.0.48",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
7
7
  "test": "npx prettier --check ./src",
8
- "prettier": "npx prettier -w ./src"
8
+ "prettier": "npx prettier -w ./src",
9
+ "prepare": "husky install"
9
10
  },
10
11
  "files": [
11
12
  "src"
@@ -21,16 +22,26 @@
21
22
  "@popperjs/core": "^2.11.2",
22
23
  "@tailwindcss/forms": "^0.4.0",
23
24
  "@tailwindcss/typography": "^0.5.0",
24
- "@tiptap/extension-image": "^2.0.0-beta.27",
25
- "@tiptap/extension-link": "^2.0.0-beta.37",
26
- "@tiptap/extension-placeholder": "^2.0.0-beta.48",
25
+ "@tiptap/extension-image": "^2.0.0-beta.30",
26
+ "@tiptap/extension-link": "^2.0.0-beta.43",
27
+ "@tiptap/extension-mention": "^2.0.0-beta.102",
28
+ "@tiptap/extension-placeholder": "^2.0.0-beta.53",
27
29
  "@tiptap/extension-text-align": "^2.0.0-beta.31",
28
- "@tiptap/starter-kit": "^2.0.0-beta.183",
29
- "@tiptap/vue-3": "^2.0.0-beta.90",
30
+ "@tiptap/starter-kit": "^2.0.0-beta.191",
31
+ "@tiptap/vue-3": "^2.0.0-beta.96",
30
32
  "autoprefixer": "^10.4.2",
31
33
  "feather-icons": "^4.28.0",
32
34
  "postcss": "^8.4.5",
33
35
  "socket.io-client": "^4.5.1",
34
- "tailwindcss": "^3.0.12"
36
+ "tailwindcss": "^3.0.12",
37
+ "tippy.js": "^6.3.7"
38
+ },
39
+ "devDependencies": {
40
+ "husky": ">=6",
41
+ "lint-staged": ">=10",
42
+ "prettier": "2.7.1"
43
+ },
44
+ "lint-staged": {
45
+ "*.{js,css,md,vue}": "prettier --write"
35
46
  }
36
47
  }
@@ -1,31 +1,23 @@
1
1
  <template>
2
- <Combobox v-model="selectedValue" nullable>
2
+ <Combobox v-model="selectedValue" nullable v-slot="{ open: isComboboxOpen }">
3
3
  <Popover class="w-full">
4
4
  <template #target="{ open: openPopover }">
5
- <div class="relative w-full">
6
- <ComboboxInput
7
- :displayValue="displayValue"
8
- class="w-full placeholder-gray-500 form-input"
9
- type="text"
10
- @change="
11
- (e) => {
12
- query = e.target.value
13
- openPopover()
14
- }
15
- "
16
- @focus="
5
+ <div class="w-full">
6
+ <ComboboxButton
7
+ class="flex items-center justify-between w-full py-1.5 pl-3 pr-2 rounded-md bg-gray-100"
8
+ :class="{ 'rounded-b-none': isComboboxOpen }"
9
+ @click="
17
10
  () => {
18
11
  openPopover()
19
- toggleCombobox(true)
20
12
  }
21
13
  "
22
- @keydown="toggleCombobox(true)"
23
- autocomplete="off"
24
- v-bind="$attrs"
25
- />
26
- <ComboboxButton
27
- class="absolute inset-y-0 right-0 flex items-center pr-2"
28
14
  >
15
+ <span class="text-base" v-if="selectedValue">
16
+ {{ displayValue(selectedValue) }}
17
+ </span>
18
+ <span class="text-base text-gray-500" v-else>
19
+ {{ placeholder || '' }}
20
+ </span>
29
21
  <FeatherIcon
30
22
  name="chevron-down"
31
23
  class="w-4 h-4 text-gray-500"
@@ -36,12 +28,27 @@
36
28
  </template>
37
29
  <template #body>
38
30
  <ComboboxOptions
39
- :class="[
40
- 'p-1.5 bg-white rounded-md shadow-md rounded-t-none max-h-[11rem] overflow-y-auto',
41
- { hidden: !showCombobox },
42
- ]"
43
- :static="true"
31
+ class="px-1.5 pb-1.5 bg-white rounded-md shadow-md rounded-t-none max-h-[11rem] overflow-y-auto"
32
+ static
33
+ v-show="isComboboxOpen"
44
34
  >
35
+ <div
36
+ class="flex items-st items-stretch space-x-1.5 sticky top-0 pt-1.5 mb-1.5 bg-white"
37
+ >
38
+ <ComboboxInput
39
+ class="w-full placeholder-gray-500 form-input"
40
+ type="text"
41
+ @change="
42
+ (e) => {
43
+ query = e.target.value
44
+ }
45
+ "
46
+ :value="query"
47
+ autocomplete="off"
48
+ placeholder="Search by keyword"
49
+ />
50
+ <Button icon="x" @click="selectedValue = null" />
51
+ </div>
45
52
  <ComboboxOption
46
53
  as="template"
47
54
  v-for="option in filteredOptions"
@@ -81,8 +88,7 @@ import Popover from './Popover.vue'
81
88
 
82
89
  export default {
83
90
  name: 'Autocomplete',
84
- inheritAttrs: false,
85
- props: ['modelValue', 'options'],
91
+ props: ['modelValue', 'options', 'placeholder'],
86
92
  emits: ['update:modelValue', 'change'],
87
93
  components: {
88
94
  Popover,
@@ -94,7 +100,6 @@ export default {
94
100
  },
95
101
  data() {
96
102
  return {
97
- showCombobox: false,
98
103
  query: '',
99
104
  }
100
105
  },
@@ -107,7 +112,7 @@ export default {
107
112
  return this.valuePropPassed ? this.$attrs.value : this.modelValue
108
113
  },
109
114
  set(val) {
110
- setTimeout(() => this.toggleCombobox(false), 0)
115
+ this.query = ''
111
116
  this.$emit(this.valuePropPassed ? 'change' : 'update:modelValue', val)
112
117
  },
113
118
  },
@@ -130,12 +135,6 @@ export default {
130
135
  }
131
136
  return option?.label
132
137
  },
133
- toggleCombobox(value) {
134
- value = Boolean(value)
135
- if (this.showCombobox !== value) {
136
- this.showCombobox = value
137
- }
138
- },
139
138
  },
140
139
  }
141
140
  </script>
@@ -48,7 +48,7 @@ export default {
48
48
  'stroke-width': this.strokeWidth,
49
49
  width: null,
50
50
  height: null,
51
- class: [icon.attrs.class],
51
+ class: [icon.attrs.class, 'shrink-0'],
52
52
  innerHTML: icon.contents,
53
53
  },
54
54
  this.$attrs
@@ -208,10 +208,13 @@ export default {
208
208
  this.isOpen = false
209
209
  },
210
210
  onMouseover() {
211
+ this.mouseover = true
211
212
  if (this.trigger === 'hover') {
212
213
  if (this.hoverDelay) {
213
214
  this.hoverTimer = setTimeout(() => {
214
- this.open()
215
+ if (this.mouseover) {
216
+ this.open()
217
+ }
215
218
  }, Number(this.hoverDelay) * 1000)
216
219
  } else {
217
220
  this.open()
@@ -219,6 +222,7 @@ export default {
219
222
  }
220
223
  },
221
224
  onMouseleave() {
225
+ this.mouseover = false
222
226
  if (this.hoverTimer) {
223
227
  clearTimeout(this.hoverTimer)
224
228
  }
@@ -0,0 +1,72 @@
1
+ <template>
2
+ <slot v-bind="{ openDialog, resetAddImage }"></slot>
3
+ <Dialog
4
+ :options="{ title: 'Add Image' }"
5
+ v-model="addImageDialog.show"
6
+ @after-leave="resetAddImage"
7
+ >
8
+ <template #body-content>
9
+ <label
10
+ class="relative py-1 bg-gray-100 rounded-lg cursor-pointer 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 px-2 py-1 text-base select-none">
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="w-full mt-2 rounded-lg"
26
+ />
27
+ </template>
28
+ <template #actions>
29
+ <Button appearance="primary" @click="addImage(addImageDialog.url)">
30
+ Insert Image
31
+ </Button>
32
+ </template>
33
+ </Dialog>
34
+ </template>
35
+ <script>
36
+ import fileToBase64 from '../../utils/file-to-base64'
37
+ import Dialog from '../Dialog.vue'
38
+
39
+ export default {
40
+ name: 'InsertImage',
41
+ props: ['editor'],
42
+ expose: ['openDialog'],
43
+ data() {
44
+ return {
45
+ addImageDialog: { url: '', file: null, show: false },
46
+ }
47
+ },
48
+ components: { Dialog },
49
+ methods: {
50
+ openDialog() {
51
+ this.addImageDialog.show = true
52
+ },
53
+ onImageSelect(e) {
54
+ let file = e.target.files[0]
55
+ if (!file) {
56
+ return
57
+ }
58
+ this.addImageDialog.file = file
59
+ fileToBase64(file).then((base64) => {
60
+ this.addImageDialog.url = base64
61
+ })
62
+ },
63
+ addImage(src) {
64
+ this.editor.chain().focus().setImage({ src }).run()
65
+ this.resetAddImage()
66
+ },
67
+ resetAddImage() {
68
+ this.addImageDialog = { show: false, url: null, file: null }
69
+ },
70
+ },
71
+ }
72
+ </script>
@@ -0,0 +1,93 @@
1
+ <template>
2
+ <div
3
+ v-if="items.length"
4
+ class="p-1 text-base bg-white border rounded-lg shadow-lg min-w-40"
5
+ >
6
+ <button
7
+ :class="[
8
+ index === selectedIndex ? 'bg-gray-100' : 'text-gray-900',
9
+ 'whitespace-nowrap flex rounded-md items-center w-full px-2 py-2 text-sm',
10
+ ]"
11
+ v-for="(item, index) in items"
12
+ :key="index"
13
+ @click="selectItem(index)"
14
+ @mouseover="selectedIndex = index"
15
+ >
16
+ {{ item.label }}
17
+ </button>
18
+ </div>
19
+ </template>
20
+
21
+ <script>
22
+ export default {
23
+ props: {
24
+ items: {
25
+ type: Array,
26
+ required: true,
27
+ },
28
+ command: {
29
+ type: Function,
30
+ required: true,
31
+ },
32
+ },
33
+ data() {
34
+ return {
35
+ selectedIndex: 0,
36
+ }
37
+ },
38
+ watch: {
39
+ items() {
40
+ this.selectedIndex = 0
41
+ },
42
+ },
43
+ methods: {
44
+ onKeyDown({ event }) {
45
+ if (event.key === 'ArrowUp') {
46
+ this.upHandler()
47
+ return true
48
+ }
49
+ if (event.key === 'ArrowDown') {
50
+ this.downHandler()
51
+ return true
52
+ }
53
+ if (event.key === 'Enter') {
54
+ this.enterHandler()
55
+ return true
56
+ }
57
+ return false
58
+ },
59
+ upHandler() {
60
+ this.selectedIndex =
61
+ (this.selectedIndex + this.items.length - 1) % this.items.length
62
+ },
63
+ downHandler() {
64
+ this.selectedIndex = (this.selectedIndex + 1) % this.items.length
65
+ },
66
+ enterHandler() {
67
+ this.selectItem(this.selectedIndex)
68
+ },
69
+ selectItem(index) {
70
+ const item = this.items[index]
71
+ if (item) {
72
+ this.command({ id: item.value, label: item.label })
73
+ }
74
+ },
75
+ },
76
+ }
77
+ </script>
78
+
79
+ <style>
80
+ .item {
81
+ display: block;
82
+ margin: 0;
83
+ width: 100%;
84
+ text-align: left;
85
+ background: transparent;
86
+ border-radius: 0.4rem;
87
+ border: 1px solid transparent;
88
+ padding: 0.2rem 0.4rem;
89
+ }
90
+ .item.is-selected {
91
+ border-color: #000;
92
+ }
93
+ </style>
@@ -67,43 +67,12 @@
67
67
  </Button>
68
68
  </template>
69
69
  </Dialog>
70
- <Dialog
71
- :options="{ title: 'Add Image' }"
72
- v-model="addImageDialog.show"
73
- @after-leave="resetAddImage"
74
- >
75
- <template #body-content>
76
- <label
77
- class="relative py-1 bg-gray-100 rounded-lg cursor-pointer focus-within:bg-gray-200 hover:bg-gray-200"
78
- >
79
- <input
80
- type="file"
81
- class="w-full opacity-0"
82
- @change="onImageSelect"
83
- accept="image/*"
84
- />
85
- <span class="absolute inset-0 px-2 py-1 text-base select-none">
86
- {{
87
- addImageDialog.file ? 'Select another image' : 'Select an image'
88
- }}
89
- </span>
90
- </label>
91
- <img
92
- v-if="addImageDialog.url"
93
- :src="addImageDialog.url"
94
- class="w-full mt-2 rounded-lg"
95
- />
96
- </template>
97
- <template #actions>
98
- <Button appearance="primary" @click="addImage(addImageDialog.url)">
99
- Insert Image
100
- </Button>
101
- </template>
102
- </Dialog>
70
+ <InsertImage ref="insertImage" :editor="editor" />
103
71
  </div>
104
72
  </template>
105
73
  <script>
106
74
  import { Popover, Dialog, Input, Button } from 'frappe-ui'
75
+ import InsertImage from './InsertImage.vue'
107
76
  export default {
108
77
  name: 'TipTapMenu',
109
78
  props: ['editor', 'buttons'],
@@ -112,11 +81,11 @@ export default {
112
81
  Dialog,
113
82
  Input,
114
83
  Button,
84
+ InsertImage,
115
85
  },
116
86
  data() {
117
87
  return {
118
88
  setLinkDialog: { url: '', show: false },
119
- addImageDialog: { url: '', file: null, show: false },
120
89
  }
121
90
  },
122
91
  methods: {
@@ -128,7 +97,7 @@ export default {
128
97
  this.setLinkDialog.url = existingURL
129
98
  }
130
99
  } else if (button.label === 'Image') {
131
- this.addImageDialog.show = true
100
+ this.$refs.insertImage.openDialog()
132
101
  } else {
133
102
  button.action(this.editor)
134
103
  }
@@ -150,26 +119,6 @@ export default {
150
119
  this.setLinkDialog.show = false
151
120
  this.setLinkDialog.url = ''
152
121
  },
153
- onImageSelect(e) {
154
- let file = e.target.files[0]
155
- if (!file) {
156
- return
157
- }
158
- this.addImageDialog.file = file
159
- let reader = new FileReader()
160
- reader.onloadend = () => {
161
- let base64string = reader.result
162
- this.addImageDialog.url = base64string
163
- }
164
- reader.readAsDataURL(file)
165
- },
166
- addImage(src) {
167
- this.editor.chain().focus().setImage({ src }).run()
168
- this.resetAddImage()
169
- },
170
- resetAddImage() {
171
- this.addImageDialog = { show: false, url: null, file: null }
172
- },
173
122
  },
174
123
  }
175
124
  </script>
@@ -41,12 +41,6 @@
41
41
  </button>
42
42
  </FloatingMenu>
43
43
  <editor-content :editor="editor" />
44
- <span
45
- v-if="!content"
46
- class="absolute inset-y-0 text-base text-gray-500 pointer-events-none"
47
- >
48
- {{ placeholder }}
49
- </span>
50
44
  </div>
51
45
  </template>
52
46
 
@@ -55,8 +49,9 @@ import { Editor, EditorContent, BubbleMenu, FloatingMenu } from '@tiptap/vue-3'
55
49
  import StarterKit from '@tiptap/starter-kit'
56
50
  import Placeholder from '@tiptap/extension-placeholder'
57
51
  import TextAlign from '@tiptap/extension-text-align'
58
- import Image from '@tiptap/extension-image'
52
+ import Image from './image-extension'
59
53
  import Link from '@tiptap/extension-link'
54
+ import configureMention from './mention'
60
55
  import Menu from './Menu.vue'
61
56
  import commands from './commands'
62
57
  import { normalizeClass } from 'vue'
@@ -70,17 +65,48 @@ export default {
70
65
  FloatingMenu,
71
66
  Menu,
72
67
  },
73
- props: [
74
- 'content',
75
- 'placeholder',
76
- 'editorClass',
77
- 'editable',
78
- 'fixedMenu',
79
- 'bubbleMenu',
80
- 'floatingMenu',
81
- 'extensions',
82
- 'starterkitOptions',
83
- ],
68
+ props: {
69
+ content: {
70
+ type: String,
71
+ default: null,
72
+ },
73
+ placeholder: {
74
+ type: String,
75
+ default: '',
76
+ },
77
+ editorClass: {
78
+ type: [String, Array, Object],
79
+ default: '',
80
+ },
81
+ editable: {
82
+ type: Boolean,
83
+ default: true,
84
+ },
85
+ bubbleMenu: {
86
+ type: [Boolean, Array],
87
+ default: false,
88
+ },
89
+ fixedMenu: {
90
+ type: [Boolean, Array],
91
+ default: false,
92
+ },
93
+ floatingMenu: {
94
+ type: [Boolean, Array],
95
+ default: false,
96
+ },
97
+ extensions: {
98
+ type: Array,
99
+ default: () => [],
100
+ },
101
+ starterkitOptions: {
102
+ type: Object,
103
+ default: () => ({}),
104
+ },
105
+ mentions: {
106
+ type: Array,
107
+ default: () => [],
108
+ },
109
+ },
84
110
  emits: ['change'],
85
111
  expose: ['editor'],
86
112
  data() {
@@ -119,13 +145,12 @@ export default {
119
145
  TextAlign.configure({
120
146
  types: ['heading', 'paragraph'],
121
147
  }),
122
- Image.configure({
123
- allowBase64: true,
124
- }),
148
+ Image,
125
149
  Link,
126
150
  Placeholder.configure({
127
- placeholder: this.placeholder || 'Write something...',
151
+ placeholder: this.placeholder,
128
152
  }),
153
+ configureMention(this.mentions),
129
154
  ...(this.extensions || []),
130
155
  ],
131
156
  onUpdate: ({ editor }) => {
@@ -248,4 +273,8 @@ function createEditorButton(option) {
248
273
  pointer-events: none;
249
274
  height: 0;
250
275
  }
276
+ .mention {
277
+ font-weight: 600;
278
+ box-decoration-break: clone;
279
+ }
251
280
  </style>
@@ -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,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,35 @@
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
+ class="px-2 py-1 text-xs text-white bg-gray-800 border border-gray-100 rounded-lg shadow-xl"
10
+ >
11
+ {{ text }}
12
+ </div>
13
+ </slot>
14
+ </template>
15
+ </Popover>
16
+ </template>
17
+ <script>
18
+ import Popover from './Popover.vue'
19
+ export default {
20
+ name: 'Tooltip',
21
+ components: { Popover },
22
+ props: {
23
+ hoverDelay: {
24
+ default: 0.5,
25
+ },
26
+ placement: {
27
+ default: 'bottom',
28
+ },
29
+ text: {
30
+ type: String,
31
+ default: '',
32
+ },
33
+ },
34
+ }
35
+ </script>
package/src/index.js CHANGED
@@ -23,6 +23,7 @@ export { default as Resource } from './components/Resource.vue'
23
23
  export { default as Spinner } from './components/Spinner.vue'
24
24
  export { default as SuccessMessage } from './components/SuccessMessage.vue'
25
25
  export { default as TextEditor } from './components/TextEditor'
26
+ export { default as Tooltip } from './components/Tooltip.vue'
26
27
 
27
28
  // directives
28
29
  export { default as onOutsideClickDirective } from './directives/onOutsideClick.js'
@@ -0,0 +1,9 @@
1
+ export default (file) => {
2
+ return new Promise((resolve) => {
3
+ let reader = new FileReader()
4
+ reader.onloadend = () => {
5
+ resolve(reader.result)
6
+ }
7
+ reader.readAsDataURL(file)
8
+ })
9
+ }
@@ -201,6 +201,7 @@ export function createDocumentResource(options, vm) {
201
201
  doctype: options.doctype,
202
202
  name: options.name,
203
203
  doc: null,
204
+ auto: true,
204
205
  get: createResource(
205
206
  {
206
207
  method: 'frappe.client.get',
@@ -238,6 +239,8 @@ export function createDocumentResource(options, vm) {
238
239
  onSuccess() {
239
240
  out.doc = null
240
241
  options.delete?.onSuccess?.call(vm, data)
242
+ // delete from list resources
243
+ deleteRowInListResource(out.doctype, out.name)
241
244
  },
242
245
  onError: options.delete?.onError,
243
246
  },
@@ -295,8 +298,6 @@ export function createDocumentResource(options, vm) {
295
298
  return doc
296
299
  }
297
300
 
298
- // fetch the doc
299
- out.get.fetch()
300
301
  // cache
301
302
  documentCache[cacheKey] = out
302
303
  return out
@@ -323,6 +324,7 @@ export function createListResource(options, vm, getResource) {
323
324
  data: null,
324
325
  next,
325
326
  hasNextPage: true,
327
+ auto: true,
326
328
  list: createResource(
327
329
  {
328
330
  method: 'frappe.client.get_list',
@@ -451,7 +453,6 @@ export function createListResource(options, vm, getResource) {
451
453
  out.order_by = updatedOptions.order_by
452
454
  out.start = updatedOptions.start
453
455
  out.limit = updatedOptions.limit
454
- out.list.fetch()
455
456
  }
456
457
 
457
458
  function transform(data) {
@@ -481,9 +482,6 @@ export function createListResource(options, vm, getResource) {
481
482
  out.list.fetch()
482
483
  }
483
484
 
484
- // fetch list
485
- out.list.fetch()
486
-
487
485
  if (cacheKey) {
488
486
  // cache
489
487
  listCache[cacheKey] = out
@@ -515,6 +513,18 @@ function updateRowInListResource(doctype, doc) {
515
513
  }
516
514
  }
517
515
 
516
+ function deleteRowInListResource(doctype, docname) {
517
+ let resources = listResources[doctype] || []
518
+ for (let resource of resources) {
519
+ if (resource.originalData) {
520
+ resource.originalData = resource.originalData.filter(
521
+ (row) => row.name !== docname
522
+ )
523
+ resource.data = resource.transform(resource.originalData)
524
+ }
525
+ }
526
+ }
527
+
518
528
  function revertRowInListResource(doctype, doc) {
519
529
  let resources = listResources[doctype] || []
520
530
  for (let resource of resources) {
@@ -593,7 +603,7 @@ let createMixin = (mixinOptions) => ({
593
603
  resource.update(updatedOptions)
594
604
  }
595
605
  if (resource && resource.auto) {
596
- resource.fetch()
606
+ resource.reload()
597
607
  }
598
608
  },
599
609
  {
@@ -607,8 +617,8 @@ let createMixin = (mixinOptions) => ({
607
617
  mixinOptions.getResource
608
618
  )
609
619
  this._resources[key] = resource
610
- if (resource.auto) {
611
- resource.fetch()
620
+ if (resource && resource.auto) {
621
+ resource.reload()
612
622
  }
613
623
  }
614
624
  }