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'
140+ import { logger } from ' ../helpers/logger.js'
132141import ShowImageModal from ' ../components/ImageView/ShowImageModal.vue'
133142import { useAttachmentResolver } from ' ../components/Editor.provider.js'
134143import { emit } from ' @nextcloud/event-bus'
@@ -149,6 +158,7 @@ export default {
149158 ImageIcon,
150159 DeleteIcon,
151160 NcButton,
161+ NcBlurHash,
152162 ShowImageModal,
153163 NodeViewWrapper,
154164 },
@@ -160,7 +170,13 @@ export default {
160170 data () {
161171 return {
162172 attachment: null ,
173+ attachmentPromise: null ,
163174 imageLoaded: false ,
175+ imageWidth: 0 ,
176+ imageHeight: 0 ,
177+ wrapperWidth: 0 ,
178+ resizeObserver: null ,
179+ imageBlurhash: null ,
164180 loaded: false ,
165181 failed: false ,
166182 showIcons: false ,
@@ -199,6 +215,25 @@ export default {
199215
200216 return this .loaded && this .imageLoaded
201217 },
218+ canDisplayPlaceholder () {
219+ return this .imageHeight > 0
220+ },
221+ blurhashSize () {
222+ if (this .imageWidth > 0 && this .imageHeight > 0 ) {
223+ const ratio = this .imageWidth / this .imageHeight
224+ const newWidth =
225+ this .wrapperWidth - 12 > this .imageWidth
226+ ? this .imageWidth
227+ : this .wrapperWidth - 12
228+ const newHeight = newWidth / ratio
229+
230+ return {
231+ width: ` ${ newWidth} px` ,
232+ height: ` ${ newHeight} px` ,
233+ }
234+ }
235+ return {}
236+ },
202237 src: {
203238 get () {
204239 return this .node .attrs .src || ' '
@@ -233,6 +268,10 @@ export default {
233268 })
234269 },
235270 mounted () {
271+ this .attachmentPromise = this .$attachmentResolver .resolve (this .src )
272+ this .loadAttachmentMetadata ()
273+ this .setupResizeObserver ()
274+
236275 this .$nextTick (() => {
237276 // nextTick is necessary, intersection detection is slightly unreliable without it
238277 const options = {
@@ -254,10 +293,43 @@ export default {
254293 },
255294 beforeUnmount () {
256295 this .loadIntersectionObserver ? .disconnect ()
296+ this .resizeObserver ? .disconnect ()
257297 },
258298 methods: {
299+ setupResizeObserver () {
300+ if (! this .$refs .wrapper ) return
301+
302+ this .resizeObserver = new ResizeObserver ((entries ) => {
303+ const width = entries[0 ].contentRect .width
304+ if (width > 0 ) {
305+ this .wrapperWidth = width
306+ }
307+ })
308+
309+ this .resizeObserver .observe (this .$refs .wrapper )
310+ },
311+ async loadAttachmentMetadata () {
312+ try {
313+ this .attachment = await this .attachmentPromise
314+
315+ const metadata = this .attachment ? .metadata || null
316+
317+ if (metadata) {
318+ const size = metadata[' photos-size' ]? .value
319+ this .imageWidth = size? .width || 0
320+ this .imageHeight = size? .height || 0
321+
322+ this .imageBlurhash = metadata .blurhash ? .value || null
323+ }
324+ } catch (err) {
325+ // TODO: bump up to warn when the Photos dependency is gone (i.e., we can expect the metadata to exist)
326+ logger .debug (' Failed to load attachment metadata' , { err })
327+ }
328+ },
259329 async loadPreview () {
260- this .attachment = await this .$attachmentResolver .resolve (this .src )
330+ if (! this .attachment ) {
331+ this .attachment = await this .attachmentPromise
332+ }
261333 if (! this .attachment .previewUrl ) {
262334 throw new Error (' Attachment source was not resolved' )
263335 }
@@ -268,6 +340,9 @@ export default {
268340 this .imageLoaded = true
269341 this .loaded = true
270342 this .attachmentSize = this .attachment .size
343+ // once the image is loaded, we can stop tracking the container width
344+ // since we only use it for sizing the placeholder
345+ this .resizeObserver ? .disconnect ()
271346 }
272347 img .onerror = (e ) => {
273348 reject (new LoadImageError (e, this .attachment .previewUrl ))
@@ -426,6 +501,12 @@ export default {
426501 height: 100px ;
427502}
428503
504+ .image__placeholder {
505+ padding: 7px 6px ;
506+ margin- bottom: 26px ;
507+ position: relative;
508+ }
509+
429510.image__main {
430511 max- height: calc (100vh - 50px - 50px );
431512}
0 commit comments