blob: b69e2f2c32d0c2525a6e6f4f8fd24a8a51b64142 [file] [log] [blame]
Copybara botbe50d492023-11-30 00:16:42 +01001
2/**
3 * An API for observing changes to Element’s size.
4 *
5 * @See https://wicg.github.io/ResizeObserver/
6 * @ee https://github.com/pelotoncycle/resize-observer
7 *
8 */
9
10import intervalFunction from './interval-function';
11
12((window, document) => {
13 'use strict';
14
15 if (typeof window.ResizeObserver !== 'undefined') {
16 return;
17 }
18
19 document.resizeObservers = [];
20
21 /**
22 * The content rect is defined in section 2.3 ResizeObserverEntry of the spec
23 * @param target the element to calculate the content rect for
24 * @return {{top: (Number|number), left: (Number|number), width: number, height: number}}
25 *
26 * Note:
27 * Avoid using margins on the observed element. The calculation can return incorrect values when margins are involved.
28 *
29 * The following CSS will report incorrect width (Chrome OSX):
30 *
31 * <div id="outer" style="width: 300px; height:300px; background-color: green;overflow:auto;">
32 * <div id="observed" style="width: 400px; height:400px; background-color: yellow; margin:30px; border: 20px solid red; padding:10px;">
33 * </div>
34 * </div>
35 *
36 * The calculated width is 280. The actual (correct) width is 340 since Chrome clips the margin.
37 *
38 * Use an outer container if you really need a "margin":
39 *
40 * <div id="outer" style="width: 300px; height:300px; background-color: green;overflow:auto; padding:30px;">
41 * <div id="observed" style="width: 400px; height:400px; background-color: yellow; margin: 0; border: 20px solid red; padding:10px;">
42 * </div>
43 * </div>
44 *
45 * A more detailed explanation can be fund here:
46 * http://stackoverflow.com/questions/21064101/understanding-offsetwidth-clientwidth-scrollwidth-and-height-respectively
47 */
48 const getContentRect = target => {
49 const cs = window.getComputedStyle(target);
50 const r = target.getBoundingClientRect();
51 const top = parseFloat(cs.paddingTop) || 0;
52 const left = parseFloat(cs.paddingLeft) || 0;
53 const width = r.width - (
54 (parseFloat(cs.marginLeft) || 0) +
55 (parseFloat(cs.marginRight) || 0) +
56 (parseFloat(cs.borderLeftWidth) || 0) +
57 (parseFloat(cs.borderRightWidth) || 0) +
58 (left) +
59 (parseFloat(cs.paddingRight) || 0)
60 );
61 const height = r.height - (
62 (parseFloat(cs.marginTop) || 0) +
63 (parseFloat(cs.marginBottom) || 0) +
64 (parseFloat(cs.borderTopWidth) || 0) +
65 (parseFloat(cs.borderBottomWidth) || 0) +
66 (top) +
67 (parseFloat(cs.paddingBottom) || 0)
68 );
69 return {width: width, height: height, top: top, left: left};
70 };
71
72 const dimensionHasChanged = (target, lastWidth, lastHeight) => {
73 const {width, height} = getContentRect(target);
74 return width !== lastWidth || height !== lastHeight;
75 };
76
77
78 /**
79 * ResizeObservation holds observation information for a single Element.
80 * @param target
81 * @return {{target: *, broadcastWidth, broadcastHeight, isOrphan: (function()), isActive: (function())}}
82 * @constructor
83 */
84 const ResizeObservation = target => {
85 const {width, height} = getContentRect(target);
86
87 return {
88 target: target,
89 broadcastWidth: width,
90 broadcastHeight: height,
91
92 isOrphan() {
93 return !this.target || !this.target.parentNode;
94 },
95 isActive() {
96 return dimensionHasChanged(this.target, this.broadcastWidth, this.broadcastHeight);
97 }
98 };
99 };
100
101 /**
102 * A snapshot of the observed element
103 * @param target
104 * @param rect
105 * @return {{target: *, contentRect: *}}
106 * @constructor
107 */
108 const ResizeObserverEntry = (target, rect) => {
109 return {
110 target: target,
111 contentRect: rect
112 };
113 };
114
115
116 /**
117 * The ResizeObserver is used to observe changes to Element's content rect.
118 */
119 class ResizeObserver {
120
121 /**
122 * Constructor for instantiating new Resize observers.
123 * @param callback void (sequence<ResizeObserverEntry> entries). The function which will be called on each resize.
124 * @throws {TypeError}
125 */
126 constructor( callback ) {
127
128 if(typeof callback !== 'function') {
129 throw new TypeError('callback parameter must be a function');
130 }
131
132 this.callback_ = callback;
133 this.observationTargets_ = [];
134 this.activeTargets_ = [];
135
136 document.resizeObservers.push(this);
137 }
138
139 /**
140 * A list of ResizeObservations. It represents all Elements being observed.
141 *
142 * @return {Array}
143 */
144 get observationTargets() {
145 return this.observationTargets_;
146 }
147
148 /**
149 * A list of ResizeObservations. It represents all Elements whose size has
150 * changed since last observation broadcast that are eligible for broadcast.
151 *
152 * @return {Array}
153 */
154 get activeTargets() {
155 return this.activeTargets_;
156 }
157
158 /**
159 * Adds target to the list of observed elements.
160 * @param {HTMLElement} target The target to observe
161 */
162 observe(target) {
163 if(target) {
164 if (!(target instanceof HTMLElement)) {
165 throw new TypeError('target parameter must be an HTMLElement');
166 }
167 if (!this.observationTargets_.find(t => t.target === target)) {
168 this.observationTargets_.push(ResizeObservation(target));
169 resizeController.start();
170 }
171 }
172 }
173
174 /**
175 * Removes target from the list of observed elements.
176 * @param target The target to remove
177 */
178 unobserve(target) {
179 const i = this.observationTargets_.findIndex(t => t.target === target);
180 if(i > -1) {
181 this.observationTargets_.splice(i, 1);
182 }
183 }
184
185 /**
186 * Stops the ResizeObserver instance from receiving notifications of resize changes.
187 * Until the observe() method is used again, observer's callback will not be invoked.
188 */
189 disconnect() {
190 this.observationTargets_ = [];
191 this.activeTargets_ = [];
192 }
193
194 /**
195 * Removes the ResizeObserver from the list of observers
196 */
197 destroy() {
198 this.disconnect();
199 const i = document.resizeObservers.findIndex(o => o === this);
200 if(i > -1) {
201 document.resizeObservers.splice(i, 1);
202 }
203 }
204
205 deleteOrphansAndPopulateActiveTargets_() {
206
207 // Works, but two iterations
208 //this.observationTargets_ = this.observationTargets_.filter( resizeObervation => !resizeObervation.isOrphan());
209 //this.activeTargets_ = this.observationTargets_.filter( resizeObervation => resizeObervation.isActive());
210
211 // Same result as above, one iteration
212 /*
213 this.activeTargets_ = [];
214 let n = this.observationTargets_.length-1;
215 while(n >= 0) {
216 if(this.observationTargets_[n].isOrphan()) {
217 this.observationTargets_.splice(n, 1);
218 }
219 else if(this.observationTargets_[n].isActive()) {
220 this.activeTargets_.push(this.observationTargets_[n]);
221 }
222 n -= 1;
223 }
224 */
225
226 // Same result as above - but reduce is cooler :-)
227 this.activeTargets_ = this.observationTargets_.reduceRight( (prev, resizeObservation, index, arr) => {
228 if(resizeObservation.isOrphan()) {
229 arr.splice(index, 1);
230 }
231 else if(resizeObservation.isActive()) {
232 prev.push(resizeObservation);
233 }
234 return prev;
235 }, []);
236 }
237
238 broadcast_() {
239 this.deleteOrphansAndPopulateActiveTargets_();
240 if (this.activeTargets_.length > 0) {
241 const entries = [];
242 for (const resizeObservation of this.activeTargets_) {
243 const rect = getContentRect(resizeObservation.target);
244 resizeObservation.broadcastWidth = rect.width;
245 resizeObservation.broadcastHeight = rect.height;
246 entries.push(ResizeObserverEntry(resizeObservation.target, rect));
247 }
248 this.callback_(entries);
249 this.activeTargets_ = [];
250 }
251 }
252 }
253
254
255 //let interval = require('./interval-function');
256
257 /**
258 * Broadcasts Element.resize events
259 * @return {{start: (function()), stop: (function())}}
260 * @constructor
261 */
262 const ResizeController = () => {
263
264 const shouldStop = () => {
265 return document.resizeObservers.findIndex( resizeObserver => resizeObserver.observationTargets.length > 0 ) > -1;
266 };
267
268 const execute = () => {
269 //console.log('***** Execute');
270 for(const resizeObserver of document.resizeObservers) {
271 resizeObserver.broadcast_();
272 }
273
274 return shouldStop();
275 };
276
277 const interval = intervalFunction(200);
278
279 return {
280 start() {
281 if(!interval.started) {
282 //console.log('***** Start poll');
283 interval.start(execute);
284 }
285 }
286 };
287 };
288
289 window.ResizeObserver = ResizeObserver;
290
291 const resizeController = ResizeController();
292 //console.log('***** ResizeObserver ready');
293
294})(window, document);