[d565449] | 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<ResizeObserverSPI>}
|
---|
| 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 | }
|
---|