Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
1492be0
chore(b-img-lazy): switch IntersectionObserver to use private `v-b-vi…
tmorehouse Aug 29, 2019
5816d43
Update img-lazy.js
tmorehouse Aug 29, 2019
a9b8881
Merge branch 'dev' into tmorehouse/img-lazy
jacobmllr95 Aug 29, 2019
68b6698
Merge branch 'dev' into tmorehouse/img-lazy
jacobmllr95 Aug 29, 2019
439d2df
remove scroll event fallback
tmorehouse Aug 29, 2019
c1b1bae
Update img-lazy.js
tmorehouse Aug 29, 2019
3d74dac
Update visible.js
tmorehouse Aug 29, 2019
cc89a14
Update img-lazy.js
tmorehouse Aug 29, 2019
0167f1d
Update img-lazy.spec.js
tmorehouse Aug 29, 2019
c901cfc
Update img-lazy.spec.js
tmorehouse Aug 29, 2019
48d5236
Update img-lazy.spec.js
tmorehouse Aug 29, 2019
dc234aa
Update img-lazy.spec.js
tmorehouse Aug 29, 2019
1635289
Update img-lazy.spec.js
tmorehouse Aug 30, 2019
644051b
Update img-lazy.spec.js
tmorehouse Aug 30, 2019
77a5b11
Update img-lazy.js
tmorehouse Aug 30, 2019
6a183e1
Update img-lazy.spec.js
tmorehouse Aug 30, 2019
55a6421
Update visible.js
tmorehouse Aug 30, 2019
c89a816
Update card-img-lazy.spec.js
tmorehouse Aug 30, 2019
683074c
Update img-lazy.spec.js
tmorehouse Aug 30, 2019
f3b931f
Update img-lazy.spec.js
tmorehouse Aug 30, 2019
26c40c7
Update card-img-lazy.spec.js
tmorehouse Aug 30, 2019
91d784c
Update visible.js
tmorehouse Aug 30, 2019
b8ed868
Update img-lazy.spec.js
tmorehouse Aug 30, 2019
41a22b2
Update img-lazy.spec.js
tmorehouse Aug 30, 2019
77aac7d
Update img-lazy.js
tmorehouse Aug 30, 2019
a29f9f7
Update img-lazy.spec.js
tmorehouse Aug 30, 2019
4514848
Update img-lazy.spec.js
tmorehouse Aug 30, 2019
620a829
Update img-lazy.spec.js
tmorehouse Aug 30, 2019
fd1dc84
Update img-lazy.spec.js
tmorehouse Aug 30, 2019
ffefa38
Update img-lazy.spec.js
tmorehouse Aug 30, 2019
7d1691a
Update img-lazy.js
tmorehouse Aug 30, 2019
2ff128b
Update img-lazy.spec.js
tmorehouse Aug 30, 2019
8de93c0
Update img-lazy.js
tmorehouse Aug 30, 2019
50861f9
Update img-lazy.spec.js
tmorehouse Aug 30, 2019
100e115
Update img-lazy.spec.js
tmorehouse Aug 30, 2019
24b1e30
Update visible.js
tmorehouse Aug 30, 2019
71a8b87
Update visible.js
tmorehouse Aug 30, 2019
44df4de
Update img-lazy.spec.js
tmorehouse Aug 30, 2019
300be3f
Update img-lazy.spec.js
tmorehouse Aug 30, 2019
a523ae4
Update visible.js
tmorehouse Aug 30, 2019
c53b1fa
Update README.md
tmorehouse Aug 30, 2019
496dabf
Update visible.js
jacobmllr95 Aug 30, 2019
c0f3242
Update img-lazy.js
jacobmllr95 Aug 30, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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