-
Notifications
You must be signed in to change notification settings - Fork 15
/
index.coffee
165 lines (130 loc) · 5.07 KB
/
index.coffee
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
# Mixin definition
export default
# Public interface
props:
# Add listeners and check if in viewport immediately
inViewportActive:
type: Boolean
default: true
# Only update once by default. The assumption is that it will be used for
# one-time buildins
inViewportOnce:
type: Boolean
default: false
# The IntersectionObserver root margin adds offsets to when the now and
# fully get updated.
inViewportRootMargin:
type: [Number, String]
default: '0px 0px -1px 0px'
# Specify the IntersectionObserver root to use.
inViewportRoot:
type: [String, Function, Object]
default: undefined
# The IntersectionObserver threshold defines the intersection ratios that
# fire the observer callback
inViewportThreshold:
type: [Number, Array]
default: -> [0, 1] # Fire on enter/leave and fully enter/leave
# Bindings that are used by the host component
data: -> inViewport:
# Public props
now: null # Is in viewport
fully: null # Is fully in viewport
above: null # Is partially or fully above the viewport
below: null # Is partially or fully below the viewport
# Internal props
listening: false
maxThreshold: 1
# Lifecycle hooks
mounted: -> @$nextTick(@inViewportInit)
unmounted: -> @removeInViewportHandlers()
computed:
# Add the maxThreshold to the @inViewportThreshold prop so that the handler
# is fired for elements that are taller than the viewport
inViewportThresholdWithMax: ->
# Support number and array thresholds
threshold =
if typeof @inViewportThreshold == 'object'
then @inViewportThreshold
else [ @inViewportThreshold ]
# Add only if not already in the threshold list
if @inViewport.maxThreshold in threshold
then threshold
else threshold.concat @inViewport.maxThreshold
# Watch props and data
watch:
# Add or remove event handlers handlers
inViewportActive: (active) ->
if active
then @addInViewportHandlers()
else @removeInViewportHandlers()
# If any of the Observer options change, re-init.
inViewportRootMargin: -> @reInitInViewportMixin()
inViewportRoot: -> @reInitInViewportMixin()
inViewportThresholdWithMax:
handler: (now, old) ->
# In IE, this kept getting retriggered, so doing a manual comparison
# of old and new before deciding whether to take action.
@reInitInViewportMixin() unless now.toString() == old.toString()
deep: true
# Public API
methods:
# Re-init
reInitInViewportMixin: ->
@removeInViewportHandlers()
@inViewportInit()
# Instantiate
inViewportInit: -> @addInViewportHandlers() if @inViewportActive
# Add listeners
addInViewportHandlers: ->
# Don't add twice
return if @inViewport.listening
@inViewport.listening = true
# Create IntersectionObserver instance
@inViewportObserver = new IntersectionObserver @updateInViewport,
root: switch typeof @inViewportRoot
when 'function' then @inViewportRoot()
when 'string' then document.querySelector @inViewportRoot
when 'object' then @inViewportRoot # Expects to be a DOMElement
else undefined
rootMargin: @inViewportRootMargin
threshold: @inViewportThresholdWithMax
# Start listening
@inViewportObserver.observe @$el
# Remove listeners
removeInViewportHandlers: ->
# Don't remove twice
return unless @inViewport.listening
@inViewport.listening = false
# Destroy instance, which also removes listeners
@inViewportObserver?.disconnect()
delete @inViewportObserver
# Handle state changes. There should only ever be one entry and we're
# destructuring the properties we care about since they have long names.
updateInViewport: ([..., {
boundingClientRect: target,
rootBounds: root
}]) ->
# Cleanup if root is missing, like if the element is removed from DOM
return @removeInViewportHandlers() unless target and root
# Get the maximum threshold ratio, which is less than 1 when the
# element is taller than the viewport. The height may be 0 when the
# parent element is hidden.
@inViewport.maxThreshold = if target.height > 0
then Math.min 1, root.height / target.height else 1
# Check if some part of the target is in the root box. The isIntersecting
# property from the IntersectionObserver was not used because it reports
# the case where a box is immediately offscreen as intersecting, even
# though no aprt of it is visible.
@inViewport.now = target.top <= root.bottom and target.bottom > root.top
# Calculate above and below. The +1 on the bottom check co-incides with
# the default root-margin which has a -1 on the bottom margin.
@inViewport.above = target.top < root.top
@inViewport.below = target.bottom > root.bottom + 1
# Determine whether fully in viewport. The rules are different based on
# whether the target is taller than the viewport.
@inViewport.fully = if target.height > root.height
then target.top <= root.top and target.bottom >= root.bottom + 1
else not @inViewport.above and not @inViewport.below
# If set to update "once", remove listeners if in viewport
@removeInViewportHandlers() if @inViewportOnce and @inViewport.now