UNPKG

vuetify

Version:

Vue Material Component Framework

293 lines (259 loc) 7.71 kB
// Styles import './VImg.sass' // Directives import intersect from '../../directives/intersect' // Types import { VNode } from 'vue' import { PropValidator } from 'vue/types/options' // Components import VResponsive from '../VResponsive' // Mixins import Themeable from '../../mixins/themeable' // Utils import mixins from '../../util/mixins' import mergeData from '../../util/mergeData' import { consoleWarn } from '../../util/console' // not intended for public use, this is passed in by vuetify-loader export interface srcObject { src: string srcset?: string lazySrc: string aspect: number } const hasIntersect = typeof window !== 'undefined' && 'IntersectionObserver' in window /* @vue/component */ export default mixins( VResponsive, Themeable, ).extend({ name: 'v-img', directives: { intersect }, props: { alt: String, contain: Boolean, eager: Boolean, gradient: String, lazySrc: String, options: { type: Object, // For more information on types, navigate to: // https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API default: () => ({ root: undefined, rootMargin: undefined, threshold: undefined, }), } as PropValidator<IntersectionObserverInit>, position: { type: String, default: 'center center', }, sizes: String, src: { type: [String, Object], default: '', } as PropValidator<string | srcObject>, srcset: String, transition: { type: [Boolean, String], default: 'fade-transition', }, }, data () { return { currentSrc: '', // Set from srcset image: null as HTMLImageElement | null, isLoading: true, calculatedAspectRatio: undefined as number | undefined, naturalWidth: undefined as number | undefined, hasError: false, } }, computed: { computedAspectRatio (): number { return Number(this.normalisedSrc.aspect || this.calculatedAspectRatio) }, normalisedSrc (): srcObject { return this.src && typeof this.src === 'object' ? { src: this.src.src, srcset: this.srcset || this.src.srcset, lazySrc: this.lazySrc || this.src.lazySrc, aspect: Number(this.aspectRatio || this.src.aspect), } : { src: this.src, srcset: this.srcset, lazySrc: this.lazySrc, aspect: Number(this.aspectRatio || 0), } }, __cachedImage (): VNode | [] { if (!(this.normalisedSrc.src || this.normalisedSrc.lazySrc || this.gradient)) return [] const backgroundImage: string[] = [] const src = this.isLoading ? this.normalisedSrc.lazySrc : this.currentSrc if (this.gradient) backgroundImage.push(`linear-gradient(${this.gradient})`) if (src) backgroundImage.push(`url("${src}")`) const image = this.$createElement('div', { staticClass: 'v-image__image', class: { 'v-image__image--preload': this.isLoading, 'v-image__image--contain': this.contain, 'v-image__image--cover': !this.contain, }, style: { backgroundImage: backgroundImage.join(', '), backgroundPosition: this.position, }, key: +this.isLoading, }) /* istanbul ignore if */ if (!this.transition) return image return this.$createElement('transition', { attrs: { name: this.transition, mode: 'in-out', }, }, [image]) }, }, watch: { src () { // Force re-init when src changes if (!this.isLoading) this.init(undefined, undefined, true) else this.loadImage() }, '$vuetify.breakpoint.width': 'getSrc', }, mounted () { this.init() }, methods: { init ( entries?: IntersectionObserverEntry[], observer?: IntersectionObserver, isIntersecting?: boolean ) { // If the current browser supports the intersection // observer api, the image is not observable, and // the eager prop isn't being used, do not load if ( hasIntersect && !isIntersecting && !this.eager ) return if (this.normalisedSrc.lazySrc) { const lazyImg = new Image() lazyImg.src = this.normalisedSrc.lazySrc this.pollForSize(lazyImg, null) } /* istanbul ignore else */ if (this.normalisedSrc.src) this.loadImage() }, onLoad () { this.getSrc() this.isLoading = false this.$emit('load', this.src) }, onError () { this.hasError = true this.$emit('error', this.src) }, getSrc () { /* istanbul ignore else */ if (this.image) this.currentSrc = this.image.currentSrc || this.image.src }, loadImage () { const image = new Image() this.image = image image.onload = () => { /* istanbul ignore if */ if (image.decode) { image.decode().catch((err: DOMException) => { consoleWarn( `Failed to decode image, trying to render anyway\n\n` + `src: ${this.normalisedSrc.src}` + (err.message ? `\nOriginal error: ${err.message}` : ''), this ) }).then(this.onLoad) } else { this.onLoad() } } image.onerror = this.onError this.hasError = false image.src = this.normalisedSrc.src this.sizes && (image.sizes = this.sizes) this.normalisedSrc.srcset && (image.srcset = this.normalisedSrc.srcset) this.aspectRatio || this.pollForSize(image) this.getSrc() }, pollForSize (img: HTMLImageElement, timeout: number | null = 100) { const poll = () => { const { naturalHeight, naturalWidth } = img if (naturalHeight || naturalWidth) { this.naturalWidth = naturalWidth this.calculatedAspectRatio = naturalWidth / naturalHeight } else { timeout != null && !this.hasError && setTimeout(poll, timeout) } } poll() }, genContent () { const content: VNode = VResponsive.options.methods.genContent.call(this) if (this.naturalWidth) { this._b(content.data!, 'div', { style: { width: `${this.naturalWidth}px` }, }) } return content }, __genPlaceholder (): VNode | void { if (this.$slots.placeholder) { const placeholder = this.isLoading ? [this.$createElement('div', { staticClass: 'v-image__placeholder', }, this.$slots.placeholder)] : [] if (!this.transition) return placeholder[0] return this.$createElement('transition', { props: { appear: true, name: this.transition, }, }, placeholder) } }, }, render (h): VNode { const node = VResponsive.options.render.call(this, h) const data = mergeData(node.data!, { staticClass: 'v-image', attrs: { 'aria-label': this.alt, role: this.alt ? 'img' : undefined, }, class: this.themeClasses, // Only load intersect directive if it // will work in the current browser. directives: hasIntersect ? [{ name: 'intersect', modifiers: { once: true }, value: { handler: this.init, options: this.options, }, }] : undefined, }) node.children = [ this.__cachedSizer, this.__cachedImage, this.__genPlaceholder(), this.genContent(), ] as VNode[] return h(node.tag, data, node.children) }, })