forked from ampproject/amphtml
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathintersection-observer.js
198 lines (185 loc) · 7.07 KB
/
intersection-observer.js
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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
/**
* Copyright 2015 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Observable} from './observable';
import {assert} from './asserts';
import {layoutRectLtwh, rectIntersection, moveLayoutRect} from './layout-rect';
import {listen, postMessage} from './iframe-helper';
import {parseUrl} from './url';
/**
* Produces a change entry for that should be compatible with
* IntersectionObserverEntry.
*
* Mutates passed in rootBounds to have x and y according to spec.
*
* @param {number} time Time when values below were measured.
* @param {!LayoutRect} rootBounds Equivalent to viewport.getRect()
* @param {!LayoutRect} elementLayoutBox Layout box of the element
* that may intersect with the rootBounds.
* @return {!IntersectionObserverEntry} A change entry.
* @private
*/
export function getIntersectionChangeEntry(
measureTime, rootBounds, elementLayoutBox) {
// Building an IntersectionObserverEntry.
// http://rawgit.com/slightlyoff/IntersectionObserver/master/index.html#intersectionobserverentry
// These should always be equal assuming rootBounds cannot have negative
// dimension.
rootBounds.x = rootBounds.left;
rootBounds.y = rootBounds.top;
const boundingClientRect =
moveLayoutRect(elementLayoutBox, -1 * rootBounds.x, -1 * rootBounds.y);
assert(boundingClientRect.width >= 0 &&
boundingClientRect.height >= 0, 'Negative dimensions in ad.');
boundingClientRect.x = boundingClientRect.left;
boundingClientRect.y = boundingClientRect.top;
const intersectionRect =
rectIntersection(rootBounds, elementLayoutBox) ||
// No intersection.
layoutRectLtwh(0, 0, 0, 0);
intersectionRect.x = intersectionRect.left;
intersectionRect.y = intersectionRect.top;
return {
time: measureTime,
rootBounds,
boundingClientRect,
intersectionRect,
};
}
/**
* The IntersectionObserver class lets any element share its viewport
* intersection data with an iframe of its choice (most likely contained within
* the element itself.). When instantiated the class will start listening for
* a 'send-intersection' postMessage from the iframe, and only then would start
* sending intersection data to the iframe. The intersection data would be sent
* when the element is moved inside or outside the viewport as well as on
* scroll and resize.
* The element should create an IntersectionObserver instance once the Iframe
* element is created.
* The IntersectionObserver class exposes a `fire` method that would send the
* intersection data to the iframe.
* The IntersectionObserver class exposes a `onViewportCallback` method that
* should be called inside if the viewportCallback of the element. This would
* let the element sent intersection data automatically when there element comes
* inside or goes outside the viewport and also manage sending intersection data
* onscroll and resize.
* Note: The IntersectionObserver would not send any data over to the iframe if
* it had not requested the intersection data already via a postMessage.
*/
export class IntersectionObserver extends Observable {
/**
* @param {!BaseElement} element.
* @param {!Element} iframe Iframe element to which would request intersection
* data.
* @param {?boolean} opt_is3p Set to `true` when the iframe is 3'rd party.
* @constructor
* @extends {Observable}
*/
constructor(baseElement, iframe, opt_is3p) {
super();
/** @private @const */
this.baseElement_ = baseElement;
/** @private {?Element} */
this.iframe_ = iframe;
/** @private {boolean} */
this.is3p_ = opt_is3p || false;
/** @private {boolean} */
this.shouldSendIntersectionChanges_ = false;
/** @private {Array<function>} */
this.unlisteners_ = [];
this.init_();
}
init_() {
// Triggered by context.observeIntersection(…) inside the ad/iframe.
// We use listen instead of listenOnce, because a single ad/iframe might
// have multiple parties wanting to receive viewability data.
// The second time this is called, it doesn't do much but it
// guarantees that the receiver gets an initial intersection change
// record.
this.unlisteners_.push(listen(this.iframe_, 'send-intersections', () => {
this.startSendingIntersectionChanges_();
}, this.is3p_));
this.unlisteners_.push(this.add(() => {
this.sendElementIntersection_();
}));
}
dispose() {
//used only in tests.
this.unlisteners_.forEach(unlisten => {
unlisten();
});
this.unlisteners_ = [];
}
/**
* Called via postMessage from the child iframe when the ad/iframe starts
* observing its position in the viewport.
* Sets a flag, measures the iframe position if necessary and sends
* one change record to the iframe.
* Note that this method may be called more than once if a single ad
* has multiple parties interested in viewability data.
* @private
*/
startSendingIntersectionChanges_() {
this.shouldSendIntersectionChanges_ = true;
this.baseElement_.getVsync().measure(() => {
this.sendElementIntersection_();
});
}
/**
* Triggered by the AmpElement to when it either enters or exits the visible
* viewport.
* @param {boolean} inViewport true if the element is in viewport.
*/
onViewportCallback(inViewport) {
// Lets the ad know that it became visible or no longer is.
this.fire();
// And update the ad about its position in the viewport while
// it is visible.
if (inViewport) {
const send = this.fire.bind(this);
// Scroll events.
const unlistenScroll = this.baseElement_.getViewport().onScroll(send);
// Throttled scroll events. Also fires for resize events.
const unlistenChanged = this.baseElement_.getViewport().onChanged(send);
this.unlistenViewportChanges_ = () => {
unlistenScroll();
unlistenChanged();
};
} else if (this.unlistenViewportChanges_) {
this.unlistenViewportChanges_();
this.unlistenViewportChanges_ = null;
}
}
/**
* Sends 'intersection' message to ad/iframe with intersection change records
* if this has been activated and we measured the layout box of the iframe
* at least once.
* @private
*/
sendElementIntersection_() {
if (!this.shouldSendIntersectionChanges_) {
return;
}
const change = this.baseElement_.element.getIntersectionChangeEntry();
const targetOrigin =
this.iframe_.src ? parseUrl(this.iframe_.src).origin : '*';
postMessage(
this.iframe_,
'intersection',
{changes: [change]},
targetOrigin,
this.is3p_);
}
}