Skip to content

Commit 38af137

Browse files
Matthew MoyaMatthew Moya
authored andcommitted
Intersection observer for scroll monitoring
1 parent 049942f commit 38af137

File tree

11 files changed

+268
-181
lines changed

11 files changed

+268
-181
lines changed

docs/error.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,6 @@ Example:
1818
});
1919
```
2020

21-
## Error 3
22-
Description: Sizes must be of type `Array` unless breakpoints have been specified
23-
24-
2521
## Error 2
2622
Description: The Plugin or Network has not been included in your bundle.
2723
Please manually include the script tag associated with this plugin or network.
@@ -42,6 +38,10 @@ Example:
4238
</script>
4339
```
4440

41+
## Error 3
42+
Description: Sizes must be of type `Array` unless breakpoints have been specified
43+
44+
4545
## Error 4
4646
Description: An ad must be passed into the GenericPlugin class. If your Plugin inherits from GenericPlugin
4747
and overrides the constructor make sure you are calling "super" and that you are passing in an

docs/lazy-load-plugin.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ In order to avoid the penalties imposed by loading slow creatives at load time,
1414
The AdJS Auto Render plugin delays the loading of the creative until the creative is in the viewport. This ensures that
1515
the creatives only load when the visitor is ready to see them.
1616

17+
## External Dependencies
18+
This Plugin utilizes IntersectionObserver which is only fully supported in modern browsers.
19+
If you require support for older browsers, please be sure to include a polyfill in your application.
20+
1721
## Installation
1822
Depending on your method of implementation, AdJS packages may be installed via different methods.
1923
Please follow the directions for your relevant method.
@@ -68,6 +72,7 @@ The Auto Render Plugin adds two options to ad instantiation
6872
|autoRender|false|When set to true, AdJS will automatically render the ad when it enters the viewport (plus the offset provided if any)|
6973
|renderOffset|0|A measurement in pixels or percentage that AdJS will use to determine how far away from the viewport to load the ad|
7074
|enableByScroll|false|If enabled, offsets which evaluate to in view on load will not allow execution until user scrolls|
75+
|clearOnExitViewport|false|If enabled, ad will be cleared when scrolled back out of view|
7176

7277
## Examples
7378

@@ -85,6 +90,7 @@ const page = new AdJS.Page(Network, {
8590
autoLoad: true,
8691
renderOffset: 0,
8792
enableByScroll: false,
93+
clearOnExitViewport: false,
8894
}
8995
});
9096
```

docs/refresh-plugin.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ Viewability is a binary metric. Your creative will either generate a "viewable i
33

44
The AdJS refresh plugin helps you maximize your impressions per page by refreshing/fetching a new creative after your Ad has been considered as "viewed". By default the AutoRefresh Plugin will only refresh an Ad after 30 seconds of view time.
55

6+
## External Dependencies
7+
This Plugin utilizes IntersectionObserver which is only fully supported in modern browsers.
8+
If you require support for older browsers, please be sure to include a polyfill in your application.
9+
610
## Installation
711
Depending on your method of implementation, AdJS packages may be installed via different methods.
812
Please follow the directions for your relevant method.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "adjs",
3-
"version": "2.0.0-beta.24",
3+
"version": "2.0.0-beta.26",
44
"description": "Ad Library to simplify and optimize integration with ad networks such as DFP",
55
"main": "./core.js",
66
"types": "./types.d.ts",

src/plugins/AutoRefresh.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { LOG_LEVELS } from '../types';
22
import dispatchEvent from '../utils/dispatchEvent';
3-
import ScrollMonitor from '../utils/scrollMonitor';
3+
import ITXObserver from '../utils/intersectionObserver';
44
import GenericPlugin from './GenericPlugin';
55

66
const ONE_SECOND = 1000;
@@ -19,18 +19,18 @@ class AutoRefresh extends GenericPlugin {
1919
}
2020

2121
public beforeClear() {
22-
ScrollMonitor.unsubscribe(this.ad.el.id);
22+
ITXObserver.unsubscribe(this.ad.el.id);
2323
dispatchEvent(this.ad.id, LOG_LEVELS.INFO, 'AutoRefresh Plugin', 'Ad viewability monitor has been removed.');
2424
}
2525

2626
public beforeDestroy() {
27-
ScrollMonitor.unsubscribe(this.ad.el.id);
27+
ITXObserver.unsubscribe(this.ad.el.id);
2828
dispatchEvent(this.ad.id, LOG_LEVELS.INFO, 'AutoRefresh Plugin', 'Ad viewability monitor has been removed.');
2929
}
3030

3131
private startMonitoringViewability(): void {
3232
const { container, configuration: { offset = 0 }, el } = this.ad;
33-
ScrollMonitor.subscribe(
33+
ITXObserver.subscribe(
3434
el.id,
3535
container,
3636
offset,

src/plugins/AutoRender.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { LOG_LEVELS } from '../types';
22
import dispatchEvent from '../utils/dispatchEvent';
3-
import ScrollMonitor from '../utils/scrollMonitor';
3+
import ITXObserver from '../utils/intersectionObserver';
44
import GenericPlugin from './GenericPlugin';
55

66
class AutoRender extends GenericPlugin {
@@ -11,12 +11,21 @@ class AutoRender extends GenericPlugin {
1111

1212
const {
1313
container,
14-
configuration: { renderOffset, offset, enableByScroll },
14+
configuration: { renderOffset, offset, enableByScroll, clearOnExitViewport },
1515
el: { id } } = this.ad;
1616

1717
const finalOffset = renderOffset || offset || 0;
1818

19-
ScrollMonitor.subscribe(id, container, finalOffset, this.onEnterViewport, undefined, undefined, enableByScroll);
19+
ITXObserver.subscribe(
20+
id,
21+
container,
22+
finalOffset,
23+
this.onEnterViewport,
24+
undefined,
25+
clearOnExitViewport ? this.onExitViewport : undefined,
26+
enableByScroll,
27+
);
28+
2029
dispatchEvent(this.ad.id, LOG_LEVELS.INFO, 'AutoRender Plugin', `Ad's scroll monitor has been created.`);
2130
}
2231

