vuetifyjs / vuetify
1
// Styles
2 1
import './VImg.sass'
3

4
// Directives
5 1
import intersect from '../../directives/intersect'
6

7
// Types
8
import { VNode } from 'vue'
9
import { PropValidator } from 'vue/types/options'
10

11
// Components
12 1
import VResponsive from '../VResponsive'
13

14
// Mixins
15 1
import Themeable from '../../mixins/themeable'
16

17
// Utils
18 1
import mixins from '../../util/mixins'
19 1
import mergeData from '../../util/mergeData'
20 1
import { consoleWarn } from '../../util/console'
21

22
// not intended for public use, this is passed in by vuetify-loader
23
export interface srcObject {
24
  src: string
25
  srcset?: string
26
  lazySrc: string
27
  aspect: number
28
}
29

30 1
const hasIntersect = typeof window !== 'undefined' && 'IntersectionObserver' in window
31

32
/* @vue/component */
33 1
export default mixins(
34
  VResponsive,
35
  Themeable,
36
).extend({
37
  name: 'v-img',
38

39
  directives: { intersect },
40

41
  props: {
42
    alt: String,
43
    contain: Boolean,
44
    eager: Boolean,
45
    gradient: String,
46
    lazySrc: String,
47
    options: {
48
      type: Object,
49
      // For more information on types, navigate to:
50
      // https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
51 1
      default: () => ({
52
        root: undefined,
53
        rootMargin: undefined,
54
        threshold: undefined,
55
      }),
56
    } as PropValidator<IntersectionObserverInit>,
57
    position: {
58
      type: String,
59
      default: 'center center',
60
    },
61
    sizes: String,
62
    src: {
63
      type: [String, Object],
64
      default: '',
65
    } as PropValidator<string | srcObject>,
66
    srcset: String,
67
    transition: {
68
      type: [Boolean, String],
69
      default: 'fade-transition',
70
    },
71
  },
72

73 1
  data () {
74 1
    return {
75
      currentSrc: '', // Set from srcset
76
      image: null as HTMLImageElement | null,
77
      isLoading: true,
78
      calculatedAspectRatio: undefined as number | undefined,
79
      naturalWidth: undefined as number | undefined,
80
      hasError: false,
81
    }
82
  },
83

84
  computed: {
85 1
    computedAspectRatio (): number {
86 1
      return Number(this.normalisedSrc.aspect || this.calculatedAspectRatio)
87
    },
88 1
    normalisedSrc (): srcObject {
89 1
      return this.src && typeof this.src === 'object'
90 1
        ? {
91
          src: this.src.src,
92 1
          srcset: this.srcset || this.src.srcset,
93 1
          lazySrc: this.lazySrc || this.src.lazySrc,
94 1
          aspect: Number(this.aspectRatio || this.src.aspect),
95 1
        } : {
96
          src: this.src,
97
          srcset: this.srcset,
98
          lazySrc: this.lazySrc,
99 1
          aspect: Number(this.aspectRatio || 0),
100
        }
101
    },
102 1
    __cachedImage (): VNode | [] {
103 1
      if (!(this.normalisedSrc.src || this.normalisedSrc.lazySrc || this.gradient)) return []
104

105 1
      const backgroundImage: string[] = []
106 1
      const src = this.isLoading ? this.normalisedSrc.lazySrc : this.currentSrc
107

108 1
      if (this.gradient) backgroundImage.push(`linear-gradient(${this.gradient})`)
109 1
      if (src) backgroundImage.push(`url("${src}")`)
110

111 1
      const image = this.$createElement('div', {
112
        staticClass: 'v-image__image',
113
        class: {
114
          'v-image__image--preload': this.isLoading,
115
          'v-image__image--contain': this.contain,
116
          'v-image__image--cover': !this.contain,
117
        },
118
        style: {
119
          backgroundImage: backgroundImage.join(', '),
120
          backgroundPosition: this.position,
121
        },
122
        key: +this.isLoading,
123
      })
124

125
      /* istanbul ignore if */
126 1
      if (!this.transition) return image
127

128 1
      return this.$createElement('transition', {
129
        attrs: {
130
          name: this.transition,
131
          mode: 'in-out',
132
        },
133
      }, [image])
134
    },
135
  },
136

137
  watch: {
138 1
    src () {
139
      // Force re-init when src changes
140 1
      if (!this.isLoading) this.init(undefined, undefined, true)
141 1
      else this.loadImage()
142
    },
143
    '$vuetify.breakpoint.width': 'getSrc',
144
  },
145

146 1
  mounted () {
147 1
    this.init()
148
  },
149

150
  methods: {
151
    init (
152
      entries?: IntersectionObserverEntry[],
153
      observer?: IntersectionObserver,
154 1
      isIntersecting?: boolean
155
    ) {
156
      // If the current browser supports the intersection
157
      // observer api, the image is not observable, and
158
      // the eager prop isn't being used, do not load
159 1
      if (
160 1
        hasIntersect &&
161 1
        !isIntersecting &&
162 1
        !this.eager
163 1
      ) return
164

165 1
      if (this.normalisedSrc.lazySrc) {
166 1
        const lazyImg = new Image()
167 1
        lazyImg.src = this.normalisedSrc.lazySrc
168 1
        this.pollForSize(lazyImg, null)
169
      }
170
      /* istanbul ignore else */
171 1
      if (this.normalisedSrc.src) this.loadImage()
172
    },
173 1
    onLoad () {
174 1
      this.getSrc()
175 1
      this.isLoading = false
176 1
      this.$emit('load', this.src)
177
    },
178 1
    onError () {
179 1
      this.hasError = true
180 1
      this.$emit('error', this.src)
181
    },
182 1
    getSrc () {
183
      /* istanbul ignore else */
184 1
      if (this.image) this.currentSrc = this.image.currentSrc || this.image.src
185
    },
186 1
    loadImage () {
187 1
      const image = new Image()
188 1
      this.image = image
189

190 1
      image.onload = () => {
191
        /* istanbul ignore if */
192 1
        if (image.decode) {
193
          image.decode().catch((err: DOMException) => {
194
            consoleWarn(
195
              `Failed to decode image, trying to render anyway\n\n` +
196
              `src: ${this.normalisedSrc.src}` +
197
              (err.message ? `\nOriginal error: ${err.message}` : ''),
198
              this
199
            )
200
          }).then(this.onLoad)
201
        } else {
202 1
          this.onLoad()
203
        }
204
      }
205 1
      image.onerror = this.onError
206

207 1
      this.hasError = false
208 1
      image.src = this.normalisedSrc.src
209 1
      this.sizes && (image.sizes = this.sizes)
210 1
      this.normalisedSrc.srcset && (image.srcset = this.normalisedSrc.srcset)
211

212 1
      this.aspectRatio || this.pollForSize(image)
213 1
      this.getSrc()
214
    },
215 1
    pollForSize (img: HTMLImageElement, timeout: number | null = 100) {
216 1
      const poll = () => {
217 1
        const { naturalHeight, naturalWidth } = img
218

219 1
        if (naturalHeight || naturalWidth) {
220 1
          this.naturalWidth = naturalWidth
221 1
          this.calculatedAspectRatio = naturalWidth / naturalHeight
222
        } else {
223 1
          timeout != null && !this.hasError && setTimeout(poll, timeout)
224
        }
225
      }
226

227 1
      poll()
228
    },
229 1
    genContent () {
230 1
      const content: VNode = VResponsive.options.methods.genContent.call(this)
231 1
      if (this.naturalWidth) {
232 1
        this._b(content.data!, 'div', {
233
          style: { width: `${this.naturalWidth}px` },
234
        })
235
      }
236

237 1
      return content
238
    },
239 1
    __genPlaceholder (): VNode | void {
240 1
      if (this.$slots.placeholder) {
241 1
        const placeholder = this.isLoading
242 1
          ? [this.$createElement('div', {
243
            staticClass: 'v-image__placeholder',
244
          }, this.$slots.placeholder)]
245 1
          : []
246

247 1
        if (!this.transition) return placeholder[0]
248

249 1
        return this.$createElement('transition', {
250
          props: {
251
            appear: true,
252
            name: this.transition,
253
          },
254
        }, placeholder)
255
      }
256
    },
257
  },
258

259 1
  render (h): VNode {
260 1
    const node = VResponsive.options.render.call(this, h)
261

262 1
    const data = mergeData(node.data!, {
263
      staticClass: 'v-image',
264
      attrs: {
265
        'aria-label': this.alt,
266 1
        role: this.alt ? 'img' : undefined,
267
      },
268
      class: this.themeClasses,
269
      // Only load intersect directive if it
270
      // will work in the current browser.
271
      directives: hasIntersect
272 1
        ? [{
273
          name: 'intersect',
274
          modifiers: { once: true },
275
          value: {
276
            handler: this.init,
277
            options: this.options,
278
          },
279
        }]
280 0
        : undefined,
281
    })
282

283 1
    node.children = [
284
      this.__cachedSizer,
285
      this.__cachedImage,
286
      this.__genPlaceholder(),
287
      this.genContent(),
288
    ] as VNode[]
289

290 1
    return h(node.tag, data, node.children)
291
  },
292
})

Read our documentation on viewing source code .

Loading