Skip to content

Commit

Permalink
♻️ (#2345) split menu bar in small components
Browse files Browse the repository at this point in the history
improves performance and responsivity
isolates responsability of image upload
  • Loading branch information
Vinicius Reis committed May 17, 2022
1 parent fab8ffd commit e44dccf
Show file tree
Hide file tree
Showing 16 changed files with 902 additions and 423 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"@tiptap/suggestion": "^2.0.0-beta.91",
"@tiptap/vue-2": "^2.0.0-beta.78",
"core-js": "^3.22.3",
"debounce": "^1.2.1",
"escape-html": "^1.0.3",
"highlight.js": "^10.7.2",
"lowlight": "^1.20.0",
Expand Down
21 changes: 21 additions & 0 deletions src/components/EditorDraggable.provider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const IS_UPLOADING_IMAGES = Symbol('editor:is-uploading-images')
export const ACTION_IMAGE_PROMPT = Symbol('editor:action:image-prompt')
export const ACTION_CHOOSE_LOCAL_IMAGE = Symbol('editor:action:upload-image')

export const useIsUploadingImagesMixin = {
inject: {
$isUploadingImages: { from: IS_UPLOADING_IMAGES, default: false },
},
}

export const useActionImagePromptMixin = {
inject: {
$callImagePrompt: { from: ACTION_IMAGE_PROMPT, default: () => {} },
},
}

export const useActionChooseLocalImageMixin = {
inject: {
$callChooseLocalImage: { from: ACTION_CHOOSE_LOCAL_IMAGE, default: () => {} },
},
}
193 changes: 193 additions & 0 deletions src/components/EditorDraggable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
<!--
- @copyright Copyright (c) 2022 Vinicius Reis <vinicius@nextcloud.com>
-
- @author Vinicius Reis <vinicius@nextcloud.com>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->

<template>
<div class="editor editor-draggable"
:class="{ draggedOver }"
@image-paste="onPaste"
@dragover.prevent.stop="setDraggedOver(true)"
@dragleave.prevent.stop="setDraggedOver(false)"
@image-drop="onEditorDrop">
<input ref="imageFileInput"
data-ref="image-file-input"
type="file"
accept="image/*"
aria-hidden="true"
class="hidden-visually"
multiple
@change="onImageUploadFilePicked">
<slot />
</div>
</template>

<script>
import { getCurrentUser } from '@nextcloud/auth'
import { showError } from '@nextcloud/dialogs'

import {
useEditorMixin,
useFileMixin,
useSyncServiceMixin,
} from './EditorWrapper.provider.js'

import {
ACTION_IMAGE_PROMPT,
ACTION_CHOOSE_LOCAL_IMAGE,
IS_UPLOADING_IMAGES,
} from './EditorDraggable.provider.js'

const IMAGE_MIMES = [
'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: 'EditorDraggable',
mixins: [useEditorMixin, useFileMixin, useSyncServiceMixin],
provide() {
const val = {}

Object.defineProperties(val, {
[ACTION_IMAGE_PROMPT]: {
get: () => this.showImagePrompt,
},
[ACTION_CHOOSE_LOCAL_IMAGE]: {
get: () => this.chooseLocalImage,
},
[IS_UPLOADING_IMAGES]: {
get: () => this.isUploadingImages,
},
})

return val
},
data: () => ({
draggedOver: false,
isUploadingImages: false,
}),
computed: {
imagePath() {
return this.$file.relativePath.split('/').slice(0, -1).join('/')
},
},
methods: {
setDraggedOver(val) {
this.draggedOver = val
},
onPaste(e) {
this.uploadImageFiles(e.detail.files)
},
onEditorDrop(e) {
this.uploadImageFiles(e.detail.files, e.detail.position)
this.draggedOver = false
},
onImageUploadFilePicked(event) {
this.uploadImageFiles(event.target.files)
// Clear input to ensure that the change event will be emitted if
// the same file is picked again.
event.target.value = ''
},
chooseLocalImage() {
this.$refs.imageFileInput.click()
},
async uploadImageFiles(files, position = null) {
if (!files) {
return
}

this.uploadingImages = true

const uploadPromises = [...files].map((file) => {
return this.uploadImageFile(file, position)
})

return Promise.all(uploadPromises)
.catch(err => {
console.error(err)
showError(err?.response?.data?.error || err.message)
})
.then(() => {
this.uploadingImages = false
})
},
async uploadImageFile(file, position = null) {
if (!IMAGE_MIMES.includes(file.type)) {
showError(t('text', 'Image file format not supported'))
return
}

return this.$syncService.uploadImage(file).then((response) => {
this.insertAttachmentImage(response.data?.name, response.data?.id, position)
}).catch((error) => {
console.error(error)
showError(error?.response?.data?.error)
})
},
showImagePrompt() {
const currentUser = getCurrentUser()
if (!currentUser) {
return
}

OC.dialogs.filepicker(t('text', 'Insert an image'), (filePath) => {
this.insertImagePath(filePath)
}, false, [], true, undefined, this.imagePath)
},
insertImagePath(imagePath) {
this.isUploadingImages = true

return this.$syncService.insertImageFile(imagePath).then((response) => {
this.insertAttachmentImage(response.data?.name, response.data?.id)
}).catch((error) => {
console.error(error)
showError(error?.response?.data?.error || error.message)
}).then(() => {
this.isUploadingImages = false
})
},
insertAttachmentImage(name, fileId, position = null) {
// inspired by the fixedEncodeURIComponent function suggested in
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
const src = 'text://image?imageFileName='
+ encodeURIComponent(name).replace(/[!'()*]/g, (c) => {
return '%' + c.charCodeAt(0).toString(16).toUpperCase()
})
// 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, '')

const chain = position
? this.$editor.chain()
: this.$editor.chain().focus(position)

chain.setImage({ src, alt }).focus().run()
},
},
}
</script>
80 changes: 80 additions & 0 deletions src/components/EditorWrapper.provider.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,93 @@
/*
* @copyright Copyright (c) 2022 Vinicius Reis <vinicius@nextcloud.com>
*
* @author Vinicius Reis <vinicius@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

export const EDITOR = Symbol('tiptap:editor')
export const SYNC_SERVICE = Symbol('sync:service')
export const DOCUMENT = Symbol('editor:document')
export const IS_PUBLIC = Symbol('editor:is-public')
export const FILE = Symbol('editor:file')
export const IS_RICH_EDITOR = Symbol('editor:is-rich-editor')
export const IS_MOBILE = Symbol('editor:is-mobile')
export const RELATIVE_PATH = Symbol('editor:relative-path')
export const IS_UPLOADING_IMAGES = Symbol('editor:is-uploading-images')
export const ACTION_IMAGE_PROMPT = Symbol('action:image-prompt')

export const useEditorMixin = {
inject: {
$editor: { from: EDITOR, default: null },
},
}

export const useSyncServiceMixin = {
inject: {
$syncService: { from: SYNC_SERVICE, default: null },
},
}

export const useIsPublic = {
inject: {
$isPublic: { from: IS_PUBLIC, default: false },
},
}

export const useIsRichEditorMixin = {
inject: {
$isRichEditor: { from: IS_RICH_EDITOR, default: false },
},
}

export const useIsMobileMixin = {
inject: {
$isMobile: { from: IS_MOBILE, default: false },
},
}

export const useDocumentMixin = {
inject: {
$document: { from: DOCUMENT, default: null },
},
}

export const useIsUploadingImagesMixin = {
inject: {
$isUploadingImages: { from: IS_UPLOADING_IMAGES, default: false },
},
}

export const useRelativePathMixin = {
inject: {
$relativePath: { from: RELATIVE_PATH, default: null },
},
}

export const useFileMixin = {
inject: {
$file: {
from: FILE,
default: () => ({
fileId: 0,
relativePath: null,
document: null,
}),
},
},
}
Loading

0 comments on commit e44dccf

Please sign in to comment.