frappe-ui 0.0.78 → 0.0.80

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.78",
3
+ "version": "0.0.80",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
@@ -1,9 +1,9 @@
1
1
  <template>
2
- <slot v-bind="{ openDialog, resetAddImage }"></slot>
2
+ <slot v-bind="{ onClick: openDialog }"></slot>
3
3
  <Dialog
4
4
  :options="{ title: 'Add Image' }"
5
5
  v-model="addImageDialog.show"
6
- @after-leave="resetAddImage"
6
+ @after-leave="reset"
7
7
  >
8
8
  <template #body-content>
9
9
  <label
@@ -29,12 +29,14 @@
29
29
  <Button appearance="primary" @click="addImage(addImageDialog.url)">
30
30
  Insert Image
31
31
  </Button>
32
+ <Button @click="reset"> Cancel </Button>
32
33
  </template>
33
34
  </Dialog>
34
35
  </template>
35
36
  <script>
36
37
  import fileToBase64 from '../../utils/file-to-base64'
37
38
  import Dialog from '../Dialog.vue'
39
+ import Button from '../Button.vue'
38
40
 
39
41
  export default {
40
42
  name: 'InsertImage',
@@ -45,7 +47,7 @@ export default {
45
47
  addImageDialog: { url: '', file: null, show: false },
46
48
  }
47
49
  },
