Skip to content

Commit

Permalink
feat(b-img-lazy): switch IntersectionObserver to use private `v-b-vis…
Browse files Browse the repository at this point in the history
…ible` directive (#3977)
  • Loading branch information
tmorehouse committed Aug 30, 2019
1 parent 62fb0b6 commit 249ccfa
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 224 deletions.
24 changes: 9 additions & 15 deletions src/components/card/card-img-lazy.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand Down
5 changes: 2 additions & 3 deletions src/components/image/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
145 changes: 43 additions & 102 deletions src/components/image/img-lazy.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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: {
Expand All @@ -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,
Expand Down
Loading

0 comments on commit 249ccfa

Please sign in to comment.