` wrapper and almost all instances of the SVGGraphicsElement will be out of scope.
71 | * The ResizeObserver spec requires to deliver notifications when a non-empty visible element becomes hidden, i.e. when either this element directly or one of its parent nodes receive the `display: none` state. Same goes for when it's being removed from or added to the DOM. It's not possible to handle these cases merely by using former approaches, so you'd still need to either subscribe for DOM mutations or to continuously check the element's state.
72 |
73 | And though every approach has its own limitations, I reckon that it'd be too much of a trade-off to have those constraints when building a polyfill.
74 |
75 | ## Limitations
76 |
77 | * Notifications are delivered ~20ms after actual changes happen.
78 | * Changes caused by dynamic pseudo-classes, e.g. `:hover` and `:focus`, are not tracked. As a workaround you could add a short transition which would trigger the `transitionend` event when an element receives one of the former classes ([example](https://jsfiddle.net/que_etc/7fudzqng/)).
79 | * Delayed transitions will receive only one notification with the latest dimensions of an element.
80 |
81 | ## Building and Testing
82 |
83 | To build polyfill. Creates UMD bundle in the `dist` folder:
84 |
85 | ```sh
86 | npm run build
87 | ```
88 |
89 | To run a code style test:
90 | ```sh
91 | npm run test:lint
92 | ```
93 |
94 | Running unit tests:
95 | ```sh
96 | npm run test:spec
97 | ```
98 |
99 | To test in a browser that is not present in karma's config file:
100 | ```sh
101 | npm run test:spec:custom
102 | ```
103 |
104 | Testing against a native implementation:
105 | ```sh
106 | npm run test:spec:native
107 | ```
108 |
109 | **NOTE:** after you invoke `spec:native` and `spec:custom` commands head to the `http://localhost:9876/debug.html` page.
110 |
111 | [travis-image]: https://travis-ci.org/que-etc/resize-observer-polyfill.svg?branch=master
112 | [travis-url]: https://travis-ci.org/que-etc/resize-observer-polyfill
113 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "resize-observer-polyfill",
3 | "description": "A polyfill for the Resize Observer API",
4 | "authors": [
5 | "Denis Rul
"
6 | ],
7 | "moduleType": [
8 | "globals",
9 | "amd",
10 | "node",
11 | "es6"
12 | ],
13 | "main": [
14 | "dist/ResizeObserver.js"
15 | ],
16 | "keywords": [
17 | "ResizeObserver",
18 | "resize",
19 | "observer",
20 | "util",
21 | "client",
22 | "browser",
23 | "polyfill",
24 | "ponyfill"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/dist/ResizeObserver.global.js:
--------------------------------------------------------------------------------
1 | (function (global, factory) {
2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
3 | typeof define === 'function' && define.amd ? define(factory) :
4 | (global.ResizeObserver = factory());
5 | }(this, (function () { 'use strict';
6 |
7 | /**
8 | * A collection of shims that provide minimal functionality of the ES6 collections.
9 | *
10 | * These implementations are not meant to be used outside of the ResizeObserver
11 | * modules as they cover only a limited range of use cases.
12 | */
13 | /* eslint-disable require-jsdoc, valid-jsdoc */
14 | var MapShim = (function () {
15 | if (typeof Map !== 'undefined') {
16 | return Map;
17 | }
18 |
19 | /**
20 | * Returns index in provided array that matches the specified key.
21 | *
22 | * @param {Array} arr
23 | * @param {*} key
24 | * @returns {number}
25 | */
26 | function getIndex(arr, key) {
27 | var result = -1;
28 |
29 | arr.some(function (entry, index) {
30 | if (entry[0] === key) {
31 | result = index;
32 |
33 | return true;
34 | }
35 |
36 | return false;
37 | });
38 |
39 | return result;
40 | }
41 |
42 | return (function () {
43 | function anonymous() {
44 | this.__entries__ = [];
45 | }
46 |
47 | var prototypeAccessors = { size: { configurable: true } };
48 |
49 | /**
50 | * @returns {boolean}
51 | */
52 | prototypeAccessors.size.get = function () {
53 | return this.__entries__.length;
54 | };
55 |
56 | /**
57 | * @param {*} key
58 | * @returns {*}
59 | */
60 | anonymous.prototype.get = function (key) {
61 | var index = getIndex(this.__entries__, key);
62 | var entry = this.__entries__[index];
63 |
64 | return entry && entry[1];
65 | };
66 |
67 | /**
68 | * @param {*} key
69 | * @param {*} value
70 | * @returns {void}
71 | */
72 | anonymous.prototype.set = function (key, value) {
73 | var index = getIndex(this.__entries__, key);
74 |
75 | if (~index) {
76 | this.__entries__[index][1] = value;
77 | } else {
78 | this.__entries__.push([key, value]);
79 | }
80 | };
81 |
82 | /**
83 | * @param {*} key
84 | * @returns {void}
85 | */
86 | anonymous.prototype.delete = function (key) {
87 | var entries = this.__entries__;
88 | var index = getIndex(entries, key);
89 |
90 | if (~index) {
91 | entries.splice(index, 1);
92 | }
93 | };
94 |
95 | /**
96 | * @param {*} key
97 | * @returns {void}
98 | */
99 | anonymous.prototype.has = function (key) {
100 | return !!~getIndex(this.__entries__, key);
101 | };
102 |
103 | /**
104 | * @returns {void}
105 | */
106 | anonymous.prototype.clear = function () {
107 | this.__entries__.splice(0);
108 | };
109 |
110 | /**
111 | * @param {Function} callback
112 | * @param {*} [ctx=null]
113 | * @returns {void}
114 | */
115 | anonymous.prototype.forEach = function (callback, ctx) {
116 | var this$1 = this;
117 | if ( ctx === void 0 ) ctx = null;
118 |
119 | for (var i = 0, list = this$1.__entries__; i < list.length; i += 1) {
120 | var entry = list[i];
121 |
122 | callback.call(ctx, entry[1], entry[0]);
123 | }
124 | };
125 |
126 | Object.defineProperties( anonymous.prototype, prototypeAccessors );
127 |
128 | return anonymous;
129 | }());
130 | })();
131 |
132 | /**
133 | * Detects whether window and document objects are available in current environment.
134 | */
135 | var isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined' && window.document === document;
136 |
137 | // Returns global object of a current environment.
138 | var global$1 = (function () {
139 | if (typeof global !== 'undefined' && global.Math === Math) {
140 | return global;
141 | }
142 |
143 | if (typeof self !== 'undefined' && self.Math === Math) {
144 | return self;
145 | }
146 |
147 | if (typeof window !== 'undefined' && window.Math === Math) {
148 | return window;
149 | }
150 |
151 | // eslint-disable-next-line no-new-func
152 | return Function('return this')();
153 | })();
154 |
155 | /**
156 | * A shim for the requestAnimationFrame which falls back to the setTimeout if
157 | * first one is not supported.
158 | *
159 | * @returns {number} Requests' identifier.
160 | */
161 | var requestAnimationFrame$1 = (function () {
162 | if (typeof requestAnimationFrame === 'function') {
163 | // It's required to use a bounded function because IE sometimes throws
164 | // an "Invalid calling object" error if rAF is invoked without the global
165 | // object on the left hand side.
166 | return requestAnimationFrame.bind(global$1);
167 | }
168 |
169 | return function (callback) { return setTimeout(function () { return callback(Date.now()); }, 1000 / 60); };
170 | })();
171 |
172 | // Defines minimum timeout before adding a trailing call.
173 | var trailingTimeout = 2;
174 |
175 | /**
176 | * Creates a wrapper function which ensures that provided callback will be
177 | * invoked only once during the specified delay period.
178 | *
179 | * @param {Function} callback - Function to be invoked after the delay period.
180 | * @param {number} delay - Delay after which to invoke callback.
181 | * @returns {Function}
182 | */
183 | var throttle = function (callback, delay) {
184 | var leadingCall = false,
185 | trailingCall = false,
186 | lastCallTime = 0;
187 |
188 | /**
189 | * Invokes the original callback function and schedules new invocation if
190 | * the "proxy" was called during current request.
191 | *
192 | * @returns {void}
193 | */
194 | function resolvePending() {
195 | if (leadingCall) {
196 | leadingCall = false;
197 |
198 | callback();
199 | }
200 |
201 | if (trailingCall) {
202 | proxy();
203 | }
204 | }
205 |
206 | /**
207 | * Callback invoked after the specified delay. It will further postpone
208 | * invocation of the original function delegating it to the
209 | * requestAnimationFrame.
210 | *
211 | * @returns {void}
212 | */
213 | function timeoutCallback() {
214 | requestAnimationFrame$1(resolvePending);
215 | }
216 |
217 | /**
218 | * Schedules invocation of the original function.
219 | *
220 | * @returns {void}
221 | */
222 | function proxy() {
223 | var timeStamp = Date.now();
224 |
225 | if (leadingCall) {
226 | // Reject immediately following calls.
227 | if (timeStamp - lastCallTime < trailingTimeout) {
228 | return;
229 | }
230 |
231 | // Schedule new call to be in invoked when the pending one is resolved.
232 | // This is important for "transitions" which never actually start
233 | // immediately so there is a chance that we might miss one if change
234 | // happens amids the pending invocation.
235 | trailingCall = true;
236 | } else {
237 | leadingCall = true;
238 | trailingCall = false;
239 |
240 | setTimeout(timeoutCallback, delay);
241 | }
242 |
243 | lastCallTime = timeStamp;
244 | }
245 |
246 | return proxy;
247 | };
248 |
249 | // Minimum delay before invoking the update of observers.
250 | var REFRESH_DELAY = 20;
251 |
252 | // A list of substrings of CSS properties used to find transition events that
253 | // might affect dimensions of observed elements.
254 | var transitionKeys = ['top', 'right', 'bottom', 'left', 'width', 'height', 'size', 'weight'];
255 |
256 | // Check if MutationObserver is available.
257 | var mutationObserverSupported = typeof MutationObserver !== 'undefined';
258 |
259 | /**
260 | * Singleton controller class which handles updates of ResizeObserver instances.
261 | */
262 | var ResizeObserverController = function() {
263 | this.connected_ = false;
264 | this.mutationEventsAdded_ = false;
265 | this.mutationsObserver_ = null;
266 | this.observers_ = [];
267 |
268 | this.onTransitionEnd_ = this.onTransitionEnd_.bind(this);
269 | this.refresh = throttle(this.refresh.bind(this), REFRESH_DELAY);
270 | };
271 |
272 | /**
273 | * Adds observer to observers list.
274 | *
275 | * @param {ResizeObserverSPI} observer - Observer to be added.
276 | * @returns {void}
277 | */
278 |
279 |
280 | /**
281 | * Holds reference to the controller's instance.
282 | *
283 | * @private {ResizeObserverController}
284 | */
285 |
286 |
287 | /**
288 | * Keeps reference to the instance of MutationObserver.
289 | *
290 | * @private {MutationObserver}
291 | */
292 |
293 | /**
294 | * Indicates whether DOM listeners have been added.
295 | *
296 | * @private {boolean}
297 | */
298 | ResizeObserverController.prototype.addObserver = function (observer) {
299 | if (!~this.observers_.indexOf(observer)) {
300 | this.observers_.push(observer);
301 | }
302 |
303 | // Add listeners if they haven't been added yet.
304 | if (!this.connected_) {
305 | this.connect_();
306 | }
307 | };
308 |
309 | /**
310 | * Removes observer from observers list.
311 | *
312 | * @param {ResizeObserverSPI} observer - Observer to be removed.
313 | * @returns {void}
314 | */
315 | ResizeObserverController.prototype.removeObserver = function (observer) {
316 | var observers = this.observers_;
317 | var index = observers.indexOf(observer);
318 |
319 | // Remove observer if it's present in registry.
320 | if (~index) {
321 | observers.splice(index, 1);
322 | }
323 |
324 | // Remove listeners if controller has no connected observers.
325 | if (!observers.length && this.connected_) {
326 | this.disconnect_();
327 | }
328 | };
329 |
330 | /**
331 | * Invokes the update of observers. It will continue running updates insofar
332 | * it detects changes.
333 | *
334 | * @returns {void}
335 | */
336 | ResizeObserverController.prototype.refresh = function () {
337 | var changesDetected = this.updateObservers_();
338 |
339 | // Continue running updates if changes have been detected as there might
340 | // be future ones caused by CSS transitions.
341 | if (changesDetected) {
342 | this.refresh();
343 | }
344 | };
345 |
346 | /**
347 | * Updates every observer from observers list and notifies them of queued
348 | * entries.
349 | *
350 | * @private
351 | * @returns {boolean} Returns "true" if any observer has detected changes in
352 | * dimensions of it's elements.
353 | */
354 | ResizeObserverController.prototype.updateObservers_ = function () {
355 | // Collect observers that have active observations.
356 | var activeObservers = this.observers_.filter(function (observer) {
357 | return observer.gatherActive(), observer.hasActive();
358 | });
359 |
360 | // Deliver notifications in a separate cycle in order to avoid any
361 | // collisions between observers, e.g. when multiple instances of
362 | // ResizeObserver are tracking the same element and the callback of one
363 | // of them changes content dimensions of the observed target. Sometimes
364 | // this may result in notifications being blocked for the rest of observers.
365 | activeObservers.forEach(function (observer) { return observer.broadcastActive(); });
366 |
367 | return activeObservers.length > 0;
368 | };
369 |
370 | /**
371 | * Initializes DOM listeners.
372 | *
373 | * @private
374 | * @returns {void}
375 | */
376 | ResizeObserverController.prototype.connect_ = function () {
377 | // Do nothing if running in a non-browser environment or if listeners
378 | // have been already added.
379 | if (!isBrowser || this.connected_) {
380 | return;
381 | }
382 |
383 | // Subscription to the "Transitionend" event is used as a workaround for
384 | // delayed transitions. This way it's possible to capture at least the
385 | // final state of an element.
386 | document.addEventListener('transitionend', this.onTransitionEnd_);
387 |
388 | window.addEventListener('resize', this.refresh);
389 |
390 | if (mutationObserverSupported) {
391 | this.mutationsObserver_ = new MutationObserver(this.refresh);
392 |
393 | this.mutationsObserver_.observe(document, {
394 | attributes: true,
395 | childList: true,
396 | characterData: true,
397 | subtree: true
398 | });
399 | } else {
400 | document.addEventListener('DOMSubtreeModified', this.refresh);
401 |
402 | this.mutationEventsAdded_ = true;
403 | }
404 |
405 | this.connected_ = true;
406 | };
407 |
408 | /**
409 | * Removes DOM listeners.
410 | *
411 | * @private
412 | * @returns {void}
413 | */
414 | ResizeObserverController.prototype.disconnect_ = function () {
415 | // Do nothing if running in a non-browser environment or if listeners
416 | // have been already removed.
417 | if (!isBrowser || !this.connected_) {
418 | return;
419 | }
420 |
421 | document.removeEventListener('transitionend', this.onTransitionEnd_);
422 | window.removeEventListener('resize', this.refresh);
423 |
424 | if (this.mutationsObserver_) {
425 | this.mutationsObserver_.disconnect();
426 | }
427 |
428 | if (this.mutationEventsAdded_) {
429 | document.removeEventListener('DOMSubtreeModified', this.refresh);
430 | }
431 |
432 | this.mutationsObserver_ = null;
433 | this.mutationEventsAdded_ = false;
434 | this.connected_ = false;
435 | };
436 |
437 | /**
438 | * "Transitionend" event handler.
439 | *
440 | * @private
441 | * @param {TransitionEvent} event
442 | * @returns {void}
443 | */
444 | ResizeObserverController.prototype.onTransitionEnd_ = function (ref) {
445 | var propertyName = ref.propertyName; if ( propertyName === void 0 ) propertyName = '';
446 |
447 | // Detect whether transition may affect dimensions of an element.
448 | var isReflowProperty = transitionKeys.some(function (key) {
449 | return !!~propertyName.indexOf(key);
450 | });
451 |
452 | if (isReflowProperty) {
453 | this.refresh();
454 | }
455 | };
456 |
457 | /**
458 | * Returns instance of the ResizeObserverController.
459 | *
460 | * @returns {ResizeObserverController}
461 | */
462 | ResizeObserverController.getInstance = function () {
463 | if (!this.instance_) {
464 | this.instance_ = new ResizeObserverController();
465 | }
466 |
467 | return this.instance_;
468 | };
469 |
470 | ResizeObserverController.instance_ = null;
471 |
472 | /**
473 | * Defines non-writable/enumerable properties of the provided target object.
474 | *
475 | * @param {Object} target - Object for which to define properties.
476 | * @param {Object} props - Properties to be defined.
477 | * @returns {Object} Target object.
478 | */
479 | var defineConfigurable = (function (target, props) {
480 | for (var i = 0, list = Object.keys(props); i < list.length; i += 1) {
481 | var key = list[i];
482 |
483 | Object.defineProperty(target, key, {
484 | value: props[key],
485 | enumerable: false,
486 | writable: false,
487 | configurable: true
488 | });
489 | }
490 |
491 | return target;
492 | });
493 |
494 | /**
495 | * Returns the global object associated with provided element.
496 | *
497 | * @param {Object} target
498 | * @returns {Object}
499 | */
500 | var getWindowOf = (function (target) {
501 | // Assume that the element is an instance of Node, which means that it
502 | // has the "ownerDocument" property from which we can retrieve a
503 | // corresponding global object.
504 | var ownerGlobal = target && target.ownerDocument && target.ownerDocument.defaultView;
505 |
506 | // Return the local global object if it's not possible extract one from
507 | // provided element.
508 | return ownerGlobal || global$1;
509 | });
510 |
511 | // Placeholder of an empty content rectangle.
512 | var emptyRect = createRectInit(0, 0, 0, 0);
513 |
514 | /**
515 | * Converts provided string to a number.
516 | *
517 | * @param {number|string} value
518 | * @returns {number}
519 | */
520 | function toFloat(value) {
521 | return parseFloat(value) || 0;
522 | }
523 |
524 | /**
525 | * Extracts borders size from provided styles.
526 | *
527 | * @param {CSSStyleDeclaration} styles
528 | * @param {...string} positions - Borders positions (top, right, ...)
529 | * @returns {number}
530 | */
531 | function getBordersSize(styles) {
532 | var positions = [], len = arguments.length - 1;
533 | while ( len-- > 0 ) positions[ len ] = arguments[ len + 1 ];
534 |
535 | return positions.reduce(function (size, position) {
536 | var value = styles['border-' + position + '-width'];
537 |
538 | return size + toFloat(value);
539 | }, 0);
540 | }
541 |
542 | /**
543 | * Extracts paddings sizes from provided styles.
544 | *
545 | * @param {CSSStyleDeclaration} styles
546 | * @returns {Object} Paddings box.
547 | */
548 | function getPaddings(styles) {
549 | var positions = ['top', 'right', 'bottom', 'left'];
550 | var paddings = {};
551 |
552 | for (var i = 0, list = positions; i < list.length; i += 1) {
553 | var position = list[i];
554 |
555 | var value = styles['padding-' + position];
556 |
557 | paddings[position] = toFloat(value);
558 | }
559 |
560 | return paddings;
561 | }
562 |
563 | /**
564 | * Calculates content rectangle of provided SVG element.
565 | *
566 | * @param {SVGGraphicsElement} target - Element content rectangle of which needs
567 | * to be calculated.
568 | * @returns {DOMRectInit}
569 | */
570 | function getSVGContentRect(target) {
571 | var bbox = target.getBBox();
572 |
573 | return createRectInit(0, 0, bbox.width, bbox.height);
574 | }
575 |
576 | /**
577 | * Calculates content rectangle of provided HTMLElement.
578 | *
579 | * @param {HTMLElement} target - Element for which to calculate the content rectangle.
580 | * @returns {DOMRectInit}
581 | */
582 | function getHTMLElementContentRect(target) {
583 | // Client width & height properties can't be
584 | // used exclusively as they provide rounded values.
585 | var clientWidth = target.clientWidth;
586 | var clientHeight = target.clientHeight;
587 |
588 | // By this condition we can catch all non-replaced inline, hidden and
589 | // detached elements. Though elements with width & height properties less
590 | // than 0.5 will be discarded as well.
591 | //
592 | // Without it we would need to implement separate methods for each of
593 | // those cases and it's not possible to perform a precise and performance
594 | // effective test for hidden elements. E.g. even jQuery's ':visible' filter
595 | // gives wrong results for elements with width & height less than 0.5.
596 | if (!clientWidth && !clientHeight) {
597 | return emptyRect;
598 | }
599 |
600 | var styles = getWindowOf(target).getComputedStyle(target);
601 | var paddings = getPaddings(styles);
602 | var horizPad = paddings.left + paddings.right;
603 | var vertPad = paddings.top + paddings.bottom;
604 |
605 | // Computed styles of width & height are being used because they are the
606 | // only dimensions available to JS that contain non-rounded values. It could
607 | // be possible to utilize the getBoundingClientRect if only it's data wasn't
608 | // affected by CSS transformations let alone paddings, borders and scroll bars.
609 | var width = toFloat(styles.width),
610 | height = toFloat(styles.height);
611 |
612 | // Width & height include paddings and borders when the 'border-box' box
613 | // model is applied (except for IE).
614 | if (styles.boxSizing === 'border-box') {
615 | // Following conditions are required to handle Internet Explorer which
616 | // doesn't include paddings and borders to computed CSS dimensions.
617 | //
618 | // We can say that if CSS dimensions + paddings are equal to the "client"
619 | // properties then it's either IE, and thus we don't need to subtract
620 | // anything, or an element merely doesn't have paddings/borders styles.
621 | if (Math.round(width + horizPad) !== clientWidth) {
622 | width -= getBordersSize(styles, 'left', 'right') + horizPad;
623 | }
624 |
625 | if (Math.round(height + vertPad) !== clientHeight) {
626 | height -= getBordersSize(styles, 'top', 'bottom') + vertPad;
627 | }
628 | }
629 |
630 | // Following steps can't be applied to the document's root element as its
631 | // client[Width/Height] properties represent viewport area of the window.
632 | // Besides, it's as well not necessary as the itself neither has
633 | // rendered scroll bars nor it can be clipped.
634 | if (!isDocumentElement(target)) {
635 | // In some browsers (only in Firefox, actually) CSS width & height
636 | // include scroll bars size which can be removed at this step as scroll
637 | // bars are the only difference between rounded dimensions + paddings
638 | // and "client" properties, though that is not always true in Chrome.
639 | var vertScrollbar = Math.round(width + horizPad) - clientWidth;
640 | var horizScrollbar = Math.round(height + vertPad) - clientHeight;
641 |
642 | // Chrome has a rather weird rounding of "client" properties.
643 | // E.g. for an element with content width of 314.2px it sometimes gives
644 | // the client width of 315px and for the width of 314.7px it may give
645 | // 314px. And it doesn't happen all the time. So just ignore this delta
646 | // as a non-relevant.
647 | if (Math.abs(vertScrollbar) !== 1) {
648 | width -= vertScrollbar;
649 | }
650 |
651 | if (Math.abs(horizScrollbar) !== 1) {
652 | height -= horizScrollbar;
653 | }
654 | }
655 |
656 | return createRectInit(paddings.left, paddings.top, width, height);
657 | }
658 |
659 | /**
660 | * Checks whether provided element is an instance of the SVGGraphicsElement.
661 | *
662 | * @param {Element} target - Element to be checked.
663 | * @returns {boolean}
664 | */
665 | var isSVGGraphicsElement = (function () {
666 | // Some browsers, namely IE and Edge, don't have the SVGGraphicsElement
667 | // interface.
668 | if (typeof SVGGraphicsElement !== 'undefined') {
669 | return function (target) { return target instanceof getWindowOf(target).SVGGraphicsElement; };
670 | }
671 |
672 | // If it's so, then check that element is at least an instance of the
673 | // SVGElement and that it has the "getBBox" method.
674 | // eslint-disable-next-line no-extra-parens
675 | return function (target) { return target instanceof getWindowOf(target).SVGElement && typeof target.getBBox === 'function'; };
676 | })();
677 |
678 | /**
679 | * Checks whether provided element is a document element ().
680 | *
681 | * @param {Element} target - Element to be checked.
682 | * @returns {boolean}
683 | */
684 | function isDocumentElement(target) {
685 | return target === getWindowOf(target).document.documentElement;
686 | }
687 |
688 | /**
689 | * Calculates an appropriate content rectangle for provided html or svg element.
690 | *
691 | * @param {Element} target - Element content rectangle of which needs to be calculated.
692 | * @returns {DOMRectInit}
693 | */
694 | function getContentRect(target) {
695 | if (!isBrowser) {
696 | return emptyRect;
697 | }
698 |
699 | if (isSVGGraphicsElement(target)) {
700 | return getSVGContentRect(target);
701 | }
702 |
703 | return getHTMLElementContentRect(target);
704 | }
705 |
706 | /**
707 | * Creates rectangle with an interface of the DOMRectReadOnly.
708 | * Spec: https://drafts.fxtf.org/geometry/#domrectreadonly
709 | *
710 | * @param {DOMRectInit} rectInit - Object with rectangle's x/y coordinates and dimensions.
711 | * @returns {DOMRectReadOnly}
712 | */
713 | function createReadOnlyRect(ref) {
714 | var x = ref.x;
715 | var y = ref.y;
716 | var width = ref.width;
717 | var height = ref.height;
718 |
719 | // If DOMRectReadOnly is available use it as a prototype for the rectangle.
720 | var Constr = typeof DOMRectReadOnly !== 'undefined' ? DOMRectReadOnly : Object;
721 | var rect = Object.create(Constr.prototype);
722 |
723 | // Rectangle's properties are not writable and non-enumerable.
724 | defineConfigurable(rect, {
725 | x: x, y: y, width: width, height: height,
726 | top: y,
727 | right: x + width,
728 | bottom: height + y,
729 | left: x
730 | });
731 |
732 | return rect;
733 | }
734 |
735 | /**
736 | * Creates DOMRectInit object based on the provided dimensions and the x/y coordinates.
737 | * Spec: https://drafts.fxtf.org/geometry/#dictdef-domrectinit
738 | *
739 | * @param {number} x - X coordinate.
740 | * @param {number} y - Y coordinate.
741 | * @param {number} width - Rectangle's width.
742 | * @param {number} height - Rectangle's height.
743 | * @returns {DOMRectInit}
744 | */
745 | function createRectInit(x, y, width, height) {
746 | return { x: x, y: y, width: width, height: height };
747 | }
748 |
749 | /**
750 | * Class that is responsible for computations of the content rectangle of
751 | * provided DOM element and for keeping track of it's changes.
752 | */
753 | var ResizeObservation = function(target) {
754 | this.broadcastWidth = 0;
755 | this.broadcastHeight = 0;
756 | this.contentRect_ = createRectInit(0, 0, 0, 0);
757 |
758 | this.target = target;
759 | };
760 |
761 | /**
762 | * Updates content rectangle and tells whether it's width or height properties
763 | * have changed since the last broadcast.
764 | *
765 | * @returns {boolean}
766 | */
767 |
768 |
769 | /**
770 | * Reference to the last observed content rectangle.
771 | *
772 | * @private {DOMRectInit}
773 | */
774 |
775 |
776 | /**
777 | * Broadcasted width of content rectangle.
778 | *
779 | * @type {number}
780 | */
781 | ResizeObservation.prototype.isActive = function () {
782 | var rect = getContentRect(this.target);
783 |
784 | this.contentRect_ = rect;
785 |
786 | return rect.width !== this.broadcastWidth || rect.height !== this.broadcastHeight;
787 | };
788 |
789 | /**
790 | * Updates 'broadcastWidth' and 'broadcastHeight' properties with a data
791 | * from the corresponding properties of the last observed content rectangle.
792 | *
793 | * @returns {DOMRectInit} Last observed content rectangle.
794 | */
795 | ResizeObservation.prototype.broadcastRect = function () {
796 | var rect = this.contentRect_;
797 |
798 | this.broadcastWidth = rect.width;
799 | this.broadcastHeight = rect.height;
800 |
801 | return rect;
802 | };
803 |
804 | var ResizeObserverEntry = function(target, rectInit) {
805 | var contentRect = createReadOnlyRect(rectInit);
806 |
807 | // According to the specification following properties are not writable
808 | // and are also not enumerable in the native implementation.
809 | //
810 | // Property accessors are not being used as they'd require to define a
811 | // private WeakMap storage which may cause memory leaks in browsers that
812 | // don't support this type of collections.
813 | defineConfigurable(this, { target: target, contentRect: contentRect });
814 | };
815 |
816 | var ResizeObserverSPI = function(callback, controller, callbackCtx) {
817 | this.activeObservations_ = [];
818 | this.observations_ = new MapShim();
819 |
820 | if (typeof callback !== 'function') {
821 | throw new TypeError('The callback provided as parameter 1 is not a function.');
822 | }
823 |
824 | this.callback_ = callback;
825 | this.controller_ = controller;
826 | this.callbackCtx_ = callbackCtx;
827 | };
828 |
829 | /**
830 | * Starts observing provided element.
831 | *
832 | * @param {Element} target - Element to be observed.
833 | * @returns {void}
834 | */
835 |
836 |
837 | /**
838 | * Registry of the ResizeObservation instances.
839 | *
840 | * @private {Map}
841 | */
842 |
843 |
844 | /**
845 | * Public ResizeObserver instance which will be passed to the callback
846 | * function and used as a value of it's "this" binding.
847 | *
848 | * @private {ResizeObserver}
849 | */
850 |
851 | /**
852 | * Collection of resize observations that have detected changes in dimensions
853 | * of elements.
854 | *
855 | * @private {Array}
856 | */
857 | ResizeObserverSPI.prototype.observe = function (target) {
858 | if (!arguments.length) {
859 | throw new TypeError('1 argument required, but only 0 present.');
860 | }
861 |
862 | // Do nothing if current environment doesn't have the Element interface.
863 | if (typeof Element === 'undefined' || !(Element instanceof Object)) {
864 | return;
865 | }
866 |
867 | if (!(target instanceof getWindowOf(target).Element)) {
868 | throw new TypeError('parameter 1 is not of type "Element".');
869 | }
870 |
871 | var observations = this.observations_;
872 |
873 | // Do nothing if element is already being observed.
874 | if (observations.has(target)) {
875 | return;
876 | }
877 |
878 | observations.set(target, new ResizeObservation(target));
879 |
880 | this.controller_.addObserver(this);
881 |
882 | // Force the update of observations.
883 | this.controller_.refresh();
884 | };
885 |
886 | /**
887 | * Stops observing provided element.
888 | *
889 | * @param {Element} target - Element to stop observing.
890 | * @returns {void}
891 | */
892 | ResizeObserverSPI.prototype.unobserve = function (target) {
893 | if (!arguments.length) {
894 | throw new TypeError('1 argument required, but only 0 present.');
895 | }
896 |
897 | // Do nothing if current environment doesn't have the Element interface.
898 | if (typeof Element === 'undefined' || !(Element instanceof Object)) {
899 | return;
900 | }
901 |
902 | if (!(target instanceof getWindowOf(target).Element)) {
903 | throw new TypeError('parameter 1 is not of type "Element".');
904 | }
905 |
906 | var observations = this.observations_;
907 |
908 | // Do nothing if element is not being observed.
909 | if (!observations.has(target)) {
910 | return;
911 | }
912 |
913 | observations.delete(target);
914 |
915 | if (!observations.size) {
916 | this.controller_.removeObserver(this);
917 | }
918 | };
919 |
920 | /**
921 | * Stops observing all elements.
922 | *
923 | * @returns {void}
924 | */
925 | ResizeObserverSPI.prototype.disconnect = function () {
926 | this.clearActive();
927 | this.observations_.clear();
928 | this.controller_.removeObserver(this);
929 | };
930 |
931 | /**
932 | * Collects observation instances the associated element of which has changed
933 | * it's content rectangle.
934 | *
935 | * @returns {void}
936 | */
937 | ResizeObserverSPI.prototype.gatherActive = function () {
938 | var this$1 = this;
939 |
940 | this.clearActive();
941 |
942 | this.observations_.forEach(function (observation) {
943 | if (observation.isActive()) {
944 | this$1.activeObservations_.push(observation);
945 | }
946 | });
947 | };
948 |
949 | /**
950 | * Invokes initial callback function with a list of ResizeObserverEntry
951 | * instances collected from active resize observations.
952 | *
953 | * @returns {void}
954 | */
955 | ResizeObserverSPI.prototype.broadcastActive = function () {
956 | // Do nothing if observer doesn't have active observations.
957 | if (!this.hasActive()) {
958 | return;
959 | }
960 |
961 | var ctx = this.callbackCtx_;
962 |
963 | // Create ResizeObserverEntry instance for every active observation.
964 | var entries = this.activeObservations_.map(function (observation) {
965 | return new ResizeObserverEntry(observation.target, observation.broadcastRect());
966 | });
967 |
968 | this.callback_.call(ctx, entries, ctx);
969 | this.clearActive();
970 | };
971 |
972 | /**
973 | * Clears the collection of active observations.
974 | *
975 | * @returns {void}
976 | */
977 | ResizeObserverSPI.prototype.clearActive = function () {
978 | this.activeObservations_.splice(0);
979 | };
980 |
981 | /**
982 | * Tells whether observer has active observations.
983 | *
984 | * @returns {boolean}
985 | */
986 | ResizeObserverSPI.prototype.hasActive = function () {
987 | return this.activeObservations_.length > 0;
988 | };
989 |
990 | // Registry of internal observers. If WeakMap is not available use current shim
991 | // for the Map collection as it has all required methods and because WeakMap
992 | // can't be fully polyfilled anyway.
993 | var observers = typeof WeakMap !== 'undefined' ? new WeakMap() : new MapShim();
994 |
995 | /**
996 | * ResizeObserver API. Encapsulates the ResizeObserver SPI implementation
997 | * exposing only those methods and properties that are defined in the spec.
998 | */
999 | var ResizeObserver = function(callback) {
1000 | if (!(this instanceof ResizeObserver)) {
1001 | throw new TypeError('Cannot call a class as a function.');
1002 | }
1003 | if (!arguments.length) {
1004 | throw new TypeError('1 argument required, but only 0 present.');
1005 | }
1006 |
1007 | var controller = ResizeObserverController.getInstance();
1008 | var observer = new ResizeObserverSPI(callback, controller, this);
1009 |
1010 | observers.set(this, observer);
1011 | };
1012 |
1013 | // Expose public methods of ResizeObserver.
1014 | ['observe', 'unobserve', 'disconnect'].forEach(function (method) {
1015 | ResizeObserver.prototype[method] = function () {
1016 | return (ref = observers.get(this))[method].apply(ref, arguments);
1017 | var ref;
1018 | };
1019 | });
1020 |
1021 | var index = (function () {
1022 | // Export existing implementation if available.
1023 | if (typeof global$1.ResizeObserver !== 'undefined') {
1024 | return global$1.ResizeObserver;
1025 | }
1026 |
1027 | global$1.ResizeObserver = ResizeObserver;
1028 |
1029 | return ResizeObserver;
1030 | })();
1031 |
1032 | return index;
1033 |
1034 | })));
1035 |
--------------------------------------------------------------------------------
/dist/ResizeObserver.js:
--------------------------------------------------------------------------------
1 | (function (global, factory) {
2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
3 | typeof define === 'function' && define.amd ? define(factory) :
4 | (global.ResizeObserver = factory());
5 | }(this, (function () { 'use strict';
6 |
7 | /**
8 | * A collection of shims that provide minimal functionality of the ES6 collections.
9 | *
10 | * These implementations are not meant to be used outside of the ResizeObserver
11 | * modules as they cover only a limited range of use cases.
12 | */
13 | /* eslint-disable require-jsdoc, valid-jsdoc */
14 | var MapShim = (function () {
15 | if (typeof Map !== 'undefined') {
16 | return Map;
17 | }
18 | /**
19 | * Returns index in provided array that matches the specified key.
20 | *
21 | * @param {Array} arr
22 | * @param {*} key
23 | * @returns {number}
24 | */
25 | function getIndex(arr, key) {
26 | var result = -1;
27 | arr.some(function (entry, index) {
28 | if (entry[0] === key) {
29 | result = index;
30 | return true;
31 | }
32 | return false;
33 | });
34 | return result;
35 | }
36 | return /** @class */ (function () {
37 | function class_1() {
38 | this.__entries__ = [];
39 | }
40 | Object.defineProperty(class_1.prototype, "size", {
41 | /**
42 | * @returns {boolean}
43 | */
44 | get: function () {
45 | return this.__entries__.length;
46 | },
47 | enumerable: true,
48 | configurable: true
49 | });
50 | /**
51 | * @param {*} key
52 | * @returns {*}
53 | */
54 | class_1.prototype.get = function (key) {
55 | var index = getIndex(this.__entries__, key);
56 | var entry = this.__entries__[index];
57 | return entry && entry[1];
58 | };
59 | /**
60 | * @param {*} key
61 | * @param {*} value
62 | * @returns {void}
63 | */
64 | class_1.prototype.set = function (key, value) {
65 | var index = getIndex(this.__entries__, key);
66 | if (~index) {
67 | this.__entries__[index][1] = value;
68 | }
69 | else {
70 | this.__entries__.push([key, value]);
71 | }
72 | };
73 | /**
74 | * @param {*} key
75 | * @returns {void}
76 | */
77 | class_1.prototype.delete = function (key) {
78 | var entries = this.__entries__;
79 | var index = getIndex(entries, key);
80 | if (~index) {
81 | entries.splice(index, 1);
82 | }
83 | };
84 | /**
85 | * @param {*} key
86 | * @returns {void}
87 | */
88 | class_1.prototype.has = function (key) {
89 | return !!~getIndex(this.__entries__, key);
90 | };
91 | /**
92 | * @returns {void}
93 | */
94 | class_1.prototype.clear = function () {
95 | this.__entries__.splice(0);
96 | };
97 | /**
98 | * @param {Function} callback
99 | * @param {*} [ctx=null]
100 | * @returns {void}
101 | */
102 | class_1.prototype.forEach = function (callback, ctx) {
103 | if (ctx === void 0) { ctx = null; }
104 | for (var _i = 0, _a = this.__entries__; _i < _a.length; _i++) {
105 | var entry = _a[_i];
106 | callback.call(ctx, entry[1], entry[0]);
107 | }
108 | };
109 | return class_1;
110 | }());
111 | })();
112 |
113 | /**
114 | * Detects whether window and document objects are available in current environment.
115 | */
116 | var isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined' && window.document === document;
117 |
118 | // Returns global object of a current environment.
119 | var global$1 = (function () {
120 | if (typeof global !== 'undefined' && global.Math === Math) {
121 | return global;
122 | }
123 | if (typeof self !== 'undefined' && self.Math === Math) {
124 | return self;
125 | }
126 | if (typeof window !== 'undefined' && window.Math === Math) {
127 | return window;
128 | }
129 | // eslint-disable-next-line no-new-func
130 | return Function('return this')();
131 | })();
132 |
133 | /**
134 | * A shim for the requestAnimationFrame which falls back to the setTimeout if
135 | * first one is not supported.
136 | *
137 | * @returns {number} Requests' identifier.
138 | */
139 | var requestAnimationFrame$1 = (function () {
140 | if (typeof requestAnimationFrame === 'function') {
141 | // It's required to use a bounded function because IE sometimes throws
142 | // an "Invalid calling object" error if rAF is invoked without the global
143 | // object on the left hand side.
144 | return requestAnimationFrame.bind(global$1);
145 | }
146 | return function (callback) { return setTimeout(function () { return callback(Date.now()); }, 1000 / 60); };
147 | })();
148 |
149 | // Defines minimum timeout before adding a trailing call.
150 | var trailingTimeout = 2;
151 | /**
152 | * Creates a wrapper function which ensures that provided callback will be
153 | * invoked only once during the specified delay period.
154 | *
155 | * @param {Function} callback - Function to be invoked after the delay period.
156 | * @param {number} delay - Delay after which to invoke callback.
157 | * @returns {Function}
158 | */
159 | function throttle (callback, delay) {
160 | var leadingCall = false, trailingCall = false, lastCallTime = 0;
161 | /**
162 | * Invokes the original callback function and schedules new invocation if
163 | * the "proxy" was called during current request.
164 | *
165 | * @returns {void}
166 | */
167 | function resolvePending() {
168 | if (leadingCall) {
169 | leadingCall = false;
170 | callback();
171 | }
172 | if (trailingCall) {
173 | proxy();
174 | }
175 | }
176 | /**
177 | * Callback invoked after the specified delay. It will further postpone
178 | * invocation of the original function delegating it to the
179 | * requestAnimationFrame.
180 | *
181 | * @returns {void}
182 | */
183 | function timeoutCallback() {
184 | requestAnimationFrame$1(resolvePending);
185 | }
186 | /**
187 | * Schedules invocation of the original function.
188 | *
189 | * @returns {void}
190 | */
191 | function proxy() {
192 | var timeStamp = Date.now();
193 | if (leadingCall) {
194 | // Reject immediately following calls.
195 | if (timeStamp - lastCallTime < trailingTimeout) {
196 | return;
197 | }
198 | // Schedule new call to be in invoked when the pending one is resolved.
199 | // This is important for "transitions" which never actually start
200 | // immediately so there is a chance that we might miss one if change
201 | // happens amids the pending invocation.
202 | trailingCall = true;
203 | }
204 | else {
205 | leadingCall = true;
206 | trailingCall = false;
207 | setTimeout(timeoutCallback, delay);
208 | }
209 | lastCallTime = timeStamp;
210 | }
211 | return proxy;
212 | }
213 |
214 | // Minimum delay before invoking the update of observers.
215 | var REFRESH_DELAY = 20;
216 | // A list of substrings of CSS properties used to find transition events that
217 | // might affect dimensions of observed elements.
218 | var transitionKeys = ['top', 'right', 'bottom', 'left', 'width', 'height', 'size', 'weight'];
219 | // Check if MutationObserver is available.
220 | var mutationObserverSupported = typeof MutationObserver !== 'undefined';
221 | /**
222 | * Singleton controller class which handles updates of ResizeObserver instances.
223 | */
224 | var ResizeObserverController = /** @class */ (function () {
225 | /**
226 | * Creates a new instance of ResizeObserverController.
227 | *
228 | * @private
229 | */
230 | function ResizeObserverController() {
231 | /**
232 | * Indicates whether DOM listeners have been added.
233 | *
234 | * @private {boolean}
235 | */
236 | this.connected_ = false;
237 | /**
238 | * Tells that controller has subscribed for Mutation Events.
239 | *
240 | * @private {boolean}
241 | */
242 | this.mutationEventsAdded_ = false;
243 | /**
244 | * Keeps reference to the instance of MutationObserver.
245 | *
246 | * @private {MutationObserver}
247 | */
248 | this.mutationsObserver_ = null;
249 | /**
250 | * A list of connected observers.
251 | *
252 | * @private {Array}
253 | */
254 | this.observers_ = [];
255 | this.onTransitionEnd_ = this.onTransitionEnd_.bind(this);
256 | this.refresh = throttle(this.refresh.bind(this), REFRESH_DELAY);
257 | }
258 | /**
259 | * Adds observer to observers list.
260 | *
261 | * @param {ResizeObserverSPI} observer - Observer to be added.
262 | * @returns {void}
263 | */
264 | ResizeObserverController.prototype.addObserver = function (observer) {
265 | if (!~this.observers_.indexOf(observer)) {
266 | this.observers_.push(observer);
267 | }
268 | // Add listeners if they haven't been added yet.
269 | if (!this.connected_) {
270 | this.connect_();
271 | }
272 | };
273 | /**
274 | * Removes observer from observers list.
275 | *
276 | * @param {ResizeObserverSPI} observer - Observer to be removed.
277 | * @returns {void}
278 | */
279 | ResizeObserverController.prototype.removeObserver = function (observer) {
280 | var observers = this.observers_;
281 | var index = observers.indexOf(observer);
282 | // Remove observer if it's present in registry.
283 | if (~index) {
284 | observers.splice(index, 1);
285 | }
286 | // Remove listeners if controller has no connected observers.
287 | if (!observers.length && this.connected_) {
288 | this.disconnect_();
289 | }
290 | };
291 | /**
292 | * Invokes the update of observers. It will continue running updates insofar
293 | * it detects changes.
294 | *
295 | * @returns {void}
296 | */
297 | ResizeObserverController.prototype.refresh = function () {
298 | var changesDetected = this.updateObservers_();
299 | // Continue running updates if changes have been detected as there might
300 | // be future ones caused by CSS transitions.
301 | if (changesDetected) {
302 | this.refresh();
303 | }
304 | };
305 | /**
306 | * Updates every observer from observers list and notifies them of queued
307 | * entries.
308 | *
309 | * @private
310 | * @returns {boolean} Returns "true" if any observer has detected changes in
311 | * dimensions of it's elements.
312 | */
313 | ResizeObserverController.prototype.updateObservers_ = function () {
314 | // Collect observers that have active observations.
315 | var activeObservers = this.observers_.filter(function (observer) {
316 | return observer.gatherActive(), observer.hasActive();
317 | });
318 | // Deliver notifications in a separate cycle in order to avoid any
319 | // collisions between observers, e.g. when multiple instances of
320 | // ResizeObserver are tracking the same element and the callback of one
321 | // of them changes content dimensions of the observed target. Sometimes
322 | // this may result in notifications being blocked for the rest of observers.
323 | activeObservers.forEach(function (observer) { return observer.broadcastActive(); });
324 | return activeObservers.length > 0;
325 | };
326 | /**
327 | * Initializes DOM listeners.
328 | *
329 | * @private
330 | * @returns {void}
331 | */
332 | ResizeObserverController.prototype.connect_ = function () {
333 | // Do nothing if running in a non-browser environment or if listeners
334 | // have been already added.
335 | if (!isBrowser || this.connected_) {
336 | return;
337 | }
338 | // Subscription to the "Transitionend" event is used as a workaround for
339 | // delayed transitions. This way it's possible to capture at least the
340 | // final state of an element.
341 | document.addEventListener('transitionend', this.onTransitionEnd_);
342 | window.addEventListener('resize', this.refresh);
343 | if (mutationObserverSupported) {
344 | this.mutationsObserver_ = new MutationObserver(this.refresh);
345 | this.mutationsObserver_.observe(document, {
346 | attributes: true,
347 | childList: true,
348 | characterData: true,
349 | subtree: true
350 | });
351 | }
352 | else {
353 | document.addEventListener('DOMSubtreeModified', this.refresh);
354 | this.mutationEventsAdded_ = true;
355 | }
356 | this.connected_ = true;
357 | };
358 | /**
359 | * Removes DOM listeners.
360 | *
361 | * @private
362 | * @returns {void}
363 | */
364 | ResizeObserverController.prototype.disconnect_ = function () {
365 | // Do nothing if running in a non-browser environment or if listeners
366 | // have been already removed.
367 | if (!isBrowser || !this.connected_) {
368 | return;
369 | }
370 | document.removeEventListener('transitionend', this.onTransitionEnd_);
371 | window.removeEventListener('resize', this.refresh);
372 | if (this.mutationsObserver_) {
373 | this.mutationsObserver_.disconnect();
374 | }
375 | if (this.mutationEventsAdded_) {
376 | document.removeEventListener('DOMSubtreeModified', this.refresh);
377 | }
378 | this.mutationsObserver_ = null;
379 | this.mutationEventsAdded_ = false;
380 | this.connected_ = false;
381 | };
382 | /**
383 | * "Transitionend" event handler.
384 | *
385 | * @private
386 | * @param {TransitionEvent} event
387 | * @returns {void}
388 | */
389 | ResizeObserverController.prototype.onTransitionEnd_ = function (_a) {
390 | var _b = _a.propertyName, propertyName = _b === void 0 ? '' : _b;
391 | // Detect whether transition may affect dimensions of an element.
392 | var isReflowProperty = transitionKeys.some(function (key) {
393 | return !!~propertyName.indexOf(key);
394 | });
395 | if (isReflowProperty) {
396 | this.refresh();
397 | }
398 | };
399 | /**
400 | * Returns instance of the ResizeObserverController.
401 | *
402 | * @returns {ResizeObserverController}
403 | */
404 | ResizeObserverController.getInstance = function () {
405 | if (!this.instance_) {
406 | this.instance_ = new ResizeObserverController();
407 | }
408 | return this.instance_;
409 | };
410 | /**
411 | * Holds reference to the controller's instance.
412 | *
413 | * @private {ResizeObserverController}
414 | */
415 | ResizeObserverController.instance_ = null;
416 | return ResizeObserverController;
417 | }());
418 |
419 | /**
420 | * Defines non-writable/enumerable properties of the provided target object.
421 | *
422 | * @param {Object} target - Object for which to define properties.
423 | * @param {Object} props - Properties to be defined.
424 | * @returns {Object} Target object.
425 | */
426 | var defineConfigurable = (function (target, props) {
427 | for (var _i = 0, _a = Object.keys(props); _i < _a.length; _i++) {
428 | var key = _a[_i];
429 | Object.defineProperty(target, key, {
430 | value: props[key],
431 | enumerable: false,
432 | writable: false,
433 | configurable: true
434 | });
435 | }
436 | return target;
437 | });
438 |
439 | /**
440 | * Returns the global object associated with provided element.
441 | *
442 | * @param {Object} target
443 | * @returns {Object}
444 | */
445 | var getWindowOf = (function (target) {
446 | // Assume that the element is an instance of Node, which means that it
447 | // has the "ownerDocument" property from which we can retrieve a
448 | // corresponding global object.
449 | var ownerGlobal = target && target.ownerDocument && target.ownerDocument.defaultView;
450 | // Return the local global object if it's not possible extract one from
451 | // provided element.
452 | return ownerGlobal || global$1;
453 | });
454 |
455 | // Placeholder of an empty content rectangle.
456 | var emptyRect = createRectInit(0, 0, 0, 0);
457 | /**
458 | * Converts provided string to a number.
459 | *
460 | * @param {number|string} value
461 | * @returns {number}
462 | */
463 | function toFloat(value) {
464 | return parseFloat(value) || 0;
465 | }
466 | /**
467 | * Extracts borders size from provided styles.
468 | *
469 | * @param {CSSStyleDeclaration} styles
470 | * @param {...string} positions - Borders positions (top, right, ...)
471 | * @returns {number}
472 | */
473 | function getBordersSize(styles) {
474 | var positions = [];
475 | for (var _i = 1; _i < arguments.length; _i++) {
476 | positions[_i - 1] = arguments[_i];
477 | }
478 | return positions.reduce(function (size, position) {
479 | var value = styles['border-' + position + '-width'];
480 | return size + toFloat(value);
481 | }, 0);
482 | }
483 | /**
484 | * Extracts paddings sizes from provided styles.
485 | *
486 | * @param {CSSStyleDeclaration} styles
487 | * @returns {Object} Paddings box.
488 | */
489 | function getPaddings(styles) {
490 | var positions = ['top', 'right', 'bottom', 'left'];
491 | var paddings = {};
492 | for (var _i = 0, positions_1 = positions; _i < positions_1.length; _i++) {
493 | var position = positions_1[_i];
494 | var value = styles['padding-' + position];
495 | paddings[position] = toFloat(value);
496 | }
497 | return paddings;
498 | }
499 | /**
500 | * Calculates content rectangle of provided SVG element.
501 | *
502 | * @param {SVGGraphicsElement} target - Element content rectangle of which needs
503 | * to be calculated.
504 | * @returns {DOMRectInit}
505 | */
506 | function getSVGContentRect(target) {
507 | var bbox = target.getBBox();
508 | return createRectInit(0, 0, bbox.width, bbox.height);
509 | }
510 | /**
511 | * Calculates content rectangle of provided HTMLElement.
512 | *
513 | * @param {HTMLElement} target - Element for which to calculate the content rectangle.
514 | * @returns {DOMRectInit}
515 | */
516 | function getHTMLElementContentRect(target) {
517 | // Client width & height properties can't be
518 | // used exclusively as they provide rounded values.
519 | var clientWidth = target.clientWidth, clientHeight = target.clientHeight;
520 | // By this condition we can catch all non-replaced inline, hidden and
521 | // detached elements. Though elements with width & height properties less
522 | // than 0.5 will be discarded as well.
523 | //
524 | // Without it we would need to implement separate methods for each of
525 | // those cases and it's not possible to perform a precise and performance
526 | // effective test for hidden elements. E.g. even jQuery's ':visible' filter
527 | // gives wrong results for elements with width & height less than 0.5.
528 | if (!clientWidth && !clientHeight) {
529 | return emptyRect;
530 | }
531 | var styles = getWindowOf(target).getComputedStyle(target);
532 | var paddings = getPaddings(styles);
533 | var horizPad = paddings.left + paddings.right;
534 | var vertPad = paddings.top + paddings.bottom;
535 | // Computed styles of width & height are being used because they are the
536 | // only dimensions available to JS that contain non-rounded values. It could
537 | // be possible to utilize the getBoundingClientRect if only it's data wasn't
538 | // affected by CSS transformations let alone paddings, borders and scroll bars.
539 | var width = toFloat(styles.width), height = toFloat(styles.height);
540 | // Width & height include paddings and borders when the 'border-box' box
541 | // model is applied (except for IE).
542 | if (styles.boxSizing === 'border-box') {
543 | // Following conditions are required to handle Internet Explorer which
544 | // doesn't include paddings and borders to computed CSS dimensions.
545 | //
546 | // We can say that if CSS dimensions + paddings are equal to the "client"
547 | // properties then it's either IE, and thus we don't need to subtract
548 | // anything, or an element merely doesn't have paddings/borders styles.
549 | if (Math.round(width + horizPad) !== clientWidth) {
550 | width -= getBordersSize(styles, 'left', 'right') + horizPad;
551 | }
552 | if (Math.round(height + vertPad) !== clientHeight) {
553 | height -= getBordersSize(styles, 'top', 'bottom') + vertPad;
554 | }
555 | }
556 | // Following steps can't be applied to the document's root element as its
557 | // client[Width/Height] properties represent viewport area of the window.
558 | // Besides, it's as well not necessary as the itself neither has
559 | // rendered scroll bars nor it can be clipped.
560 | if (!isDocumentElement(target)) {
561 | // In some browsers (only in Firefox, actually) CSS width & height
562 | // include scroll bars size which can be removed at this step as scroll
563 | // bars are the only difference between rounded dimensions + paddings
564 | // and "client" properties, though that is not always true in Chrome.
565 | var vertScrollbar = Math.round(width + horizPad) - clientWidth;
566 | var horizScrollbar = Math.round(height + vertPad) - clientHeight;
567 | // Chrome has a rather weird rounding of "client" properties.
568 | // E.g. for an element with content width of 314.2px it sometimes gives
569 | // the client width of 315px and for the width of 314.7px it may give
570 | // 314px. And it doesn't happen all the time. So just ignore this delta
571 | // as a non-relevant.
572 | if (Math.abs(vertScrollbar) !== 1) {
573 | width -= vertScrollbar;
574 | }
575 | if (Math.abs(horizScrollbar) !== 1) {
576 | height -= horizScrollbar;
577 | }
578 | }
579 | return createRectInit(paddings.left, paddings.top, width, height);
580 | }
581 | /**
582 | * Checks whether provided element is an instance of the SVGGraphicsElement.
583 | *
584 | * @param {Element} target - Element to be checked.
585 | * @returns {boolean}
586 | */
587 | var isSVGGraphicsElement = (function () {
588 | // Some browsers, namely IE and Edge, don't have the SVGGraphicsElement
589 | // interface.
590 | if (typeof SVGGraphicsElement !== 'undefined') {
591 | return function (target) { return target instanceof getWindowOf(target).SVGGraphicsElement; };
592 | }
593 | // If it's so, then check that element is at least an instance of the
594 | // SVGElement and that it has the "getBBox" method.
595 | // eslint-disable-next-line no-extra-parens
596 | return function (target) { return (target instanceof getWindowOf(target).SVGElement &&
597 | typeof target.getBBox === 'function'); };
598 | })();
599 | /**
600 | * Checks whether provided element is a document element ().
601 | *
602 | * @param {Element} target - Element to be checked.
603 | * @returns {boolean}
604 | */
605 | function isDocumentElement(target) {
606 | return target === getWindowOf(target).document.documentElement;
607 | }
608 | /**
609 | * Calculates an appropriate content rectangle for provided html or svg element.
610 | *
611 | * @param {Element} target - Element content rectangle of which needs to be calculated.
612 | * @returns {DOMRectInit}
613 | */
614 | function getContentRect(target) {
615 | if (!isBrowser) {
616 | return emptyRect;
617 | }
618 | if (isSVGGraphicsElement(target)) {
619 | return getSVGContentRect(target);
620 | }
621 | return getHTMLElementContentRect(target);
622 | }
623 | /**
624 | * Creates rectangle with an interface of the DOMRectReadOnly.
625 | * Spec: https://drafts.fxtf.org/geometry/#domrectreadonly
626 | *
627 | * @param {DOMRectInit} rectInit - Object with rectangle's x/y coordinates and dimensions.
628 | * @returns {DOMRectReadOnly}
629 | */
630 | function createReadOnlyRect(_a) {
631 | var x = _a.x, y = _a.y, width = _a.width, height = _a.height;
632 | // If DOMRectReadOnly is available use it as a prototype for the rectangle.
633 | var Constr = typeof DOMRectReadOnly !== 'undefined' ? DOMRectReadOnly : Object;
634 | var rect = Object.create(Constr.prototype);
635 | // Rectangle's properties are not writable and non-enumerable.
636 | defineConfigurable(rect, {
637 | x: x, y: y, width: width, height: height,
638 | top: y,
639 | right: x + width,
640 | bottom: height + y,
641 | left: x
642 | });
643 | return rect;
644 | }
645 | /**
646 | * Creates DOMRectInit object based on the provided dimensions and the x/y coordinates.
647 | * Spec: https://drafts.fxtf.org/geometry/#dictdef-domrectinit
648 | *
649 | * @param {number} x - X coordinate.
650 | * @param {number} y - Y coordinate.
651 | * @param {number} width - Rectangle's width.
652 | * @param {number} height - Rectangle's height.
653 | * @returns {DOMRectInit}
654 | */
655 | function createRectInit(x, y, width, height) {
656 | return { x: x, y: y, width: width, height: height };
657 | }
658 |
659 | /**
660 | * Class that is responsible for computations of the content rectangle of
661 | * provided DOM element and for keeping track of it's changes.
662 | */
663 | var ResizeObservation = /** @class */ (function () {
664 | /**
665 | * Creates an instance of ResizeObservation.
666 | *
667 | * @param {Element} target - Element to be observed.
668 | */
669 | function ResizeObservation(target) {
670 | /**
671 | * Broadcasted width of content rectangle.
672 | *
673 | * @type {number}
674 | */
675 | this.broadcastWidth = 0;
676 | /**
677 | * Broadcasted height of content rectangle.
678 | *
679 | * @type {number}
680 | */
681 | this.broadcastHeight = 0;
682 | /**
683 | * Reference to the last observed content rectangle.
684 | *
685 | * @private {DOMRectInit}
686 | */
687 | this.contentRect_ = createRectInit(0, 0, 0, 0);
688 | this.target = target;
689 | }
690 | /**
691 | * Updates content rectangle and tells whether it's width or height properties
692 | * have changed since the last broadcast.
693 | *
694 | * @returns {boolean}
695 | */
696 | ResizeObservation.prototype.isActive = function () {
697 | var rect = getContentRect(this.target);
698 | this.contentRect_ = rect;
699 | return (rect.width !== this.broadcastWidth ||
700 | rect.height !== this.broadcastHeight);
701 | };
702 | /**
703 | * Updates 'broadcastWidth' and 'broadcastHeight' properties with a data
704 | * from the corresponding properties of the last observed content rectangle.
705 | *
706 | * @returns {DOMRectInit} Last observed content rectangle.
707 | */
708 | ResizeObservation.prototype.broadcastRect = function () {
709 | var rect = this.contentRect_;
710 | this.broadcastWidth = rect.width;
711 | this.broadcastHeight = rect.height;
712 | return rect;
713 | };
714 | return ResizeObservation;
715 | }());
716 |
717 | var ResizeObserverEntry = /** @class */ (function () {
718 | /**
719 | * Creates an instance of ResizeObserverEntry.
720 | *
721 | * @param {Element} target - Element that is being observed.
722 | * @param {DOMRectInit} rectInit - Data of the element's content rectangle.
723 | */
724 | function ResizeObserverEntry(target, rectInit) {
725 | var contentRect = createReadOnlyRect(rectInit);
726 | // According to the specification following properties are not writable
727 | // and are also not enumerable in the native implementation.
728 | //
729 | // Property accessors are not being used as they'd require to define a
730 | // private WeakMap storage which may cause memory leaks in browsers that
731 | // don't support this type of collections.
732 | defineConfigurable(this, { target: target, contentRect: contentRect });
733 | }
734 | return ResizeObserverEntry;
735 | }());
736 |
737 | var ResizeObserverSPI = /** @class */ (function () {
738 | /**
739 | * Creates a new instance of ResizeObserver.
740 | *
741 | * @param {ResizeObserverCallback} callback - Callback function that is invoked
742 | * when one of the observed elements changes it's content dimensions.
743 | * @param {ResizeObserverController} controller - Controller instance which
744 | * is responsible for the updates of observer.
745 | * @param {ResizeObserver} callbackCtx - Reference to the public
746 | * ResizeObserver instance which will be passed to callback function.
747 | */
748 | function ResizeObserverSPI(callback, controller, callbackCtx) {
749 | /**
750 | * Collection of resize observations that have detected changes in dimensions
751 | * of elements.
752 | *
753 | * @private {Array}
754 | */
755 | this.activeObservations_ = [];
756 | /**
757 | * Registry of the ResizeObservation instances.
758 | *
759 | * @private {Map}
760 | */
761 | this.observations_ = new MapShim();
762 | if (typeof callback !== 'function') {
763 | throw new TypeError('The callback provided as parameter 1 is not a function.');
764 | }
765 | this.callback_ = callback;
766 | this.controller_ = controller;
767 | this.callbackCtx_ = callbackCtx;
768 | }
769 | /**
770 | * Starts observing provided element.
771 | *
772 | * @param {Element} target - Element to be observed.
773 | * @returns {void}
774 | */
775 | ResizeObserverSPI.prototype.observe = function (target) {
776 | if (!arguments.length) {
777 | throw new TypeError('1 argument required, but only 0 present.');
778 | }
779 | // Do nothing if current environment doesn't have the Element interface.
780 | if (typeof Element === 'undefined' || !(Element instanceof Object)) {
781 | return;
782 | }
783 | if (!(target instanceof getWindowOf(target).Element)) {
784 | throw new TypeError('parameter 1 is not of type "Element".');
785 | }
786 | var observations = this.observations_;
787 | // Do nothing if element is already being observed.
788 | if (observations.has(target)) {
789 | return;
790 | }
791 | observations.set(target, new ResizeObservation(target));
792 | this.controller_.addObserver(this);
793 | // Force the update of observations.
794 | this.controller_.refresh();
795 | };
796 | /**
797 | * Stops observing provided element.
798 | *
799 | * @param {Element} target - Element to stop observing.
800 | * @returns {void}
801 | */
802 | ResizeObserverSPI.prototype.unobserve = function (target) {
803 | if (!arguments.length) {
804 | throw new TypeError('1 argument required, but only 0 present.');
805 | }
806 | // Do nothing if current environment doesn't have the Element interface.
807 | if (typeof Element === 'undefined' || !(Element instanceof Object)) {
808 | return;
809 | }
810 | if (!(target instanceof getWindowOf(target).Element)) {
811 | throw new TypeError('parameter 1 is not of type "Element".');
812 | }
813 | var observations = this.observations_;
814 | // Do nothing if element is not being observed.
815 | if (!observations.has(target)) {
816 | return;
817 | }
818 | observations.delete(target);
819 | if (!observations.size) {
820 | this.controller_.removeObserver(this);
821 | }
822 | };
823 | /**
824 | * Stops observing all elements.
825 | *
826 | * @returns {void}
827 | */
828 | ResizeObserverSPI.prototype.disconnect = function () {
829 | this.clearActive();
830 | this.observations_.clear();
831 | this.controller_.removeObserver(this);
832 | };
833 | /**
834 | * Collects observation instances the associated element of which has changed
835 | * it's content rectangle.
836 | *
837 | * @returns {void}
838 | */
839 | ResizeObserverSPI.prototype.gatherActive = function () {
840 | var _this = this;
841 | this.clearActive();
842 | this.observations_.forEach(function (observation) {
843 | if (observation.isActive()) {
844 | _this.activeObservations_.push(observation);
845 | }
846 | });
847 | };
848 | /**
849 | * Invokes initial callback function with a list of ResizeObserverEntry
850 | * instances collected from active resize observations.
851 | *
852 | * @returns {void}
853 | */
854 | ResizeObserverSPI.prototype.broadcastActive = function () {
855 | // Do nothing if observer doesn't have active observations.
856 | if (!this.hasActive()) {
857 | return;
858 | }
859 | var ctx = this.callbackCtx_;
860 | // Create ResizeObserverEntry instance for every active observation.
861 | var entries = this.activeObservations_.map(function (observation) {
862 | return new ResizeObserverEntry(observation.target, observation.broadcastRect());
863 | });
864 | this.callback_.call(ctx, entries, ctx);
865 | this.clearActive();
866 | };
867 | /**
868 | * Clears the collection of active observations.
869 | *
870 | * @returns {void}
871 | */
872 | ResizeObserverSPI.prototype.clearActive = function () {
873 | this.activeObservations_.splice(0);
874 | };
875 | /**
876 | * Tells whether observer has active observations.
877 | *
878 | * @returns {boolean}
879 | */
880 | ResizeObserverSPI.prototype.hasActive = function () {
881 | return this.activeObservations_.length > 0;
882 | };
883 | return ResizeObserverSPI;
884 | }());
885 |
886 | // Registry of internal observers. If WeakMap is not available use current shim
887 | // for the Map collection as it has all required methods and because WeakMap
888 | // can't be fully polyfilled anyway.
889 | var observers = typeof WeakMap !== 'undefined' ? new WeakMap() : new MapShim();
890 | /**
891 | * ResizeObserver API. Encapsulates the ResizeObserver SPI implementation
892 | * exposing only those methods and properties that are defined in the spec.
893 | */
894 | var ResizeObserver = /** @class */ (function () {
895 | /**
896 | * Creates a new instance of ResizeObserver.
897 | *
898 | * @param {ResizeObserverCallback} callback - Callback that is invoked when
899 | * dimensions of the observed elements change.
900 | */
901 | function ResizeObserver(callback) {
902 | if (!(this instanceof ResizeObserver)) {
903 | throw new TypeError('Cannot call a class as a function.');
904 | }
905 | if (!arguments.length) {
906 | throw new TypeError('1 argument required, but only 0 present.');
907 | }
908 | var controller = ResizeObserverController.getInstance();
909 | var observer = new ResizeObserverSPI(callback, controller, this);
910 | observers.set(this, observer);
911 | }
912 | return ResizeObserver;
913 | }());
914 | // Expose public methods of ResizeObserver.
915 | [
916 | 'observe',
917 | 'unobserve',
918 | 'disconnect'
919 | ].forEach(function (method) {
920 | ResizeObserver.prototype[method] = function () {
921 | var _a;
922 | return (_a = observers.get(this))[method].apply(_a, arguments);
923 | };
924 | });
925 |
926 | var index = (function () {
927 | // Export existing implementation if available.
928 | if (typeof global$1.ResizeObserver !== 'undefined') {
929 | return global$1.ResizeObserver;
930 | }
931 | return ResizeObserver;
932 | })();
933 |
934 | return index;
935 |
936 | })));
937 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | const typescript = require('rollup-plugin-typescript');
2 |
3 | const launchers = {
4 | windows: {
5 | SL_CHROME_CURRENT: {
6 | base: 'SauceLabs',
7 | platform: 'Windows 10',
8 | browserName: 'chrome'
9 | },
10 | SL_CHROME_PRECEDING: {
11 | base: 'SauceLabs',
12 | platform: 'Windows 10',
13 | browserName: 'chrome',
14 | version: 'latest-1'
15 | },
16 | SL_FIREFOX_CURRENT: {
17 | base: 'SauceLabs',
18 | platform: 'Windows 10',
19 | browserName: 'firefox'
20 | },
21 | SL_FIREFOX_PRECEDING: {
22 | base: 'SauceLabs',
23 | platform: 'Windows 10',
24 | browserName: 'firefox',
25 | version: 'latest-1'
26 | },
27 | SL_EDGE_17: {
28 | base: 'SauceLabs',
29 | platform: 'Windows 10',
30 | browserName: 'MicrosoftEdge',
31 | version: '17.17134'
32 | },
33 | SL_EDGE_16: {
34 | base: 'SauceLabs',
35 | platform: 'Windows 10',
36 | browserName: 'MicrosoftEdge',
37 | version: '16.16299'
38 | },
39 | SL_IE_11: {
40 | base: 'SauceLabs',
41 | browserName: 'internet explorer',
42 | platform: 'Windows 8.1',
43 | version: '11.0'
44 | },
45 | SL_IE_10: {
46 | base: 'SauceLabs',
47 | browserName: 'internet explorer',
48 | platform: 'Windows 7',
49 | version: '10.0'
50 | },
51 | SL_IE_9: {
52 | base: 'SauceLabs',
53 | browserName: 'internet explorer',
54 | platform: 'Windows 7',
55 | version: '9.0'
56 | }
57 | },
58 | linux: {
59 | SL_CHROME_CURRENT: {
60 | base: 'SauceLabs',
61 | browserName: 'chrome',
62 | platform: 'Linux'
63 | },
64 | SL_CHROME_PRECEDING: {
65 | base: 'SauceLabs',
66 | browserName: 'chrome',
67 | platform: 'Linux',
68 | version: 'latest-1'
69 | },
70 | SL_FIREFOX_CURRENT: {
71 | base: 'SauceLabs',
72 | platform: 'Linux',
73 | browserName: 'firefox'
74 | },
75 | SL_FIREFOX_PRECEDING: {
76 | base: 'SauceLabs',
77 | platform: 'Linux',
78 | browserName: 'firefox',
79 | version: 'latest-1'
80 | }
81 | },
82 | osx: {
83 | SL_CHROME_CURRENT: {
84 | base: 'SauceLabs',
85 | platform: 'macOS 10.13',
86 | browserName: 'chrome'
87 | },
88 | SL_CHROME_PRECEDING: {
89 | base: 'SauceLabs',
90 | platform: 'macOS 10.13',
91 | browserName: 'chrome',
92 | version: 'latest-1'
93 | },
94 | SL_SAFARI_12: {
95 | base: 'SauceLabs',
96 | platform: 'macOS 10.13',
97 | browserName: 'safari',
98 | version: '12.0'
99 | },
100 | SL_SAFARI_11: {
101 | base: 'SauceLabs',
102 | platform: 'macOS 10.13',
103 | browserName: 'safari',
104 | version: '11.1'
105 | }
106 | },
107 | ios: {
108 | SL_IOS_12: {
109 | base: 'SauceLabs',
110 | browserName: 'safari',
111 | deviceName: 'iPhone Simulator',
112 | platform: 'iOS',
113 | version: '12.0'
114 | },
115 | SL_IOS_11: {
116 | base: 'SauceLabs',
117 | browserName: 'safari',
118 | deviceName: 'iPhone Simulator',
119 | platform: 'iOS',
120 | version: '11.3'
121 | }
122 | }
123 | };
124 |
125 | module.exports = function (config) {
126 | const reporters = ['spec'];
127 |
128 | let browsers = [],
129 | customLaunchers = {};
130 |
131 | if (config.sauce) {
132 | if (!process.env.SAUCE_USERNAME || !process.env.SAUCE_ACCESS_KEY) {
133 | // eslint-disable-next-line no-console
134 | console.log('SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables are not defined.');
135 |
136 | process.exit(1);
137 | }
138 |
139 | // SauceLabs may randomly disconnect browsers if all of them are being
140 | // tested together. So, I'll keep launchers separated by the platform
141 | // until I figure out a cleaner way to run CI tests.
142 | customLaunchers = launchers[config.sauce];
143 | browsers = Object.keys(customLaunchers);
144 |
145 | reporters.push('saucelabs');
146 | } else {
147 | reporters.push('kjhtml');
148 | }
149 |
150 | config.set({
151 | singleRun: true,
152 | frameworks: ['jasmine'],
153 | files: [
154 | './node_modules/promise-polyfill/dist/polyfill.js',
155 | 'tests/**/*.spec.js'
156 | ],
157 | plugins: [
158 | 'karma-chrome-launcher',
159 | 'karma-firefox-launcher',
160 | 'karma-jasmine',
161 | 'karma-jasmine-html-reporter',
162 | 'karma-rollup-preprocessor',
163 | 'karma-sauce-launcher',
164 | 'karma-sourcemap-loader',
165 | 'karma-spec-reporter'
166 | ],
167 | port: 9876,
168 | captureTimeout: 4 * 60 * 1000,
169 | browserNoActivityTimeout: 4 * 60 * 1000,
170 | browserDisconnectTimeout: 10 * 1000,
171 | concurrency: 2,
172 | browserDisconnectTolerance: 3,
173 | reporters,
174 | browsers,
175 | customLaunchers,
176 | client: {
177 | native: config.native === true
178 | },
179 | preprocessors: {
180 | 'tests/*.js': ['rollup', 'sourcemap']
181 | },
182 | rollupPreprocessor: {
183 | plugins: [
184 | typescript({
185 | target: 'es5',
186 | include: [
187 | 'src/**/*',
188 | 'tests/**/*'
189 | ]
190 | })
191 | ],
192 | output: {
193 | format: 'iife',
194 | sourcemap: 'inline'
195 | }
196 | },
197 | sauceLabs: {
198 | testName: 'resize-observer-polyfill',
199 | public: 'public'
200 | }
201 | });
202 | };
203 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "resize-observer-polyfill",
3 | "author": "Denis Rul ",
4 | "version": "1.5.1",
5 | "description": "A polyfill for the Resize Observer API",
6 | "main": "dist/ResizeObserver.js",
7 | "module": "dist/ResizeObserver.es.js",
8 | "scripts": {
9 | "build": "rollup -c && cpy src/index.js.flow dist --rename=ResizeObserver.js.flow",
10 | "test": "npm run test:lint && npm run test:spec",
11 | "test:ci": "npm run test:lint && npm run test:spec:sauce && npm run test:spec:node",
12 | "test:ci:pull": "npm run test:lint && karma start --browsers Firefox && npm run test:spec:node",
13 | "test:lint": "node ./node_modules/eslint/bin/eslint.js \"**/*.js\" --ignore-pattern \"/dist/\"",
14 | "test:spec": "karma start --browsers Chrome && npm run test:spec:node",
15 | "test:spec:sauce": "karma start --sauce=windows && karma start --sauce=linux && karma start --sauce=osx",
16 | "test:spec:node": "npm run build && node tests/node/index.js",
17 | "test:spec:custom": "karma start --no-browsers",
18 | "test:spec:native": "karma start --no-browsers --native"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/que-etc/resize-observer-polyfill.git"
23 | },
24 | "license": "MIT",
25 | "bugs": {
26 | "url": "https://github.com/que-etc/resize-observer-polyfill/issues"
27 | },
28 | "types": "src/index.d.ts",
29 | "files": [
30 | "src/",
31 | "dist/"
32 | ],
33 | "keywords": [
34 | "ResizeObserver",
35 | "resize",
36 | "observer",
37 | "util",
38 | "client",
39 | "browser",
40 | "polyfill",
41 | "ponyfill"
42 | ],
43 | "homepage": "https://github.com/que-etc/resize-observer-polyfill",
44 | "devDependencies": {
45 | "babel-eslint": "10.0.1",
46 | "cpy-cli": "2.0.0",
47 | "eslint": "5.10.0",
48 | "jasmine": "2.8.0",
49 | "jasmine-core": "2.8.0",
50 | "karma": "3.1.3",
51 | "karma-chrome-launcher": "2.2.0",
52 | "karma-firefox-launcher": "1.1.0",
53 | "karma-jasmine": "1.1.2",
54 | "karma-jasmine-html-reporter": "0.2.2",
55 | "karma-rollup-preprocessor": "6.1.1",
56 | "karma-sauce-launcher": "1.2.0",
57 | "karma-sourcemap-loader": "0.3.7",
58 | "karma-spec-reporter": "0.0.32",
59 | "promise-polyfill": "8.1.0",
60 | "rollup": "0.67.4",
61 | "rollup-plugin-typescript": "1.0.0",
62 | "typescript": "3.2.2"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import pkg from './package.json';
2 | import typescript from 'rollup-plugin-typescript';
3 |
4 | export default {
5 | input: 'src/index.js',
6 | output: [{
7 | name: 'ResizeObserver',
8 | file: pkg.main,
9 | format: 'umd'
10 | }, {
11 | file: pkg.module,
12 | format: 'es'
13 | }],
14 | plugins: [
15 | typescript({
16 | target: 'es5',
17 | include: ['src/**/*']
18 | })
19 | ]
20 | };
21 |
--------------------------------------------------------------------------------
/src/ResizeObservation.js:
--------------------------------------------------------------------------------
1 | import {createRectInit, getContentRect} from './utils/geometry.js';
2 |
3 | /**
4 | * Class that is responsible for computations of the content rectangle of
5 | * provided DOM element and for keeping track of it's changes.
6 | */
7 | export default class ResizeObservation {
8 | /**
9 | * Reference to the observed element.
10 | *
11 | * @type {Element}
12 | */
13 | target;
14 |
15 | /**
16 | * Broadcasted width of content rectangle.
17 | *
18 | * @type {number}
19 | */
20 | broadcastWidth = 0;
21 |
22 | /**
23 | * Broadcasted height of content rectangle.
24 | *
25 | * @type {number}
26 | */
27 | broadcastHeight = 0;
28 |
29 | /**
30 | * Reference to the last observed content rectangle.
31 | *
32 | * @private {DOMRectInit}
33 | */
34 | contentRect_ = createRectInit(0, 0, 0, 0);
35 |
36 | /**
37 | * Creates an instance of ResizeObservation.
38 | *
39 | * @param {Element} target - Element to be observed.
40 | */
41 | constructor(target) {
42 | this.target = target;
43 | }
44 |
45 | /**
46 | * Updates content rectangle and tells whether it's width or height properties
47 | * have changed since the last broadcast.
48 | *
49 | * @returns {boolean}
50 | */
51 | isActive() {
52 | const rect = getContentRect(this.target);
53 |
54 | this.contentRect_ = rect;
55 |
56 | return (
57 | rect.width !== this.broadcastWidth ||
58 | rect.height !== this.broadcastHeight
59 | );
60 | }
61 |
62 | /**
63 | * Updates 'broadcastWidth' and 'broadcastHeight' properties with a data
64 | * from the corresponding properties of the last observed content rectangle.
65 | *
66 | * @returns {DOMRectInit} Last observed content rectangle.
67 | */
68 | broadcastRect() {
69 | const rect = this.contentRect_;
70 |
71 | this.broadcastWidth = rect.width;
72 | this.broadcastHeight = rect.height;
73 |
74 | return rect;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/ResizeObserver.js:
--------------------------------------------------------------------------------
1 | import {Map} from './shims/es6-collections.js';
2 | import ResizeObserverController from './ResizeObserverController.js';
3 | import ResizeObserverSPI from './ResizeObserverSPI.js';
4 |
5 | // Registry of internal observers. If WeakMap is not available use current shim
6 | // for the Map collection as it has all required methods and because WeakMap
7 | // can't be fully polyfilled anyway.
8 | const observers = typeof WeakMap !== 'undefined' ? new WeakMap() : new Map();
9 |
10 | /**
11 | * ResizeObserver API. Encapsulates the ResizeObserver SPI implementation
12 | * exposing only those methods and properties that are defined in the spec.
13 | */
14 | class ResizeObserver {
15 | /**
16 | * Creates a new instance of ResizeObserver.
17 | *
18 | * @param {ResizeObserverCallback} callback - Callback that is invoked when
19 | * dimensions of the observed elements change.
20 | */
21 | constructor(callback) {
22 | if (!(this instanceof ResizeObserver)) {
23 | throw new TypeError('Cannot call a class as a function.');
24 | }
25 | if (!arguments.length) {
26 | throw new TypeError('1 argument required, but only 0 present.');
27 | }
28 |
29 | const controller = ResizeObserverController.getInstance();
30 | const observer = new ResizeObserverSPI(callback, controller, this);
31 |
32 | observers.set(this, observer);
33 | }
34 | }
35 |
36 | // Expose public methods of ResizeObserver.
37 | [
38 | 'observe',
39 | 'unobserve',
40 | 'disconnect'
41 | ].forEach(method => {
42 | ResizeObserver.prototype[method] = function () {
43 | return observers.get(this)[method](...arguments);
44 | };
45 | });
46 |
47 | export default ResizeObserver;
48 |
--------------------------------------------------------------------------------
/src/ResizeObserverController.js:
--------------------------------------------------------------------------------
1 | import isBrowser from './utils/isBrowser.js';
2 | import throttle from './utils/throttle.js';
3 |
4 | // Minimum delay before invoking the update of observers.
5 | const REFRESH_DELAY = 20;
6 |
7 | // A list of substrings of CSS properties used to find transition events that
8 | // might affect dimensions of observed elements.
9 | const transitionKeys = ['top', 'right', 'bottom', 'left', 'width', 'height', 'size', 'weight'];
10 |
11 | // Check if MutationObserver is available.
12 | const mutationObserverSupported = typeof MutationObserver !== 'undefined';
13 |
14 | /**
15 | * Singleton controller class which handles updates of ResizeObserver instances.
16 | */
17 | export default class ResizeObserverController {
18 | /**
19 | * Indicates whether DOM listeners have been added.
20 | *
21 | * @private {boolean}
22 | */
23 | connected_ = false;
24 |
25 | /**
26 | * Tells that controller has subscribed for Mutation Events.
27 | *
28 | * @private {boolean}
29 | */
30 | mutationEventsAdded_ = false;
31 |
32 | /**
33 | * Keeps reference to the instance of MutationObserver.
34 | *
35 | * @private {MutationObserver}
36 | */
37 | mutationsObserver_ = null;
38 |
39 | /**
40 | * A list of connected observers.
41 | *
42 | * @private {Array}
43 | */
44 | observers_ = [];
45 |
46 | /**
47 | * Holds reference to the controller's instance.
48 | *
49 | * @private {ResizeObserverController}
50 | */
51 | static instance_ = null;
52 |
53 | /**
54 | * Creates a new instance of ResizeObserverController.
55 | *
56 | * @private
57 | */
58 | constructor() {
59 | this.onTransitionEnd_ = this.onTransitionEnd_.bind(this);
60 | this.refresh = throttle(this.refresh.bind(this), REFRESH_DELAY);
61 | }
62 |
63 | /**
64 | * Adds observer to observers list.
65 | *
66 | * @param {ResizeObserverSPI} observer - Observer to be added.
67 | * @returns {void}
68 | */
69 | addObserver(observer) {
70 | if (!~this.observers_.indexOf(observer)) {
71 | this.observers_.push(observer);
72 | }
73 |
74 | // Add listeners if they haven't been added yet.
75 | if (!this.connected_) {
76 | this.connect_();
77 | }
78 | }
79 |
80 | /**
81 | * Removes observer from observers list.
82 | *
83 | * @param {ResizeObserverSPI} observer - Observer to be removed.
84 | * @returns {void}
85 | */
86 | removeObserver(observer) {
87 | const observers = this.observers_;
88 | const index = observers.indexOf(observer);
89 |
90 | // Remove observer if it's present in registry.
91 | if (~index) {
92 | observers.splice(index, 1);
93 | }
94 |
95 | // Remove listeners if controller has no connected observers.
96 | if (!observers.length && this.connected_) {
97 | this.disconnect_();
98 | }
99 | }
100 |
101 | /**
102 | * Invokes the update of observers. It will continue running updates insofar
103 | * it detects changes.
104 | *
105 | * @returns {void}
106 | */
107 | refresh() {
108 | const changesDetected = this.updateObservers_();
109 |
110 | // Continue running updates if changes have been detected as there might
111 | // be future ones caused by CSS transitions.
112 | if (changesDetected) {
113 | this.refresh();
114 | }
115 | }
116 |
117 | /**
118 | * Updates every observer from observers list and notifies them of queued
119 | * entries.
120 | *
121 | * @private
122 | * @returns {boolean} Returns "true" if any observer has detected changes in
123 | * dimensions of it's elements.
124 | */
125 | updateObservers_() {
126 | // Collect observers that have active observations.
127 | const activeObservers = this.observers_.filter(observer => {
128 | return observer.gatherActive(), observer.hasActive();
129 | });
130 |
131 | // Deliver notifications in a separate cycle in order to avoid any
132 | // collisions between observers, e.g. when multiple instances of
133 | // ResizeObserver are tracking the same element and the callback of one
134 | // of them changes content dimensions of the observed target. Sometimes
135 | // this may result in notifications being blocked for the rest of observers.
136 | activeObservers.forEach(observer => observer.broadcastActive());
137 |
138 | return activeObservers.length > 0;
139 | }
140 |
141 | /**
142 | * Initializes DOM listeners.
143 | *
144 | * @private
145 | * @returns {void}
146 | */
147 | connect_() {
148 | // Do nothing if running in a non-browser environment or if listeners
149 | // have been already added.
150 | if (!isBrowser || this.connected_) {
151 | return;
152 | }
153 |
154 | // Subscription to the "Transitionend" event is used as a workaround for
155 | // delayed transitions. This way it's possible to capture at least the
156 | // final state of an element.
157 | document.addEventListener('transitionend', this.onTransitionEnd_);
158 |
159 | window.addEventListener('resize', this.refresh);
160 |
161 | if (mutationObserverSupported) {
162 | this.mutationsObserver_ = new MutationObserver(this.refresh);
163 |
164 | this.mutationsObserver_.observe(document, {
165 | attributes: true,
166 | childList: true,
167 | characterData: true,
168 | subtree: true
169 | });
170 | } else {
171 | document.addEventListener('DOMSubtreeModified', this.refresh);
172 |
173 | this.mutationEventsAdded_ = true;
174 | }
175 |
176 | this.connected_ = true;
177 | }
178 |
179 | /**
180 | * Removes DOM listeners.
181 | *
182 | * @private
183 | * @returns {void}
184 | */
185 | disconnect_() {
186 | // Do nothing if running in a non-browser environment or if listeners
187 | // have been already removed.
188 | if (!isBrowser || !this.connected_) {
189 | return;
190 | }
191 |
192 | document.removeEventListener('transitionend', this.onTransitionEnd_);
193 | window.removeEventListener('resize', this.refresh);
194 |
195 | if (this.mutationsObserver_) {
196 | this.mutationsObserver_.disconnect();
197 | }
198 |
199 | if (this.mutationEventsAdded_) {
200 | document.removeEventListener('DOMSubtreeModified', this.refresh);
201 | }
202 |
203 | this.mutationsObserver_ = null;
204 | this.mutationEventsAdded_ = false;
205 | this.connected_ = false;
206 | }
207 |
208 | /**
209 | * "Transitionend" event handler.
210 | *
211 | * @private
212 | * @param {TransitionEvent} event
213 | * @returns {void}
214 | */
215 | onTransitionEnd_({propertyName = ''}) {
216 | // Detect whether transition may affect dimensions of an element.
217 | const isReflowProperty = transitionKeys.some(key => {
218 | return !!~propertyName.indexOf(key);
219 | });
220 |
221 | if (isReflowProperty) {
222 | this.refresh();
223 | }
224 | }
225 |
226 | /**
227 | * Returns instance of the ResizeObserverController.
228 | *
229 | * @returns {ResizeObserverController}
230 | */
231 | static getInstance() {
232 | if (!this.instance_) {
233 | this.instance_ = new ResizeObserverController();
234 | }
235 |
236 | return this.instance_;
237 | }
238 | }
239 |
--------------------------------------------------------------------------------
/src/ResizeObserverEntry.js:
--------------------------------------------------------------------------------
1 | import {createReadOnlyRect} from './utils/geometry.js';
2 | import defineConfigurable from './utils/defineConfigurable.js';
3 |
4 | export default class ResizeObserverEntry {
5 | /**
6 | * Element size of which has changed.
7 | * Spec: https://wicg.github.io/ResizeObserver/#dom-resizeobserverentry-target
8 | *
9 | * @readonly
10 | * @type {Element}
11 | */
12 | target;
13 |
14 | /**
15 | * Element's content rectangle.
16 | * Spec: https://wicg.github.io/ResizeObserver/#dom-resizeobserverentry-contentrect
17 | *
18 | * @readonly
19 | * @type {DOMRectReadOnly}
20 | */
21 | contentRect;
22 |
23 | /**
24 | * Creates an instance of ResizeObserverEntry.
25 | *
26 | * @param {Element} target - Element that is being observed.
27 | * @param {DOMRectInit} rectInit - Data of the element's content rectangle.
28 | */
29 | constructor(target, rectInit) {
30 | const contentRect = createReadOnlyRect(rectInit);
31 |
32 | // According to the specification following properties are not writable
33 | // and are also not enumerable in the native implementation.
34 | //
35 | // Property accessors are not being used as they'd require to define a
36 | // private WeakMap storage which may cause memory leaks in browsers that
37 | // don't support this type of collections.
38 | defineConfigurable(this, {target, contentRect});
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/ResizeObserverSPI.js:
--------------------------------------------------------------------------------
1 | import {Map} from './shims/es6-collections.js';
2 | import ResizeObservation from './ResizeObservation.js';
3 | import ResizeObserverEntry from './ResizeObserverEntry.js';
4 | import getWindowOf from './utils/getWindowOf.js';
5 |
6 | export default class ResizeObserverSPI {
7 | /**
8 | * Collection of resize observations that have detected changes in dimensions
9 | * of elements.
10 | *
11 | * @private {Array}
12 | */
13 | activeObservations_ = [];
14 |
15 | /**
16 | * Reference to the callback function.
17 | *
18 | * @private {ResizeObserverCallback}
19 | */
20 | callback_;
21 |
22 | /**
23 | * Public ResizeObserver instance which will be passed to the callback
24 | * function and used as a value of it's "this" binding.
25 | *
26 | * @private {ResizeObserver}
27 | */
28 | callbackCtx_;
29 |
30 | /**
31 | * Reference to the associated ResizeObserverController.
32 | *
33 | * @private {ResizeObserverController}
34 | */
35 | controller_;
36 |
37 | /**
38 | * Registry of the ResizeObservation instances.
39 | *
40 | * @private {Map}
41 | */
42 | observations_ = new Map();
43 |
44 | /**
45 | * Creates a new instance of ResizeObserver.
46 | *
47 | * @param {ResizeObserverCallback} callback - Callback function that is invoked
48 | * when one of the observed elements changes it's content dimensions.
49 | * @param {ResizeObserverController} controller - Controller instance which
50 | * is responsible for the updates of observer.
51 | * @param {ResizeObserver} callbackCtx - Reference to the public
52 | * ResizeObserver instance which will be passed to callback function.
53 | */
54 | constructor(callback, controller, callbackCtx) {
55 | if (typeof callback !== 'function') {
56 | throw new TypeError('The callback provided as parameter 1 is not a function.');
57 | }
58 |
59 | this.callback_ = callback;
60 | this.controller_ = controller;
61 | this.callbackCtx_ = callbackCtx;
62 | }
63 |
64 | /**
65 | * Starts observing provided element.
66 | *
67 | * @param {Element} target - Element to be observed.
68 | * @returns {void}
69 | */
70 | observe(target) {
71 | if (!arguments.length) {
72 | throw new TypeError('1 argument required, but only 0 present.');
73 | }
74 |
75 | // Do nothing if current environment doesn't have the Element interface.
76 | if (typeof Element === 'undefined' || !(Element instanceof Object)) {
77 | return;
78 | }
79 |
80 | if (!(target instanceof getWindowOf(target).Element)) {
81 | throw new TypeError('parameter 1 is not of type "Element".');
82 | }
83 |
84 | const observations = this.observations_;
85 |
86 | // Do nothing if element is already being observed.
87 | if (observations.has(target)) {
88 | return;
89 | }
90 |
91 | observations.set(target, new ResizeObservation(target));
92 |
93 | this.controller_.addObserver(this);
94 |
95 | // Force the update of observations.
96 | this.controller_.refresh();
97 | }
98 |
99 | /**
100 | * Stops observing provided element.
101 | *
102 | * @param {Element} target - Element to stop observing.
103 | * @returns {void}
104 | */
105 | unobserve(target) {
106 | if (!arguments.length) {
107 | throw new TypeError('1 argument required, but only 0 present.');
108 | }
109 |
110 | // Do nothing if current environment doesn't have the Element interface.
111 | if (typeof Element === 'undefined' || !(Element instanceof Object)) {
112 | return;
113 | }
114 |
115 | if (!(target instanceof getWindowOf(target).Element)) {
116 | throw new TypeError('parameter 1 is not of type "Element".');
117 | }
118 |
119 | const observations = this.observations_;
120 |
121 | // Do nothing if element is not being observed.
122 | if (!observations.has(target)) {
123 | return;
124 | }
125 |
126 | observations.delete(target);
127 |
128 | if (!observations.size) {
129 | this.controller_.removeObserver(this);
130 | }
131 | }
132 |
133 | /**
134 | * Stops observing all elements.
135 | *
136 | * @returns {void}
137 | */
138 | disconnect() {
139 | this.clearActive();
140 | this.observations_.clear();
141 | this.controller_.removeObserver(this);
142 | }
143 |
144 | /**
145 | * Collects observation instances the associated element of which has changed
146 | * it's content rectangle.
147 | *
148 | * @returns {void}
149 | */
150 | gatherActive() {
151 | this.clearActive();
152 |
153 | this.observations_.forEach(observation => {
154 | if (observation.isActive()) {
155 | this.activeObservations_.push(observation);
156 | }
157 | });
158 | }
159 |
160 | /**
161 | * Invokes initial callback function with a list of ResizeObserverEntry
162 | * instances collected from active resize observations.
163 | *
164 | * @returns {void}
165 | */
166 | broadcastActive() {
167 | // Do nothing if observer doesn't have active observations.
168 | if (!this.hasActive()) {
169 | return;
170 | }
171 |
172 | const ctx = this.callbackCtx_;
173 |
174 | // Create ResizeObserverEntry instance for every active observation.
175 | const entries = this.activeObservations_.map(observation => {
176 | return new ResizeObserverEntry(
177 | observation.target,
178 | observation.broadcastRect()
179 | );
180 | });
181 |
182 | this.callback_.call(ctx, entries, ctx);
183 | this.clearActive();
184 | }
185 |
186 | /**
187 | * Clears the collection of active observations.
188 | *
189 | * @returns {void}
190 | */
191 | clearActive() {
192 | this.activeObservations_.splice(0);
193 | }
194 |
195 | /**
196 | * Tells whether observer has active observations.
197 | *
198 | * @returns {boolean}
199 | */
200 | hasActive() {
201 | return this.activeObservations_.length > 0;
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/src/index.d.ts:
--------------------------------------------------------------------------------
1 | interface DOMRectReadOnly {
2 | readonly x: number;
3 | readonly y: number;
4 | readonly width: number;
5 | readonly height: number;
6 | readonly top: number;
7 | readonly right: number;
8 | readonly bottom: number;
9 | readonly left: number;
10 | }
11 |
12 | declare global {
13 | interface ResizeObserverCallback {
14 | (entries: ResizeObserverEntry[], observer: ResizeObserver): void
15 | }
16 |
17 | interface ResizeObserverEntry {
18 | readonly target: Element;
19 | readonly contentRect: DOMRectReadOnly;
20 | }
21 |
22 | interface ResizeObserver {
23 | observe(target: Element): void;
24 | unobserve(target: Element): void;
25 | disconnect(): void;
26 | }
27 | }
28 |
29 | declare var ResizeObserver: {
30 | prototype: ResizeObserver;
31 | new(callback: ResizeObserverCallback): ResizeObserver;
32 | }
33 |
34 | interface ResizeObserver {
35 | observe(target: Element): void;
36 | unobserve(target: Element): void;
37 | disconnect(): void;
38 | }
39 |
40 | export default ResizeObserver;
41 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import ResizeObserverPolyfill from './ResizeObserver.js';
2 | import global from './shims/global.js';
3 |
4 | export default (() => {
5 | // Export existing implementation if available.
6 | if (typeof global.ResizeObserver !== 'undefined') {
7 | return global.ResizeObserver;
8 | }
9 |
10 | return ResizeObserverPolyfill;
11 | })();
12 |
--------------------------------------------------------------------------------
/src/index.js.flow:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | class DOMRectReadOnly {
4 | +x: number;
5 | +y: number;
6 | +width: number;
7 | +height: number;
8 | +top: number;
9 | +right: number;
10 | +bottom: number;
11 | +left: number;
12 | }
13 |
14 | class ResizeObserverEntry {
15 | +target: Element;
16 | +contentRect: DOMRectReadOnly;
17 | }
18 |
19 | type Entries = $ReadOnlyArray;
20 |
21 | type ResizeObserverCallback = {
22 | (entries: Entries, observer: ResizeObserver): void
23 | };
24 |
25 | declare class ResizeObserver {
26 | constructor(ResizeObserverCallback): ResizeObserver;
27 | observe(target: Element): void;
28 | unobserve(target: Element): void;
29 | disconnect(): void;
30 | };
31 |
32 | declare export default typeof ResizeObserver;
33 |
--------------------------------------------------------------------------------
/src/shims/es6-collections.js:
--------------------------------------------------------------------------------
1 | /**
2 | * A collection of shims that provide minimal functionality of the ES6 collections.
3 | *
4 | * These implementations are not meant to be used outside of the ResizeObserver
5 | * modules as they cover only a limited range of use cases.
6 | */
7 | /* eslint-disable require-jsdoc, valid-jsdoc */
8 | const MapShim = (() => {
9 | if (typeof Map !== 'undefined') {
10 | return Map;
11 | }
12 |
13 | /**
14 | * Returns index in provided array that matches the specified key.
15 | *
16 | * @param {Array} arr
17 | * @param {*} key
18 | * @returns {number}
19 | */
20 | function getIndex(arr, key) {
21 | let result = -1;
22 |
23 | arr.some((entry, index) => {
24 | if (entry[0] === key) {
25 | result = index;
26 |
27 | return true;
28 | }
29 |
30 | return false;
31 | });
32 |
33 | return result;
34 | }
35 |
36 | return class {
37 | constructor() {
38 | this.__entries__ = [];
39 | }
40 |
41 | /**
42 | * @returns {boolean}
43 | */
44 | get size() {
45 | return this.__entries__.length;
46 | }
47 |
48 | /**
49 | * @param {*} key
50 | * @returns {*}
51 | */
52 | get(key) {
53 | const index = getIndex(this.__entries__, key);
54 | const entry = this.__entries__[index];
55 |
56 | return entry && entry[1];
57 | }
58 |
59 | /**
60 | * @param {*} key
61 | * @param {*} value
62 | * @returns {void}
63 | */
64 | set(key, value) {
65 | const index = getIndex(this.__entries__, key);
66 |
67 | if (~index) {
68 | this.__entries__[index][1] = value;
69 | } else {
70 | this.__entries__.push([key, value]);
71 | }
72 | }
73 |
74 | /**
75 | * @param {*} key
76 | * @returns {void}
77 | */
78 | delete(key) {
79 | const entries = this.__entries__;
80 | const index = getIndex(entries, key);
81 |
82 | if (~index) {
83 | entries.splice(index, 1);
84 | }
85 | }
86 |
87 | /**
88 | * @param {*} key
89 | * @returns {void}
90 | */
91 | has(key) {
92 | return !!~getIndex(this.__entries__, key);
93 | }
94 |
95 | /**
96 | * @returns {void}
97 | */
98 | clear() {
99 | this.__entries__.splice(0);
100 | }
101 |
102 | /**
103 | * @param {Function} callback
104 | * @param {*} [ctx=null]
105 | * @returns {void}
106 | */
107 | forEach(callback, ctx = null) {
108 | for (const entry of this.__entries__) {
109 | callback.call(ctx, entry[1], entry[0]);
110 | }
111 | }
112 | };
113 | })();
114 |
115 | export {MapShim as Map};
116 |
--------------------------------------------------------------------------------
/src/shims/global.js:
--------------------------------------------------------------------------------
1 | // Returns global object of a current environment.
2 | export default (() => {
3 | if (typeof global !== 'undefined' && global.Math === Math) {
4 | return global;
5 | }
6 |
7 | if (typeof self !== 'undefined' && self.Math === Math) {
8 | return self;
9 | }
10 |
11 | if (typeof window !== 'undefined' && window.Math === Math) {
12 | return window;
13 | }
14 |
15 | // eslint-disable-next-line no-new-func
16 | return Function('return this')();
17 | })();
18 |
--------------------------------------------------------------------------------
/src/shims/requestAnimationFrame.js:
--------------------------------------------------------------------------------
1 | import global from './global.js';
2 |
3 | /**
4 | * A shim for the requestAnimationFrame which falls back to the setTimeout if
5 | * first one is not supported.
6 | *
7 | * @returns {number} Requests' identifier.
8 | */
9 | export default (() => {
10 | if (typeof requestAnimationFrame === 'function') {
11 | // It's required to use a bounded function because IE sometimes throws
12 | // an "Invalid calling object" error if rAF is invoked without the global
13 | // object on the left hand side.
14 | return requestAnimationFrame.bind(global);
15 | }
16 |
17 | return callback => setTimeout(() => callback(Date.now()), 1000 / 60);
18 | })();
19 |
--------------------------------------------------------------------------------
/src/utils/defineConfigurable.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Defines non-writable/enumerable properties of the provided target object.
3 | *
4 | * @param {Object} target - Object for which to define properties.
5 | * @param {Object} props - Properties to be defined.
6 | * @returns {Object} Target object.
7 | */
8 | export default (target, props) => {
9 | for (const key of Object.keys(props)) {
10 | Object.defineProperty(target, key, {
11 | value: props[key],
12 | enumerable: false,
13 | writable: false,
14 | configurable: true
15 | });
16 | }
17 |
18 | return target;
19 | };
20 |
--------------------------------------------------------------------------------
/src/utils/geometry.js:
--------------------------------------------------------------------------------
1 | import defineConfigurable from './defineConfigurable.js';
2 | import getWindowOf from './getWindowOf.js';
3 | import isBrowser from './isBrowser.js';
4 |
5 | // Placeholder of an empty content rectangle.
6 | const emptyRect = createRectInit(0, 0, 0, 0);
7 |
8 | /**
9 | * Converts provided string to a number.
10 | *
11 | * @param {number|string} value
12 | * @returns {number}
13 | */
14 | function toFloat(value) {
15 | return parseFloat(value) || 0;
16 | }
17 |
18 | /**
19 | * Extracts borders size from provided styles.
20 | *
21 | * @param {CSSStyleDeclaration} styles
22 | * @param {...string} positions - Borders positions (top, right, ...)
23 | * @returns {number}
24 | */
25 | function getBordersSize(styles, ...positions) {
26 | return positions.reduce((size, position) => {
27 | const value = styles['border-' + position + '-width'];
28 |
29 | return size + toFloat(value);
30 | }, 0);
31 | }
32 |
33 | /**
34 | * Extracts paddings sizes from provided styles.
35 | *
36 | * @param {CSSStyleDeclaration} styles
37 | * @returns {Object} Paddings box.
38 | */
39 | function getPaddings(styles) {
40 | const positions = ['top', 'right', 'bottom', 'left'];
41 | const paddings = {};
42 |
43 | for (const position of positions) {
44 | const value = styles['padding-' + position];
45 |
46 | paddings[position] = toFloat(value);
47 | }
48 |
49 | return paddings;
50 | }
51 |
52 | /**
53 | * Calculates content rectangle of provided SVG element.
54 | *
55 | * @param {SVGGraphicsElement} target - Element content rectangle of which needs
56 | * to be calculated.
57 | * @returns {DOMRectInit}
58 | */
59 | function getSVGContentRect(target) {
60 | const bbox = target.getBBox();
61 |
62 | return createRectInit(0, 0, bbox.width, bbox.height);
63 | }
64 |
65 | /**
66 | * Calculates content rectangle of provided HTMLElement.
67 | *
68 | * @param {HTMLElement} target - Element for which to calculate the content rectangle.
69 | * @returns {DOMRectInit}
70 | */
71 | function getHTMLElementContentRect(target) {
72 | // Client width & height properties can't be
73 | // used exclusively as they provide rounded values.
74 | const {clientWidth, clientHeight} = target;
75 |
76 | // By this condition we can catch all non-replaced inline, hidden and
77 | // detached elements. Though elements with width & height properties less
78 | // than 0.5 will be discarded as well.
79 | //
80 | // Without it we would need to implement separate methods for each of
81 | // those cases and it's not possible to perform a precise and performance
82 | // effective test for hidden elements. E.g. even jQuery's ':visible' filter
83 | // gives wrong results for elements with width & height less than 0.5.
84 | if (!clientWidth && !clientHeight) {
85 | return emptyRect;
86 | }
87 |
88 | const styles = getWindowOf(target).getComputedStyle(target);
89 | const paddings = getPaddings(styles);
90 | const horizPad = paddings.left + paddings.right;
91 | const vertPad = paddings.top + paddings.bottom;
92 |
93 | // Computed styles of width & height are being used because they are the
94 | // only dimensions available to JS that contain non-rounded values. It could
95 | // be possible to utilize the getBoundingClientRect if only it's data wasn't
96 | // affected by CSS transformations let alone paddings, borders and scroll bars.
97 | let width = toFloat(styles.width),
98 | height = toFloat(styles.height);
99 |
100 | // Width & height include paddings and borders when the 'border-box' box
101 | // model is applied (except for IE).
102 | if (styles.boxSizing === 'border-box') {
103 | // Following conditions are required to handle Internet Explorer which
104 | // doesn't include paddings and borders to computed CSS dimensions.
105 | //
106 | // We can say that if CSS dimensions + paddings are equal to the "client"
107 | // properties then it's either IE, and thus we don't need to subtract
108 | // anything, or an element merely doesn't have paddings/borders styles.
109 | if (Math.round(width + horizPad) !== clientWidth) {
110 | width -= getBordersSize(styles, 'left', 'right') + horizPad;
111 | }
112 |
113 | if (Math.round(height + vertPad) !== clientHeight) {
114 | height -= getBordersSize(styles, 'top', 'bottom') + vertPad;
115 | }
116 | }
117 |
118 | // Following steps can't be applied to the document's root element as its
119 | // client[Width/Height] properties represent viewport area of the window.
120 | // Besides, it's as well not necessary as the itself neither has
121 | // rendered scroll bars nor it can be clipped.
122 | if (!isDocumentElement(target)) {
123 | // In some browsers (only in Firefox, actually) CSS width & height
124 | // include scroll bars size which can be removed at this step as scroll
125 | // bars are the only difference between rounded dimensions + paddings
126 | // and "client" properties, though that is not always true in Chrome.
127 | const vertScrollbar = Math.round(width + horizPad) - clientWidth;
128 | const horizScrollbar = Math.round(height + vertPad) - clientHeight;
129 |
130 | // Chrome has a rather weird rounding of "client" properties.
131 | // E.g. for an element with content width of 314.2px it sometimes gives
132 | // the client width of 315px and for the width of 314.7px it may give
133 | // 314px. And it doesn't happen all the time. So just ignore this delta
134 | // as a non-relevant.
135 | if (Math.abs(vertScrollbar) !== 1) {
136 | width -= vertScrollbar;
137 | }
138 |
139 | if (Math.abs(horizScrollbar) !== 1) {
140 | height -= horizScrollbar;
141 | }
142 | }
143 |
144 | return createRectInit(paddings.left, paddings.top, width, height);
145 | }
146 |
147 | /**
148 | * Checks whether provided element is an instance of the SVGGraphicsElement.
149 | *
150 | * @param {Element} target - Element to be checked.
151 | * @returns {boolean}
152 | */
153 | const isSVGGraphicsElement = (() => {
154 | // Some browsers, namely IE and Edge, don't have the SVGGraphicsElement
155 | // interface.
156 | if (typeof SVGGraphicsElement !== 'undefined') {
157 | return target => target instanceof getWindowOf(target).SVGGraphicsElement;
158 | }
159 |
160 | // If it's so, then check that element is at least an instance of the
161 | // SVGElement and that it has the "getBBox" method.
162 | // eslint-disable-next-line no-extra-parens
163 | return target => (
164 | target instanceof getWindowOf(target).SVGElement &&
165 | typeof target.getBBox === 'function'
166 | );
167 | })();
168 |
169 | /**
170 | * Checks whether provided element is a document element ().
171 | *
172 | * @param {Element} target - Element to be checked.
173 | * @returns {boolean}
174 | */
175 | function isDocumentElement(target) {
176 | return target === getWindowOf(target).document.documentElement;
177 | }
178 |
179 | /**
180 | * Calculates an appropriate content rectangle for provided html or svg element.
181 | *
182 | * @param {Element} target - Element content rectangle of which needs to be calculated.
183 | * @returns {DOMRectInit}
184 | */
185 | export function getContentRect(target) {
186 | if (!isBrowser) {
187 | return emptyRect;
188 | }
189 |
190 | if (isSVGGraphicsElement(target)) {
191 | return getSVGContentRect(target);
192 | }
193 |
194 | return getHTMLElementContentRect(target);
195 | }
196 |
197 | /**
198 | * Creates rectangle with an interface of the DOMRectReadOnly.
199 | * Spec: https://drafts.fxtf.org/geometry/#domrectreadonly
200 | *
201 | * @param {DOMRectInit} rectInit - Object with rectangle's x/y coordinates and dimensions.
202 | * @returns {DOMRectReadOnly}
203 | */
204 | export function createReadOnlyRect({x, y, width, height}) {
205 | // If DOMRectReadOnly is available use it as a prototype for the rectangle.
206 | const Constr = typeof DOMRectReadOnly !== 'undefined' ? DOMRectReadOnly : Object;
207 | const rect = Object.create(Constr.prototype);
208 |
209 | // Rectangle's properties are not writable and non-enumerable.
210 | defineConfigurable(rect, {
211 | x, y, width, height,
212 | top: y,
213 | right: x + width,
214 | bottom: height + y,
215 | left: x
216 | });
217 |
218 | return rect;
219 | }
220 |
221 | /**
222 | * Creates DOMRectInit object based on the provided dimensions and the x/y coordinates.
223 | * Spec: https://drafts.fxtf.org/geometry/#dictdef-domrectinit
224 | *
225 | * @param {number} x - X coordinate.
226 | * @param {number} y - Y coordinate.
227 | * @param {number} width - Rectangle's width.
228 | * @param {number} height - Rectangle's height.
229 | * @returns {DOMRectInit}
230 | */
231 | export function createRectInit(x, y, width, height) {
232 | return {x, y, width, height};
233 | }
234 |
--------------------------------------------------------------------------------
/src/utils/getWindowOf.js:
--------------------------------------------------------------------------------
1 | import global from '../shims/global.js';
2 |
3 | /**
4 | * Returns the global object associated with provided element.
5 | *
6 | * @param {Object} target
7 | * @returns {Object}
8 | */
9 | export default target => {
10 | // Assume that the element is an instance of Node, which means that it
11 | // has the "ownerDocument" property from which we can retrieve a
12 | // corresponding global object.
13 | const ownerGlobal = target && target.ownerDocument && target.ownerDocument.defaultView;
14 |
15 | // Return the local global object if it's not possible extract one from
16 | // provided element.
17 | return ownerGlobal || global;
18 | };
19 |
--------------------------------------------------------------------------------
/src/utils/isBrowser.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Detects whether window and document objects are available in current environment.
3 | */
4 | export default typeof window !== 'undefined' && typeof document !== 'undefined' && window.document === document;
5 |
--------------------------------------------------------------------------------
/src/utils/throttle.js:
--------------------------------------------------------------------------------
1 | import requestAnimationFrame from '../shims/requestAnimationFrame.js';
2 |
3 | // Defines minimum timeout before adding a trailing call.
4 | const trailingTimeout = 2;
5 |
6 | /**
7 | * Creates a wrapper function which ensures that provided callback will be
8 | * invoked only once during the specified delay period.
9 | *
10 | * @param {Function} callback - Function to be invoked after the delay period.
11 | * @param {number} delay - Delay after which to invoke callback.
12 | * @returns {Function}
13 | */
14 | export default function (callback, delay) {
15 | let leadingCall = false,
16 | trailingCall = false,
17 | lastCallTime = 0;
18 |
19 | /**
20 | * Invokes the original callback function and schedules new invocation if
21 | * the "proxy" was called during current request.
22 | *
23 | * @returns {void}
24 | */
25 | function resolvePending() {
26 | if (leadingCall) {
27 | leadingCall = false;
28 |
29 | callback();
30 | }
31 |
32 | if (trailingCall) {
33 | proxy();
34 | }
35 | }
36 |
37 | /**
38 | * Callback invoked after the specified delay. It will further postpone
39 | * invocation of the original function delegating it to the
40 | * requestAnimationFrame.
41 | *
42 | * @returns {void}
43 | */
44 | function timeoutCallback() {
45 | requestAnimationFrame(resolvePending);
46 | }
47 |
48 | /**
49 | * Schedules invocation of the original function.
50 | *
51 | * @returns {void}
52 | */
53 | function proxy() {
54 | const timeStamp = Date.now();
55 |
56 | if (leadingCall) {
57 | // Reject immediately following calls.
58 | if (timeStamp - lastCallTime < trailingTimeout) {
59 | return;
60 | }
61 |
62 | // Schedule new call to be in invoked when the pending one is resolved.
63 | // This is important for "transitions" which never actually start
64 | // immediately so there is a chance that we might miss one if change
65 | // happens amids the pending invocation.
66 | trailingCall = true;
67 | } else {
68 | leadingCall = true;
69 | trailingCall = false;
70 |
71 | setTimeout(timeoutCallback, delay);
72 | }
73 |
74 | lastCallTime = timeStamp;
75 | }
76 |
77 | return proxy;
78 | }
79 |
--------------------------------------------------------------------------------
/tests/ResizeObserver.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-nested-callbacks, no-shadow, require-jsdoc */
2 | import {ResizeObserver, ResizeObserverEntry} from './resources/observer';
3 | import {createAsyncSpy, wait} from './resources/helpers';
4 |
5 | let observer = null,
6 | observer2 = null,
7 | elements = {},
8 | styles;
9 |
10 | // eslint-disable-next-line
11 | const emptyFn = () => {};
12 | const css = `
13 | #root {
14 | display: inline-block;
15 | }
16 |
17 | #container {
18 | min-width: 600px;
19 | background: #37474f;
20 | }
21 |
22 | #target1, #target2 {
23 | width: 200px;
24 | height: 200px;
25 | }
26 |
27 | #target1 {
28 | background: #4285f4;
29 | }
30 |
31 | #target2 {
32 | background: #fbbc05;
33 | }
34 | `;
35 | const template = `
36 |
42 | `;
43 |
44 | const timeout = 300;
45 |
46 | function appendStyles() {
47 | styles = document.createElement('style');
48 |
49 | styles.id = 'styles';
50 | document.head.appendChild(styles);
51 |
52 | styles.innerHTML = css;
53 | }
54 |
55 | function removeStyles() {
56 | document.head.removeChild(styles);
57 |
58 | styles = null;
59 | }
60 |
61 | function appendElements() {
62 | document.body.insertAdjacentHTML('beforeend', template);
63 |
64 | elements = {
65 | root: document.getElementById('root'),
66 | container: document.getElementById('container'),
67 | target1: document.getElementById('target1'),
68 | target2: document.getElementById('target2'),
69 | target3: document.getElementById('target3')
70 | };
71 | }
72 |
73 | function removeElements() {
74 | if (document.body.contains(elements.root)) {
75 | document.body.removeChild(elements.root);
76 | }
77 |
78 | elements = {};
79 | }
80 |
81 | describe('ResizeObserver', () => {
82 | beforeEach(() => {
83 | appendStyles();
84 | appendElements();
85 | });
86 |
87 | afterEach(() => {
88 | if (observer) {
89 | observer.disconnect();
90 | observer = null;
91 | }
92 |
93 | if (observer2) {
94 | observer2.disconnect();
95 | observer2 = null;
96 | }
97 |
98 | removeStyles();
99 | removeElements();
100 | });
101 |
102 | describe('constructor', () => {
103 | /* eslint-disable no-new */
104 | it('throws an error if no arguments are provided', () => {
105 | expect(() => {
106 | new ResizeObserver();
107 | }).toThrowError(/1 argument required/i);
108 | });
109 |
110 | it('throws an error if callback is not a function', () => {
111 | expect(() => {
112 | new ResizeObserver(true);
113 | }).toThrowError(/function/i);
114 |
115 | expect(() => {
116 | new ResizeObserver({});
117 | }).toThrowError(/function/i);
118 |
119 | expect(() => {
120 | new ResizeObserver(emptyFn);
121 | }).not.toThrow();
122 | });
123 |
124 | /* eslint-enable no-new */
125 | });
126 |
127 | describe('observe', () => {
128 | it('throws an error if no arguments are provided', () => {
129 | observer = new ResizeObserver(emptyFn);
130 |
131 | expect(() => {
132 | observer.observe();
133 | }).toThrowError(/1 argument required/i);
134 | });
135 |
136 | it('throws an error if target is not an Element', () => {
137 | observer = new ResizeObserver(emptyFn);
138 |
139 | expect(() => {
140 | observer.observe(true);
141 | }).toThrowError(/Element/i);
142 |
143 | expect(() => {
144 | observer.observe(null);
145 | }).toThrowError(/Element/i);
146 |
147 | expect(() => {
148 | observer.observe({});
149 | }).toThrowError(/Element/i);
150 |
151 | expect(() => {
152 | observer.observe(document.createTextNode(''));
153 | }).toThrowError(/Element/i);
154 | });
155 |
156 | it('triggers when observation begins', done => {
157 | observer = new ResizeObserver(done);
158 |
159 | observer.observe(elements.target1);
160 | });
161 |
162 | it('triggers with correct arguments', done => {
163 | observer = new ResizeObserver(function (...args) {
164 | const [entries, instance] = args;
165 |
166 | expect(args.length).toEqual(2);
167 |
168 | expect(Array.isArray(entries)).toBe(true);
169 | expect(entries.length).toEqual(1);
170 |
171 | expect(entries[0] instanceof ResizeObserverEntry).toBe(true);
172 |
173 | expect(entries[0].target).toBe(elements.target1);
174 | expect(typeof entries[0].contentRect).toBe('object');
175 |
176 | expect(instance).toBe(observer);
177 |
178 | // eslint-disable-next-line no-invalid-this
179 | expect(this).toBe(observer);
180 |
181 | done();
182 | });
183 |
184 | observer.observe(elements.target1);
185 | });
186 |
187 | it('preserves the initial order of elements', done => {
188 | const spy = createAsyncSpy();
189 |
190 | observer = new ResizeObserver(spy);
191 |
192 | observer.observe(elements.target2);
193 | observer.observe(elements.target1);
194 |
195 | spy.nextCall().then(entries => {
196 | expect(entries.length).toBe(2);
197 |
198 | expect(entries[0].target).toBe(elements.target2);
199 | expect(entries[1].target).toBe(elements.target1);
200 | }).then(async () => {
201 | elements.target1.style.height = '400px';
202 | elements.target2.style.height = '100px';
203 |
204 | const entries = await spy.nextCall();
205 |
206 | expect(entries.length).toBe(2);
207 |
208 | expect(entries[0].target).toBe(elements.target2);
209 | expect(entries[1].target).toBe(elements.target1);
210 | }).then(done).catch(done.fail);
211 | });
212 |
213 | // Checks that gathering of active observations and broadcasting of
214 | // notifications happens in separate cycles.
215 | it('doesn\'t block notifications when multiple observers are used', done => {
216 | const spy = createAsyncSpy();
217 | const spy2 = createAsyncSpy();
218 |
219 | const defaultWidth = getComputedStyle(elements.target1).width;
220 |
221 | let shouldRestoreDefault = false;
222 |
223 | observer = new ResizeObserver((...args) => {
224 | spy(...args);
225 |
226 | if (shouldRestoreDefault) {
227 | elements.target1.style.width = defaultWidth;
228 | }
229 | });
230 |
231 | observer2 = new ResizeObserver((...args) => {
232 | spy2(...args);
233 |
234 | if (shouldRestoreDefault) {
235 | elements.target1.style.width = defaultWidth;
236 | }
237 | });
238 |
239 | observer.observe(elements.target1);
240 | observer2.observe(elements.target1);
241 |
242 | Promise.all([
243 | spy.nextCall(),
244 | spy2.nextCall()
245 | ]).then(() => {
246 | shouldRestoreDefault = true;
247 |
248 | elements.target1.style.width = '220px';
249 |
250 | return Promise.all([
251 | spy.nextCall().then(spy.nextCall),
252 | spy2.nextCall().then(spy2.nextCall)
253 | ]);
254 | }).then(done).catch(done.fail);
255 | });
256 |
257 | it('doesn\'t notify of already observed elements', done => {
258 | const spy = createAsyncSpy();
259 |
260 | observer = new ResizeObserver(spy);
261 |
262 | observer.observe(elements.target1);
263 |
264 | spy.nextCall().then(entries => {
265 | expect(entries.length).toBe(1);
266 | expect(entries[0].target).toBe(elements.target1);
267 | }).then(async () => {
268 | observer.observe(elements.target1);
269 |
270 | await wait(timeout);
271 |
272 | expect(spy).toHaveBeenCalledTimes(1);
273 |
274 | elements.target1.style.width = '220px';
275 |
276 | const entries = await spy.nextCall();
277 |
278 | expect(entries.length).toBe(1);
279 | expect(entries[0].target).toBe(elements.target1);
280 | }).then(done).catch(done.fail);
281 | });
282 |
283 | it('handles elements that are not yet in the DOM', done => {
284 | elements.root.removeChild(elements.container);
285 | elements.container.removeChild(elements.target1);
286 |
287 | const spy = createAsyncSpy();
288 |
289 | observer = new ResizeObserver(spy);
290 |
291 | observer.observe(elements.target1);
292 |
293 | wait(timeout).then(() => {
294 | expect(spy).not.toHaveBeenCalled();
295 | }).then(async () => {
296 | elements.container.appendChild(elements.target1);
297 |
298 | await wait(timeout);
299 |
300 | expect(spy).not.toHaveBeenCalled();
301 | }).then(async () => {
302 | elements.root.appendChild(elements.container);
303 |
304 | const entries = await spy.nextCall();
305 |
306 | expect(entries.length).toBe(1);
307 | expect(entries[0].target).toBe(elements.target1);
308 |
309 | expect(entries[0].contentRect.width).toBe(200);
310 | expect(entries[0].contentRect.height).toBe(200);
311 | }).then(done).catch(done.fail);
312 | });
313 |
314 | it('triggers when an element is removed from DOM', done => {
315 | const spy = createAsyncSpy();
316 |
317 | observer = new ResizeObserver(spy);
318 |
319 | observer.observe(elements.target1);
320 | observer.observe(elements.target2);
321 |
322 | spy.nextCall().then(entries => {
323 | expect(spy).toHaveBeenCalledTimes(1);
324 |
325 | expect(entries.length).toBe(2);
326 |
327 | expect(entries[0].target).toBe(elements.target1);
328 | expect(entries[1].target).toBe(elements.target2);
329 | }).then(async () => {
330 | elements.container.removeChild(elements.target1);
331 |
332 | const entries = await spy.nextCall();
333 |
334 | expect(entries.length).toBe(1);
335 | expect(entries[0].target).toBe(elements.target1);
336 |
337 | expect(entries[0].contentRect.width).toBe(0);
338 | expect(entries[0].contentRect.height).toBe(0);
339 | expect(entries[0].contentRect.top).toBe(0);
340 | expect(entries[0].contentRect.right).toBe(0);
341 | expect(entries[0].contentRect.bottom).toBe(0);
342 | expect(entries[0].contentRect.left).toBe(0);
343 | }).then(async () => {
344 | elements.root.removeChild(elements.container);
345 |
346 | const entries = await spy.nextCall();
347 |
348 | expect(entries.length).toBe(1);
349 | expect(entries[0].target).toBe(elements.target2);
350 |
351 | expect(entries[0].contentRect.width).toBe(0);
352 | expect(entries[0].contentRect.height).toBe(0);
353 | expect(entries[0].contentRect.top).toBe(0);
354 | expect(entries[0].contentRect.right).toBe(0);
355 | expect(entries[0].contentRect.bottom).toBe(0);
356 | expect(entries[0].contentRect.left).toBe(0);
357 | }).then(done).catch(done.fail);
358 | });
359 |
360 | it('handles resizing of the documentElement', done => {
361 | const spy = createAsyncSpy();
362 | const docElement = document.documentElement;
363 | const styles = window.getComputedStyle(docElement);
364 |
365 | observer = new ResizeObserver(spy);
366 |
367 | observer.observe(document.documentElement);
368 |
369 | spy.nextCall().then(entries => {
370 | const width = parseFloat(styles.width);
371 | const height = parseFloat(styles.height);
372 |
373 | expect(entries.length).toBe(1);
374 |
375 | expect(entries[0].target).toBe(docElement);
376 |
377 | expect(entries[0].contentRect.width).toBe(width);
378 | expect(entries[0].contentRect.height).toBe(height);
379 | expect(entries[0].contentRect.top).toBe(0);
380 | expect(entries[0].contentRect.right).toBe(width);
381 | expect(entries[0].contentRect.bottom).toBe(height);
382 | expect(entries[0].contentRect.left).toBe(0);
383 | }).then(async () => {
384 | document.body.removeChild(elements.root);
385 |
386 | const width = parseFloat(styles.width);
387 | const height = parseFloat(styles.height);
388 |
389 | const entries = await spy.nextCall();
390 |
391 | expect(entries.length).toBe(1);
392 |
393 | expect(entries[0].target).toBe(docElement);
394 |
395 | expect(entries[0].contentRect.width).toBe(width);
396 | expect(entries[0].contentRect.height).toBe(height);
397 | expect(entries[0].contentRect.top).toBe(0);
398 | expect(entries[0].contentRect.right).toBe(width);
399 | expect(entries[0].contentRect.bottom).toBe(height);
400 | expect(entries[0].contentRect.left).toBe(0);
401 | }).then(done).catch(done.fail);
402 | });
403 |
404 | it('handles hidden elements', done => {
405 | const spy = createAsyncSpy();
406 |
407 | observer = new ResizeObserver(spy);
408 |
409 | elements.root.style.display = 'none';
410 | elements.target1.style.display = 'none';
411 |
412 | observer.observe(elements.target1);
413 |
414 | wait(timeout).then(() => {
415 | expect(spy).not.toHaveBeenCalled();
416 | }).then(async () => {
417 | elements.target1.style.display = 'block';
418 |
419 | await wait(timeout);
420 |
421 | expect(spy).not.toHaveBeenCalled();
422 | }).then(async () => {
423 | elements.root.style.display = 'block';
424 | elements.target1.style.position = 'fixed';
425 |
426 | const entries = await spy.nextCall();
427 |
428 | expect(entries.length).toBe(1);
429 | expect(entries[0].target).toBe(elements.target1);
430 |
431 | expect(entries[0].contentRect.width).toBe(200);
432 | expect(entries[0].contentRect.height).toBe(200);
433 | expect(entries[0].contentRect.top).toBe(0);
434 | expect(entries[0].contentRect.right).toBe(200);
435 | expect(entries[0].contentRect.bottom).toBe(200);
436 | expect(entries[0].contentRect.left).toBe(0);
437 | }).then(async () => {
438 | elements.root.style.display = 'none';
439 | elements.target1.style.padding = '10px';
440 |
441 | const entries = await spy.nextCall();
442 |
443 | expect(entries.length).toBe(1);
444 | expect(entries[0].target).toBe(elements.target1);
445 |
446 | expect(entries[0].contentRect.width).toBe(0);
447 | expect(entries[0].contentRect.height).toBe(0);
448 | expect(entries[0].contentRect.top).toBe(0);
449 | expect(entries[0].contentRect.right).toBe(0);
450 | expect(entries[0].contentRect.bottom).toBe(0);
451 | expect(entries[0].contentRect.left).toBe(0);
452 | }).then(done).catch(done.fail);
453 | });
454 |
455 | it('handles empty elements', done => {
456 | const spy = createAsyncSpy();
457 |
458 | elements.target1.style.width = '0px';
459 | elements.target1.style.height = '0px';
460 | elements.target1.style.padding = '10px';
461 |
462 | observer = new ResizeObserver(spy);
463 |
464 | observer.observe(elements.target1);
465 | observer.observe(elements.target2);
466 |
467 | spy.nextCall().then(entries => {
468 | expect(entries.length).toBe(1);
469 | expect(entries[0].target).toBe(elements.target2);
470 |
471 | expect(entries[0].contentRect.width).toBe(200);
472 | expect(entries[0].contentRect.height).toBe(200);
473 | expect(entries[0].contentRect.top).toBe(0);
474 | expect(entries[0].contentRect.right).toBe(200);
475 | expect(entries[0].contentRect.bottom).toBe(200);
476 | expect(entries[0].contentRect.left).toBe(0);
477 | }).then(async () => {
478 | elements.target1.style.width = '200px';
479 | elements.target1.style.height = '200px';
480 |
481 | elements.target2.style.width = '0px';
482 | elements.target2.style.height = '0px';
483 | elements.target2.padding = '10px';
484 |
485 | const entries = await spy.nextCall();
486 |
487 | expect(entries.length).toBe(2);
488 |
489 | expect(entries[0].target).toBe(elements.target1);
490 | expect(entries[1].target).toBe(elements.target2);
491 |
492 | expect(entries[0].contentRect.width).toBe(200);
493 | expect(entries[0].contentRect.height).toBe(200);
494 |
495 | expect(entries[1].contentRect.width).toEqual(0);
496 | expect(entries[1].contentRect.height).toBe(0);
497 | expect(entries[1].contentRect.top).toBe(0);
498 | expect(entries[1].contentRect.right).toBe(0);
499 | expect(entries[1].contentRect.bottom).toBe(0);
500 | expect(entries[1].contentRect.left).toBe(0);
501 | }).then(done).catch(done.fail);
502 | });
503 |
504 | it('handles paddings', done => {
505 | const spy = createAsyncSpy();
506 |
507 | elements.target1.style.padding = '2px 4px 6px 8px';
508 |
509 | observer = new ResizeObserver(spy);
510 |
511 | observer.observe(elements.target1);
512 |
513 | spy.nextCall().then(entries => {
514 | expect(entries.length).toBe(1);
515 |
516 | expect(entries[0].target).toBe(elements.target1);
517 |
518 | expect(entries[0].contentRect.width).toBe(200);
519 | expect(entries[0].contentRect.height).toBe(200);
520 | expect(entries[0].contentRect.top).toBe(2);
521 | expect(entries[0].contentRect.right).toBe(208);
522 | expect(entries[0].contentRect.bottom).toBe(202);
523 | expect(entries[0].contentRect.left).toBe(8);
524 | }).then(async () => {
525 | elements.target1.style.padding = '3px 6px';
526 |
527 | await wait(timeout);
528 |
529 | expect(spy).toHaveBeenCalledTimes(1);
530 | }).then(async () => {
531 | elements.target1.style.boxSizing = 'border-box';
532 |
533 | const entries = await spy.nextCall();
534 |
535 | expect(entries.length).toBe(1);
536 |
537 | expect(entries[0].target).toBe(elements.target1);
538 |
539 | expect(entries[0].contentRect.width).toBe(188);
540 | expect(entries[0].contentRect.height).toBe(194);
541 | expect(entries[0].contentRect.top).toBe(3);
542 | expect(entries[0].contentRect.right).toBe(194);
543 | expect(entries[0].contentRect.bottom).toBe(197);
544 | expect(entries[0].contentRect.left).toBe(6);
545 | }).then(async () => {
546 | elements.target1.style.padding = '0px 6px';
547 |
548 | const entries = await spy.nextCall();
549 |
550 | expect(spy).toHaveBeenCalledTimes(3);
551 |
552 | expect(entries.length).toBe(1);
553 |
554 | expect(entries[0].target).toBe(elements.target1);
555 |
556 | expect(entries[0].contentRect.width).toBe(188);
557 | expect(entries[0].contentRect.height).toBe(200);
558 | expect(entries[0].contentRect.top).toBe(0);
559 | expect(entries[0].contentRect.right).toBe(194);
560 | expect(entries[0].contentRect.bottom).toBe(200);
561 | expect(entries[0].contentRect.left).toBe(6);
562 | }).then(async () => {
563 | elements.target1.style.padding = '0px';
564 |
565 | const entries = await spy.nextCall();
566 |
567 | expect(entries.length).toBe(1);
568 |
569 | expect(entries[0].target).toBe(elements.target1);
570 |
571 | expect(entries[0].contentRect.width).toBe(200);
572 | expect(entries[0].contentRect.height).toBe(200);
573 | expect(entries[0].contentRect.top).toBe(0);
574 | expect(entries[0].contentRect.right).toBe(200);
575 | expect(entries[0].contentRect.bottom).toBe(200);
576 | expect(entries[0].contentRect.left).toBe(0);
577 | }).then(done).catch(done.fail);
578 | });
579 |
580 | it('handles borders', done => {
581 | const spy = createAsyncSpy();
582 |
583 | elements.target1.style.border = '10px solid black';
584 |
585 | observer = new ResizeObserver(spy);
586 |
587 | observer.observe(elements.target1);
588 |
589 | spy.nextCall().then(entries => {
590 | expect(entries.length).toBe(1);
591 |
592 | expect(entries[0].target).toBe(elements.target1);
593 |
594 | expect(entries[0].contentRect.width).toBe(200);
595 | expect(entries[0].contentRect.height).toBe(200);
596 | expect(entries[0].contentRect.top).toBe(0);
597 | expect(entries[0].contentRect.right).toBe(200);
598 | expect(entries[0].contentRect.bottom).toBe(200);
599 | expect(entries[0].contentRect.left).toBe(0);
600 | }).then(async () => {
601 | elements.target1.style.border = '5px solid black';
602 |
603 | await wait(timeout);
604 |
605 | expect(spy).toHaveBeenCalledTimes(1);
606 | }).then(async () => {
607 | elements.target1.style.boxSizing = 'border-box';
608 |
609 | const entries = await spy.nextCall();
610 |
611 | expect(entries.length).toBe(1);
612 |
613 | expect(entries[0].target).toBe(elements.target1);
614 |
615 | expect(entries[0].contentRect.width).toBe(190);
616 | expect(entries[0].contentRect.height).toBe(190);
617 | expect(entries[0].contentRect.top).toBe(0);
618 | expect(entries[0].contentRect.right).toBe(190);
619 | expect(entries[0].contentRect.bottom).toBe(190);
620 | expect(entries[0].contentRect.left).toBe(0);
621 | }).then(async () => {
622 | elements.target1.style.borderTop = '';
623 | elements.target1.style.borderBottom = '';
624 |
625 | const entries = await spy.nextCall();
626 |
627 | expect(entries.length).toBe(1);
628 |
629 | expect(entries[0].target).toBe(elements.target1);
630 |
631 | expect(entries[0].contentRect.width).toBe(190);
632 | expect(entries[0].contentRect.height).toBe(200);
633 | expect(entries[0].contentRect.top).toBe(0);
634 | expect(entries[0].contentRect.right).toBe(190);
635 | expect(entries[0].contentRect.bottom).toBe(200);
636 | expect(entries[0].contentRect.left).toBe(0);
637 | }).then(async () => {
638 | elements.target1.style.borderLeft = '';
639 | elements.target1.style.borderRight = '';
640 |
641 | const entries = await spy.nextCall();
642 |
643 | expect(entries.length).toBe(1);
644 |
645 | expect(entries[0].target).toBe(elements.target1);
646 |
647 | expect(entries[0].contentRect.width).toBe(200);
648 | expect(entries[0].contentRect.height).toBe(200);
649 | expect(entries[0].contentRect.top).toBe(0);
650 | expect(entries[0].contentRect.right).toBe(200);
651 | expect(entries[0].contentRect.bottom).toBe(200);
652 | expect(entries[0].contentRect.left).toBe(0);
653 | }).then(done).catch(done.fail);
654 | });
655 |
656 | it('doesn\'t notify when position changes', done => {
657 | const spy = createAsyncSpy();
658 |
659 | elements.target1.style.position = 'relative';
660 | elements.target1.style.top = '7px';
661 | elements.target1.style.left = '5px;';
662 | elements.target1.style.padding = '2px 3px';
663 |
664 | observer = new ResizeObserver(spy);
665 |
666 | observer.observe(elements.target1);
667 |
668 | spy.nextCall().then(entries => {
669 | expect(entries.length).toBe(1);
670 |
671 | expect(entries[0].target).toBe(elements.target1);
672 |
673 | expect(entries[0].contentRect.width).toBe(200);
674 | expect(entries[0].contentRect.height).toBe(200);
675 | expect(entries[0].contentRect.top).toBe(2);
676 | expect(entries[0].contentRect.right).toBe(203);
677 | expect(entries[0].contentRect.bottom).toBe(202);
678 | expect(entries[0].contentRect.left).toBe(3);
679 | }).then(async () => {
680 | elements.target1.style.left = '10px';
681 | elements.target1.style.top = '20px';
682 |
683 | await wait(timeout);
684 |
685 | expect(spy).toHaveBeenCalledTimes(1);
686 | }).then(done).catch(done.fail);
687 | });
688 |
689 | it('ignores scroll bars size', done => {
690 | const spy = createAsyncSpy();
691 |
692 | observer = new ResizeObserver(spy);
693 |
694 | elements.root.style.width = '100px';
695 | elements.root.style.height = '250px';
696 | elements.root.style.overflow = 'auto';
697 |
698 | elements.container.style.minWidth = '0px';
699 |
700 | observer.observe(elements.root);
701 |
702 | spy.nextCall().then(entries => {
703 | expect(entries.length).toBe(1);
704 | expect(entries[0].target).toBe(elements.root);
705 |
706 | expect(entries[0].contentRect.width).toBe(elements.root.clientWidth);
707 | expect(entries[0].contentRect.height).toBe(elements.root.clientHeight);
708 |
709 | // It is not possible to run further tests if browser has overlaid scroll bars.
710 | if (
711 | elements.root.clientWidth === elements.root.offsetWidth &&
712 | elements.root.clientHeight === elements.root.offsetHeight
713 | ) {
714 | return Promise.resolve();
715 | }
716 |
717 | return (async () => {
718 | const width = elements.root.clientWidth;
719 |
720 | elements.target1.style.width = width + 'px';
721 | elements.target2.style.width = width + 'px';
722 |
723 | const entries = await spy.nextCall();
724 |
725 | expect(entries.length).toBe(1);
726 | expect(entries[0].target).toBe(elements.root);
727 |
728 | expect(entries[0].contentRect.height).toBe(250);
729 | })().then(async () => {
730 | elements.target1.style.height = '125px';
731 | elements.target2.style.height = '125px';
732 |
733 | const entries = await spy.nextCall();
734 |
735 | expect(entries.length).toBe(1);
736 | expect(entries[0].target).toBe(elements.root);
737 |
738 | expect(entries[0].contentRect.width).toBe(100);
739 | });
740 | }).then(done).catch(done.fail);
741 | });
742 |
743 | it('doesn\'t trigger for a non-replaced inline elements', done => {
744 | const spy = createAsyncSpy();
745 |
746 | observer = new ResizeObserver(spy);
747 |
748 | elements.target1.style.display = 'inline';
749 | elements.target1.style.padding = '10px';
750 |
751 | observer.observe(elements.target1);
752 |
753 | wait(timeout).then(() => {
754 | expect(spy).not.toHaveBeenCalled();
755 | }).then(async () => {
756 | elements.target1.style.position = 'absolute';
757 |
758 | const entries = await spy.nextCall();
759 |
760 | expect(entries.length).toBe(1);
761 | expect(entries[0].target).toBe(elements.target1);
762 |
763 | expect(entries[0].contentRect.width).toBe(200);
764 | expect(entries[0].contentRect.height).toBe(200);
765 | expect(entries[0].contentRect.top).toBe(10);
766 | expect(entries[0].contentRect.left).toBe(10);
767 | }).then(async () => {
768 | elements.target1.style.position = 'static';
769 |
770 | const entries = await spy.nextCall();
771 |
772 | expect(entries.length).toBe(1);
773 | expect(entries[0].target).toBe(elements.target1);
774 |
775 | expect(entries[0].contentRect.width).toBe(0);
776 | expect(entries[0].contentRect.height).toBe(0);
777 | expect(entries[0].contentRect.top).toBe(0);
778 | expect(entries[0].contentRect.right).toBe(0);
779 | expect(entries[0].contentRect.bottom).toBe(0);
780 | expect(entries[0].contentRect.left).toBe(0);
781 | }).then(async () => {
782 | elements.target1.style.width = '150px';
783 |
784 | await wait(timeout);
785 |
786 | expect(spy).toHaveBeenCalledTimes(2);
787 | }).then(async () => {
788 | elements.target1.style.display = 'block';
789 |
790 | const entries = await spy.nextCall();
791 |
792 | expect(entries.length).toBe(1);
793 | expect(entries[0].target).toBe(elements.target1);
794 |
795 | expect(entries[0].contentRect.width).toBe(150);
796 | expect(entries[0].contentRect.height).toBe(200);
797 | expect(entries[0].contentRect.top).toBe(10);
798 | expect(entries[0].contentRect.left).toBe(10);
799 | }).then(async () => {
800 | elements.target1.style.display = 'inline';
801 |
802 | const entries = await spy.nextCall();
803 |
804 | expect(entries.length).toBe(1);
805 | expect(entries[0].target).toBe(elements.target1);
806 |
807 | expect(entries[0].contentRect.width).toBe(0);
808 | expect(entries[0].contentRect.height).toBe(0);
809 | expect(entries[0].contentRect.top).toBe(0);
810 | expect(entries[0].contentRect.right).toBe(0);
811 | expect(entries[0].contentRect.bottom).toBe(0);
812 | expect(entries[0].contentRect.left).toBe(0);
813 | }).then(done).catch(done.fail);
814 | });
815 |
816 | it('handles replaced inline elements', done => {
817 | elements.root.insertAdjacentHTML('beforeend', `
818 |
821 | `
822 | );
823 |
824 | const spy = createAsyncSpy();
825 | const replaced = document.getElementById('replaced-inline');
826 |
827 | observer = new ResizeObserver(spy);
828 |
829 | observer.observe(replaced);
830 |
831 | spy.nextCall().then(entries => {
832 | expect(entries.length).toBe(1);
833 | expect(entries[0].target).toBe(replaced);
834 |
835 | expect(entries[0].contentRect.width).toBe(200);
836 | expect(entries[0].contentRect.height).toBe(30);
837 | expect(entries[0].contentRect.top).toBe(5);
838 | expect(entries[0].contentRect.right).toBe(206);
839 | expect(entries[0].contentRect.bottom).toBe(35);
840 | expect(entries[0].contentRect.left).toBe(6);
841 | }).then(async () => {
842 | replaced.style.width = '190px';
843 |
844 | const entries = await spy.nextCall();
845 |
846 | expect(entries.length).toBe(1);
847 | expect(entries[0].target).toBe(replaced);
848 |
849 | expect(entries[0].contentRect.width).toBe(190);
850 | expect(entries[0].contentRect.height).toBe(30);
851 | expect(entries[0].contentRect.top).toBe(5);
852 | expect(entries[0].contentRect.right).toBe(196);
853 | expect(entries[0].contentRect.bottom).toBe(35);
854 | expect(entries[0].contentRect.left).toBe(6);
855 | }).then(async () => {
856 | replaced.style.boxSizing = 'border-box';
857 |
858 | const entries = await spy.nextCall();
859 |
860 | expect(entries.length).toBe(1);
861 | expect(entries[0].target).toBe(replaced);
862 |
863 | expect(entries[0].contentRect.width).toBe(174);
864 | expect(entries[0].contentRect.height).toBe(16);
865 | expect(entries[0].contentRect.top).toBe(5);
866 | expect(entries[0].contentRect.right).toBe(180);
867 | expect(entries[0].contentRect.bottom).toBe(21);
868 | expect(entries[0].contentRect.left).toBe(6);
869 | }).then(done).catch(done.fail);
870 | });
871 |
872 | it('handles fractional dimensions', done => {
873 | elements.target1.style.width = '200.5px';
874 | elements.target1.style.height = '200.5px';
875 | elements.target1.style.padding = '6.3px 3.3px';
876 | elements.target1.style.border = '11px solid black';
877 |
878 | const spy = createAsyncSpy();
879 |
880 | observer = new ResizeObserver(spy);
881 |
882 | observer.observe(elements.target1);
883 |
884 | spy.nextCall().then(entries => {
885 | expect(entries.length).toBe(1);
886 | expect(entries[0].target).toBe(elements.target1);
887 |
888 | expect(entries[0].contentRect.width).toBeCloseTo(200.5, 1);
889 | expect(entries[0].contentRect.height).toBeCloseTo(200.5, 1);
890 | expect(entries[0].contentRect.top).toBeCloseTo(6.3, 1);
891 | expect(entries[0].contentRect.right).toBeCloseTo(203.8, 1);
892 | expect(entries[0].contentRect.bottom).toBeCloseTo(206.8, 1);
893 | expect(entries[0].contentRect.left).toBeCloseTo(3.3, 1);
894 | }).then(async () => {
895 | elements.target1.style.padding = '7.8px 3.8px';
896 |
897 | await wait(timeout);
898 |
899 | expect(spy).toHaveBeenCalledTimes(1);
900 | }).then(async () => {
901 | elements.target1.style.boxSizing = 'border-box';
902 |
903 | const entries = await spy.nextCall();
904 |
905 | expect(entries.length).toBe(1);
906 | expect(entries[0].target).toBe(elements.target1);
907 |
908 | expect(entries[0].contentRect.width).toBeCloseTo(170.9, 1);
909 | expect(entries[0].contentRect.height).toBeCloseTo(162.9, 1);
910 | expect(entries[0].contentRect.top).toBeCloseTo(7.8, 1);
911 | expect(entries[0].contentRect.right).toBeCloseTo(174.7, 1);
912 | expect(entries[0].contentRect.bottom).toBeCloseTo(170.7, 1);
913 | expect(entries[0].contentRect.left).toBeCloseTo(3.8, 1);
914 | }).then(async () => {
915 | elements.target1.style.padding = '7.9px 3.9px';
916 |
917 | const entries = await spy.nextCall();
918 |
919 | expect(entries.length).toBe(1);
920 | expect(entries[0].target).toBe(elements.target1);
921 |
922 | expect(entries[0].contentRect.width).toBeCloseTo(170.7, 1);
923 | expect(entries[0].contentRect.height).toBeCloseTo(162.7, 1);
924 | expect(entries[0].contentRect.top).toBeCloseTo(7.9, 1);
925 | expect(entries[0].contentRect.right).toBeCloseTo(174.6, 1);
926 | expect(entries[0].contentRect.bottom).toBeCloseTo(170.6, 1);
927 | expect(entries[0].contentRect.left).toBeCloseTo(3.9, 1);
928 | }).then(async () => {
929 | elements.target1.style.width = '200px';
930 |
931 | const entries = await spy.nextCall();
932 |
933 | expect(entries.length).toBe(1);
934 | expect(entries[0].target).toBe(elements.target1);
935 |
936 | expect(entries[0].contentRect.width).toBeCloseTo(170.2, 1);
937 | expect(entries[0].contentRect.height).toBeCloseTo(162.7, 1);
938 | expect(entries[0].contentRect.top).toBeCloseTo(7.9, 1);
939 | expect(entries[0].contentRect.right).toBeCloseTo(174.1, 1);
940 | expect(entries[0].contentRect.bottom).toBeCloseTo(170.6, 1);
941 | expect(entries[0].contentRect.left).toBeCloseTo(3.9, 1);
942 | }).then(done).catch(done.fail);
943 | });
944 |
945 | it('handles SVGGraphicsElement', done => {
946 | elements.root.insertAdjacentHTML('beforeend', `
947 |
958 | `);
959 |
960 | const spy = createAsyncSpy();
961 | const svgRoot = document.getElementById('svg-root');
962 | const svgRect = document.getElementById('svg-rect');
963 |
964 | observer = new ResizeObserver(spy);
965 |
966 | observer.observe(svgRect);
967 |
968 | spy.nextCall().then(entries => {
969 | expect(entries.length).toBe(1);
970 |
971 | expect(entries[0].target).toBe(svgRect);
972 |
973 | expect(entries[0].contentRect.width).toBe(200);
974 | expect(entries[0].contentRect.height).toBe(150);
975 | expect(entries[0].contentRect.top).toBe(0);
976 | expect(entries[0].contentRect.right).toBe(200);
977 | expect(entries[0].contentRect.bottom).toBe(150);
978 | expect(entries[0].contentRect.left).toBe(0);
979 | }).then(async () => {
980 | svgRect.setAttribute('width', 250);
981 | svgRect.setAttribute('height', 200);
982 |
983 | const entries = await spy.nextCall();
984 |
985 | expect(entries.length).toBe(1);
986 |
987 | expect(entries[0].target).toBe(svgRect);
988 |
989 | expect(entries[0].contentRect.width).toBe(250);
990 | expect(entries[0].contentRect.height).toBe(200);
991 | expect(entries[0].contentRect.top).toBe(0);
992 | expect(entries[0].contentRect.right).toBe(250);
993 | expect(entries[0].contentRect.bottom).toBe(200);
994 | expect(entries[0].contentRect.left).toBe(0);
995 | }).then(async () => {
996 | observer.observe(svgRoot);
997 |
998 | const entries = await spy.nextCall();
999 |
1000 | expect(entries.length).toBe(1);
1001 |
1002 | expect(entries[0].target).toBe(svgRoot);
1003 |
1004 | expect(entries[0].contentRect.width).toBe(250);
1005 | expect(entries[0].contentRect.height).toBe(200);
1006 | expect(entries[0].contentRect.top).toBe(0);
1007 | expect(entries[0].contentRect.right).toBe(250);
1008 | expect(entries[0].contentRect.bottom).toBe(200);
1009 | expect(entries[0].contentRect.left).toBe(0);
1010 | }).then(done).catch(done.fail);
1011 | });
1012 |
1013 | it('doesn\'t observe svg elements that don\'t implement the SVGGraphicsElement interface', done => {
1014 | elements.root.insertAdjacentHTML('beforeend', `
1015 |
1027 | `);
1028 |
1029 | const spy = createAsyncSpy();
1030 | const svgGrad = document.getElementById('gradient');
1031 | const svgCircle = document.getElementById('circle');
1032 |
1033 | observer = new ResizeObserver(spy);
1034 |
1035 | observer.observe(svgGrad);
1036 |
1037 | wait(timeout).then(() => {
1038 | expect(spy).not.toHaveBeenCalled();
1039 |
1040 | observer.observe(svgCircle);
1041 |
1042 | return spy.nextCall();
1043 | }).then(entries => {
1044 | expect(spy).toHaveBeenCalledTimes(1);
1045 |
1046 | expect(entries.length).toBe(1);
1047 | expect(entries[0].target).toBe(svgCircle);
1048 |
1049 | expect(entries[0].contentRect.top).toBe(0);
1050 | expect(entries[0].contentRect.left).toBe(0);
1051 | expect(entries[0].contentRect.width).toBe(100);
1052 | expect(entries[0].contentRect.height).toBe(100);
1053 | }).then(done).catch(done.fail);
1054 | });
1055 |
1056 | it('handles IE11 issue with the MutationObserver: https://jsfiddle.net/x2r3jpuz/2/', done => {
1057 | const spy = createAsyncSpy();
1058 |
1059 | elements.root.insertAdjacentHTML('beforeend', `
1060 |
1061 |
1062 |
1063 | `);
1064 |
1065 | observer = new ResizeObserver(spy);
1066 |
1067 | observer.observe(elements.root);
1068 |
1069 | spy.nextCall().then(async () => {
1070 | const elem = elements.root.querySelector('strong');
1071 |
1072 | // IE11 crashes at this step if MuatationObserver is used.
1073 | elem.textContent = 'a';
1074 | elem.textContent = 'b';
1075 |
1076 | await wait(timeout);
1077 | }).then(done).catch(done.fail);
1078 | });
1079 |
1080 | if (typeof document.body.style.transform !== 'undefined') {
1081 | it('doesn\'t notify of transformations', done => {
1082 | const spy = createAsyncSpy();
1083 |
1084 | observer = new ResizeObserver(spy);
1085 |
1086 | observer.observe(elements.target1);
1087 |
1088 | spy.nextCall().then(entries => {
1089 | expect(entries.length).toBe(1);
1090 | expect(entries[0].target).toBe(elements.target1);
1091 |
1092 | expect(entries[0].contentRect.width).toBe(200);
1093 | expect(entries[0].contentRect.height).toBe(200);
1094 | expect(entries[0].contentRect.top).toBe(0);
1095 | expect(entries[0].contentRect.left).toBe(0);
1096 | }).then(async () => {
1097 | elements.container.style.transform = 'scale(0.5)';
1098 | elements.target2.style.transform = 'scale(0.5)';
1099 |
1100 | observer.observe(elements.target2);
1101 |
1102 | const entries = await spy.nextCall();
1103 |
1104 | expect(entries.length).toBe(1);
1105 | expect(entries[0].target).toBe(elements.target2);
1106 |
1107 | expect(entries[0].contentRect.width).toBe(200);
1108 | expect(entries[0].contentRect.height).toBe(200);
1109 | expect(entries[0].contentRect.top).toBe(0);
1110 | expect(entries[0].contentRect.left).toBe(0);
1111 | }).then(async () => {
1112 | elements.container.style.transform = '';
1113 | elements.target2.style.transform = '';
1114 |
1115 | await wait(timeout);
1116 |
1117 | expect(spy).toHaveBeenCalledTimes(2);
1118 | }).then(done).catch(done.fail);
1119 | });
1120 | }
1121 |
1122 | if (typeof document.body.style.transition !== 'undefined') {
1123 | it('handles transitions', done => {
1124 | elements.target1.style.transition = 'width 1s';
1125 |
1126 | const spy = createAsyncSpy();
1127 |
1128 | observer = new ResizeObserver(spy);
1129 |
1130 | observer.observe(elements.target1);
1131 |
1132 | spy.nextCall().then(async () => {
1133 | const transitionEnd = new Promise(resolve => {
1134 | const callback = () => {
1135 | elements.target1.removeEventListener('transitionend', callback);
1136 | resolve();
1137 | };
1138 |
1139 | elements.target1.addEventListener('transitionend', callback);
1140 | });
1141 |
1142 | await wait(20);
1143 |
1144 | elements.target1.style.width = '600px';
1145 |
1146 | await transitionEnd;
1147 | await wait(timeout);
1148 |
1149 | // eslint-disable-next-line prefer-destructuring
1150 | const entries = spy.calls.mostRecent().args[0];
1151 |
1152 | expect(entries[0].target).toBe(elements.target1);
1153 | expect(Math.round(entries[0].contentRect.width)).toBe(600);
1154 | }).then(done).catch(done.fail);
1155 | });
1156 | }
1157 | });
1158 |
1159 | describe('unobserve', () => {
1160 | it('throws an error if no arguments have been provided', () => {
1161 | observer = new ResizeObserver(emptyFn);
1162 |
1163 | expect(() => {
1164 | observer.unobserve();
1165 | }).toThrowError(/1 argument required/i);
1166 | });
1167 |
1168 | it('throws an error if target is not an Element', () => {
1169 | observer = new ResizeObserver(emptyFn);
1170 |
1171 | expect(() => {
1172 | observer.unobserve(true);
1173 | }).toThrowError(/Element/i);
1174 |
1175 | expect(() => {
1176 | observer.unobserve(null);
1177 | }).toThrowError(/Element/i);
1178 |
1179 | expect(() => {
1180 | observer.unobserve({});
1181 | }).toThrowError(/Element/i);
1182 |
1183 | expect(() => {
1184 | observer.unobserve(document.createTextNode(''));
1185 | }).toThrowError(/Element/i);
1186 | });
1187 |
1188 | it('stops observing single element', done => {
1189 | const spy = createAsyncSpy();
1190 |
1191 | observer = new ResizeObserver(spy);
1192 |
1193 | observer.observe(elements.target1);
1194 | observer.observe(elements.target2);
1195 |
1196 | spy.nextCall().then(entries => {
1197 | expect(spy).toHaveBeenCalledTimes(1);
1198 |
1199 | expect(entries.length).toBe(2);
1200 |
1201 | expect(entries[0].target).toBe(elements.target1);
1202 | expect(entries[1].target).toBe(elements.target2);
1203 | }).then(async () => {
1204 | observer.unobserve(elements.target1);
1205 |
1206 | elements.target1.style.width = '50px';
1207 | elements.target2.style.width = '50px';
1208 |
1209 | const entries = await spy.nextCall();
1210 |
1211 | expect(spy).toHaveBeenCalledTimes(2);
1212 |
1213 | expect(entries.length).toBe(1);
1214 | expect(entries[0].target).toBe(elements.target2);
1215 | expect(entries[0].contentRect.width).toBe(50);
1216 | }).then(async () => {
1217 | elements.target2.style.width = '100px';
1218 |
1219 | observer.unobserve(elements.target2);
1220 |
1221 | await wait(timeout);
1222 |
1223 | expect(spy).toHaveBeenCalledTimes(2);
1224 | }).then(done).catch(done.fail);
1225 | });
1226 |
1227 | it('doesn\'t prevent gathered observations from being notified', done => {
1228 | const spy = createAsyncSpy();
1229 | const spy2 = createAsyncSpy();
1230 |
1231 | let shouldUnobserve = false;
1232 |
1233 | observer = new ResizeObserver((...args) => {
1234 | spy(...args);
1235 |
1236 | if (shouldUnobserve) {
1237 | observer2.unobserve(elements.target1);
1238 | }
1239 | });
1240 |
1241 | observer2 = new ResizeObserver((...args) => {
1242 | spy2(...args);
1243 |
1244 | if (shouldUnobserve) {
1245 | observer.unobserve(elements.target1);
1246 | }
1247 | });
1248 |
1249 | observer.observe(elements.target1);
1250 | observer2.observe(elements.target1);
1251 |
1252 | Promise.all([
1253 | spy.nextCall(),
1254 | spy2.nextCall()
1255 | ]).then(() => {
1256 | shouldUnobserve = true;
1257 |
1258 | elements.target1.style.width = '220px';
1259 |
1260 | return Promise.all([spy.nextCall(), spy2.nextCall()]);
1261 | }).then(done).catch(done.fail);
1262 | });
1263 |
1264 | it('handles elements that are not observed', done => {
1265 | const spy = createAsyncSpy();
1266 |
1267 | observer = new ResizeObserver(spy);
1268 |
1269 | observer.unobserve(elements.target1);
1270 |
1271 | wait(timeout).then(() => {
1272 | expect(spy).not.toHaveBeenCalled();
1273 | }).then(done).catch(done.fail);
1274 | });
1275 | });
1276 |
1277 | describe('disconnect', () => {
1278 | it('stops observing all elements', done => {
1279 | const spy = createAsyncSpy();
1280 |
1281 | observer = new ResizeObserver(spy);
1282 |
1283 | observer.observe(elements.target1);
1284 | observer.observe(elements.target2);
1285 |
1286 | spy.nextCall().then(entries => {
1287 | expect(entries.length).toBe(2);
1288 |
1289 | expect(entries[0].target).toBe(elements.target1);
1290 | expect(entries[1].target).toBe(elements.target2);
1291 | }).then(async () => {
1292 | elements.target1.style.width = '600px';
1293 | elements.target2.style.width = '600px';
1294 |
1295 | observer.disconnect();
1296 |
1297 | await wait(timeout);
1298 |
1299 | expect(spy).toHaveBeenCalledTimes(1);
1300 | }).then(done).catch(done.fail);
1301 | });
1302 |
1303 | it('prevents gathered observations from being notified', done => {
1304 | const spy = createAsyncSpy();
1305 | const spy2 = createAsyncSpy();
1306 |
1307 | let shouldDisconnect = false;
1308 |
1309 | observer = new ResizeObserver((...args) => {
1310 | spy(...args);
1311 |
1312 | if (shouldDisconnect) {
1313 | observer2.disconnect();
1314 | }
1315 | });
1316 |
1317 | observer2 = new ResizeObserver((...args) => {
1318 | spy2(...args);
1319 |
1320 | if (shouldDisconnect) {
1321 | observer.disconnect();
1322 | }
1323 | });
1324 |
1325 | observer.observe(elements.target1);
1326 | observer2.observe(elements.target1);
1327 |
1328 | Promise.all([
1329 | spy.nextCall(),
1330 | spy2.nextCall()
1331 | ]).then(async () => {
1332 | shouldDisconnect = true;
1333 |
1334 | elements.target1.style.width = '220px';
1335 |
1336 | await Promise.race([spy.nextCall(), spy2.nextCall()]);
1337 | await wait(10);
1338 |
1339 | if (spy.calls.count() === 2) {
1340 | expect(spy2).toHaveBeenCalledTimes(1);
1341 | }
1342 |
1343 | if (spy2.calls.count() === 2) {
1344 | expect(spy).toHaveBeenCalledTimes(1);
1345 | }
1346 | }).then(done).catch(done.fail);
1347 | });
1348 |
1349 | it('doesn\'t destroy observer', done => {
1350 | const spy = createAsyncSpy();
1351 |
1352 | observer = new ResizeObserver(spy);
1353 |
1354 | observer.observe(elements.target1);
1355 |
1356 | spy.nextCall().then(async () => {
1357 | elements.target1.style.width = '600px';
1358 |
1359 | observer.disconnect();
1360 |
1361 | await wait(timeout);
1362 |
1363 | observer.observe(elements.target1);
1364 |
1365 | const entries = await spy.nextCall();
1366 |
1367 | expect(spy).toHaveBeenCalledTimes(2);
1368 |
1369 | expect(entries.length).toBe(1);
1370 |
1371 | expect(entries[0].target).toBe(elements.target1);
1372 | expect(entries[0].contentRect.width).toBe(600);
1373 | }).then(done).catch(done.fail);
1374 | });
1375 | });
1376 | });
1377 |
--------------------------------------------------------------------------------
/tests/ResizeObserverEntry.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-nested-callbacks, require-jsdoc */
2 | import {ResizeObserver} from './resources/observer';
3 |
4 | const NEW_VALUE = Date.now();
5 |
6 | /**
7 | * Checks whether the specified property is present in provided object
8 | * and that it's neither enumerable nor writable.
9 | *
10 | * @param {Object} target
11 | * @param {string} prop
12 | * @returns {boolean}
13 | */
14 | function isReadOnlyAttr(target, prop) {
15 | if (!(prop in target)) {
16 | return false;
17 | }
18 |
19 | const keys = Object.keys(target);
20 |
21 | // Property shouldn't be enumerable.
22 | if (~keys.indexOf(prop)) {
23 | return false;
24 | }
25 |
26 | // Property shouldn't be writable.
27 | try {
28 | target[prop] = NEW_VALUE;
29 | } catch (e) {
30 | // An error is expected in 'strict' mode
31 | // for the major browsers.
32 | }
33 |
34 | if (target[prop] === NEW_VALUE) {
35 | return false;
36 | }
37 |
38 | // Properties' descriptor can be changed.
39 | try {
40 | Object.defineProperty(target, prop, {
41 | value: NEW_VALUE
42 | });
43 | } catch (e) {
44 | // If property is configurable
45 | // an error shouldn't be thrown.
46 | return false;
47 | }
48 |
49 | return target[prop] === NEW_VALUE;
50 | }
51 |
52 | function getEntry() {
53 | return new Promise(resolve => {
54 | const observer = new ResizeObserver(entries => {
55 | observer.disconnect();
56 |
57 | resolve(entries[0]);
58 | });
59 |
60 | observer.observe(document.body);
61 | });
62 | }
63 |
64 | describe('ResizeObserverEntry', () => {
65 | describe('constructor', () => {
66 | it('properties are readonly and not enumerable', done => {
67 | getEntry().then(entry => {
68 | expect(isReadOnlyAttr(entry, 'target')).toBe(true);
69 | expect(isReadOnlyAttr(entry, 'contentRect')).toBe(true);
70 | }).then(done);
71 | });
72 |
73 | it('content rectangle is an instance of the DOMRectReadOnly', done => {
74 | getEntry().then(entry => {
75 | const rectKeys = ['x', 'y', 'width', 'height', 'top', 'right', 'bottom', 'left'];
76 | const {contentRect} = entry;
77 |
78 | if (typeof DOMRectReadOnly === 'function') {
79 | expect(Object.getPrototypeOf(contentRect)).toBe(DOMRectReadOnly.prototype);
80 | }
81 |
82 | for (const key of rectKeys) {
83 | expect(isReadOnlyAttr(contentRect, key)).toBe(true);
84 | }
85 | }).then(done);
86 | });
87 | });
88 | });
89 |
--------------------------------------------------------------------------------
/tests/node/ResizeObserver.spec.node.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-nested-callbacks */
2 | const ResizeObserver = require('../../dist/ResizeObserver');
3 |
4 | let observer;
5 |
6 | // eslint-disable-next-line
7 | const emptyFn = () => {};
8 |
9 | describe('ResizeObserver', () => {
10 | afterEach(() => {
11 | if (observer) {
12 | observer.disconnect();
13 | }
14 |
15 | observer = null;
16 | });
17 |
18 | describe('constructor', () => {
19 | it('throws an error if no arguments are provided', () => {
20 | expect(() => {
21 | observer = new ResizeObserver();
22 | }).toThrowError(/1 argument required/i);
23 | });
24 |
25 | it('throws an error if callback is not a function', () => {
26 | expect(() => {
27 | observer = new ResizeObserver(true);
28 | }).toThrowError(/function/i);
29 |
30 | expect(() => {
31 | observer = new ResizeObserver({});
32 | }).toThrowError(/function/i);
33 |
34 | expect(() => {
35 | observer = new ResizeObserver(emptyFn);
36 | }).not.toThrow();
37 | });
38 | });
39 |
40 | describe('observe', () => {
41 | it('throws an error if no arguments are provided', () => {
42 | observer = new ResizeObserver(emptyFn);
43 |
44 | expect(() => {
45 | observer.observe();
46 | }).toThrowError(/1 argument required/i);
47 |
48 | expect(() => {
49 | observer.observe({});
50 | }).not.toThrow();
51 | });
52 | });
53 |
54 | describe('unobserve', () => {
55 | it('throws an error if no arguments are provided', () => {
56 | observer = new ResizeObserver(emptyFn);
57 |
58 | expect(() => {
59 | observer.unobserve();
60 | }).toThrowError(/1 argument required/i);
61 |
62 | expect(() => {
63 | observer.unobserve({});
64 | }).not.toThrow();
65 | });
66 | });
67 |
68 | describe('disconnect', () => {
69 | it('doesnt throw an error', () => {
70 | observer = new ResizeObserver(emptyFn);
71 |
72 | expect(() => {
73 | observer.disconnect();
74 | }).not.toThrow();
75 | });
76 | });
77 | });
78 |
--------------------------------------------------------------------------------
/tests/node/index.js:
--------------------------------------------------------------------------------
1 | const Jasmine = require('jasmine');
2 |
3 | const jasmine = new Jasmine();
4 |
5 | /* eslint-disable camelcase */
6 | jasmine.loadConfig({
7 | spec_dir: 'tests/node',
8 | spec_files: [
9 | '*.spec.node.js'
10 | ]
11 | });
12 |
13 | /* eslint-enable camelcase */
14 |
15 | jasmine.execute();
16 |
--------------------------------------------------------------------------------
/tests/resources/helpers.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Creates an overlay function for the Jasmine's "Spy" object with the additional
3 | * "nextCall" method, which in turn creates a promise that will be resolved on
4 | * the next invocation of the spy function.
5 | *
6 | * @returns {Function}
7 | */
8 | export function createAsyncSpy() {
9 | const origSpy = jasmine.createSpy(...arguments);
10 | const queue = [];
11 |
12 | const asyncSpy = function (...args) {
13 | // eslint-disable-next-line no-invalid-this
14 | const result = origSpy.apply(this, args);
15 |
16 | for (const resolve of queue) {
17 | resolve(args[0]);
18 | }
19 |
20 | queue.splice(0);
21 |
22 | return result;
23 | };
24 |
25 | asyncSpy.nextCall = () => new Promise(resolve => queue.push(resolve));
26 |
27 | for (const key of Object.keys(origSpy)) {
28 | asyncSpy[key] = origSpy[key];
29 | }
30 |
31 | return asyncSpy;
32 | }
33 |
34 | /**
35 | * Promise wrapper around the "setTimeout" method.
36 | *
37 | * @param {number} timeout
38 | * @returns {Promise}
39 | */
40 | export const wait = timeout => new Promise(resolve => setTimeout(resolve, timeout));
41 |
--------------------------------------------------------------------------------
/tests/resources/observer.js:
--------------------------------------------------------------------------------
1 | import ResizeObserverEntryPolyfill from '../../src/ResizeObserverEntry';
2 | import ResizeObserverPolyfill from '../../src/ResizeObserver';
3 |
4 | let ResizeObserver = ResizeObserverPolyfill,
5 | ResizeObserverEntry = ResizeObserverEntryPolyfill;
6 |
7 | if (window.__karma__.config.native) {
8 | window.addEventListener('error', error => {
9 | if (/loop limit/.test(error.message)) {
10 | error.stopImmediatePropagation();
11 | }
12 | });
13 |
14 | ResizeObserver = window.ResizeObserver || {};
15 | ResizeObserverEntry = window.ResizeObserverEntry || {};
16 | }
17 |
18 | export {ResizeObserver, ResizeObserverEntry};
19 |
--------------------------------------------------------------------------------