frappe-ui 0.0.47 → 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,6 +1,6 @@
1
1
  {
2
2
  "name": "frappe-ui",
3
- "version": "0.0.47",
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": {
@@ -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>
@@ -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>
@@ -49,7 +49,7 @@ import { Editor, EditorContent, BubbleMenu, FloatingMenu } from '@tiptap/vue-3'
49
49
  import StarterKit from '@tiptap/starter-kit'
50
50
  import Placeholder from '@tiptap/extension-placeholder'
51
51
  import TextAlign from '@tiptap/extension-text-align'
52
- import Image from '@tiptap/extension-image'
52
+ import Image from './image-extension'
53
53
  import Link from '@tiptap/extension-link'
54
54
  import configureMention from './mention'
55
55
  import Menu from './Menu.vue'
@@ -145,9 +145,7 @@ export default {
145
145
  TextAlign.configure({
146
146
  types: ['heading', 'paragraph'],
147
147
  }),
148
- Image.configure({
149
- allowBase64: true,
150
- }),
148
+ Image,
151
149
  Link,
152
150
  Placeholder.configure({
153
151
  placeholder: this.placeholder,
@@ -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
+ }
@@ -24,7 +24,7 @@ export default {
24
24
  default: 0.5,
25
25
  },
26
26
  placement: {
27
- default: 'bottom-start',
27
+ default: 'bottom',
28
28
  },
29
29
  text: {
30
30
  type: String,
@@ -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
+ }