quasar
Version:
Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
336 lines (274 loc) • 7.87 kB
JavaScript
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)
}
}
})