Skip to content

Commit 190bcc3

Browse files
committed
backporting the root implementation with memoize feature
1 parent b10891c commit 190bcc3

17 files changed

+274
-150
lines changed

USAGE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ The `Watcher` constructor can be passed 3 different options:
111111

112112
* `time` - The time threshold
113113
* `ratio` - The ratio threshold
114-
* `rootMargin` - The [rootMargin](https://wicg.github.io/IntersectionObserver/#dom-intersectionobserverinit-rootmargin) in object form.
114+
* `rootMargin` - The [rootMargin](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#Intersection_observer_options) in object form.
115+
* `root` - The [root](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#Intersection_observer_options) element with respect to which we want to watch the target. By default it is window.
115116

116117
## Utility API
117118

src/intersection-observer.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ export class SpanielIntersectionObserver implements IntersectionObserver {
109109
takeRecords(): IntersectionObserverEntry[] {
110110
return [];
111111
}
112+
forceStateValidation() {
113+
this.scheduler.forceStateValidation();
114+
}
112115
private generateEntryEvent(frame: Frame, clientRect: DOMRectReadOnly, el: Element): EntryEvent {
113116
let count: number = 0;
114117
let entry = generateEntry(frame, clientRect, el, this.rootMarginObj);
@@ -132,14 +135,14 @@ export class SpanielIntersectionObserver implements IntersectionObserver {
132135
this.id = generateToken();
133136
options.threshold = options.threshold || 0;
134137
this.rootMarginObj = rootMarginToDOMMargin(options.rootMargin || '0px');
135-
138+
this.root = options.root;
136139
if (Array.isArray(options.threshold)) {
137140
this.thresholds = <Array<number>>options.threshold;
138141
} else {
139142
this.thresholds = [<number>options.threshold];
140143
}
141144

142-
this.scheduler = new ElementScheduler();
145+
this.scheduler = new ElementScheduler(null, this.root);
143146
}
144147
};
145148

@@ -182,8 +185,8 @@ export class IntersectionObserverEntry implements IntersectionObserverEntryInit
182185
export function generateEntry(frame: Frame, clientRect: DOMRectReadOnly, el: Element, rootMargin: DOMMargin): IntersectionObserverEntry {
183186
let { top, bottom, left, right } = clientRect;
184187
let rootBounds: ClientRect = {
185-
left: rootMargin.left,
186-
top: rootMargin.top,
188+
left: frame.left + rootMargin.left,
189+
top: frame.top + rootMargin.top,
187190
bottom: rootMargin.bottom,
188191
right: rootMargin.right,
189192
width: frame.width - (rootMargin.right + rootMargin.left),

src/metal/interfaces.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,30 @@ export interface FrameInterface {
5252
scrollLeft: number;
5353
width: number;
5454
height: number;
55+
x: number;
56+
y: number;
57+
top: number;
58+
left: number;
5559
}
5660

5761
export interface MetaInterface {
62+
scrollTop: number;
63+
scrollLeft: number;
5864
width: number;
5965
height: number;
60-
scrollLeft: number;
61-
scrollTop: number;
66+
x: number;
67+
y: number;
68+
top: number;
69+
left: number;
6270
}
6371

64-
export interface OnWindowIsDirtyInterface {
65-
fn: any;
66-
scope: any;
67-
id: string;
72+
export interface SpanielClientRectInterface {
73+
width: number;
74+
height: number;
75+
x: number;
76+
y: number;
77+
bottom: number;
78+
top: number;
79+
left: number;
80+
right: number;
6881
}

src/metal/scheduler.ts

Lines changed: 72 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import {
1414
SchedulerInterface,
1515
ElementSchedulerInterface,
1616
FrameInterface,
17-
QueueInterface
17+
QueueInterface,
18+
MetaInterface,
19+
SpanielClientRectInterface
1820
} from './interfaces';
1921
import W from './window-proxy';
2022

@@ -29,45 +31,85 @@ const TOKEN_SEED = 'xxxx'.replace(/[xy]/g, function(c) {
2931
});
3032
let tokenCounter = 0;
3133

32-
function generateRandomToken() {
33-
return Math.floor(Math.random() * (9999999 - 0o0)).toString(16);
34-
}
35-
3634
export class Frame implements FrameInterface {
3735
constructor(
3836
public timestamp: number,
3937
public scrollTop: number,
4038
public scrollLeft: number,
4139
public width: number,
42-
public height: number
40+
public height: number,
41+
public x: number,
42+
public y: number,
43+
public top: number,
44+
public left: number
4345
) {}
44-
static generate(): Frame {
46+
static generate(root: Element | Window = window): Frame {
47+
const rootMeta = this.revalidateRootMeta(root);
4548
return new Frame(
4649
Date.now(),
47-
W.meta.scrollTop,
48-
W.meta.scrollLeft,
49-
W.meta.width,
50-
W.meta.height
50+
rootMeta.scrollTop,
51+
rootMeta.scrollLeft,
52+
rootMeta.width,
53+
rootMeta.height,
54+
rootMeta.x,
55+
rootMeta.y,
56+
rootMeta.top,
57+
rootMeta.left
5158
);
5259
}
60+
static revalidateRootMeta(root: any = window): MetaInterface {
61+
let _rootMeta: MetaInterface = {
62+
width: 0,
63+
height: 0,
64+
scrollTop: 0,
65+
scrollLeft: 0,
66+
x: 0,
67+
y: 0,
68+
top: 0,
69+
left: 0
70+
};
71+
72+
if (root === window) {
73+
_rootMeta.scrollTop = W.getScrollTop();
74+
_rootMeta.scrollLeft = W.getScrollLeft();
75+
_rootMeta.width = W.getWidth();
76+
_rootMeta.height = W.getHeight();
77+
}else if (root) {
78+
let _clientRect = getBoundingClientRect(root);
79+
_rootMeta.scrollTop = root.scrollTop;
80+
_rootMeta.scrollLeft = root.scrollLeft;
81+
_rootMeta.width = _clientRect.width;
82+
_rootMeta.height = _clientRect.height;
83+
_rootMeta.x = _clientRect.x;
84+
_rootMeta.y = _clientRect.y;
85+
_rootMeta.top = _clientRect.top;
86+
_rootMeta.left = _clientRect.left;
87+
}
88+
89+
return _rootMeta;
90+
}
5391
}
5492

5593
export function generateToken() {
5694
return tokenCounter++ + TOKEN_SEED;
5795
}
5896

5997
export abstract class BaseScheduler {
98+
protected root: Element | Window;
6099
protected engine: EngineInterface;
61100
protected queue: QueueInterface;
62101
protected isTicking: Boolean = false;
63102
protected toRemove: Array<string| Element | Function> = [];
103+
protected id?: string;
64104

65-
constructor(customEngine?: EngineInterface) {
105+
constructor(customEngine?: EngineInterface, root: Element | Window = window) {
66106
if (customEngine) {
67107
this.engine = customEngine;
68108
} else {
69109
this.engine = getGlobalEngine();
70110
}
111+
112+
this.root = root;
71113
}
72114
protected abstract applyQueue(frame: Frame): void;
73115

@@ -81,8 +123,7 @@ export abstract class BaseScheduler {
81123
}
82124
this.toRemove = [];
83125
}
84-
85-
this.applyQueue(Frame.generate());
126+
this.applyQueue(Frame.generate(this.root));
86127
this.engine.scheduleRead(this.tick.bind(this));
87128
}
88129
}
@@ -97,7 +138,7 @@ export abstract class BaseScheduler {
97138
let frame: Frame = null;
98139
this.engine.scheduleRead(() => {
99140
clientRect = getBoundingClientRect(el);
100-
frame = Frame.generate();
141+
frame = Frame.generate(this.root);
101142
});
102143
this.engine.scheduleWork(() => {
103144
callback(clientRect, frame);
@@ -108,14 +149,20 @@ export abstract class BaseScheduler {
108149
}
109150
unwatchAll() {
110151
this.queue.clear();
111-
W.__destroy__();
112152
}
113153
startTicking() {
114154
if (!this.isTicking) {
115155
this.isTicking = true;
116156
this.engine.scheduleRead(this.tick.bind(this));
117157
}
118158
}
159+
forceStateValidation() {
160+
++W.version;
161+
W.meta.height = W.getHeight();
162+
W.meta.width = W.getWidth();
163+
W.meta.scrollLeft = W.getScrollLeft();
164+
W.meta.scrollTop = W.getScrollTop();
165+
}
119166
}
120167

121168
export class Scheduler extends BaseScheduler implements SchedulerInterface {
@@ -140,7 +187,7 @@ export class Scheduler extends BaseScheduler implements SchedulerInterface {
140187
export class PredicatedScheduler extends Scheduler implements SchedulerInterface {
141188
predicate: (frame: Frame) => Boolean;
142189
constructor(predicate: (frame: Frame) => Boolean) {
143-
super(null);
190+
super(null, window);
144191
this.predicate = predicate;
145192
}
146193
applyQueue(frame: Frame) {
@@ -152,13 +199,15 @@ export class PredicatedScheduler extends Scheduler implements SchedulerInterface
152199

153200
export class ElementScheduler extends BaseScheduler implements ElementSchedulerInterface {
154201
protected queue: DOMQueue;
155-
protected isDirty: boolean = false;
156-
protected id: string = '';
202+
protected lastVersion: number = W.version;
157203

158-
constructor(customEngine?: EngineInterface) {
159-
super(customEngine);
204+
constructor(customEngine?: EngineInterface, root?: Element | Window) {
205+
super(customEngine, root);
160206
this.queue = new DOMQueue();
161-
this.id = generateRandomToken();
207+
}
208+
209+
get isDirty(): boolean {
210+
return W.version !== this.lastVersion;
162211
}
163212

164213
applyQueue(frame: Frame) {
@@ -172,11 +221,10 @@ export class ElementScheduler extends BaseScheduler implements ElementSchedulerI
172221
callback(frame, id, clientRect);
173222
}
174223

175-
this.isDirty = false;
224+
this.lastVersion = W.version;
176225
}
177226

178227
watch(el: Element, callback: (frame: FrameInterface, id: string, clientRect?: ClientRect | null) => void, id?: string): string {
179-
this.initWindowIsDirtyListeners();
180228
this.startTicking();
181229
id = id || generateToken();
182230
let clientRect = null;
@@ -189,14 +237,6 @@ export class ElementScheduler extends BaseScheduler implements ElementSchedulerI
189237
});
190238
return id;
191239
}
192-
193-
initWindowIsDirtyListeners() {
194-
W.onWindowIsDirtyListeners.push({ fn: this.windowIsDirtyHandler, scope: this, id: this.id });
195-
}
196-
197-
windowIsDirtyHandler() {
198-
this.isDirty = true;
199-
}
200240
}
201241

202242
let globalScheduler: Scheduler = null;

0 commit comments

Comments
 (0)