@@ -28,6 +37,15 @@ class AutoRender extends GenericPlugin {
2837
dispatchEvent(this.ad.id, LOG_LEVELS.INFO, 'AutoRender Plugin', 'Ad has entered the viewport. Calling render().');
2938
this.ad.render();
3039
}
40+
41+
private onExitViewport = () => {
42+
if (!this.isEnabled('autoRender')) {
43+
return;
44+
}
45+
46+
dispatchEvent(this.ad.id, LOG_LEVELS.INFO, 'AutoRender Plugin', 'Ad has exited the viewport. Calling clear().');
47+
this.ad.clear();
48+
}
3149
}
3250

3351
export default AutoRender;

src/types.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ export interface IAdConfiguration {
165165
autoRender?: boolean;
166166
sticky?: boolean;
167167
enableByScroll?: boolean;
168+
clearOnExitViewport?: boolean;
168169

169170
breakpoints?: IAdBreakpoints;
170171
offset?: number;
@@ -180,14 +181,45 @@ export interface IAdConfiguration {
180181
targeting?: IAdTargeting;
181182
}
182183

183-
export interface IScrollMonitorRegisteredAd {
184+
export interface IIntersectionObserverRegisteredAd {
184185
element: HTMLElement;
185186
offset: number;
186187
inView: boolean;
187188
fullyInView: boolean;
188189
enableByScroll?: boolean;
189-
hasViewBeenScrolled: boolean;
190190
onEnterViewport: any[];
191191
onFullyEnterViewport: any[];
192192
onExitViewport: any[];
193193
}
194+
195+
interface IBounds {
196+
readonly height: number;
197+
readonly width: number;
198+
readonly top: number;
199+
readonly left: number;
200+
readonly right: number;
201+
readonly bottom: number;
202+
}
203+
204+
export interface IIntersectionObserverEntry {
205+
readonly time: number;
206+
readonly rootBounds: IBounds;
207+
readonly boundingClientRect: IBounds;
208+
readonly intersectionRect: IBounds;
209+
readonly intersectionRatio: number;
210+
readonly target: Element;
211+
}
212+
213+
export type IntersectionObserverCallback = (entries: IntersectionObserverEntry[]) => void;
214+
215+
export declare class IIntersectionObserver {
216+
public readonly root: Element | null;
217+
public readonly rootMargin: string;
218+
public readonly thresholds?: any;
219+
constructor(callback: IntersectionObserverCallback, options?: IntersectionObserverInit);
220+
221+
public observe(target: Element): void;
222+
public unobserve(target: Element): void;
223+
public disconnect(): void;
224+
public takeRecords(): IntersectionObserverEntry[];
225+
}

src/utils/intersectionObserver.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { IIntersectionObserver, IIntersectionObserverEntry, IIntersectionObserverRegisteredAd } from '../types';
2+
3+
class ITXObserver {
4+
public static observer: IIntersectionObserver;
5+
public static registeredAds: { [key: string]: IIntersectionObserverRegisteredAd } = {};
6+
public static adCount: number = 0;
7+
public static hasViewBeenScrolled: boolean = false;
8+
9+
public static monitorViewport() {
10+
if (ITXObserver.observer) {
11+
return;
12+
}
13+
14+
window.addEventListener('scroll', ITXObserver.onFirstScroll, false);
15+
16+
ITXObserver.observer = new IntersectionObserver((entries: any) => {
17+
ITXObserver.handleIntersect(entries);
18+
}, { threshold: [0, 0.25, 1] });
19+
}
20+
21+
public static handleIntersect = (entries: IIntersectionObserverEntry[]) => {
22+
entries.forEach((event: IIntersectionObserverEntry) => {
23+
const ad = ITXObserver.registeredAds[event.target.id];
24+
25+
if (ad.enableByScroll && !ITXObserver.hasViewBeenScrolled) {
26+
return;
27+
}
28+
29+
const ratio = event.intersectionRatio;
30+
const fullyInView = ratio === 1;
31+
const inView = ratio > 0;
32+
33+
if (fullyInView && !ad.fullyInView) {
34+
ad.onFullyEnterViewport.forEach((fn) => fn());
35+
}
36+
37+
if (inView && !ad.inView) {
38+
ad.onEnterViewport.forEach((fn) => fn());
39+
}
40+
41+
if (!inView && ad.inView) {
42+
ad.onExitViewport.forEach((fn) => fn());
43+
}
44+
45+
ad.inView = inView;
46+
ad.fullyInView = fullyInView;
47+
});
48+
}
49+
50+
public static subscribe = (
51+
id: string,
52+
element: HTMLElement,
53+
offset: number = 0,
54+
onEnterViewport?: () => any,
55+
onFullyEnterViewport?: () => any,
56+
onExitViewport?: () => any,
57+
enableByScroll?: boolean,
58+
) => {
59+
ITXObserver.monitorViewport();
60+
61+
const existingAd = ITXObserver.getAdIfExists(id);
62+
63+
if (existingAd) {
64+
if (onEnterViewport) {
65+
existingAd.onEnterViewport.push(onEnterViewport);
66+
}
67+
68+
if (onFullyEnterViewport) {
69+
existingAd.onFullyEnterViewport.push(onFullyEnterViewport);
70+
}
71+
72+
if (onExitViewport) {
73+
existingAd.onExitViewport.push(onExitViewport);
74+
}
75+
76+
return;
77+
}
78+
79+
const ad: IIntersectionObserverRegisteredAd = {
80+
element,
81+
offset,
82+
inView: false,
83+
fullyInView: false,
84+
enableByScroll,
85+
onEnterViewport: onEnterViewport ? [onEnterViewport] : [],
86+
onFullyEnterViewport: onFullyEnterViewport ? [onFullyEnterViewport] : [],
87+
onExitViewport: onExitViewport ? [onExitViewport] : [],
88+
};
89+
90+
ad.element.id = id;
91+
ITXObserver.registeredAds[id] = ad;
92+
ITXObserver.observer.observe(ad.element);
93+
++ITXObserver.adCount;
94+
}
95+
96+
public static unsubscribe = (id: string) => {
97+
const ad = ITXObserver.registeredAds[id];
98+
99+
if (!ad) {
100+
return;
101+
}
102+
103+
ITXObserver.observer.unobserve(ad.element);
104+
105+
delete ITXObserver.registeredAds[id];
106+
--ITXObserver.adCount;
107+
}
108+
109+
private static getAdIfExists = (id: string) => {
110+
return ITXObserver.registeredAds[id];
111+
}
112+
113+
private static onFirstScroll = () => {
114+
ITXObserver.hasViewBeenScrolled = true;
115+
window.removeEventListener('scroll', ITXObserver.onFirstScroll, false);
116+
}
117+
}
118+
119+
export default ITXObserver;

0 commit comments

Comments
 (0)