frappe-ui 0.1.135 → 0.1.136

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.135",
3
+ "version": "0.1.136",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "main": "./src/index.ts",
6
6
  "scripts": {
@@ -27,10 +27,12 @@
27
27
  "author": "Frappe Technologies Pvt. Ltd.",
28
28
  "license": "MIT",
29
29
  "dependencies": {
30
+ "@floating-ui/vue": "^1.1.6",
30
31
  "@headlessui/vue": "^1.7.14",
31
32
  "@popperjs/core": "^2.11.2",
32
33
  "@tailwindcss/forms": "^0.5.3",
33
34
  "@tailwindcss/typography": "^0.5.16",
35
+ "@tiptap/core": "^2.11.7",
34
36
  "@tiptap/extension-code-block-lowlight": "^2.11.5",
35
37
  "@tiptap/extension-color": "^2.0.3",
36
38
  "@tiptap/extension-highlight": "^2.0.3",
@@ -58,6 +60,9 @@
58
60
  "lucide-static": "^0.479.0",
59
61
  "ora": "5.4.1",
60
62
  "prettier": "^3.3.2",
63
+ "prosemirror-model": "^1.25.1",
64
+ "prosemirror-state": "^1.4.3",
65
+ "prosemirror-view": "^1.39.2",
61
66
  "radix-vue": "^1.5.3",
62
67
  "reka-ui": "^2.0.2",
63
68
  "showdown": "^2.1.0",
@@ -7,18 +7,18 @@
7
7
  ></div>
8
8
  </template>
9
9
 
10
- <script>
11
- export default {
12
- name: 'ErrorMessage',
13
- props: ['message'],
14
- computed: {
15
- errorMessage() {
16
- if (!this.message) return ''
17
- if (this.message instanceof Error) {
18
- return this.message.messages || this.message.message
19
- }
20
- return this.message
21
- },
22
- },
23
- }
10
+ <script setup lang="ts">
11
+ import { computed } from 'vue'
12
+
13
+ const props = defineProps<{
14
+ message?: string | Error
15
+ }>()
16
+
17
+ const errorMessage = computed(() => {
18
+ if (!props.message) return ''
19
+ if (props.message instanceof Error) {
20
+ return (props.message as any).messages || props.message.message
21
+ }
22
+ return props.message
23
+ })
24
24
  </script>
@@ -50,7 +50,7 @@
50
50
  </template>
51
51
 
52
52
  <script lang="ts" setup>
53
- import { computed } from '@vue/reactivity'
53
+ import { computed } from 'vue'
54
54
 
55
55
  const MIN_VALUE = 0
56
56
  const MAX_VALUE = 100
@@ -15,8 +15,7 @@
15
15
  </template>
16
16
 
17
17
  <script>
18
- import { normalizeClass } from 'vue'
19
- import { computed } from '@vue/reactivity'
18
+ import { normalizeClass, computed } from 'vue'
20
19
  import { Editor, EditorContent, VueNodeViewRenderer } from '@tiptap/vue-3'
21
20
  import StarterKit from '@tiptap/starter-kit'
22
21
  import Placeholder from '@tiptap/extension-placeholder'
@@ -25,7 +24,7 @@ import Table from '@tiptap/extension-table'
25
24
  import TableCell from '@tiptap/extension-table-cell'
26
25
  import TableHeader from '@tiptap/extension-table-header'
27
26
  import TableRow from '@tiptap/extension-table-row'
28
- import ImageExtension from './image-extension'
27
+ import { ImageExtension } from './extensions/image'
29
28
  import ImageViewerExtension from './image-viewer-extension'
30
29
  import VideoExtension from './video-extension'
31
30
  import LinkExtension from './link-extension'
@@ -0,0 +1,263 @@
1
+ <script setup lang="ts">
2
+ import { ref, onMounted } from 'vue'
3
+ import { NodeViewWrapper, nodeViewProps } from '@tiptap/vue-3'
4
+ import LoadingIndicator from '../../../LoadingIndicator.vue'
5
+ import ErrorMessage from '../../../ErrorMessage.vue'
6
+ import LucideMoveDiagonal2 from '~icons/lucide/move-diagonal-2'
7
+ import LucideAlignLeft from '~icons/lucide/align-left'
8
+ import LucideAlignCenter from '~icons/lucide/align-center'
9
+ import LucideAlignRight from '~icons/lucide/align-right'
10
+
11
+ const props = defineProps(nodeViewProps)
12
+
13
+ const imageRef = ref<HTMLImageElement | null>(null)
14
+ const containerRef = ref<HTMLDivElement | null>(null)
15
+ const isResizing = ref(false)
16
+ const startDragX = ref(0)
17
+ const startWidth = ref(0)
18
+ const originalAspectRatio = ref(1)
19
+
20
+ function selectImage() {
21
+ props.editor.commands.setNodeSelection(props.getPos())
22
+ }
23
+
24
+ const caption = ref(props.node.attrs.alt || '')
25
+ const isEditable = ref(false)
26
+
27
+ onMounted(() => {
28
+ isEditable.value = props.editor.isEditable
29
+ if (imageRef.value) {
30
+ // Ensure initial aspect ratio is captured if dimensions are available
31
+ const initialWidth = props.node.attrs.width || imageRef.value.naturalWidth
32
+ const initialHeight =
33
+ props.node.attrs.height || imageRef.value.naturalHeight
34
+ if (initialWidth && initialHeight) {
35
+ originalAspectRatio.value = initialHeight / initialWidth
36
+ }
37
+ }
38
+ })
39
+
40
+ props.editor.on('update', () => {
41
+ isEditable.value = props.editor.isEditable
42
+ })
43
+
44
+ function updateCaption(event: Event) {
45
+ const newCaption = (event.target as HTMLInputElement).value
46
+ caption.value = newCaption
47
+ props.updateAttributes({ alt: newCaption })
48
+ }
49
+
50
+ function handleKeydown(event: KeyboardEvent) {
51
+ if (event.key === 'Enter') {
52
+ event.preventDefault()
53
+ createParagraphAfterImage()
54
+ } else if (event.key === 'Escape' || event.key === 'ArrowDown') {
55
+ event.preventDefault()
56
+ setCursorAfterImage()
57
+ }
58
+ if (event.key === 'ArrowUp') {
59
+ event.preventDefault()
60
+ setCursorBeforeImage()
61
+ }
62
+ }
63
+
64
+ function setCursorAt(pos: number) {
65
+ props.editor.commands.focus()
66
+ props.editor.chain().setTextSelection(pos).scrollIntoView().run()
67
+ }
68
+
69
+ function createParagraphAfterImage() {
70
+ const pos = props.getPos()
71
+ props.editor.commands.focus()
72
+ props.editor
73
+ .chain()
74
+ .setTextSelection(pos + 1)
75
+ .createParagraphNear()
76
+ .scrollIntoView()
77
+ .run()
78
+ }
79
+
80
+ function setCursorAfterImage() {
81
+ const pos = props.getPos()
82
+ setCursorAt(pos + 1)
83
+ }
84
+
85
+ function setCursorBeforeImage() {
86
+ const pos = props.getPos()
87
+ setCursorAt(pos - 1)
88
+ }
89
+
90
+ function startResize(event: MouseEvent) {
91
+ if (!isEditable.value) return
92
+ selectImage()
93
+ isResizing.value = true
94
+ startDragX.value = event.clientX
95
+ startWidth.value = imageRef.value?.offsetWidth || props.node.attrs.width || 0
96
+
97
+ // Calculate aspect ratio from current attributes or natural dimensions
98
+ const width = props.node.attrs.width || imageRef.value?.naturalWidth
99
+ const height = props.node.attrs.height || imageRef.value?.naturalHeight
100
+ if (width && height) {
101
+ originalAspectRatio.value = height / width
102
+ } else {
103
+ originalAspectRatio.value = 1
104
+ }
105
+
106
+ window.addEventListener('mousemove', handleResize)
107
+ window.addEventListener('mouseup', stopResize)
108
+ document.body.style.cursor = 'ew-resize'
109
+ }
110
+
111
+ function handleResize(event: MouseEvent) {
112
+ if (!isResizing.value || !imageRef.value || !containerRef.value) return
113
+
114
+ const editorElement = props.editor.view.dom
115
+ const editorWidth = editorElement.clientWidth
116
+
117
+ const deltaX = event.clientX - startDragX.value
118
+ let newWidth = startWidth.value + deltaX
119
+
120
+ // Add constraints (e.g., minimum width and maximum width based on editor)
121
+ newWidth = Math.max(50, Math.min(newWidth, editorWidth))
122
+
123
+ const newHeight = newWidth * originalAspectRatio.value
124
+
125
+ // Apply temporary styles for visual feedback
126
+ imageRef.value.style.width = `${newWidth}px`
127
+ imageRef.value.style.height = `${newHeight}px`
128
+ containerRef.value.style.width = `${newWidth}px`
129
+ }
130
+
131
+ function stopResize() {
132
+ if (!isResizing.value) return
133
+
134
+ isResizing.value = false
135
+ window.removeEventListener('mousemove', handleResize)
136
+ window.removeEventListener('mouseup', stopResize)
137
+ document.body.style.cursor = ''
138
+
139
+ if (imageRef.value && containerRef.value) {
140
+ const finalWidth = imageRef.value.offsetWidth
141
+ const finalHeight = imageRef.value.offsetHeight
142
+ props.updateAttributes({ width: finalWidth, height: finalHeight })
143
+
144
+ // Clear temporary styles after updating attributes
145
+ imageRef.value.style.width = ''
146
+ imageRef.value.style.height = ''
147
+ containerRef.value.style.width = ''
148
+ }
149
+ }
150
+
151
+ function setAlignment(align: 'left' | 'center' | 'right') {
152
+ props.editor.commands.setImageAlign(align)
153
+ }
154
+ </script>
155
+
156
+ <template>
157
+ <NodeViewWrapper>
158
+ <div
159
+ ref="containerRef"
160
+ class="relative overflow-hidden not-prose my-6 rounded-[2px] block max-w-full"
161
+ :class="[
162
+ { 'ring-2 ring-outline-gray-3 ring-offset-2': selected },
163
+ node.attrs.align === 'center' ? 'mx-auto' : '',
164
+ node.attrs.align === 'right' ? 'ml-auto mr-0' : '',
165
+ node.attrs.align === 'left' ? 'mr-auto ml-0' : '',
166
+ ]"
167
+ :style="{ width: node.attrs.width ? `${node.attrs.width}px` : 'auto' }"
168
+ >
169
+ <div class="relative">
170
+ <img
171
+ v-if="node.attrs.src"
172
+ ref="imageRef"
173
+ class="rounded-[2px]"
174
+ :src="node.attrs.src"
175
+ :alt="node.attrs.alt || ''"
176
+ :width="node.attrs.width"
177
+ :height="node.attrs.height"
178
+ @click.stop="selectImage"
179
+ />
180
+
181
+ <div class="absolute bottom-2 right-2 flex items-center gap-2">
182
+ <!-- Alignment Controls -->
183
+ <div
184
+ v-if="selected && isEditable"
185
+ class="flex divide-x divide-outline-gray-5 rounded bg-black/65"
186
+ >
187
+ <button
188
+ @click.stop="setAlignment('left')"
189
+ :class="[
190
+ 'px-1.5 py-1 hover:text-ink-white',
191
+ node.attrs.align === 'left'
192
+ ? 'text-ink-white'
193
+ : 'text-ink-gray-4',
194
+ ]"
195
+ >
196
+ <LucideAlignLeft class="size-4" />
197
+ </button>
198
+ <button
199
+ @click.stop="setAlignment('center')"
200
+ :class="[
201
+ 'px-1.5 py-1 hover:text-ink-white',
202
+ node.attrs.align === 'center'
203
+ ? 'text-ink-white'
204
+ : 'text-ink-gray-4',
205
+ ]"
206
+ >
207
+ <LucideAlignCenter class="size-4" />
208
+ </button>
209
+ <button
210
+ @click.stop="setAlignment('right')"
211
+ :class="[
212
+ 'px-1.5 py-1 hover:text-ink-white',
213
+ node.attrs.align === 'right'
214
+ ? 'text-ink-white'
215
+ : 'text-ink-gray-4',
216
+ ]"
217
+ >
218
+ <LucideAlignRight class="size-4" />
219
+ </button>
220
+ </div>
221
+
222
+ <!-- Resize Handle -->
223
+ <button
224
+ v-if="selected && isEditable"
225
+ class="cursor-nw-resize bg-black/65 rounded p-1"
226
+ @mousedown.prevent="startResize"
227
+ >
228
+ <LucideMoveDiagonal2 class="text-white size-4" />
229
+ </button>
230
+ </div>
231
+
232
+ <!-- Loading indicator overlay -->
233
+ <div
234
+ v-if="node.attrs.loading"
235
+ class="inset-0 absolute flex items-center justify-center z-10"
236
+ >
237
+ <div
238
+ class="bg-gray-900/80 p-2 inset-0 leading-none rounded-sm flex flex-col items-center justify-center gap-2"
239
+ >
240
+ <div class="flex items-center gap-2">
241
+ <LoadingIndicator class="text-gray-100 size-4" />
242
+ <span class="text-gray-100">Uploading...</span>
243
+ </div>
244
+ </div>
245
+ </div>
246
+ </div>
247
+
248
+ <input
249
+ v-if="(isEditable || node.attrs.alt) && !node.attrs.error"
250
+ v-model="caption"
251
+ class="w-full text-center bg-transparent text-sm text-ink-gray-6 h-7 border-none focus:ring-0 placeholder-ink-gray-4"
252
+ placeholder="Add caption"
253
+ :disabled="!isEditable"
254
+ @change="updateCaption"
255
+ @keydown="handleKeydown"
256
+ />
257
+
258
+ <div v-if="node.attrs.error" class="w-full py-1.5">
259
+ <ErrorMessage :message="`Upload Failed: ${node.attrs.error}`" />
260
+ </div>
261
+ </div>
262
+ </NodeViewWrapper>
263
+ </template>
@@ -8,9 +8,9 @@ import ImageNodeView from './ImageNodeView.vue'
8
8
  import { Plugin, Selection, Transaction, EditorState } from 'prosemirror-state'
