Skip to content

Commit c3a1b17

Browse files
author
Julien Veyssier
committed
add image upload via drag'n'drop
remove image upload by link move 'image insertion from files' from MenuBar to EditorWrapper allow uploading multiple files Signed-off-by: Julien Veyssier <[email protected]>
1 parent 0d8aff0 commit c3a1b17

4 files changed

Lines changed: 84 additions & 118 deletions

File tree

cypress/integration/images.spec.js

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -139,23 +139,6 @@ describe('Test all image insertion methods', () => {
139139
})
140140
})
141141

142-
it('Insert an image from a link', () => {
143-
cy.openFile('test.md')
144-
clickOnImageAction(ACTION_INSERT_FROM_LINK, (popoverId) => {
145-
const requestAlias = 'insertLinkRequest'
146-
cy.intercept({ method: 'POST', url: '**/link' }).as(requestAlias)
147-
148-
cy.log('Type and validate')
149-
cy.get('div#' + popoverId + ' li:nth-child(3) input[type=text]')
150-
.type('https://nextcloud.com/wp-content/themes/next/assets/img/headers/engineering-small.jpg', { waitForAnimations: true })
151-
.type('{enter}', { waitForAnimations: true })
152-
// Clicking on the validation button is an alternative to typing {enter}
153-
// cy.get('div#' + popoverId + ' li:nth-child(3) form > label').click()
154-
155-
waitForRequestAndCheckImage(requestAlias)
156-
})
157-
})
158-
159142
it('Upload a local image', () => {
160143
cy.openFile('test.md')
161144
// in this case we almost could just attach the file to the input
@@ -174,7 +157,7 @@ describe('Test all image insertion methods', () => {
174157

175158
it('test if image files are in the attachment folder', () => {
176159
// check we stored the image names/ids
177-
cy.expect(Object.keys(attachmentFileNameToId)).to.have.lengthOf(3)
160+
cy.expect(Object.keys(attachmentFileNameToId)).to.have.lengthOf(2)
178161

179162
cy.get(`#fileList tr[data-file="test.md"]`, { timeout: 10000 })
180163
.should('have.attr', 'data-id')

src/components/EditorWrapper.vue

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@
3434
</p>
3535
</div>
3636
<div v-if="displayed" id="editor-wrapper" :class="{'has-conflicts': hasSyncCollission, 'icon-loading': !contentLoaded && !hasConnectionIssue, 'richEditor': isRichEditor, 'show-color-annotations': showAuthorAnnotations}">
37-
<div v-if="tiptap" id="editor">
37+
<div v-if="tiptap"
38+
id="editor"
39+
:class="{ draggedOver }"
40+
@dragover.prevent.stop="draggedOver = true"
41+
@dragleave.prevent.stop="draggedOver = false"
42+
@drop.prevent.stop="onEditorDrop">
3843
<MenuBar v-if="renderMenus"
3944
ref="menubar"
4045
:editor="tiptap"
@@ -45,7 +50,10 @@
4550
:is-public="isPublic"
4651
:autohide="autohide"
4752
:loaded.sync="menubarLoaded"
48-
@show-help="showHelp">
53+
:uploading-image="nbUploadingImages > 0"
54+
@show-help="showHelp"
55+
@image-insert="insertImagePath"
56+
@image-upload="uploadImageFiles">
4957
<div id="editor-session-list">
5058
<div v-tooltip="lastSavedStatusTooltip" class="save-status" :class="lastSavedStatusClass">
5159
{{ lastSavedStatus }}
@@ -81,6 +89,7 @@
8189
import Vue from 'vue'
8290
import escapeHtml from 'escape-html'
8391
import moment from '@nextcloud/moment'
92+
import { showError } from '@nextcloud/dialogs'
8493
8594
import { SyncService, ERROR_TYPE, IDLE_TIMEOUT } from './../services/SyncService'
8695
import { endpointUrl, getRandomGuestName } from './../helpers'
@@ -99,6 +108,18 @@ import { Step } from 'prosemirror-transform'
99108
100109
const EDITOR_PUSH_DEBOUNCE = 200
101110
111+
const imageMimes = [
112+
'image/png',
113+
'image/jpeg',
114+
'image/jpg',
115+
'image/gif',
116+
'image/x-xbitmap',
117+
'image/x-ms-bmp',
118+
'image/bmp',
119+
'image/svg+xml',
120+
'image/webp',
121+
]
122+
102123
export default {
103124
name: 'EditorWrapper',
104125
components: {
@@ -179,6 +200,8 @@ export default {
179200
readOnly: true,
180201
forceRecreate: false,
181202
menubarLoaded: false,
203+
nbUploadingImages: 0,
204+
draggedOver: false,
182205
183206
saveStatusPolling: null,
184207
displayHelp: false,
@@ -543,6 +566,51 @@ export default {
543566
hideHelp() {
544567
this.displayHelp = false
545568
},
569+
onEditorDrop(e) {
570+
this.uploadImageFiles(e.dataTransfer.files)
571+
this.draggedOver = false
572+
},
573+
uploadImageFiles(files) {
574+
if (files) {
575+
files.forEach((file) => {
576+
this.uploadImageFile(file)
577+
})
578+
}
579+
},
580+
uploadImageFile(file) {
581+
if (!imageMimes.includes(file.type)) {
582+
showError(t('text', 'Image file format not supported'))
583+
return
584+
}
585+
586+
this.nbUploadingImages++
587+
this.syncService.uploadImage(file).then((response) => {
588+
this.insertAttachmentImage(response.data?.name, response.data?.id)
589+
}).catch((error) => {
590+
console.error(error)
591+
showError(error?.response?.data?.error)
592+
}).then(() => {
593+
this.nbUploadingImages--
594+
})
595+
},
596+
insertImagePath(imagePath) {
597+
this.nbUploadingImages++
598+
this.syncService.insertImageFile(imagePath).then((response) => {
599+
this.insertAttachmentImage(response.data?.name, response.data?.id)
600+
}).catch((error) => {
601+
console.error(error)
602+
showError(error?.response?.data?.error)
603+
}).then(() => {
604+
this.nbUploadingImages--
605+
})
606+
},
607+
insertAttachmentImage(name, fileId) {
608+
const src = 'text://image?imageFileName=' + encodeURIComponent(name)
609+
// simply get rid of brackets to make sure link text is valid
610+
// as it does not need to be unique and matching the real file name
611+
const alt = name.replaceAll(/[[\]]/g, '')
612+
this.tiptap.chain().setImage({ src, alt }).focus().run()
613+
},
546614
},
547615
}
548616
</script>
@@ -778,6 +846,9 @@ export default {
778846
779847
// relative position for the alignment of the menububble
780848
#editor {
849+
&.draggedOver {
850+
background-color: var(--color-primary-light);
851+
}
781852
.content-wrapper {
782853
position: relative;
783854
}

src/components/MenuBar.vue

Lines changed: 9 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
accept="image/*"
2828
aria-hidden="true"
2929
class="hidden-visually"
30+
:multiple="true"
3031
@change="onImageUploadFilePicked">
3132
<div v-if="isRichEditor" ref="menubar" class="menubar-icons">
3233
<template v-for="(icon, $index) in allIcons">
@@ -46,7 +47,7 @@
4647
class="submenu"
4748
:default-icon="'icon-image'"
4849
@open="toggleChildMenu(icon)"
49-
@close="onImageActionClose; toggleChildMenu(icon)">
50+
@close="toggleChildMenu(icon)">
5051
<button slot="icon"
5152
:class="{ 'icon-image': true, 'loading-small': uploadingImage }"
5253
:title="icon.label"
@@ -65,20 +66,6 @@
6566
@click="showImagePrompt()">
6667
{{ t('text', 'Insert from Files') }}
6768
</ActionButton>
68-
<ActionButton v-if="!showImageLinkPrompt"
69-
icon="icon-link"
70-
:close-after-click="false"
71-
:disabled="uploadingImage"
72-
@click="showImageLinkPrompt = true">
73-
{{ t('text', 'Insert from link') }}
74-
</ActionButton>
75-
<ActionInput v-else
76-
icon="icon-link"
77-
:value="imageLink"
78-
@update:value="onImageLinkUpdateValue"
79-
@submit="onImageLinkSubmit()">
80-
{{ t('text', 'Image link to insert') }}
81-
</ActionInput>
8269
</Actions>
8370
<button v-else-if="icon.class"
8471
v-show="$index < iconCount"
@@ -137,30 +124,15 @@ import isMobile from './../mixins/isMobile'
137124
138125
import Actions from '@nextcloud/vue/dist/Components/Actions'
139126
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
140-
import ActionInput from '@nextcloud/vue/dist/Components/ActionInput'
141127
import PopoverMenu from '@nextcloud/vue/dist/Components/PopoverMenu'
142128
import EmojiPicker from '@nextcloud/vue/dist/Components/EmojiPicker'
143129
import ClickOutside from 'vue-click-outside'
144130
import { getCurrentUser } from '@nextcloud/auth'
145-
import { showError } from '@nextcloud/dialogs'
146-
147-
const imageMimes = [
148-
'image/png',
149-
'image/jpeg',
150-
'image/jpg',
151-
'image/gif',
152-
'image/x-xbitmap',
153-
'image/x-ms-bmp',
154-
'image/bmp',
155-
'image/svg+xml',
156-
'image/webp',
157-
]
158131
159132
export default {
160133
name: 'MenuBar',
161134
components: {
162135
ActionButton,
163-
ActionInput,
164136
PopoverMenu,
165137
Actions,
166138
EmojiPicker,
@@ -204,6 +176,10 @@ export default {
204176
required: false,
205177
default: 0,
206178
},
179+
uploadingImage: {
180+
type: Boolean,
181+
default: false,
182+
},
207183
},
208184
data: () => {
209185
return {
@@ -212,9 +188,6 @@ export default {
212188
forceRecompute: 0,
213189
submenuVisibility: {},
214190
lastImagePath: null,
215-
showImageLinkPrompt: false,
216-
uploadingImage: false,
217-
imageLink: '',
218191
icons: [...menuBarIcons],
219192
}
220193
},
@@ -353,86 +326,24 @@ export default {
353326
this.refocus()
354327
}
355328
},
356-
onImageActionClose() {
357-
this.showImageLinkPrompt = false
358-
},
359329
onUploadImage() {
360330
this.$refs.imageFileInput.click()
361331
},
362332
onImageUploadFilePicked(event) {
363-
this.uploadingImage = true
364-
const files = event.target.files
365-
const image = files[0]
366-
if (!imageMimes.includes(image.type)) {
367-
showError(t('text', 'Image format not supported'))
368-
this.uploadingImage = false
369-
return
370-
}
371-
333+
this.$emit('image-upload', event.target.files)
372334
// Clear input to ensure that the change event will be emitted if
373335
// the same file is picked again.
374336
event.target.value = ''
375-
376-
this.syncService.uploadImage(image).then((response) => {
377-
this.insertAttachmentImage(response.data?.name, response.data?.id)
378-
}).catch((error) => {
379-
console.error(error)
380-
showError(error?.response?.data?.error)
381-
}).then(() => {
382-
this.uploadingImage = false
383-
})
384-
},
385-
onImageLinkUpdateValue(newImageLink) {
386-
// this avoids the input being reset on each file polling
387-
this.imageLink = newImageLink
388-
},
389-
onImageLinkSubmit() {
390-
if (!this.imageLink) {
391-
return
392-
}
393-
this.uploadingImage = true
394-
this.showImageLinkPrompt = false
395-
this.$refs.imageActions[0].closeMenu()
396-
397-
this.syncService.insertImageLink(this.imageLink).then((response) => {
398-
this.insertAttachmentImage(response.data?.name, response.data?.id)
399-
}).catch((error) => {
400-
console.error(error)
401-
showError(error?.response?.data?.error)
402-
}).then(() => {
403-
this.uploadingImage = false
404-
this.imageLink = ''
405-
})
406-
},
407-
onImagePathSubmit(imagePath) {
408-
this.uploadingImage = true
409-
this.$refs.imageActions[0].closeMenu()
410-
411-
this.syncService.insertImageFile(imagePath).then((response) => {
412-
this.insertAttachmentImage(response.data?.name, response.data?.id)
413-
}).catch((error) => {
414-
console.error(error)
415-
showError(error?.response?.data?.error)
416-
}).then(() => {
417-
this.uploadingImage = false
418-
})
419337
},
420338
showImagePrompt() {
421339
const currentUser = getCurrentUser()
422340
if (!currentUser) {
423341
return
424342
}
425-
OC.dialogs.filepicker(t('text', 'Insert an image'), (file) => {
426-
this.onImagePathSubmit(file)
343+
OC.dialogs.filepicker(t('text', 'Insert an image'), (filePath) => {
344+
this.$emit('image-insert', filePath)
427345
}, false, [], true, undefined, this.imagePath)
428346
},
429-
insertAttachmentImage(name, fileId) {
430-
const src = 'text://image?imageFileName=' + encodeURIComponent(name)
431-
// simply get rid of brackets to make sure link text is valid
432-
// as it does not need to be unique and matching the real file name
433-
const alt = name.replaceAll(/[[\]]/g, '')
434-
this.editor.chain().setImage({ src, alt }).focus().run()
435-
},
436347
optimalPathTo(targetFile) {
437348
const absolutePath = targetFile.split('/')
438349
const relativePath = this.relativePathTo(targetFile).split('/')

src/nodes/ImageView.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ export default {
318318
max-width: 80%;
319319
border: none;
320320
text-align: center;
321+
background-color: transparent;
321322
}
322323
}
323324

0 commit comments

Comments
 (0)