diff options
author | Vinicius Reis <vinicius.reis@nextcloud.com> | 2022-06-01 19:40:21 +0300 |
---|---|---|
committer | Julius Härtl <jus@bitgrid.net> | 2022-06-09 11:35:37 +0300 |
commit | f7451b505062a2ebf750ac421f6fa159f2ec8a6a (patch) | |
tree | d43b4afd1548bf334ac1326474284f4f86d9e013 /src | |
parent | 1db24932381f62971edbe4ea88f84d82e0b9aedd (diff) |
🩹 (#2463): show a falback image when image can't be loaded
Signed-off-by: Vinicius Reis <vinicius.reis@nextcloud.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/components/icons.js | 6 | ||||
-rw-r--r-- | src/nodes/ImageView.vue | 92 |
2 files changed, 75 insertions, 23 deletions
diff --git a/src/components/icons.js b/src/components/icons.js index feec62da1..926a4d477 100644 --- a/src/components/icons.js +++ b/src/components/icons.js @@ -21,7 +21,6 @@ * */ -import MDI_Loading from 'vue-material-design-icons/Loading.vue' import MDI_Check from 'vue-material-design-icons/Check.vue' import MDI_CodeTags from 'vue-material-design-icons/CodeTags.vue' import MDI_Danger from 'vue-material-design-icons/AlertDecagram.vue' @@ -45,9 +44,11 @@ import MDI_FormatQuote from 'vue-material-design-icons/FormatQuoteClose.vue' import MDI_FormatStrikethrough from 'vue-material-design-icons/FormatStrikethrough.vue' import MDI_FormatUnderline from 'vue-material-design-icons/FormatUnderline.vue' import MDI_Help from 'vue-material-design-icons/HelpCircle.vue' +import MDI_Image from 'vue-material-design-icons/ImageOutline.vue' import MDI_Images from 'vue-material-design-icons/ImageMultipleOutline.vue' import MDI_Info from 'vue-material-design-icons/Information.vue' import MDI_Link from 'vue-material-design-icons/Link.vue' +import MDI_Loading from 'vue-material-design-icons/Loading.vue' import MDI_Lock from 'vue-material-design-icons/Lock.vue' import MDI_Positive from 'vue-material-design-icons/CheckboxMarkedCircle.vue' import MDI_Redo from 'vue-material-design-icons/ArrowURightTop.vue' @@ -57,6 +58,7 @@ import MDI_TableAddColumnBefore from 'vue-material-design-icons/TableColumnPlusB import MDI_TableAddRowAfter from 'vue-material-design-icons/TableRowPlusAfter.vue' import MDI_TableAddRowBefore from 'vue-material-design-icons/TableRowPlusBefore.vue' import MDI_TableSettings from 'vue-material-design-icons/TableCog.vue' +import MDI_TrashCan from 'vue-material-design-icons/TrashCan.vue' import MDI_Undo from 'vue-material-design-icons/ArrowULeftTop.vue' import MDI_Upload from 'vue-material-design-icons/Upload.vue' import MDI_Warn from 'vue-material-design-icons/Alert.vue' @@ -109,6 +111,7 @@ export const FormatQuote = makeIcon(MDI_FormatQuote) export const FormatStrikethrough = makeIcon(MDI_FormatStrikethrough) export const FormatUnderline = makeIcon(MDI_FormatUnderline) export const Help = makeIcon(MDI_Help) +export const Image = makeIcon(MDI_Image) export const Images = makeIcon(MDI_Images) export const Info = makeIcon(MDI_Info) export const Link = makeIcon(MDI_Link) @@ -121,6 +124,7 @@ export const TableAddColumnBefore = makeIcon(MDI_TableAddColumnBefore) export const TableAddRowAfter = makeIcon(MDI_TableAddRowAfter) export const TableAddRowBefore = makeIcon(MDI_TableAddRowBefore) export const TableSettings = makeIcon(MDI_TableSettings) +export const TrashCan = makeIcon(MDI_TrashCan) export const Undo = makeIcon(MDI_Undo) export const Upload = makeIcon(MDI_Upload) export const Warn = makeIcon(MDI_Warn) diff --git a/src/nodes/ImageView.vue b/src/nodes/ImageView.vue index 8466d7760..5d65f0106 100644 --- a/src/nodes/ImageView.vue +++ b/src/nodes/ImageView.vue @@ -22,38 +22,47 @@ <template> <NodeViewWrapper> - <div class="image" + <div class="image image-view" data-component="image-view" - :class="{'icon-loading': !loaded}" + :class="{'icon-loading': !loaded, 'image-view--failed': failed}" :data-src="src"> - <div v-if="imageLoaded && isSupportedImage" + <small v-if="errorMessage" class="image__error-message"> + {{ errorMessage }} + </small> + <div v-if="canDisplayImage" v-click-outside="() => showIcons = false" class="image__view" @click="showIcons = true" @mouseover="showIcons = true" @mouseleave="showIcons = false"> <transition name="fade"> - <img v-show="loaded" - :src="imageUrl" - class="image__main" - @load="onLoaded"> + <template v-if="!failed"> + <img v-show="loaded" + :src="imageUrl" + class="image__main" + @load="onLoaded"> + </template> + <template v-else> + <ImageBroken class="image__main image__main--broken-icon" :size="100" /> + </template> </transition> <transition name="fade"> <div v-show="loaded" class="image__caption"> <input ref="altInput" type="text" + class="image__caption__input" :value="alt" @keyup.enter="updateAlt()"> <div v-if="editor.isEditable && showIcons" - class="trash-icon" + class="image__caption__delete" title="Delete this image" @click="deleteNode"> - <TrashCanIcon /> + <TrashCan /> </div> </div> </transition> </div> - <div v-else> + <div v-else class="image-view__cant_display"> <transition name="fade"> <div v-show="loaded"> <a :href="internalLinkOrImage" target="_blank"> @@ -78,7 +87,8 @@ import { generateUrl } from '@nextcloud/router' import { NodeViewWrapper } from '@tiptap/vue-2' import ClickOutside from 'vue-click-outside' -import TrashCanIcon from 'vue-material-design-icons/TrashCan.vue' +// import TrashCanIcon from 'vue-material-design-icons/TrashCan.vue' +import { ImageBroken, TrashCan } from '../components/icons.js' import store from './../mixins/store.js' import { useImageResolver } from './../components/EditorWrapper.provider.js' @@ -111,10 +121,21 @@ const getQueryVariable = (src, variable) => { } } +class ErrorLoadImage extends Error { + + constructor(reason, imageUrl) { + super(reason?.message || t('text', 'Fail to load image')) + this.reason = reason + this.imageUrl = imageUrl + } + +} + export default { name: 'ImageView', components: { - TrashCanIcon, + ImageBroken, + TrashCan, NodeViewWrapper, }, directives: { @@ -132,9 +153,21 @@ export default { failed: false, showIcons: false, imageUrl: null, + errorMessage: null, } }, computed: { + canDisplayImage() { + if (!this.isSupportedImage) { + return false + } + + if (this.failed && this.loaded) { + return true + } + + return this.loaded && this.imageLoaded + }, imageFileId() { return getQueryVariable(this.src, 'fileId') }, @@ -183,20 +216,22 @@ export default { this.failed = true this.imageLoaded = false this.loaded = true + this.errorMessage = t('text', 'Unsuported image') return } - this.init().catch((e) => { - this.onImageLoadFailure() - }) + this.init() + .catch(this.onImageLoadFailure) }, methods: { async init() { const [url, fallback] = this.$imageResolver.resolve(this.src) - this.loadImage(url).catch((e) => { + return this.loadImage(url).catch((e) => { if (fallback) { - this.loadImage(fallback) + return this.loadImage(fallback) // TODO if fallback works, rewrite the url with correct document ID } + + return Promise.reject(e) }) }, @@ -206,18 +241,20 @@ export default { img.onload = () => { this.imageUrl = imageUrl this.imageLoaded = true - resolve() + this.loaded = true + resolve(imageUrl) } img.onerror = (e) => { - reject(e) + reject(new ErrorLoadImage(e, imageUrl)) } img.src = imageUrl }) }, - onImageLoadFailure() { + onImageLoadFailure(err) { this.failed = true this.imageLoaded = false this.loaded = true + this.errorMessage = err.message }, updateAlt() { this.alt = this.$refs.altInput.value @@ -256,6 +293,10 @@ export default { height: 100px; } + .image__main--broken-icon, .image__error-message { + color: var(--color-error); + } + .image__view { text-align: center; position: relative; @@ -269,6 +310,11 @@ export default { max-height: calc(100vh - 50px - 50px); } + .image__error-message { + display: block; + text-align: center; + } + .fade-enter-active { transition: opacity .3s ease-in-out; } @@ -281,13 +327,15 @@ export default { opacity: 0; } - .trash-icon { + .image__caption__delete { position: absolute; right: 0; display: flex; justify-content: flex-end; align-items: center; - svg { + width: 20px; + height: 20px; + &, svg { cursor: pointer; } } |