Skip to content
Permalink
Browse files

feat(b-img-lazy): switch IntersectionObserver to use private `v-b-vis…

…ible` directive (#3977)
  • Loading branch information...
tmorehouse committed Aug 30, 2019
1 parent 62fb0b6 commit 249ccfa9fe7477dc094c5a48b8b9c3a64d23c455
@@ -11,17 +11,7 @@ describe('card-image', () => {
}
})
expect(wrapper.is('img')).toBe(true)
})

it('default has data src attribute', async () => {
const wrapper = mount(BCardImgLazy, {
context: {
props: {
src: 'https://picsum.photos/600/300/?image=25'
}
}
})
expect(wrapper.attributes('src')).toContain('data:image/svg+xml')
expect(wrapper.attributes('src')).toBeDefined()
})

it('default does not have alt attribute', async () => {
@@ -43,10 +33,14 @@ describe('card-image', () => {
}
}
})
expect(wrapper.attributes('width')).toBeDefined()
expect(wrapper.attributes('width')).toBe('1')
expect(wrapper.attributes('height')).toBeDefined()
expect(wrapper.attributes('height')).toBe('1')
expect(wrapper.attributes('width')).not.toBeDefined()
expect(wrapper.attributes('height')).not.toBeDefined()
// Without IntersectionObserver support, the main image is shown
// and the value of the width and height props are used (null in this case)
// expect(wrapper.attributes('width')).toBeDefined()
// expect(wrapper.attributes('width')).toBe('1')
// expect(wrapper.attributes('height')).toBeDefined()
// expect(wrapper.attributes('height')).toBe('1')
})

