/
scheduler.ts
153 lines (136 loc) · 4.17 KB
/
scheduler.ts
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
import { process } from '../ResizeObserverController';
import { prettifyConsoleOutput } from './prettify';
import { global } from './global';
const CATCH_FRAMES = 60 / 5; // Fifth of a second
// Keep original reference of raf to use later
const requestAnimationFrame = global.requestAnimationFrame;
const observerConfig = { attributes: true, characterData: true, childList: true, subtree: true };
const events = [
// Global Resize
'resize',
// Global Load
'load',
// Transitions & Animations
'transitionend',
'animationend',
'animationstart',
'animationiteration',
// Interactions
'keyup',
'keydown',
'mouseup',
'mousedown',
'mouseover',
'mouseout',
'blur',
'focus'
];
const rafSlot = new Map();
const resizeObserverSlot = new Map();
let scheduled: boolean;
const dispatchCallbacksOnNextFrame = (): void => {
if (scheduled) {
return;
}
scheduled = true;
function runSchedule(t: number): void {
scheduled = false;
const frameCallbacks: FrameRequestCallback[] = [];
const resizeObserverCallbacks: FrameRequestCallback[] = [];
rafSlot.forEach((callback): number => frameCallbacks.push(callback));
resizeObserverSlot.forEach((callback): number => resizeObserverCallbacks.push(callback));
rafSlot.clear(); resizeObserverSlot.clear();
try { // Try to run animation frame callbacks
for (let callback of frameCallbacks) {
callback(t);
}
}
finally { // Finally, run schedule
for (let callback of resizeObserverCallbacks) {
callback(t);
}
}
};
requestAnimationFrame(runSchedule)
}
class Scheduler {
private observer: MutationObserver | undefined;
private listener: () => void;
public stopped: boolean = true;
public constructor () {
this.listener = (): void => this.schedule();
}
public run (frames: number): void {
const scheduler = this;
resizeObserverSlot.set(this, function ResizeObserver (): void {
let elementsHaveResized = false;
try {
// Process Calculations
elementsHaveResized = process();
}
finally {
// Have any changes happened?
if (elementsHaveResized) {
scheduler.run(60);
}
// Should we continue to check?
else if (frames) {
scheduler.run(frames - 1);
}
// Start listening again
else {
scheduler.start();
}
}
});
dispatchCallbacksOnNextFrame();
}
public schedule (): void {
this.stop(); // Stop listeneing
this.run(CATCH_FRAMES); // Run schedule
}
private observe (): void {
const cb = (): void => this.observer && this.observer.observe(document.body, observerConfig);
/* istanbul ignore next */
document.body ? cb() : global.addEventListener('DOMContentLoaded', cb);
}
public start (): void {
if (this.stopped) {
this.stopped = false;
if ('MutationObserver' in global) {
this.observer = new MutationObserver(this.listener);
this.observe();
}
events.forEach((name): void => global.addEventListener(name, this.listener, true));
}
}
public stop (): void {
if (!this.stopped) {
this.observer && this.observer.disconnect();
events.forEach((name): void => global.removeEventListener(name, this.listener, true));
this.stopped = true;
}
}
}
const scheduler = new Scheduler();
let rafIdBase = 0;
// Override requestAnimationFrame to make sure
// calculations are performed after any changes may occur.
// * Is there another way to schedule without modifying the whole function?
global.requestAnimationFrame = function (callback): number {
if (typeof callback !== 'function') {
throw new Error('requestAnimationFrame expects 1 callback argument of type function.');
}
const handle = rafIdBase += 1;
rafSlot.set(handle, function AnimationFrame (t: number): void { return callback(t) });
dispatchCallbacksOnNextFrame();
return handle;
}
// Override cancelAnimationFrame
// as we need to handle custom removal
global.cancelAnimationFrame = function (handle): void {
rafSlot.delete(handle);
}
prettifyConsoleOutput(global.requestAnimationFrame);
prettifyConsoleOutput(global.cancelAnimationFrame);
export { scheduler };