123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238 |
- import isBrowser from './utils/isBrowser.js';
- import throttle from './utils/throttle.js';
- // Minimum delay before invoking the update of observers.
- const REFRESH_DELAY = 20;
- // A list of substrings of CSS properties used to find transition events that
- // might affect dimensions of observed elements.
- const transitionKeys = ['top', 'right', 'bottom', 'left', 'width', 'height', 'size', 'weight'];
- // Check if MutationObserver is available.
- const mutationObserverSupported = typeof MutationObserver !== 'undefined';
- /**
- * Singleton controller class which handles updates of ResizeObserver instances.
- */
- export default class ResizeObserverController {
- /**
- * Indicates whether DOM listeners have been added.
- *
- * @private {boolean}
- */
- connected_ = false;
- /**
- * Tells that controller has subscribed for Mutation Events.
- *
- * @private {boolean}
- */
- mutationEventsAdded_ = false;
- /**
- * Keeps reference to the instance of MutationObserver.
- *
- * @private {MutationObserver}
- */
- mutationsObserver_ = null;
- /**
- * A list of connected observers.
- *
- * @private {Array<ResizeObserverSPI>}
- */
- observers_ = [];
- /**
- * Holds reference to the controller's instance.
- *
- * @private {ResizeObserverController}
- */
- static instance_ = null;
- /**
- * Creates a new instance of ResizeObserverController.
- *
- * @private
- */
- constructor() {
- this.onTransitionEnd_ = this.onTransitionEnd_.bind(this);
- this.refresh = throttle(this.refresh.bind(this), REFRESH_DELAY);
- }
- /**
- * Adds observer to observers list.
- *
- * @param {ResizeObserverSPI} observer - Observer to be added.
- * @returns {void}
- */
- addObserver(observer) {
- if (!~this.observers_.indexOf(observer)) {
- this.observers_.push(observer);
- }
- // Add listeners if they haven't been added yet.
- if (!this.connected_) {
- this.connect_();
- }
- }
- /**
- * Removes observer from observers list.
- *
- * @param {ResizeObserverSPI} observer - Observer to be removed.
- * @returns {void}
- */
- removeObserver(observer) {
- const observers = this.observers_;
- const index = observers.indexOf(observer);
- // Remove observer if it's present in registry.
- if (~index) {
- observers.splice(index, 1);
- }
- // Remove listeners if controller has no connected observers.
- if (!observers.length && this.connected_) {
- this.disconnect_();
- }
- }
- /**
- * Invokes the update of observers. It will continue running updates insofar
- * it detects changes.
- *
- * @returns {void}
- */
- refresh() {
- const changesDetected = this.updateObservers_();
- // Continue running updates if changes have been detected as there might
- // be future ones caused by CSS transitions.
- if (changesDetected) {
- this.refresh();
- }
- }
- /**
- * Updates every observer from observers list and notifies them of queued
- * entries.
- *
- * @private
- * @returns {boolean} Returns "true" if any observer has detected changes in
- * dimensions of it's elements.
- */
- updateObservers_() {
- // Collect observers that have active observations.
- const activeObservers = this.observers_.filter(observer => {
- return observer.gatherActive(), observer.hasActive();
- });
- // Deliver notifications in a separate cycle in order to avoid any
- // collisions between observers, e.g. when multiple instances of
- // ResizeObserver are tracking the same element and the callback of one
- // of them changes content dimensions of the observed target. Sometimes
- // this may result in notifications being blocked for the rest of observers.
- activeObservers.forEach(observer => observer.broadcastActive());
- return activeObservers.length > 0;
- }
- /**
- * Initializes DOM listeners.
- *
- * @private
- * @returns {void}
- */
- connect_() {
- // Do nothing if running in a non-browser environment or if listeners
- // have been already added.
- if (!isBrowser || this.connected_) {
- return;
- }
- // Subscription to the "Transitionend" event is used as a workaround for
- // delayed transitions. This way it's possible to capture at least the
- // final state of an element.
- document.addEventListener('transitionend', this.onTransitionEnd_);
- window.addEventListener('resize', this.refresh);
- if (mutationObserverSupported) {
- this.mutationsObserver_ = new MutationObserver(this.refresh);
- this.mutationsObserver_.observe(document, {
- attributes: true,
- childList: true,
- characterData: true,
- subtree: true
- });
- } else {
- document.addEventListener('DOMSubtreeModified', this.refresh);
- this.mutationEventsAdded_ = true;
- }
- this.connected_ = true;
- }
- /**
- * Removes DOM listeners.
- *
- * @private
- * @returns {void}
- */
- disconnect_() {
- // Do nothing if running in a non-browser environment or if listeners
- // have been already removed.
- if (!isBrowser || !this.connected_) {
- return;
- }
- document.removeEventListener('transitionend', this.onTransitionEnd_);
- window.removeEventListener('resize', this.refresh);
- if (this.mutationsObserver_) {
- this.mutationsObserver_.disconnect();
- }
- if (this.mutationEventsAdded_) {
- document.removeEventListener('DOMSubtreeModified', this.refresh);
- }
- this.mutationsObserver_ = null;
- this.mutationEventsAdded_ = false;
- this.connected_ = false;
- }
- /**
- * "Transitionend" event handler.
- *
- * @private
- * @param {TransitionEvent} event
- * @returns {void}
- */
- onTransitionEnd_({propertyName = ''}) {
- // Detect whether transition may affect dimensions of an element.
- const isReflowProperty = transitionKeys.some(key => {
- return !!~propertyName.indexOf(key);
- });
- if (isReflowProperty) {
- this.refresh();
- }
- }
- /**
- * Returns instance of the ResizeObserverController.
- *
- * @returns {ResizeObserverController}
- */
- static getInstance() {
- if (!this.instance_) {
- this.instance_ = new ResizeObserverController();
- }
- return this.instance_;
- }
- }
|