visible.js
5.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
// v-b-visible
// Private visibility check directive
// Based on IntersectionObserver
//
// Usage:
// v-b-visibility.<margin>.<once>="<callback>"
//
// Value:
// <callback>: method to be called when visibility state changes, receives one arg:
// true: element is visible
// false: element is not visible
// null: IntersectionObserver not supported
//
// Modifiers:
// <margin>: a positive decimal value of pixels away from viewport edge
// before being considered "visible". default is 0
// <once>: keyword 'once', meaning when the element becomes visible and
// callback is called observation/notification will stop.
//
// When used in a render function:
// export default {
// directives: { 'b-visible': VBVisible },
// render(h) {
// h(
// 'div',
// {
// directives: [
// { name: 'b-visible', value=this.callback, modifiers: { '123':true, 'once':true } }
// ]
// }
// )
// }
import { RX_DIGITS } from '../../constants/regex'
import { requestAF } from '../../utils/dom'
import { isFunction } from '../../utils/inspect'
import { looseEqual } from '../../utils/loose-equal'
import { clone, keys } from '../../utils/object'
import { nextTick } from '../../vue'
const OBSERVER_PROP_NAME = '__bv__visibility_observer'
class VisibilityObserver {
constructor(el, options) {
this.el = el
this.callback = options.callback
this.margin = options.margin || 0
this.once = options.once || false
this.observer = null
this.visible = undefined
this.doneOnce = false
// Create the observer instance (if possible)
this.createObserver()
}
createObserver() {
// Remove any previous observer
if (this.observer) {
/* istanbul ignore next */
this.stop()
}
// Should only be called once and `callback` prop should be a function
if (this.doneOnce || !isFunction(this.callback)) {
/* istanbul ignore next */
return
}
// Create the observer instance
try {
// Future: Possibly add in other modifiers for left/right/top/bottom
// offsets, root element reference, and thresholds
this.observer = new IntersectionObserver(this.handler.bind(this), {
// `null` = 'viewport'
root: null,
// Pixels away from view port to consider "visible"
rootMargin: this.margin,
// Intersection ratio of el and root (as a value from 0 to 1)
threshold: 0
})
} catch {
// No IntersectionObserver support, so just stop trying to observe
this.doneOnce = true
this.observer = undefined
this.callback(null)
return
}
// Start observing in a `$nextTick()` (to allow DOM to complete rendering)
/* istanbul ignore next: IntersectionObserver not supported in JSDOM */
nextTick(() => {
requestAF(() => {
// Placed in an `if` just in case we were destroyed before
// this `requestAnimationFrame` runs
if (this.observer) {
this.observer.observe(this.el)
}
})
})
}
/* istanbul ignore next */
handler(entries) {
const entry = entries ? entries[0] : {}
const isIntersecting = Boolean(entry.isIntersecting || entry.intersectionRatio > 0.0)
if (isIntersecting !== this.visible) {
this.visible = isIntersecting
this.callback(isIntersecting)
if (this.once && this.visible) {
this.doneOnce = true
this.stop()
}
}
}
stop() {
/* istanbul ignore next */
this.observer && this.observer.disconnect()
this.observer = null
}
}
const destroy = el => {
const observer = el[OBSERVER_PROP_NAME]
if (observer && observer.stop) {
observer.stop()
}
delete el[OBSERVER_PROP_NAME]
}
const bind = (el, { value, modifiers }) => {
// `value` is the callback function
const options = {
margin: '0px',
once: false,
callback: value
}
// Parse modifiers
keys(modifiers).forEach(mod => {
/* istanbul ignore else: Until <b-img-lazy> is switched to use this directive */
if (RX_DIGITS.test(mod)) {
options.margin = `${mod}px`
} else if (mod.toLowerCase() === 'once') {
options.once = true
}
})
// Destroy any previous observer
destroy(el)
// Create new observer
el[OBSERVER_PROP_NAME] = new VisibilityObserver(el, options)
// Store the current modifiers on the object (cloned)
el[OBSERVER_PROP_NAME]._prevModifiers = clone(modifiers)
}
// When the directive options may have been updated (or element)
const componentUpdated = (el, { value, oldValue, modifiers }, vnode) => {
// Compare value/oldValue and modifiers to see if anything has changed
// and if so, destroy old observer and create new observer
/* istanbul ignore next */
modifiers = clone(modifiers)
/* istanbul ignore next */
if (
el &&
(value !== oldValue ||
!el[OBSERVER_PROP_NAME] ||
!looseEqual(modifiers, el[OBSERVER_PROP_NAME]._prevModifiers))
) {
// Re-bind on element
bind(el, { value, modifiers }, vnode)
}
}
// When directive un-binds from element
const unbind = el => {
// Remove the observer
destroy(el)
}
// Export the directive
export const VBVisible = {
bind,
componentUpdated,
unbind
}