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_;
|
}
|
}
|