Skip to content

Commit 498efb3

Browse files
SparshithNRasakusuma
authored andcommitted
50
linkedin#50 : Added Root implementation. Added couple of addtional changes, 1)few CSS, logging changes. 2) extended watcher feature to watch WRT root element, added usage information to usage.md file
1 parent 348f1ff commit 498efb3

11 files changed

+152
-23
lines changed

USAGE.md

+1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ The `Watcher` constructor can be passed 3 different options:
106106
* `time` - The time threshold
107107
* `ratio` - The ratio threshold
108108
* `rootMargin` - The [rootMargin](https://wicg.github.io/IntersectionObserver/#dom-intersectionobserverinit-rootmargin) in object form.
109+
* `root` - The [root](https://wicg.github.io/IntersectionObserver/#intersectionobserver-intersection-root) element with respect to which we want to watch the target. By default it is window.
109110

110111
## Utility API
111112

src/intersection-observer.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -132,14 +132,15 @@ export class SpanielIntersectionObserver implements IntersectionObserver {
132132
this.id = generateToken();
133133
options.threshold = options.threshold || 0;
134134
this.rootMarginObj = rootMarginToDOMMargin(options.rootMargin || '0px');
135+
this.root = options.root;
135136

136137
if (Array.isArray(options.threshold)) {
137138
this.thresholds = <Array<number>>options.threshold;
138139
} else {
139140
this.thresholds = [<number>options.threshold];
140141
}
141142

142-
this.scheduler = new ElementScheduler();
143+
this.scheduler = new ElementScheduler(null, this.root);
143144
}
144145
};
145146

@@ -182,8 +183,8 @@ export class IntersectionObserverEntry implements IntersectionObserverEntryInit
182183
export function generateEntry(frame: Frame, bcr: DOMRectReadOnly, el: Element, rootMargin: DOMMargin): IntersectionObserverEntry {
183184
let { top, bottom, left, right } = bcr;
184185
let rootBounds: ClientRect = {
185-
left: rootMargin.left,
186-
top: rootMargin.top,
186+
left: frame.x + rootMargin.left,
187+
top: frame.y + rootMargin.top,
187188
bottom: rootMargin.bottom,
188189
right: rootMargin.right,
189190
width: frame.width - (rootMargin.right + rootMargin.left),

src/metal/interfaces.ts

+2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ export interface FrameInterface {
4949
timestamp: number;
5050
scrollTop: number;
5151
scrollLeft: number;
52+
x: number;
53+
y: number;
5254
width: number;
5355
height: number;
5456
}

src/metal/scheduler.ts

+25-5
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,30 @@ export class Frame implements FrameInterface {
3232
public timestamp: number,
3333
public scrollTop: number,
3434
public scrollLeft: number,
35+
public x: number,
36+
public y: number,
3537
public width: number,
3638
public height: number
3739
) {}
38-
static generate(): Frame {
40+
static generate(root?: Element): Frame {
41+
if (root && document.documentElement.contains(root)) {
42+
let rootBcr = root.getBoundingClientRect();
43+
return new Frame(
44+
Date.now(),
45+
root.scrollTop,
46+
root.scrollLeft,
47+
rootBcr.left,
48+
rootBcr.top,
49+
rootBcr.width,
50+
rootBcr.height
51+
);
52+
}
3953
return new Frame(
4054
Date.now(),
4155
W.getScrollTop(),
4256
W.getScrollLeft(),
57+
0,
58+
0,
4359
W.getWidth(),
4460
W.getHeight()
4561
);
@@ -51,16 +67,20 @@ export function generateToken() {
5167
}
5268

5369
export abstract class BaseScheduler {
70+
protected root: Element;
5471
protected engine: EngineInterface;
5572
protected queue: QueueInterface;
5673
protected isTicking: Boolean = false;
5774
protected toRemove: Array<string| Element | Function> = [];
58-
constructor(customEngine?: EngineInterface) {
75+
constructor(customEngine?: EngineInterface, root?: Element) {
5976
if (customEngine) {
6077
this.engine = customEngine;
6178
} else {
6279
this.engine = getGlobalEngine();
6380
}
81+
if (root) {
82+
this.root = root;
83+
}
6484
}
6585
protected abstract applyQueue(frame: Frame): void;
6686

@@ -74,7 +94,7 @@ export abstract class BaseScheduler {
7494
}
7595
this.toRemove = [];
7696
}
77-
this.applyQueue(Frame.generate());
97+
this.applyQueue(Frame.generate(this.root));
7898
this.engine.scheduleRead(this.tick.bind(this));
7999
}
80100
}
@@ -143,8 +163,8 @@ export class PredicatedScheduler extends Scheduler implements SchedulerInterface
143163

144164
export class ElementScheduler extends BaseScheduler implements ElementSchedulerInterface {
145165
protected queue: DOMQueue;
146-
constructor(customEngine?: EngineInterface) {
147-
super(customEngine);
166+
constructor(customEngine?: EngineInterface, root?: Element) {
167+
super(customEngine, root);
148168
this.queue = new DOMQueue();
149169
}
150170
applyQueue(frame: Frame) {

src/native-watcher.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export interface WatcherConfig {
2525
ratio?: number;
2626
time?: number;
2727
rootMargin?: DOMString | DOMMargin;
28+
root?: SpanielTrackedElement;
2829
}
2930

3031
export type EventName = 'impressed' | 'exposed' | 'visible' | 'impression-complete';
@@ -60,7 +61,7 @@ function onEntry(entries: SpanielObserverEntry[]) {
6061
export class Watcher {
6162
observer: SpanielObserver;
6263
constructor(ObserverClass: IntersectionObserverClass, config: WatcherConfig = {}) {
63-
let { time, ratio, rootMargin } = config;
64+
let { time, ratio, rootMargin, root } = config;
6465

6566
let threshold: Threshold[] = [
6667
{
@@ -88,7 +89,8 @@ export class Watcher {
8889

8990
this.observer = new SpanielObserver(ObserverClass, onEntry, {
9091
rootMargin,
91-
threshold
92+
threshold,
93+
root
9294
});
9395
}
9496
watch(el: Element, callback: WatcherCallback) {

src/watcher.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface WatcherConfig {
2424
ratio?: number;
2525
time?: number;
2626
rootMargin?: DOMString | DOMMargin;
27+
root?: SpanielTrackedElement;
2728
}
2829

2930
export type EventName = 'impressed' | 'exposed' | 'visible' | 'impression-complete';
@@ -59,7 +60,7 @@ function onEntry(entries: SpanielObserverEntry[]) {
5960
export class Watcher {
6061
observer: SpanielObserver;
6162
constructor(config: WatcherConfig = {}) {
62-
let { time, ratio, rootMargin } = config;
63+
let { time, ratio, rootMargin, root } = config;
6364

6465
let threshold: Threshold[] = [
6566
{
@@ -87,7 +88,8 @@ export class Watcher {
8788

8889
this.observer = new SpanielObserver(onEntry, {
8990
rootMargin,
90-
threshold
91+
threshold,
92+
root
9193
});
9294
}
9395
watch(el: Element, callback: WatcherCallback) {

test/app/index.html

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
<div id="app">
88

99
</div>
10-
10+
<div id="root">
11+
</div>
1112
<script type="text/javascript" src="../../exports/spaniel.js"></script>
1213
<script type="text/javascript" src="setup.js"></script>
1314
<script type="text/javascript" src="index.js"></script>

test/app/index.js

+42
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,45 @@ let observer = new spaniel.SpanielObserver(function(changes) {
5959
}]
6060
});
6161
observer.observe(target);
62+
63+
//Example usage of SpanielObserver with a custom root element
64+
var root = document.getElementById('root');
65+
var rootTarget = document.querySelector('.tracked-item-root[data-root-target-id="5"]');
66+
var rootObserver = new spaniel.SpanielObserver(function(changes) {
67+
console.log(changes[0]);
68+
}, {
69+
root: root,
70+
rootMargin: '0px 0px',
71+
threshold: [{
72+
label: 'impressed',
73+
ratio: 0.5,
74+
time: 1000
75+
}]
76+
});
77+
78+
//Sample usage of watcher with Root element
79+
window.rootWatcher = new spaniel.Watcher({
80+
time: 100,
81+
ratio: 0.8,
82+
root: root
83+
});
84+
rootObserver.observe(rootTarget);
85+
86+
var elements = document.getElementsByClassName('tracked-item-root');
87+
88+
for (var i = 0; i < elements.length; i++) {
89+
(function(el) {
90+
if (i < 6) {
91+
var id = el.getAttribute('data-root-target-id');
92+
window.rootWatcher.watch(el, function(e, meta) {
93+
var end = meta && meta.duration ? ' for ' + meta.duration + ' milliseconds' : '';
94+
console.log('root: '+id + ' ' + e + end);
95+
GLOBAL_TEST_EVENTS.push({
96+
id: parseInt(id),
97+
e: e,
98+
meta: meta || {}
99+
});
100+
});
101+
}
102+
})(elements[i]);
103+
}

test/app/setup.js

+11
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,14 @@ for (var i = 0; i < 100; i++) {
1515
app.appendChild(el);
1616
}
1717

18+
/* Root feature test div */
19+
var root = document.getElementById('root');
20+
for (var i = 0; i < 30; i++) {
21+
var id = i + 1;
22+
var content = Math.random(0, 100);
23+
var el = document.createElement('div');
24+
el.setAttribute('class', 'tracked-item-root');
25+
el.setAttribute('data-root-target-id', id);
26+
el.innerHTML = 'ID: ' + id + ' = ' + content;
27+
root.appendChild(el);
28+
}

test/app/style.css

+14-2
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ html, body {
33
padding: 0px;
44
}
55

6-
.tracked-item {
6+
.tracked-item, .tracked-item-root {
77
height: 100px;
88
background-color: #EEE;
99
margin: 0;
1010
padding: 0;
11+
width: 150px;
1112
}
1213

13-
.tracked-item:nth-child(even) {
14+
.tracked-item:nth-child(even), .tracked-item-root:nth-child(even) {
1415
background-color: #DDD;
1516
}
1617

@@ -39,3 +40,14 @@ nav {
3940
width: 100%;
4041
height: 100px;
4142
}
43+
#app {
44+
float: left;
45+
}
46+
#root {
47+
top: 100px;
48+
left: 200px;
49+
height: 250px;
50+
width: 150px;
51+
overflow-y: scroll;
52+
position: absolute;
53+
}

test/headless/specs/intersection-observer.js

+43-8
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import {
1010
TestClass
1111
} from './../test-module';
1212

13+
import constants from './../../constants.js';
14+
15+
const { time: { IMPRESSION_THRESHOLD } } = constants;
16+
1317
testModule('IntersectionObserver', class extends TestClass {
1418
['@test observing a visible element should fire callback immediately']() {
1519
return this.context.evaluate(() => {
@@ -81,9 +85,9 @@ testModule('IntersectionObserver', class extends TestClass {
8185
});
8286
observer.observe(target);
8387
})
84-
.wait(100)
88+
.wait(IMPRESSION_THRESHOLD)
8589
.scrollTo(80)
86-
.wait(100)
90+
.wait(IMPRESSION_THRESHOLD)
8791
.getExecution()
8892
.evaluate(function() {
8993
return window.STATE.impressions;
@@ -103,11 +107,11 @@ testModule('IntersectionObserver', class extends TestClass {
103107
});
104108
observer.observe(target);
105109
})
106-
.wait(100)
110+
.wait(IMPRESSION_THRESHOLD)
107111
.scrollTo(80)
108-
.wait(100)
112+
.wait(IMPRESSION_THRESHOLD)
109113
.scrollTo(70)
110-
.wait(100)
114+
.wait(IMPRESSION_THRESHOLD)
111115
.getExecution()
112116
.evaluate(function() {
113117
return window.STATE.impressions;
@@ -128,11 +132,11 @@ testModule('IntersectionObserver', class extends TestClass {
128132
});
129133
observer.observe(target);
130134
})
131-
.wait(100)
135+
.wait(IMPRESSION_THRESHOLD)
132136
.scrollTo(105)
133-
.wait(100)
137+
.wait(IMPRESSION_THRESHOLD)
134138
.scrollTo(95)
135-
.wait(100)
139+
.wait(IMPRESSION_THRESHOLD)
136140
.getExecution()
137141
.evaluate(function() {
138142
return window.STATE.impressions;
@@ -230,4 +234,35 @@ testModule('IntersectionObserver', class extends TestClass {
230234
assert.equal(result, 6, 'Callback fired 6 times');
231235
});
232236
}
237+
238+
/* Root inlcusion test case */
239+
['@test observing a non visible element within a root and then scrolling should fire callbacks']() {
240+
return this.context.evaluate(function() {
241+
window.STATE.impressions = 0;
242+
let root = document.getElementById('root');
243+
let target = document.querySelector('.tracked-item-root[data-root-target-id="5"]');
244+
let observer = new spaniel.IntersectionObserver(function() {
245+
window.STATE.impressions++;
246+
}, {
247+
root: root,
248+
threshold: 0.6
249+
});
250+
observer.observe(target);
251+
})
252+
.wait(IMPRESSION_THRESHOLD)
253+
.evaluate(function() {
254+
root.scrollTop = 300;
255+
})
256+
.wait(IMPRESSION_THRESHOLD)
257+
.evaluate(function() {
258+
root.scrollTop = 180;
259+
})
260+
.wait(IMPRESSION_THRESHOLD)
261+
.getExecution()
262+
.evaluate(function() {
263+
return window.STATE.impressions;
264+
}).then(function(result) {
265+
assert.equal(result, 2, 'Callback fired twice');
266+
});
267+
}
233268
});

0 commit comments

Comments
 (0)