66<template >
77 <NodeViewWrapper :contenteditable =" isEditable" >
88 <figure
9+ ref =" wrapper"
910 class =" image image-view"
1011 data-component =" image-view"
1112 :data-attachment-type =" attachmentType"
105106 @close =" showImageModal = false" />
106107 </div >
107108 </div >
109+ <div v-else-if =" canDisplayPlaceholder" class =" image__placeholder" >
110+ <NcBlurHash
111+ :hash =" imageBlurhash"
112+ :style =" blurhashSize"
113+ aria-hidden =" true" />
114+ </div >
108115 <div v-else class =" image-view__cant_display" >
109116 <transition name =" fade" >
110117 <div v-show =" loaded" class =" image__caption" >
128135<script >
129136import ClickOutside from ' vue-click-outside'
130137import NcButton from ' @nextcloud/vue/components/NcButton'
138+ import NcBlurHash from ' @nextcloud/vue/components/NcBlurHash'
131139import { showError } from ' @nextcloud/dialogs'
132140import ShowImageModal from ' ../components/ImageView/ShowImageModal.vue'
133141import { useAttachmentResolver } from ' ../components/Editor.provider.js'
@@ -149,6 +157,7 @@ export default {
149157 ImageIcon,
150158 DeleteIcon,
151159 NcButton,
160+ NcBlurHash,
152161 ShowImageModal,
153162 NodeViewWrapper,
154163 },
@@ -160,7 +169,13 @@ export default {
160169 data () {
161170 return {
162171 attachment: null ,
172+ attachmentPromise: null ,
163173 imageLoaded: false ,
174+ imageWidth: 0 ,
175+ imageHeight: 0 ,
176+ wrapperWidth: 0 ,
177+ resizeObserver: null ,
178+ imageBlurhash: null ,
164179 loaded: false ,
165180 failed: false ,
166181 showIcons: false ,
@@ -199,6 +214,25 @@ export default {
199214
200215 return this .loaded && this .imageLoaded
201216 },
217+ canDisplayPlaceholder () {
218+ return this .imageHeight > 0
219+ },
220+ blurhashSize () {
221+ if (this .imageWidth > 0 && this .imageHeight > 0 ) {
222+ const ratio = this .imageWidth / this .imageHeight
223+ const newWidth =
224+ this .wrapperWidth - 12 > this .imageWidth
225+ ? this .imageWidth
226+ : this .wrapperWidth - 12
227+ const newHeight = newWidth / ratio
228+
229+ return {
230+ width: ` ${ newWidth} px` ,
231+ height: ` ${ newHeight} px` ,
232+ }
233+ }
234+ return {}
235+ },
202236 src: {
203237 get () {
204238 return this .node .attrs .src || ' '
@@ -233,6 +267,10 @@ export default {
233267 })
234268 },
235269 mounted () {
270+ this .attachmentPromise = this .$attachmentResolver .resolve (this .src )
271+ this .loadAttachmentMetadata ()
272+ this .setupResizeObserver ()
273+
236274 this .$nextTick (() => {
237275 // nextTick is necessary, intersection detection is slightly unreliable without it
238276 const options = {
@@ -254,10 +292,38 @@ export default {
254292 },
255293 beforeUnmount () {
256294 this .loadIntersectionObserver ? .disconnect ()
295+ this .resizeObserver ? .disconnect ()
257296 },
258297 methods: {
298+ setupResizeObserver () {
299+ if (! this .$refs .wrapper ) return
300+
301+ this .resizeObserver = new ResizeObserver ((entries ) => {
302+ const width = entries[0 ].contentRect .width
303+ if (width > 0 ) {
304+ this .wrapperWidth = width
305+ }
306+ })
307+
308+ this .resizeObserver .observe (this .$refs .wrapper )
309+ },
310+ async loadAttachmentMetadata () {
311+ try {
312+ this .attachment = await this .attachmentPromise
313+
314+ const size = this .attachment ? .metadata ? .[' photos-size' ]? .value
315+ this .imageWidth = size .width
316+ this .imageHeight = size .height
317+
318+ this .imageBlurhash = this .attachment ? .metadata ? .blurhash ? .value
319+ } catch (err) {
320+ throw new Error (' Failed to load attachment metadata' )
321+ }
322+ },
259323 async loadPreview () {
260- this .attachment = await this .$attachmentResolver .resolve (this .src )
324+ if (! this .attachment ) {
325+ this .attachment = await this .attachmentPromise
326+ }
261327 if (! this .attachment .previewUrl ) {
262328 throw new Error (' Attachment source was not resolved' )
263329 }
@@ -268,6 +334,9 @@ export default {
268334 this .imageLoaded = true
269335 this .loaded = true
270336 this .attachmentSize = this .attachment .size
337+ // once the image is loaded, we can stop tracking the container width
338+ // since we only use it for sizing the placeholder
339+ this .resizeObserver ? .disconnect ()
271340 }
272341 img .onerror = (e ) => {
273342 reject (new LoadImageError (e, this .attachment .previewUrl ))
@@ -426,6 +495,12 @@ export default {
426495 height: 100px ;
427496}
428497
498+ .image__placeholder {
499+ padding: 7px 6px ;
500+ margin- bottom: 26px ;
501+ position: relative;
502+ }
503+
429504.image__main {
430505 max- height: calc (100vh - 50px - 50px );
431506}
0 commit comments