9
9
  import { EditorView } from 'prosemirror-view'
10
10
  import { Node } from '@tiptap/pm/model'
11
- import fileToBase64 from '../../utils/file-to-base64'
11
+ import { fileToBase64 } from '../../../../index'
12
12
 
13
- export interface ImageOptions {
13
+ export interface ImageExtensionOptions {
14
14
  /**
15
15
  * Function to handle image uploads
16
16
  * @default null
@@ -51,6 +51,11 @@ declare module '@tiptap/core' {
51
51
  * Select an image file using the file picker and upload it
52
52
  */
53
53
  selectAndUploadImage: () => ReturnType
54
+
55
+ /**
56
+ * Set image alignment
57
+ */
58
+ setImageAlign: (align: 'left' | 'center' | 'right') => ReturnType
54
59
  }
55
60
  }
56
61
  }
@@ -58,10 +63,9 @@ declare module '@tiptap/core' {
58
63
  /**
59
64
  * Matches markdown image syntax: ![alt](src "title")
60
65
  */
61
- export const inputRegex =
62
- /(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/
66
+ const inputRegex = /(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/
63
67
 
64
- export default NodeExtension.create<ImageOptions>({
68
+ export const ImageExtension = NodeExtension.create<ImageExtensionOptions>({
65
69
  name: 'image',
66
70
 
67
71
  group: 'block',
@@ -79,6 +83,26 @@ export default NodeExtension.create<ImageOptions>({
79
83
  default: false,
80
84
  parseHTML: () => false,
81
85
  },
86
+ align: {
87
+ default: 'left',
88
+ parseHTML: (element) => {
89
+ const align = (
90
+ element.getAttribute('data-align') ||
91
+ element.getAttribute('align') ||
92
+ 'left'
93
+ ).toLowerCase()
94
+
95
+ if (['left', 'center', 'right'].includes(align)) {
96
+ return align as 'left' | 'center' | 'right'
97
+ }
98
+ return 'left'
99
+ },
100
+ renderHTML: (attributes) => {
101
+ return {
102
+ 'data-align': attributes.align || 'left',
103
+ }
104
+ },
105
+ },
82
106
  uploadId: {
83
107
  default: null,
84
108
  parseHTML: () => null,
@@ -129,6 +153,12 @@ export default NodeExtension.create<ImageOptions>({
129
153
 
130
154
  addCommands() {
131
155
  return {
156
+ setImageAlign:
157
+ (align: 'left' | 'center' | 'right') =>
158
+ ({ commands }) => {
159
+ return commands.updateAttributes(this.name, { align })
160
+ },
161
+
132
162
  setImage:
133
163
  (attributes: SetImageOptions) =>
134
164
  ({ commands, editor }) => {
@@ -139,12 +169,7 @@ export default NodeExtension.create<ImageOptions>({
139
169
 
140
170
  if (result && attributes.src) {
141
171
  findImageNodeBySource(editor.view, attributes.src, (node, pos) => {
142
- updateNodeWithDimensions(
143
- attributes.src,
144
- editor.view,
145
- pos,
146
- node.attrs,
147
- )
172
+ updateNodeWithDimensions(attributes.src, editor.view, pos)
148
173
  })
149
174
  }
150
175
 
@@ -292,12 +317,7 @@ export default NodeExtension.create<ImageOptions>({
292
317
  newImageNodes.forEach(({ node, pos }) => {
293
318
  const editor = extensionThis.editor
294
319
  if (editor) {
295
- updateNodeWithDimensions(
296
- node.attrs.src,
297
- editor.view,
298
- pos,
299
- node.attrs,
300
- )
320
+ updateNodeWithDimensions(node.attrs.src, editor.view, pos)
301
321
  }
302
322
  })
303
323
 
@@ -491,19 +511,26 @@ function updateNodeWithDimensions(
491
511
  src: string,
492
512
  view: EditorView,
493
513
  pos: number,
494
- attrs: any,
495
- ) {
514
+ ): void {
496
515
  getImageDimensions(src)
497
516
  .then((dimensions) => {
498
- const transaction = view.state.tr.setNodeMarkup(pos, undefined, {
499
- ...attrs,
500
- width: dimensions.width,
501
- height: dimensions.height,
502
- })
503
- view.dispatch(transaction)
517
+ const node = view.state.doc.nodeAt(pos)
518
+ if (!node || node.type.name !== 'image') {
519
+ return
520
+ }
521
+ const currentAttrs = node.attrs
522
+
523
+ if (currentAttrs.width == null || currentAttrs.height == null) {
524
+ const transaction = view.state.tr.setNodeMarkup(pos, undefined, {
525
+ ...currentAttrs,
526
+ width: currentAttrs.width ?? dimensions.width,
527
+ height: currentAttrs.height ?? dimensions.height,
528
+ })
529
+ view.dispatch(transaction)
530
+ }
504
531
  })
505
532
  .catch((error) => {
506
- console.error('Failed to get image dimensions:', error)
533
+ // Don't log error if it's just about dimensions for an existing node
507
534
  })
508
535
  }
509
536
 
@@ -0,0 +1,5 @@
1
+ export {
2
+ ImageExtension,
3
+ type ImageExtensionOptions,
4
+ type SetImageOptions,
5
+ } from './image-extension'
package/src/index.ts CHANGED
@@ -83,7 +83,7 @@ export { default as visibilityDirective } from './directives/visibility.js'
83
83
  // utilities
84
84
  export { default as call, createCall } from './utils/call.js'
85
85
  export { default as debounce } from './utils/debounce'
86
- export { default as fileToBase64 } from './utils/file-to-base64.js'
86
+ export { default as fileToBase64 } from './utils/file-to-base64'
87
87
  export { default as FileUploadHandler } from './utils/fileUploadHandler'
88
88
  export { usePageMeta } from './utils/pageMeta.js'
89
89
  export { dayjsLocal, dayjs } from './utils/dayjs.js'
@@ -328,6 +328,14 @@ module.exports = plugin(
328
328
  'h5 strong': {
329
329
  fontWeight: 600,
330
330
  },
331
+ 'img[data-align=right]': {
332
+ marginLeft: 'auto',
333
+ marginRight: '0',
334
+ },
335
+ 'img[data-align=center]': {
336
+ marginLeft: 'auto',
337
+ marginRight: 'auto',
338
+ },
331
339
  },
332
340
  },
333
341
  sm: {
@@ -0,0 +1,13 @@
1
+ export default (file: File): Promise<string> => {
2
+ return new Promise((resolve, reject) => {
3
+ const reader = new FileReader()
4
+ reader.onloadend = () => {
5
+ if (reader.result == null) {
6
+ reject(new Error('FileReader result is null'))
7
+ } else if (typeof reader.result === 'string') {
8
+ resolve(reader.result)
9
+ }
10
+ }
11
+ reader.readAsDataURL(file)
12
+ })
13
+ }
@@ -1,125 +0,0 @@
1
- <script setup lang="ts">
2
- import { NodeViewWrapper, nodeViewProps } from '@tiptap/vue-3'
3
- import LoadingIndicator from '../LoadingIndicator.vue'
4
- import { ref, onMounted } from 'vue'
5
- import ErrorMessage from '../ErrorMessage.vue'
6
-
7
- const props = defineProps(nodeViewProps)
8
-
9
- function selectImage() {
10
- props.editor.commands.setNodeSelection(props.getPos())
11
- }
12
-
13
- const caption = ref(props.node.attrs.alt || '')
14
- const isEditable = ref(false)
15
-
16
- onMounted(() => {
17
- isEditable.value = props.editor.isEditable
18
- })
19
-
20
- props.editor.on('update', () => {
21
- isEditable.value = props.editor.isEditable
22
- })
23
-
24
- function updateCaption(event: Event) {
25
- const newCaption = (event.target as HTMLInputElement).value
26
- caption.value = newCaption
27
- props.updateAttributes({ alt: newCaption })
28
- }
29
-
30
- function handleKeydown(event: KeyboardEvent) {
31
- if (event.key === 'Enter') {
32
- event.preventDefault()
33
- createParagraphAfterImage()
34
- } else if (event.key === 'Escape' || event.key === 'ArrowDown') {
35
- event.preventDefault()
36
- setCursorAfterImage()
37
- }
38
- if (event.key === 'ArrowUp') {
39
- event.preventDefault()
40
- setCursorBeforeImage()
41
- }
42
- }
43
-
44
- function setCursorAt(pos: number) {
45
- props.editor.commands.focus()
46
- props.editor.chain().setTextSelection(pos).scrollIntoView().run()
47
- }
48
-
49
- function createParagraphAfterImage() {
50
- const pos = props.getPos()
51
- props.editor.commands.focus()
52
- props.editor
53
- .chain()
54
- .setTextSelection(pos + 1)
55
- .createParagraphNear()
56
- .scrollIntoView()
57
- .run()
58
- }
59
-
60
- function setCursorAfterImage() {
61
- const pos = props.getPos()
62
- setCursorAt(pos + 1)
63
- }
64
-
65
- function setCursorBeforeImage() {
66
- const pos = props.getPos()
67
- setCursorAt(pos - 1)
68
- }
69
- </script>
70
-
71
- <template>
72
- <NodeViewWrapper>
73
- <div class="relative overflow-hidden not-prose my-6">
74
- <div class="relative">
75
- <img
76
- v-if="node.attrs.src"
77
- class="rounded-[2px]"
78
- :src="node.attrs.src"
79
- :alt="node.attrs.alt || ''"
80
- :width="node.attrs.width"
81
- :height="node.attrs.height"
82
- @click="selectImage"
83
- />
84
-
85
- <!-- Loading indicator overlay -->
86
- <div
87
- v-if="node.attrs.loading"
88
- class="inset-0 absolute flex items-center justify-center z-10"
89
- >
90
- <div
91
- class="bg-gray-900/80 p-2 inset-0 leading-none rounded-sm flex flex-col items-center justify-center gap-2"
92
- >
93
- <div class="flex items-center gap-2">
94
- <LoadingIndicator class="text-gray-100 size-4" />
95
- <span class="text-gray-100">Uploading...</span>
96
- </div>
97
- </div>
98
- </div>
99
-
100
- <!-- Selection overlay -->
101
- <div
102
- class="absolute pointer-events-none inset-0 rounded-[2px] bg-black/20 dark:bg-white/20 z-5 transition-opacity"
103
- :class="{
104
- 'opacity-100': selected,
105
- 'opacity-0': !selected,
106
- }"
107
- ></div>
108
- </div>
109
-
110
- <input
111
- v-if="(isEditable || node.attrs.alt) && !node.attrs.error"
112
- v-model="caption"
113
- class="w-full text-center bg-transparent text-sm text-ink-gray-6 h-7 border-none focus:ring-0 placeholder-ink-gray-4"
114
- placeholder="Add caption"
115
- :disabled="!isEditable"
116
- @change="updateCaption"
117
- @keydown="handleKeydown"
118
- />
119
-
120
- <div v-if="node.attrs.error" class="w-full py-1.5">
121
- <ErrorMessage :message="`Upload Failed: ${node.attrs.error}`" />
122
- </div>
123
- </div>
124
- </NodeViewWrapper>
125
- </template>
@@ -1,9 +0,0 @@
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
- }