diff options
author | Julien Veyssier <eneiluj@posteo.net> | 2022-03-30 11:28:52 +0300 |
---|---|---|
committer | Julien Veyssier <eneiluj@posteo.net> | 2022-03-30 14:20:35 +0300 |
commit | 63e64d2c36378022911100c6dd7e93846d4dab43 (patch) | |
tree | ece37c619780c90bd1a202acd0fcce8f23072b88 /src | |
parent | 3ac82dd268a0bf21543b482d747bb5f27e7343f9 (diff) |
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 <eneiluj@posteo.net>
Diffstat (limited to 'src')
-rw-r--r-- | src/components/EditorWrapper.vue | 75 | ||||
-rw-r--r-- | src/components/MenuBar.vue | 107 | ||||
-rw-r--r-- | src/nodes/ImageView.vue | 1 |
3 files changed, 83 insertions, 100 deletions
diff --git a/src/components/EditorWrapper.vue b/src/components/EditorWrapper.vue index e72f769d8..e589b2219 100644 --- a/src/components/EditorWrapper.vue +++ b/src/components/EditorWrapper.vue @@ -34,7 +34,12 @@ </p> </div> <div v-if="displayed" id="editor-wrapper" :class="{'has-conflicts': hasSyncCollission, 'icon-loading': !contentLoaded && !hasConnectionIssue, 'richEditor': isRichEditor, 'show-color-annotations': showAuthorAnnotations}"> - <div v-if="tiptap" id="editor"> + <div v-if="tiptap" + id="editor" + :class="{ draggedOver }" + @dragover.prevent.stop="draggedOver = true" + @dragleave.prevent.stop="draggedOver = false" + @drop.prevent.stop="onEditorDrop"> <MenuBar v-if="renderMenus" ref="menubar" :editor="tiptap" @@ -45,7 +50,10 @@ :is-public="isPublic" :autohide="autohide" :loaded.sync="menubarLoaded" - @show-help="showHelp"> + :uploading-image="nbUploadingImages > 0" + @show-help="showHelp" + @image-insert="insertImagePath" + @image-upload="uploadImageFiles"> <div id="editor-session-list"> <div v-tooltip="lastSavedStatusTooltip" class="save-status" :class="lastSavedStatusClass"> {{ lastSavedStatus }} @@ -81,6 +89,7 @@ import Vue from 'vue' import escapeHtml from 'escape-html' import moment from '@nextcloud/moment' +import { showError } from '@nextcloud/dialogs' import { SyncService, ERROR_TYPE, IDLE_TIMEOUT } from './../services/SyncService' import { endpointUrl, getRandomGuestName } from './../helpers' @@ -99,6 +108,18 @@ import { Step } from 'prosemirror-transform' const EDITOR_PUSH_DEBOUNCE = 200 +const imageMimes = [ + 'image/png', + 'image/jpeg', + 'image/jpg', + 'image/gif', + 'image/x-xbitmap', + 'image/x-ms-bmp', + 'image/bmp', + 'image/svg+xml', + 'image/webp', +] + export default { name: 'EditorWrapper', components: { @@ -179,6 +200,8 @@ export default { readOnly: true, forceRecreate: false, menubarLoaded: false, + nbUploadingImages: 0, + draggedOver: false, saveStatusPolling: null, displayHelp: false, @@ -543,6 +566,51 @@ export default { hideHelp() { this.displayHelp = false }, + onEditorDrop(e) { + this.uploadImageFiles(e.dataTransfer.files) + this.draggedOver = false + }, + uploadImageFiles(files) { + if (files) { + files.forEach((file) => { + this.uploadImageFile(file) + }) + } + }, + uploadImageFile(file) { + if (!imageMimes.includes(file.type)) { + showError(t('text', 'Image file format not supported')) + return + } + + this.nbUploadingImages++ + this.syncService.uploadImage(file).then((response) => { + this.insertAttachmentImage(response.data?.name, response.data?.id) + }).catch((error) => { + console.error(error) + showError(error?.response?.data?.error) + }).then(() => { + this.nbUploadingImages-- + }) + }, + insertImagePath(imagePath) { + this.nbUploadingImages++ + this.syncService.insertImageFile(imagePath).then((response) => { + this.insertAttachmentImage(response.data?.name, response.data?.id) + }).catch((error) => { + console.error(error) + showError(error?.response?.data?.error) + }).then(() => { + this.nbUploadingImages-- + }) + }, + insertAttachmentImage(name, fileId) { + const src = 'text://image?imageFileName=' + encodeURIComponent(name) + // simply get rid of brackets to make sure link text is valid + // as it does not need to be unique and matching the real file name + const alt = name.replaceAll(/[[\]]/g, '') + this.tiptap.chain().setImage({ src, alt }).focus().run() + }, }, } </script> @@ -778,6 +846,9 @@ export default { // relative position for the alignment of the menububble #editor { + &.draggedOver { + background-color: var(--color-primary-light); + } .content-wrapper { position: relative; } diff --git a/src/components/MenuBar.vue b/src/components/MenuBar.vue index 7c27439da..60af3cc97 100644 --- a/src/components/MenuBar.vue +++ b/src/components/MenuBar.vue @@ -27,6 +27,7 @@ accept="image/*" aria-hidden="true" class="hidden-visually" + :multiple="true" @change="onImageUploadFilePicked"> <div v-if="isRichEditor" ref="menubar" class="menubar-icons"> <template v-for="(icon, $index) in allIcons"> @@ -46,7 +47,7 @@ class="submenu" :default-icon="'icon-image'" @open="toggleChildMenu(icon)" - @close="onImageActionClose; toggleChildMenu(icon)"> + @close="toggleChildMenu(icon)"> <button slot="icon" :class="{ 'icon-image': true, 'loading-small': uploadingImage }" :title="icon.label" @@ -65,20 +66,6 @@ @click="showImagePrompt()"> {{ t('text', 'Insert from Files') }} </ActionButton> - <ActionButton v-if="!showImageLinkPrompt" - icon="icon-link" - :close-after-click="false" - :disabled="uploadingImage" - @click="showImageLinkPrompt = true"> - {{ t('text', 'Insert from link') }} - </ActionButton> - <ActionInput v-else - icon="icon-link" - :value="imageLink" - @update:value="onImageLinkUpdateValue" - @submit="onImageLinkSubmit()"> - {{ t('text', 'Image link to insert') }} - </ActionInput> </Actions> <button v-else-if="icon.class" v-show="$index < iconCount" @@ -137,30 +124,15 @@ import isMobile from './../mixins/isMobile' import Actions from '@nextcloud/vue/dist/Components/Actions' import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' -import ActionInput from '@nextcloud/vue/dist/Components/ActionInput' import PopoverMenu from '@nextcloud/vue/dist/Components/PopoverMenu' import EmojiPicker from '@nextcloud/vue/dist/Components/EmojiPicker' import ClickOutside from 'vue-click-outside' import { getCurrentUser } from '@nextcloud/auth' -import { showError } from '@nextcloud/dialogs' - -const imageMimes = [ - 'image/png', - 'image/jpeg', - 'image/jpg', - 'image/gif', - 'image/x-xbitmap', - 'image/x-ms-bmp', - 'image/bmp', - 'image/svg+xml', - 'image/webp', -] export default { name: 'MenuBar', components: { ActionButton, - ActionInput, PopoverMenu, Actions, EmojiPicker, @@ -204,6 +176,10 @@ export default { required: false, default: 0, }, + uploadingImage: { + type: Boolean, + default: false, + }, }, data: () => { return { @@ -212,9 +188,6 @@ export default { forceRecompute: 0, submenuVisibility: {}, lastImagePath: null, - showImageLinkPrompt: false, - uploadingImage: false, - imageLink: '', icons: [...menuBarIcons], } }, @@ -353,86 +326,24 @@ export default { this.refocus() } }, - onImageActionClose() { - this.showImageLinkPrompt = false - }, onUploadImage() { this.$refs.imageFileInput.click() }, onImageUploadFilePicked(event) { - this.uploadingImage = true - const files = event.target.files - const image = files[0] - if (!imageMimes.includes(image.type)) { - showError(t('text', 'Image format not supported')) - this.uploadingImage = false - return - } - + this.$emit('image-upload', event.target.files) // Clear input to ensure that the change event will be emitted if // the same file is picked again. event.target.value = '' - - this.syncService.uploadImage(image).then((response) => { - this.insertAttachmentImage(response.data?.name, response.data?.id) - }).catch((error) => { - console.error(error) - showError(error?.response?.data?.error) - }).then(() => { - this.uploadingImage = false - }) - }, - onImageLinkUpdateValue(newImageLink) { - // this avoids the input being reset on each file polling - this.imageLink = newImageLink - }, - onImageLinkSubmit() { - if (!this.imageLink) { - return - } - this.uploadingImage = true - this.showImageLinkPrompt = false - this.$refs.imageActions[0].closeMenu() - - this.syncService.insertImageLink(this.imageLink).then((response) => { - this.insertAttachmentImage(response.data?.name, response.data?.id) - }).catch((error) => { - console.error(error) - showError(error?.response?.data?.error) - }).then(() => { - this.uploadingImage = false - this.imageLink = '' - }) - }, - onImagePathSubmit(imagePath) { - this.uploadingImage = true - this.$refs.imageActions[0].closeMenu() - - this.syncService.insertImageFile(imagePath).then((response) => { - this.insertAttachmentImage(response.data?.name, response.data?.id) - }).catch((error) => { - console.error(error) - showError(error?.response?.data?.error) - }).then(() => { - this.uploadingImage = false - }) }, showImagePrompt() { const currentUser = getCurrentUser() if (!currentUser) { return } - OC.dialogs.filepicker(t('text', 'Insert an image'), (file) => { - this.onImagePathSubmit(file) + OC.dialogs.filepicker(t('text', 'Insert an image'), (filePath) => { + this.$emit('image-insert', filePath) }, false, [], true, undefined, this.imagePath) }, - insertAttachmentImage(name, fileId) { - const src = 'text://image?imageFileName=' + encodeURIComponent(name) - // simply get rid of brackets to make sure link text is valid - // as it does not need to be unique and matching the real file name - const alt = name.replaceAll(/[[\]]/g, '') - this.editor.chain().setImage({ src, alt }).focus().run() - }, optimalPathTo(targetFile) { const absolutePath = targetFile.split('/') const relativePath = this.relativePathTo(targetFile).split('/') diff --git a/src/nodes/ImageView.vue b/src/nodes/ImageView.vue index b97ee1613..751ce2f84 100644 --- a/src/nodes/ImageView.vue +++ b/src/nodes/ImageView.vue @@ -318,6 +318,7 @@ export default { max-width: 80%; border: none; text-align: center; + background-color: transparent; } } |