UNPKG

quasar

Version:

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

336 lines (274 loc) 7.87 kB
import { h, ref, computed, watch, onMounted, Transition, getCurrentInstance } from 'vue' import QSpinner from '../spinner/QSpinner.js' import { isRuntimeSsrPreHydration } from '../../plugins/platform/Platform.js' import useRatio, { useRatioProps } from '../../composables/private.use-ratio/use-ratio.js' import { createComponent } from '../../utils/private.create/create.js' import { hSlot } from '../../utils/private.render/render.js' import { vmIsDestroyed } from '../../utils/private.vm/vm.js' import useTimeout from '../../composables/use-timeout/use-timeout.js' const defaultRatio = 1.7778 /* 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' }, loadingShowDelay: { type: [ Number, String ], default: 0 }, fetchpriority: { type: String, default: 'auto' }, width: String, height: String, initialRatio: { type: [ Number, String ], default: defaultRatio }, placeholderSrc: String, errorSrc: 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) const vm = getCurrentInstance() const { registerTimeout: registerLoadTimeout, removeTimeout: removeLoadTimeout } = useTimeout() const { registerTimeout: registerLoadShowTimeout, removeTimeout: removeLoadShowTimeout } = useTimeout() const placeholderImg = computed(() => ( props.placeholderSrc !== void 0 ? { src: props.placeholderSrc } : null )) const errorImg = computed(() => ( props.errorSrc !== void 0 ? { src: props.errorSrc, __qerror: true } : null )) const images = [ ref(null), ref(placeholderImg.value) ] 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` + ' q-img__image--' ) const imgStyle = computed(() => ({ ...props.imgStyle, objectFit: props.fit, objectPosition: props.position })) function setLoading () { removeLoadShowTimeout() if (props.loadingShowDelay === 0) { isLoading.value = true return } registerLoadShowTimeout(() => { isLoading.value = true }, props.loadingShowDelay) } function clearLoading () { removeLoadShowTimeout() isLoading.value = false } function onLoad ({ target }) { if (vmIsDestroyed(vm) === false) { removeLoadTimeout() naturalRatio.value = target.naturalHeight === 0 ? 0.5 : target.naturalWidth / target.naturalHeight waitForCompleteness(target, 1) } } function waitForCompleteness (target, count) { // protect against running forever if (count === 1000 || vmIsDestroyed(vm) === true) return if (target.complete === true) { onReady(target) } else { registerLoadTimeout(() => { waitForCompleteness(target, count + 1) }, 50) } } function onReady (target) { if (vmIsDestroyed(vm) === true) return position.value = position.value ^ 1 images[ position.value ].value = null clearLoading() if (target.getAttribute('__qerror') !== 'true') { hasError.value = false } emit('load', target.currentSrc || target.src) } function onError (err) { removeLoadTimeout() clearLoading() hasError.value = true images[ position.value ].value = errorImg.value images[ position.value ^ 1 ].value = placeholderImg.value emit('error', err) } function getImage (index) { const img = images[ index ].value const data = { key: 'img_' + index, class: imgClass.value, style: imgStyle.value, alt: props.alt, 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) { Object.assign(data, { class: data.class + 'current', onLoad, onError }) } else { data.class += 'loaded' } return h( 'div', { class: 'q-img__container absolute-full', key: 'img' + index }, h('img', data) ) } function getContent () { if (isLoading.value === false) { 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) { function watchSrc () { watch( () => ( props.src || props.srcset || props.sizes ? { src: props.src, srcset: props.srcset, sizes: props.sizes } : null ), imgProps => { removeLoadTimeout() hasError.value = false if (imgProps === null) { clearLoading() images[ position.value ^ 1 ].value = placeholderImg.value } else { setLoading() } images[ position.value ].value = imgProps }, { immediate: true } ) } if (isRuntimeSsrPreHydration.value === true) { onMounted(watchSrc) } else { watchSrc() } } return () => { const content = [] if (ratioStyle.value !== null) { content.push( h('div', { key: 'filler', style: ratioStyle.value }) ) } 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', { key: 'main', class: classes.value, style: style.value, role: 'img', 'aria-label': props.alt }, content) } } })