it('default has class "card-img"', async () => {
@@ -209,9 +209,8 @@ The default `blank-color` is `transparent`.
Lazy loading images uses
[`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)
if supported by the browser (or polyfill), otherwise it uses the document `scroll`, `resize`, and
`transitionend` events to determine if the image is in view in order to trigger the loading of the
final image. Scrolling of other elements is not monitored, and will not trigger image loading.
if supported by the browser (or via a polyfill) to detect with the image should be shown. If
`IntersectionObserver` support is _not detected_, then the image will _always_ be shown.

### Usage

@@ -1,14 +1,11 @@
import Vue from '../../utils/vue'
import { BImg } from './img'
import { getComponentConfig } from '../../utils/config'
import { getBCR, eventOn, eventOff } from '../../utils/dom'
import { hasIntersectionObserverSupport } from '../../utils/env'
import { VBVisible } from '../../directives/visible'
import { BImg } from './img'

const NAME = 'BImgLazy'

const THROTTLE = 100
const EVENT_OPTIONS = { passive: true, capture: false }

export const props = {
src: {
type: String,
@@ -81,24 +78,23 @@ export const props = {
default: false
},
offset: {
// Distance away from viewport (in pixels) before being
// considered "visible"
type: [Number, String],
default: 360
},
throttle: {
type: [Number, String],
default: THROTTLE
}
}

// @vue/component
export const BImgLazy = /*#__PURE__*/ Vue.extend({
name: NAME,
directives: {
bVisible: VBVisible
},
props,
data() {
return {
isShown: false,
scrollTimeout: null,
observer: null
isShown: this.show
}
},
computed: {
@@ -118,121 +114,66 @@ export const BImgLazy = /*#__PURE__*/ Vue.extend({
watch: {
show(newVal, oldVal) {
if (newVal !== oldVal) {
this.isShown = newVal
if (!newVal) {
// Make sure listeners are re-enabled if img is force set to blank
this.setListeners(true)
// If IntersectionObserver support is not available, image is always shown
const visible = hasIntersectionObserverSupport ? newVal : true
this.isShown = visible
if (visible !== newVal) {
// Ensure the show prop is synced (when no IntersectionObserver)
this.$nextTick(this.updateShowProp)
}
}
},
isShown(newVal, oldVal) {
if (newVal !== oldVal) {
// Update synched show prop
this.$emit('update:show', newVal)
this.updateShowProp()
}
}
},
created() {
this.isShown = this.show
},
mounted() {
if (this.isShown) {
this.setListeners(false)
} else {
this.setListeners(true)
}
},
activated() /* istanbul ignore next */ {
if (!this.isShown) {
this.setListeners(true)
}
},
deactivated() /* istanbul ignore next */ {
this.setListeners(false)
},
beforeDestroy() {
this.setListeners(false)
// If IntersectionObserver is not available, image is always shown
this.isShown = hasIntersectionObserverSupport ? this.show : true
},
methods: {
setListeners(on) {
if (this.scrollTimeout) {
clearTimeout(this.scrollTimeout)
this.scrollTimeout = null
}
/* istanbul ignore next: JSDOM doen't support IntersectionObserver */
if (this.observer) {
this.observer.unobserve(this.$el)
this.observer.disconnect()
this.observer = null
}
const winEvts = ['scroll', 'resize', 'orientationchange']
winEvts.forEach(evt => eventOff(window, evt, this.onScroll, EVENT_OPTIONS))
eventOff(this.$el, 'load', this.checkView, EVENT_OPTIONS)
eventOff(document, 'transitionend', this.onScroll, EVENT_OPTIONS)
if (on) {
/* istanbul ignore if: JSDOM doen't support IntersectionObserver */
if (hasIntersectionObserverSupport) {
this.observer = new IntersectionObserver(this.doShow, {
root: null, // viewport
rootMargin: `${parseInt(this.offset, 10) || 0}px`,
threshold: 0 // percent intersection
})
this.observer.observe(this.$el)
} else {
// Fallback to scroll/etc events
winEvts.forEach(evt => eventOn(window, evt, this.onScroll, EVENT_OPTIONS))
eventOn(this.$el, 'load', this.checkView, EVENT_OPTIONS)
eventOn(document, 'transitionend', this.onScroll, EVENT_OPTIONS)
}
}
updateShowProp() {
this.$emit('update:show', this.isShown)
},
doShow(entries) {
if (entries && (entries[0].isIntersecting || entries[0].intersectionRatio > 0.0)) {
doShow(visible) {
// If IntersectionObserver is not supported, the callback
// will be called with `null` rather than `true` or `false`
if ((visible || visible === null) && !this.isShown) {
this.isShown = true
this.setListeners(false)
}
},
checkView() {
// check bounding box + offset to see if we should show
/* istanbul ignore next: should rarely occur */
if (this.isShown) {
this.setListeners(false)
return
}
const offset = parseInt(this.offset, 10) || 0
const docElement = document.documentElement
const view = {
l: 0 - offset,
t: 0 - offset,
b: docElement.clientHeight + offset,
r: docElement.clientWidth + offset
}
// JSDOM Doesn't support BCR, but we fake it in the tests
const box = getBCR(this.$el)
if (box.right >= view.l && box.bottom >= view.t && box.left <= view.r && box.top <= view.b) {
// image is in view (or about to be in view)
this.doShow([{ isIntersecting: true }])
}
},
onScroll() {
/* istanbul ignore if: should rarely occur */
if (this.isShown) {
this.setListeners(false)
} else {
clearTimeout(this.scrollTimeout)
this.scrollTimeout = setTimeout(this.checkView, parseInt(this.throttle, 10) || THROTTLE)
}
}
},
render(h) {
const directives = []
if (!this.isShown) {
// We only add the visible directive if we are not shown
directives.push({
// Visible directive will silently do nothing if
// IntersectionObserver is not supported
name: 'b-visible',
// Value expects a callback (passed one arg of `visible` = `true` or `false`)
value: this.doShow,
modifiers: {
// Root margin from viewport
[`${parseInt(this.offset, 10) || 0}`]: true,
// Once the image is shown, stop observing
once: true
}
})
}

return h(BImg, {
directives,
props: {
// Computed value props
src: this.computedSrc,
blank: this.computedBlank,
width: this.computedWidth,
height: this.computedHeight,
// Passthough props
// Passthrough props
alt: this.alt,
blankColor: this.blankColor,
fluid: this.fluid,

0 comments on commit 249ccfa

Please sign in to comment.
You can’t perform that action at this time.