ResizeObserverController.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. import isBrowser from './utils/isBrowser.js';
  2. import throttle from './utils/throttle.js';
  3. // Minimum delay before invoking the update of observers.
  4. const REFRESH_DELAY = 20;
  5. // A list of substrings of CSS properties used to find transition events that
  6. // might affect dimensions of observed elements.
  7. const transitionKeys = ['top', 'right', 'bottom', 'left', 'width', 'height', 'size', 'weight'];
  8. // Check if MutationObserver is available.
  9. const mutationObserverSupported = typeof MutationObserver !== 'undefined';
  10. /**
  11. * Singleton controller class which handles updates of ResizeObserver instances.
  12. */
  13. export default class ResizeObserverController {
  14. /**
  15. * Indicates whether DOM listeners have been added.
  16. *
  17. * @private {boolean}
  18. */
  19. connected_ = false;
  20. /**
  21. * Tells that controller has subscribed for Mutation Events.
  22. *
  23. * @private {boolean}
  24. */
  25. mutationEventsAdded_ = false;
  26. /**
  27. * Keeps reference to the instance of MutationObserver.
  28. *
  29. * @private {MutationObserver}
  30. */
  31. mutationsObserver_ = null;
  32. /**
  33. * A list of connected observers.
  34. *
  35. * @private {Array<ResizeObserverSPI>}
  36. */
  37. observers_ = [];
  38. /**
  39. * Holds reference to the controller's instance.
  40. *
  41. * @private {ResizeObserverController}
  42. */
  43. static instance_ = null;
  44. /**
  45. * Creates a new instance of ResizeObserverController.
  46. *
  47. * @private
  48. */
  49. constructor() {
  50. this.onTransitionEnd_ = this.onTransitionEnd_.bind(this);
  51. this.refresh = throttle(this.refresh.bind(this), REFRESH_DELAY);
  52. }
  53. /**
  54. * Adds observer to observers list.
  55. *
  56. * @param {ResizeObserverSPI} observer - Observer to be added.
  57. * @returns {void}
  58. */
  59. addObserver(observer) {
  60. if (!~this.observers_.indexOf(observer)) {
  61. this.observers_.push(observer);
  62. }
  63. // Add listeners if they haven't been added yet.
  64. if (!this.connected_) {
  65. this.connect_();
  66. }
  67. }
  68. /**
  69. * Removes observer from observers list.
  70. *
  71. * @param {ResizeObserverSPI} observer - Observer to be removed.
  72. * @returns {void}
  73. */
  74. removeObserver(observer) {
  75. const observers = this.observers_;
  76. const index = observers.indexOf(observer);
  77. // Remove observer if it's present in registry.
  78. if (~index) {
  79. observers.splice(index, 1);
  80. }
  81. // Remove listeners if controller has no connected observers.
  82. if (!observers.length && this.connected_) {
  83. this.disconnect_();
  84. }
  85. }
  86. /**
  87. * Invokes the update of observers. It will continue running updates insofar
  88. * it detects changes.
  89. *
  90. * @returns {void}
  91. */
  92. refresh() {
  93. const changesDetected = this.updateObservers_();
  94. // Continue running updates if changes have been detected as there might
  95. // be future ones caused by CSS transitions.
  96. if (changesDetected) {
  97. this.refresh();
  98. }
  99. }
  100. /**
  101. * Updates every observer from observers list and notifies them of queued
  102. * entries.
  103. *
  104. * @private
  105. * @returns {boolean} Returns "true" if any observer has detected changes in
  106. * dimensions of it's elements.
  107. */
  108. updateObservers_() {
  109. // Collect observers that have active observations.
  110. const activeObservers = this.observers_.filter(observer => {
  111. return observer.gatherActive(), observer.hasActive();
  112. });
  113. // Deliver notifications in a separate cycle in order to avoid any
  114. // collisions between observers, e.g. when multiple instances of
  115. // ResizeObserver are tracking the same element and the callback of one
  116. // of them changes content dimensions of the observed target. Sometimes
  117. // this may result in notifications being blocked for the rest of observers.
  118. activeObservers.forEach(observer => observer.broadcastActive());
  119. return activeObservers.length > 0;
  120. }
  121. /**
  122. * Initializes DOM listeners.
  123. *
  124. * @private
  125. * @returns {void}
  126. */
  127. connect_() {
  128. // Do nothing if running in a non-browser environment or if listeners
  129. // have been already added.
  130. if (!isBrowser || this.connected_) {
  131. return;
  132. }
  133. // Subscription to the "Transitionend" event is used as a workaround for
  134. // delayed transitions. This way it's possible to capture at least the
  135. // final state of an element.
  136. document.addEventListener('transitionend', this.onTransitionEnd_);
  137. window.addEventListener('resize', this.refresh);
  138. if (mutationObserverSupported) {
  139. this.mutationsObserver_ = new MutationObserver(this.refresh);
  140. this.mutationsObserver_.observe(document, {
  141. attributes: true,
  142. childList: true,
  143. characterData: true,
  144. subtree: true
  145. });
  146. } else {
  147. document.addEventListener('DOMSubtreeModified', this.refresh);
  148. this.mutationEventsAdded_ = true;
  149. }
  150. this.connected_ = true;
  151. }
  152. /**
  153. * Removes DOM listeners.
  154. *
  155. * @private
  156. * @returns {void}
  157. */
  158. disconnect_() {
  159. // Do nothing if running in a non-browser environment or if listeners
  160. // have been already removed.
  161. if (!isBrowser || !this.connected_) {
  162. return;
  163. }
  164. document.removeEventListener('transitionend', this.onTransitionEnd_);
  165. window.removeEventListener('resize', this.refresh);
  166. if (this.mutationsObserver_) {
  167. this.mutationsObserver_.disconnect();
  168. }
  169. if (this.mutationEventsAdded_) {
  170. document.removeEventListener('DOMSubtreeModified', this.refresh);
  171. }
  172. this.mutationsObserver_ = null;
  173. this.mutationEventsAdded_ = false;
  174. this.connected_ = false;
  175. }
  176. /**
  177. * "Transitionend" event handler.
  178. *
  179. * @private
  180. * @param {TransitionEvent} event
  181. * @returns {void}
  182. */
  183. onTransitionEnd_({propertyName = ''}) {
  184. // Detect whether transition may affect dimensions of an element.
  185. const isReflowProperty = transitionKeys.some(key => {
  186. return !!~propertyName.indexOf(key);
  187. });
  188. if (isReflowProperty) {
  189. this.refresh();
  190. }
  191. }
  192. /**
  193. * Returns instance of the ResizeObserverController.
  194. *
  195. * @returns {ResizeObserverController}
  196. */
  197. static getInstance() {
  198. if (!this.instance_) {
  199. this.instance_ = new ResizeObserverController();
  200. }
  201. return this.instance_;
  202. }
  203. }