Skip to content

Commit 249ccfa

Browse files
authored
feat(b-img-lazy): switch IntersectionObserver to use private v-b-visible directive (#3977)
1 parent 62fb0b6 commit 249ccfa

File tree

5 files changed

+112
-224
lines changed

5 files changed

+112
-224
lines changed

src/components/card/card-img-lazy.spec.js

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,7 @@ describe('card-image', () => {
1111
}
1212
})
1313
expect(wrapper.is('img')).toBe(true)
14-
})
15-
16-
it('default has data src attribute', async () => {
17-
const wrapper = mount(BCardImgLazy, {
18-
context: {
19-
props: {
20-
src: 'https://picsum.photos/600/300/?image=25'
21-
}
22-
}
23-
})
24-
expect(wrapper.attributes('src')).toContain('data:image/svg+xml')
14+
expect(wrapper.attributes('src')).toBeDefined()
2515
})
2616

2717
it('default does not have alt attribute', async () => {
@@ -43,10 +33,14 @@ describe('card-image', () => {
4333
}
4434
}
4535
})
46-
expect(wrapper.attributes('width')).toBeDefined()
47-
expect(wrapper.attributes('width')).toBe('1')
48-
expect(wrapper.attributes('height')).toBeDefined()
49-
expect(wrapper.attributes('height')).toBe('1')
36+
expect(wrapper.attributes('width')).not.toBeDefined()
37+
expect(wrapper.attributes('height')).not.toBeDefined()
38+
// Without IntersectionObserver support, the main image is shown
39+
// and the value of the width and height props are used (null in this case)
40+
// expect(wrapper.attributes('width')).toBeDefined()
41+
// expect(wrapper.attributes('width')).toBe('1')
42+
// expect(wrapper.attributes('height')).toBeDefined()
43+
// expect(wrapper.attributes('height')).toBe('1')
5044
})
5145

