UNPKG

quasar

Version:

Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time

307 lines (249 loc) 7 kB
import { h, ref, computed, watch, onMounted, onBeforeUnmount, Transition } from 'vue' import QSpinner from '../spinner/QSpinner.js' import useRatio, { useRatioProps } from '../../composables/private/use-ratio.js' import { createComponent } from '../../utils/private/create.js' import { hSlot } from '../../utils/private/render.js' const defaultRatio = 16 / 9 export default createComponent({ name: 'QImg', props: { ...useRatioProps, src: String, srcset: String, sizes: String, alt: String, crossorigin: String, decoding: String, referrerpolicy: String, draggable: Boolean, loading: { type: String, default: 'lazy' }, fetchpriority: { type: String, default: 'auto' }, width: String, height: String, initialRatio: { type: [ Number, String ], default: defaultRatio }, placeholderSrc: String, fit: { type: String, default: 'cover' }, position: { type: String, default: '50% 50%' }, imgClass: String, imgStyle: Object, noSpinner: Boolean, noNativeMenu: Boolean, noTransition: Boolean, spinnerColor: String, spinnerSize: String }, emits: [ 'load', 'error' ], setup (props, { slots, emit }) { const naturalRatio = ref(props.initialRatio) const ratioStyle = useRatio(props, naturalRatio) let loadTimer = null, isDestroyed = false const images = [ ref(null), ref(getPlaceholderSrc()) ] const position = ref(0) const isLoading = ref(false) const hasError = ref(false) const classes = computed(() => `q-img q-img--${ props.noNativeMenu === true ? 'no-' : '' }menu` ) const style = computed(() => ({ width: props.width, height: props.height })) const imgClass = computed(() => `q-img__image ${ props.imgClass !== void 0 ? props.imgClass + ' ' : '' }` + `q-img__image--with${ props.noTransition === true ? 'out' : '' }-transition` ) const imgStyle = computed(() => ({ ...props.imgStyle, objectFit: props.fit, objectPosition: props.position })) watch(() => getCurrentSrc(), addImage) function getCurrentSrc () { return props.src || props.srcset || props.sizes ? { src: props.src, srcset: props.srcset, sizes: props.sizes } : null } function getPlaceholderSrc () { return props.placeholderSrc !== void 0 ? { src: props.placeholderSrc } : null } function addImage (imgProps) { if (loadTimer !== null) { clearTimeout(loadTimer) loadTimer = null } hasError.value = false if (imgProps === null) { isLoading.value = false images[ position.value ^ 1 ].value = getPlaceholderSrc() } else { isLoading.value = true } images[ position.value ].value = imgProps } function onLoad ({ target }) { if (isDestroyed === true) { return } if (loadTimer !== null) { clearTimeout(loadTimer) loadTimer = null } naturalRatio.value = target.naturalHeight === 0 ? 0.5 : target.naturalWidth / target.naturalHeight waitForCompleteness(target, 1) } function waitForCompleteness (target, count) { // protect against running forever if (isDestroyed === true || count === 1000) { return } if (target.complete === true) { onReady(target) } else { loadTimer = setTimeout(() => { loadTimer = null waitForCompleteness(target, count + 1) }, 50) } } function onReady (img) { if (isDestroyed === true) { return } position.value = position.value ^ 1 images[ position.value ].value = null isLoading.value = false hasError.value = false emit('load', img.currentSrc || img.src) } function onError (err) { if (loadTimer !== null) { clearTimeout(loadTimer) loadTimer = null } isLoading.value = false hasError.value = true images[ position.value ].value = null images[ position.value ^ 1 ].value = getPlaceholderSrc() emit('error', err) } function getImage (index) { const img = images[ index ].value const data = { key: 'img_' + index, class: imgClass.value, style: imgStyle.value, crossorigin: props.crossorigin, decoding: props.decoding, referrerpolicy: props.referrerpolicy, height: props.height, width: props.width, loading: props.loading, fetchpriority: props.fetchpriority, 'aria-hidden': 'true', draggable: props.draggable, ...img } if (position.value === index) { data.class += ' q-img__image--waiting' Object.assign(data, { onLoad, onError }) } else { data.class += ' q-img__image--loaded' } return h( 'div', { class: 'q-img__container absolute-full', key: 'img' + index }, h('img', data) ) } function getContent () { if (isLoading.value !== true) { return h('div', { key: 'content', class: 'q-img__content absolute-full q-anchor--skip' }, hSlot(slots[ hasError.value === true ? 'error' : 'default' ])) } return h('div', { key: 'loading', class: 'q-img__loading absolute-full flex flex-center' }, ( slots.loading !== void 0 ? slots.loading() : ( props.noSpinner === true ? void 0 : [ h(QSpinner, { color: props.spinnerColor, size: props.spinnerSize }) ] ) )) } if (__QUASAR_SSR_SERVER__ !== true) { if (__QUASAR_SSR_CLIENT__) { onMounted(() => { addImage(getCurrentSrc()) }) } else { addImage(getCurrentSrc()) } onBeforeUnmount(() => { isDestroyed = true if (loadTimer !== null) { clearTimeout(loadTimer) loadTimer = null } }) } return () => { const content = [] if (ratioStyle.value !== null) { content.push( h('div', { key: 'filler', style: ratioStyle.value }) ) } if (hasError.value !== true) { if (images[ 0 ].value !== null) { content.push(getImage(0)) } if (images[ 1 ].value !== null) { content.push(getImage(1)) } } content.push( h(Transition, { name: 'q-transition--fade' }, getContent) ) return h('div', { class: classes.value, style: style.value, role: 'img', 'aria-label': props.alt }, content) } } })