48
- components: { Dialog },
50
+ components: { Button, Dialog },
49
51
  methods: {
50
52
  openDialog() {
51
53
  this.addImageDialog.show = true
@@ -62,10 +64,10 @@ export default {
62
64
  },
63
65
  addImage(src) {
64
66
  this.editor.chain().focus().setImage({ src }).run()
65
- this.resetAddImage()
67
+ this.reset()
66
68
  },
67
- resetAddImage() {
68
- this.addImageDialog = { show: false, url: null, file: null }
69
+ reset() {
70
+ this.addImageDialog = this.$options.data().addImageDialog
69
71
  },
70
72
  },
71
73
  }
@@ -0,0 +1,67 @@
1
+ <template>
2
+ <slot v-bind="{ onClick: openDialog }"></slot>
3
+ <Dialog
4
+ :options="{ title: 'Set Link' }"
5
+ v-model="setLinkDialog.show"
6
+ @after-leave="reset"
7
+ >
8
+ <template #body-content>
9
+ <Input
10
+ type="text"
11
+ label="URL"
12
+ v-model="setLinkDialog.url"
13
+ @keydown.enter="(e) => setLink(e.target.value)"
14
+ />
15
+ </template>
16
+ <template #actions>
17
+ <Button appearance="primary" @click="setLink(setLinkDialog.url)">
18
+ Save
19
+ </Button>
20
+ </template>
21
+ </Dialog>
22
+ </template>
23
+ <script>
24
+ import Dialog from '../Dialog.vue'
25
+ import Button from '../Button.vue'
26
+ import Input from '../Input.vue'
27
+
28
+ export default {
29
+ name: 'InsertLink',
30
+ props: ['editor'],
31
+ components: { Button, Input, Dialog },
32
+ data() {
33
+ return {
34
+ setLinkDialog: { url: '', show: false },
35
+ }
36
+ },
37
+ methods: {
38
+ openDialog() {
39
+ let existingURL = this.editor.getAttributes('link').href
40
+ if (existingURL) {
41
+ this.setLinkDialog.url = existingURL
42
+ }
43
+ this.setLinkDialog.show = true
44
+ },
45
+ setLink(url) {
46
+ // empty
47
+ if (url === '') {
48
+ this.editor.chain().focus().extendMarkRange('link').unsetLink().run()
49
+ } else {
50
+ // update link
51
+ this.editor
52
+ .chain()
53
+ .focus()
54
+ .extendMarkRange('link')
55
+ .setLink({ href: url })
56
+ .run()
57
+ }
58
+
59
+ this.setLinkDialog.show = false
60
+ this.setLinkDialog.url = ''
61
+ },
62
+ reset() {
63
+ this.setLinkDialog = this.$options.data().setLinkDialog
64
+ },
65
+ },
66
+ }
67
+ </script>
@@ -0,0 +1,94 @@
1
+ <template>
2
+ <slot v-bind="{ onClick: openDialog }"></slot>
3
+ <Dialog
4
+ :options="{ title: 'Add Video' }"
5
+ v-model="addVideoDialog.show"
6
+ @after-leave="reset"
7
+ >
8
+ <template #body-content>
9
+ <FileUploader
10
+ file-types="video/*"
11
+ @success="(file) => (addVideoDialog.url = file.file_url)"
12
+ >
13
+ <template v-slot="{ file, progress, uploading, openFileSelector }">
14
+ <div class="flex items-center space-x-2">
15
+ <Button @click="openFileSelector">
16
+ {{
17
+ uploading
18
+ ? `Uploading ${progress}%`
19
+ : addVideoDialog.url
20
+ ? 'Change Video'
21
+ : 'Upload Video'
22
+ }}
23
+ </Button>
24
+ <Button
25
+ v-if="addVideoDialog.url"
26
+ @click="
27
+ () => {
28
+ addVideoDialog.url = null
29
+ addVideoDialog.file = null
30
+ }
31
+ "
32
+ >
33
+ Remove
34
+ </Button>
35
+ </div>
36
+ </template>
37
+ </FileUploader>
38
+ <video
39
+ v-if="addVideoDialog.url"
40
+ :src="addVideoDialog.url"
41
+ class="mt-2 w-full rounded-lg"
42
+ type="video/mp4"
43
+ controls
44
+ />
45
+ </template>
46
+ <template #actions>
47
+ <Button appearance="primary" @click="addVideo(addVideoDialog.url)">
48
+ Insert Video
49
+ </Button>
50
+ <Button @click="reset">Cancel</Button>
51
+ </template>
52
+ </Dialog>
53
+ </template>
54
+ <script>
55
+ import Button from '../Button.vue'
56
+ import Dialog from '../Dialog.vue'
57
+ import FileUploader from '../FileUploader.vue'
58
+
59
+ export default {
60
+ name: 'InsertImage',
61
+ props: ['editor'],
62
+ expose: ['openDialog'],
63
+ data() {
64
+ return {
65
+ addVideoDialog: { url: '', file: null, show: false },
66
+ }
67
+ },
68
+ components: { Button, Dialog, FileUploader },
69
+ methods: {
70
+ openDialog() {
71
+ this.addVideoDialog.show = true
72
+ },
73
+ onVideoSelect(e) {
74
+ let file = e.target.files[0]
75
+ if (!file) {
76
+ return
77
+ }
78
+ this.addVideoDialog.file = file
79
+ },
80
+
81
+ addVideo(src) {
82
+ this.editor
83
+ .chain()
84
+ .focus()
85
+ .insertContent(`<video src="${src}"></video>`)
86
+ .run()
87
+ this.reset()
88
+ },
89
+ reset() {
90
+ this.addVideoDialog = this.$options.data().addVideoDialog
91
+ },
92
+ },
93
+ }
94
+ </script>
@@ -50,88 +50,47 @@
50
50
  </template>
51
51
  </Popover>
52
52
  </div>
53
- <button
54
- v-else
55
- class="flex rounded p-1 text-gray-800 transition-colors"
56
- :class="button.isActive(editor) ? 'bg-gray-100' : 'hover:bg-gray-100'"
57
- @click="onClick(button)"
58
- :title="button.label"
59
- >
60
- <component v-if="button.icon" :is="button.icon" class="h-4 w-4" />
61
- <span class="inline-block h-4 min-w-[1rem] text-sm leading-4" v-else>
62
- {{ button.text }}
63
- </span>
64
- </button>
53
+ <component v-else :is="button.component || 'div'" v-bind="{ editor }">
54
+ <template v-slot="componentSlotProps">
55
+ <button
56
+ class="flex rounded p-1 text-gray-800 transition-colors"
57
+ :class="
58
+ button.isActive(editor) ? 'bg-gray-100' : 'hover:bg-gray-100'
59
+ "
60
+ @click="
61
+ componentSlotProps?.onClick
62
+ ? componentSlotProps.onClick(button)
63
+ : onButtonClick(button)
64
+ "
65
+ :title="button.label"
66
+ >
67
+ <component v-if="button.icon" :is="button.icon" class="h-4 w-4" />
68
+ <span
69
+ class="inline-block h-4 min-w-[1rem] text-sm leading-4"
70
+ v-else
71
+ >
72
+ {{ button.text }}
73
+ </span>
74
+ </button>
75
+ </template>
76
+ </component>
65
77
  </template>
66
78
  </div>
67
-
68
- <Dialog :options="{ title: 'Set Link' }" v-model="setLinkDialog.show">
69
- <template #body-content>
70
- <Input
71
- type="text"
72
- label="URL"
73
- v-model="setLinkDialog.url"
74
- @keydown.enter="(e) => setLink(e.target.value)"
75
- />
76
- </template>
77
- <template #actions>
78
- <Button appearance="primary" @click="setLink(setLinkDialog.url)">
79
- Save
80
- </Button>
81
- </template>
82
- </Dialog>
83
- <InsertImage ref="insertImage" :editor="editor" />
84
79
  </div>
85
80
  </template>
86
81
  <script>
87
- import { Popover, Dialog, Input, Button } from 'frappe-ui'
88
- import InsertImage from './InsertImage.vue'
82
+ import { Popover } from 'frappe-ui'
83
+
89
84
  export default {
90
85
  name: 'TipTapMenu',
91
86
  props: ['buttons'],
92
87
  inject: ['editor'],
93
88
  components: {
94
89
  Popover,
95
- Dialog,
96
- Input,
97
- Button,
98
- InsertImage,
99
- },
100
- data() {
101
- return {
102
- setLinkDialog: { url: '', show: false },
103
- }
104
90
  },
105
91
  methods: {
106
- onClick(button) {
107
- if (button.label === 'Link') {
108
- this.setLinkDialog.show = true
109
- let existingURL = this.editor.getAttributes('link').href
110
- if (existingURL) {
111
- this.setLinkDialog.url = existingURL
112
- }
113
- } else if (button.label === 'Image') {
114
- this.$refs.insertImage.openDialog()
115
- } else {
116
- button.action(this.editor)
117
- }
118
- },
119
- setLink(url) {
120
- // empty
121
- if (url === '') {
122
- this.editor.chain().focus().extendMarkRange('link').unsetLink().run()
123
- } else {
124
- // update link
125
- this.editor
126
- .chain()
127
- .focus()
128
- .extendMarkRange('link')
129
- .setLink({ href: url })
130
- .run()
131
- }
132
-
133
- this.setLinkDialog.show = false
134
- this.setLinkDialog.url = ''
92
+ onButtonClick(button) {
93
+ button.action(this.editor)
135
94
  },
136
95
  },
137
96
  }
@@ -24,6 +24,7 @@ import TableCell from '@tiptap/extension-table-cell'
24
24
  import TableHeader from '@tiptap/extension-table-header'
25
25
  import TableRow from '@tiptap/extension-table-row'
26
26
  import Image from './image-extension'
27
+ import Video from './video-extension'
27
28
  import Link from '@tiptap/extension-link'
28
29
  import configureMention from './mention'
29
30
  import TextEditorFixedMenu from './TextEditorFixedMenu.vue'
@@ -135,6 +136,7 @@ export default {
135
136
  types: ['heading', 'paragraph'],
136
137
  }),
137
138
  Image,
139
+ Video,
138
140
  Link,
139
141
  Placeholder.configure({
140
142
  showOnlyWhenEditable: false,
@@ -209,6 +211,11 @@ export default {
209
211
  height: 0;
210
212
  }
211
213
 
214
+ .ProseMirror-selectednode video,
215
+ img.ProseMirror-selectednode {
216
+ border: 2px solid theme('colors.blue.300');
217
+ }
218
+
212
219
  /* Mentions */
213
220
  .mention {
214
221
  font-weight: 600;
@@ -42,6 +42,7 @@ export default {
42
42
  'Numbered List',
43
43
  'Separator',
44
44
  'Image',
45
+ 'Video',
45
46
  'Blockquote',
46
47
  'Code',
47
48
  [
@@ -39,6 +39,7 @@ export default {
39
39
  'Align Right',
40
40
  'Separator',
41
41
  'Image',
42
+ 'Video',
42
43
  'Link',
43
44
  'Blockquote',
44
45
  'Code',
@@ -1,3 +1,4 @@
1
+ import { defineAsyncComponent } from 'vue'
1
2
  import H1 from './icons/h-1.vue'
2
3
  import H2 from './icons/h-2.vue'
3
4
  import H3 from './icons/h-3.vue'
@@ -17,6 +18,7 @@ import DoubleQuotes from './icons/double-quotes-r.vue'
17
18
  import CodeView from './icons/code-view.vue'
18
19
  import Link from './icons/link.vue'
19
20
  import Image from './icons/image-add-line.vue'
21
+ import Video from './icons/video-add-line.vue'
20
22
  import ArrowGoBack from './icons/arrow-go-back-line.vue'
21
23
  import ArrowGoForward from './icons/arrow-go-forward-line.vue'
22
24
  import Separator from './icons/separator.vue'
@@ -147,11 +149,19 @@ export default {
147
149
  label: 'Link',
148
150
  icon: Link,
149
151
  isActive: (editor) => editor.isActive('link'),
152
+ component: defineAsyncComponent(() => import('./InsertLink.vue')),
150
153
  },
151
154
  Image: {
152
155
  label: 'Image',
153
156
  icon: Image,
154
157
  isActive: (editor) => false,
158
+ component: defineAsyncComponent(() => import('./InsertImage.vue')),
159
+ },
160
+ Video: {
161
+ label: 'Video',
162
+ icon: Video,
163
+ isActive: (editor) => false,
164
+ component: defineAsyncComponent(() => import('./InsertVideo.vue')),
155
165
  },
156
166
  Undo: {
157
167
  label: 'Undo',
@@ -0,0 +1,14 @@
1
+ <template>
2
+ <svg
3
+ xmlns="http://www.w3.org/2000/svg"
4
+ viewBox="0 0 24 24"
5
+ width="24"
6
+ height="24"
7
+ >
8
+ <path fill="none" d="M0 0H24V24H0z" />
9
+ <path
10
+ d="M16 4c.552 0 1 .448 1 1v4.2l5.213-3.65c.226-.158.538-.103.697.124.058.084.09.184.09.286v12.08c0 .276-.224.5-.5.5-.103 0-.203-.032-.287-.09L17 14.8V19c0 .552-.448 1-1 1H2c-.552 0-1-.448-1-1V5c0-.552.448-1 1-1h14zm-1 2H3v12h12V6zM8 8h2v3h3v2H9.999L10 16H8l-.001-3H5v-2h3V8zm13 .841l-4 2.8v.718l4 2.8V8.84z"
11
+ fill="currentColor"
12
+ />
13
+ </svg>
14
+ </template>
@@ -0,0 +1,60 @@
1
+ import { Node, mergeAttributes } from '@tiptap/core'
2
+ // Inspired by this blog: https://www.codemzy.com/blog/tiptap-video-embed-extension
3
+
4
+ const Video = Node.create({
5
+ name: 'video',
6
+ group: 'block',
7
+ selectable: true,
8
+ draggable: true,
9
+ atom: true,
10
+
11
+ addAttributes() {
12
+ return {
13
+ src: {
14
+ default: null,
15
+ },
16
+ }
17
+ },
18
+
19
+ parseHTML() {
20
+ return [
21
+ {
22
+ tag: 'video',
23
+ },
24
+ ]
25
+ },
26
+
27
+ renderHTML({ HTMLAttributes }) {
28
+ return ['video', mergeAttributes(HTMLAttributes)]
29
+ },
30
+
31
+ addNodeView() {
32
+ return ({ editor, node }) => {
33
+ const div = document.createElement('div')
34
+ div.className =
35
+ 'relative aspect-w-16 aspect-h-9' +
36
+ (editor.isEditable ? ' cursor-pointer' : '')
37
+
38
+ const video = document.createElement('video')
39
+ if (editor.isEditable) {
40
+ video.className = 'pointer-events-none'
41
+ }
42
+ video.src = node.attrs.src
43
+ if (!editor.isEditable) {
44
+ video.setAttribute('controls', '')
45
+ } else {
46
+ let videoPill = document.createElement('div')
47
+ videoPill.className =
48
+ 'absolute top-0 right-0 text-xs m-2 bg-gray-800 text-white px-2 py-1 rounded-md'
49
+ videoPill.innerHTML = 'Video'
50
+ div.append(videoPill)
51
+ }
52
+ div.append(video)
53
+ return {
54
+ dom: div,
55
+ }
56
+ }
57
+ },
58
+ })
59
+
60
+ export default Video
@@ -118,9 +118,9 @@ export function createListResource(options, vm, getResource) {
118
118
  fieldname: values,
119
119
  }
120
120
  },
121
- onSuccess(data) {
122
- out.list.fetch()
123
- options.setValue?.onSuccess?.call(vm, data)
121
+ onSuccess(doc) {
122
+ updateRowInListResource(out.doctype, doc)
123
+ options.setValue?.onSuccess?.call(vm, doc)
124
124
  },
125
125
  onError: options.setValue?.onError,
126
126
  },
@@ -182,7 +182,7 @@ export function createListResource(options, vm, getResource) {
182
182
  function transform(data) {
183
183
  if (options.transform) {
184
184
  let returnValue = options.transform.call(vm, data)
185
- if (typeof returnValue != null) {
185
+ if (returnValue != null) {
186
186
  return returnValue
187
187
  }
188
188
  }
@@ -155,7 +155,7 @@ export function createResource(options, vm, getResource) {
155
155
  function transform(data) {
156
156
  if (options.transform) {
157
157
  let returnValue = options.transform.call(vm, data)
158
- if (typeof returnValue != null) {
158
+ if (returnValue != null) {
159
159
  return returnValue
160
160
  }
161
161
  }