Π·Π΅ΡΠΊΠ°Π»ΠΎ ΠΈΠ· https://github.com/nextcloud/text.git
π©Ή (#2463): show a falback image when image can't be loaded
Signed-off-by: Vinicius Reis <vinicius.reis@nextcloud.com>
This commit is contained in:
Π ΠΎΠ΄ΠΈΡΠ΅Π»Ρ
1db2493238
ΠΠΎΠΌΠΌΠΈΡ
f7451b5050
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
ΠΠ°Π³ΡΡΠ·ΠΊΠ°β¦
Π‘ΡΡΠ»ΠΊΠ° Π² Π½ΠΎΠ²ΠΎΠΉ Π·Π°Π΄Π°ΡΠ΅