Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/text.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJulien Veyssier <eneiluj@posteo.net>2022-03-30 11:28:52 +0300
committerJulien Veyssier <eneiluj@posteo.net>2022-03-30 14:20:35 +0300
commit63e64d2c36378022911100c6dd7e93846d4dab43 (patch)
treeece37c619780c90bd1a202acd0fcce8f23072b88 /src
parent3ac82dd268a0bf21543b482d747bb5f27e7343f9 (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.vue75
-rw-r--r--src/components/MenuBar.vue107
-rw-r--r--src/nodes/ImageView.vue1
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;
}
}