5246
it('default has class "card-img"', async () => {

src/components/image/README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,8 @@ The default `blank-color` is `transparent`.
209209
210210
Lazy loading images uses
211211
[`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)
212-
if supported by the browser (or polyfill), otherwise it uses the document `scroll`, `resize`, and
213-
`transitionend` events to determine if the image is in view in order to trigger the loading of the
214-
final image. Scrolling of other elements is not monitored, and will not trigger image loading.
212+
if supported by the browser (or via a polyfill) to detect with the image should be shown. If
213+
`IntersectionObserver` support is _not detected_, then the image will _always_ be shown.
215214

216215
### Usage
217216

src/components/image/img-lazy.js

Lines changed: 43 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import Vue from '../../utils/vue'
2-
import { BImg } from './img'
32
import { getComponentConfig } from '../../utils/config'
4-
import { getBCR, eventOn, eventOff } from '../../utils/dom'
53
import { hasIntersectionObserverSupport } from '../../utils/env'
4+
import { VBVisible } from '../../directives/visible'
5+
import { BImg } from './img'
66

77
const NAME = 'BImgLazy'
88

9-
const THROTTLE = 100
10-
const EVENT_OPTIONS = { passive: true, capture: false }
11-
129
export const props = {
1310
src: {
1411
type: String,
@@ -81,24 +78,23 @@ export const props = {
8178
default: false
8279
},
8380
offset: {
81+
// Distance away from viewport (in pixels) before being
82+
// considered "visible"
8483
type: [Number, String],
8584
default: 360
86-
},
87-
throttle: {
88-
type: [Number, String],
89-
default: THROTTLE
9085
}
9186
}
9287

9388
// @vue/component
9489
export const BImgLazy = /*#__PURE__*/ Vue.extend({
9590
name: NAME,
91+
directives: {
92+
bVisible: VBVisible
93+
},
9694
props,
9795
data() {
9896
return {
99-
isShown: false,
100-
scrollTimeout: null,
101-
observer: null
97+
isShown: this.show
10298
}
10399
},
104100
computed: {
@@ -118,121 +114,66 @@ export const BImgLazy = /*#__PURE__*/ Vue.extend({
118114
watch: {
119115
show(newVal, oldVal) {
120116
if (newVal !== oldVal) {
121-
this.isShown = newVal
122-
if (!newVal) {
123-
// Make sure listeners are re-enabled if img is force set to blank
124-
this.setListeners(true)
117+
// If IntersectionObserver support is not available, image is always shown
118+
const visible = hasIntersectionObserverSupport ? newVal : true
119+
this.isShown = visible
120+
if (visible !== newVal) {
121+
// Ensure the show prop is synced (when no IntersectionObserver)
122+
this.$nextTick(this.updateShowProp)
125123
}
126124
}
127125
},
128126
isShown(newVal, oldVal) {
129127
if (newVal !== oldVal) {
130128
// Update synched show prop
131-
this.$emit('update:show', newVal)
129+
this.updateShowProp()
132130
}
133131
}
134132
},
135-
created() {
136-
this.isShown = this.show
137-
},
138133
mounted() {
139-
if (this.isShown) {
140-
this.setListeners(false)
141-
} else {
142-
this.setListeners(true)
143-
}
144-
},
145-
activated() /* istanbul ignore next */ {
146-
if (!this.isShown) {
147-
this.setListeners(true)
148-
}
149-
},
150-
deactivated() /* istanbul ignore next */ {
151-
this.setListeners(false)
152-
},
153-
beforeDestroy() {
154-
this.setListeners(false)
134+
// If IntersectionObserver is not available, image is always shown
135+
this.isShown = hasIntersectionObserverSupport ? this.show : true
155136
},
156137
methods: {
157-
setListeners(on) {
158-
if (this.scrollTimeout) {
159-
clearTimeout(this.scrollTimeout)
160-
this.scrollTimeout = null
161-
}
162-
/* istanbul ignore next: JSDOM doen't support IntersectionObserver */
163-
if (this.observer) {
164-
this.observer.unobserve(this.$el)
165-
this.observer.disconnect()
166-
this.observer = null
167-
}
168-
const winEvts = ['scroll', 'resize', 'orientationchange']
169-
winEvts.forEach(evt => eventOff(window, evt, this.onScroll, EVENT_OPTIONS))
170-
eventOff(this.$el, 'load', this.checkView, EVENT_OPTIONS)
171-
eventOff(document, 'transitionend', this.onScroll, EVENT_OPTIONS)
172-
if (on) {
173-
/* istanbul ignore if: JSDOM doen't support IntersectionObserver */
174-
if (hasIntersectionObserverSupport) {
175-
this.observer = new IntersectionObserver(this.doShow, {
176-
root: null, // viewport
177-
rootMargin: `${parseInt(this.offset, 10) || 0}px`,
178-
threshold: 0 // percent intersection
179-
})
180-
this.observer.observe(this.$el)
181-
} else {
182-
// Fallback to scroll/etc events
183-
winEvts.forEach(evt => eventOn(window, evt, this.onScroll, EVENT_OPTIONS))
184-
eventOn(this.$el, 'load', this.checkView, EVENT_OPTIONS)
185-
eventOn(document, 'transitionend', this.onScroll, EVENT_OPTIONS)
186-
}
187-
}
138+
updateShowProp() {
139+
this.$emit('update:show', this.isShown)
188140
},
189-
doShow(entries) {
190-
if (entries && (entries[0].isIntersecting || entries[0].intersectionRatio > 0.0)) {
141+
doShow(visible) {
142+
// If IntersectionObserver is not supported, the callback
143+
// will be called with `null` rather than `true` or `false`
144+
if ((visible || visible === null) && !this.isShown) {
191145
this.isShown = true
192-
this.setListeners(false)
193-
}
194-
},
195-
checkView() {
196-
// check bounding box + offset to see if we should show
197-
/* istanbul ignore next: should rarely occur */
198-
if (this.isShown) {
199-
this.setListeners(false)
200-
return
201-
}
202-
const offset = parseInt(this.offset, 10) || 0
203-
const docElement = document.documentElement
204-
const view = {
205-
l: 0 - offset,
206-
t: 0 - offset,
207-
b: docElement.clientHeight + offset,
208-
r: docElement.clientWidth + offset
209-
}
210-
// JSDOM Doesn't support BCR, but we fake it in the tests
211-
const box = getBCR(this.$el)
212-
if (box.right >= view.l && box.bottom >= view.t && box.left <= view.r && box.top <= view.b) {
213-
// image is in view (or about to be in view)
214-
this.doShow([{ isIntersecting: true }])
215-
}
216-
},
217-
onScroll() {
218-
/* istanbul ignore if: should rarely occur */
219-
if (this.isShown) {
220-
this.setListeners(false)
221-
} else {
222-
clearTimeout(this.scrollTimeout)
223-
this.scrollTimeout = setTimeout(this.checkView, parseInt(this.throttle, 10) || THROTTLE)
224146
}
225147
}
226148
},
227149
render(h) {
150+
const directives = []
151+
if (!this.isShown) {
152+
// We only add the visible directive if we are not shown
153+
directives.push({
154+
// Visible directive will silently do nothing if
155+
// IntersectionObserver is not supported
156+
name: 'b-visible',
157+
// Value expects a callback (passed one arg of `visible` = `true` or `false`)
158+
value: this.doShow,
159+
modifiers: {
160+
// Root margin from viewport
161+
[`${parseInt(this.offset, 10) || 0}`]: true,
162+
// Once the image is shown, stop observing
163+
once: true
164+
}
165+
})
166+
}
167+
228168
return h(BImg, {
169+
directives,
229170
props: {
230171
// Computed value props
231172
src: this.computedSrc,
232173
blank: this.computedBlank,
233174
width: this.computedWidth,
234175
height: this.computedHeight,
235-
// Passthough props
176+
// Passthrough props
236177
alt: this.alt,
237178
blankColor: this.blankColor,
238179
fluid: this.fluid,

0 commit comments

Comments
 (0)