-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlib.ts
124 lines (107 loc) · 3.97 KB
/
lib.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
/**
* Payload received by a `ResizeHandler`.
*/
export interface ResizePayload<T extends HTMLElement> {
element: T;
event: UIEvent;
prevBoundingClientRect: ClientRect | DOMRect;
destroy: DestroyWatcher;
}
/**
* A callback function invoked when the
* watched element emits a "resize" event.
*/
export type ResizeHandler<T extends HTMLElement> = (payload: ResizePayload<T>) => void | Promise<void>;
/**
* A synchronous function to unobserve
* the element given to `watchResize`.
*/
export type DestroyWatcher = () => void;
/**
* Checks if the given object is a valid `HTMLElement`.
*/
function isElement(obj: any) {
try {
// Using W3 DOM2 (works for FF, Opera and Chrome)
return obj instanceof HTMLElement;
} catch (e) {
// Browsers not supporting W3 DOM2 don't have HTMLElement and
// an exception is thrown and we end up here. Testing some
// properties that all elements have (works on IE7)
return (
typeof obj === 'object' &&
obj.nodeType === 1 &&
typeof obj.style === 'object' &&
typeof obj.ownerDocument === 'object'
);
}
}
/**
* Returns a Promise that resolves when the observer is mounted. The observer
* fires "resize" events when the given DOM element's width or height changes.
*
* The resolved Promise value is a synchronous function which unobserves the
* element when called.
*
* @param element - HTMLElement to observe.
* @param handler - A callback function invoked whenever the given `element`
* resizes.
*/
export function watchResize<T extends HTMLElement>(element: T, handler: ResizeHandler<T>): Promise<DestroyWatcher> {
return new Promise<DestroyWatcher>((resolve, reject) => {
// Assert that `element` is defined and is a valid DOM node.
if (typeof element === 'undefined' || element === null) {
return reject(Promise.reject(new Error('[watch-resize] The given element must be defined.')));
}
if (!isElement(element)) {
return reject(Promise.reject(new Error('[watch-resize] The given element is not a valid DOM node.')));
}
if (!element.parentNode) {
return reject(Promise.reject(new Error('[watch-resize] The given element is not yet attached to the DOM.')));
}
// Ensure we are relatively positioned so that the nested browsing context
// is correctly sized and positioned.
if (getComputedStyle(element).position === 'static') {
element.style.position = 'relative';
}
// Create a nested browsing context using an <object> element.
const obj = document.createElement('object');
// Set type and data, etc.
obj.type = 'text/html';
obj.data = 'about:blank';
obj.tabIndex = -1;
obj.setAttribute('aria-hidden', 'true');
// Set CSSOM properties
obj.style.display = 'block';
obj.style.position = 'absolute';
obj.style.top = '0';
obj.style.left = '0';
obj.style.height = '100%';
obj.style.width = '100%';
obj.style.overflow = 'hidden';
obj.style.pointerEvents = 'none';
obj.style.zIndex = '-1';
// Save a reference to the element's current client rect.
let prevBoundingClientRect: ClientRect | DOMRect = element.getBoundingClientRect();
// When the <object> loads, apply the "resize" event listener and resolve.
obj.addEventListener('load', () => {
if (obj.contentDocument && obj.contentDocument.defaultView) {
const viewContext = obj.contentDocument.defaultView;
const listener = (event: UIEvent) => {
handler({ element, event, prevBoundingClientRect, destroy });
prevBoundingClientRect = element.getBoundingClientRect();
};
const destroy = () => {
viewContext.removeEventListener('resize', listener);
obj.remove();
};
viewContext.addEventListener('resize', listener);
resolve(destroy);
} else {
reject(new Error('[watch-resize] Failed to build a nested browsing context.'));
}
});
// Append the <object> to the target element.
element.appendChild(obj);
});
}