UNPKG

quasar

Version:

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

342 lines (280 loc) 7.78 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 ) } } })