getresizeobserver.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module utils/dom/getresizeobserver
  7. */
  8. /* globals setTimeout, clearTimeout */
  9. import mix from '../mix';
  10. import global from './global';
  11. import Rect from './rect';
  12. import DomEmitterMixin from './emittermixin';
  13. const RESIZE_CHECK_INTERVAL = 100;
  14. /**
  15. * Returns an instance of [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver).
  16. * In browsers that support the `ResizeObserver` API, the native observer instance is returned.
  17. * In other browsers, a polyfilled instance is returned instead with a compatible API.
  18. *
  19. * [Learn more](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) about the native API.
  20. *
  21. * @param {Function} callback A function called when any observed element was resized. Refer to the
  22. * native [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) API to
  23. * learn more.
  24. * @returns {module:utils/dom/getresizeobserver~ResizeObserver} An observer instance.
  25. */
  26. export default function getResizeObserver( callback ) {
  27. // TODO: One day, the `ResizeObserver` API will be supported in all modern web browsers.
  28. // When it happens, this module will no longer make sense and should be removed and
  29. // the native implementation should be used across the project to save bytes.
  30. // Check out https://caniuse.com/#feat=resizeobserver.
  31. if ( typeof global.window.ResizeObserver === 'function' ) {
  32. return new global.window.ResizeObserver( callback );
  33. } else {
  34. return new ResizeObserverPolyfill( callback );
  35. }
  36. }
  37. /**
  38. * A polyfill class for the native [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver).
  39. *
  40. * @private
  41. * @mixes module:utils/domemittermixin~DomEmitterMixin
  42. */
  43. class ResizeObserverPolyfill {
  44. /**
  45. * Creates an instance of the {@link module:utils/dom/getresizeobserver~ResizeObserverPolyfill} class.
  46. *
  47. * It synchronously reacts to resize of the window to check if observed elements' geometry changed.
  48. *
  49. * Additionally, the polyfilled observer uses a timeout to check if observed elements' geometry has changed
  50. * in some other way (dynamic layouts, scrollbars showing up, etc.), so its response can also be asynchronous.
  51. *
  52. * @param {Function} callback A function called when any observed element was resized. Refer to the
  53. * native [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) API to
  54. * learn more.
  55. */
  56. constructor( callback ) {
  57. /**
  58. * A function called when any observed {@link #_elements element} was resized.
  59. *
  60. * @readonly
  61. * @protected
  62. * @member {Function}
  63. */
  64. this._callback = callback;
  65. /**
  66. * DOM elements currently observed by the observer instance.
  67. *
  68. * @readonly
  69. * @protected
  70. * @member {Set}
  71. */
  72. this._elements = new Set();
  73. /**
  74. * Cached DOM {@link #_elements elements} bounding rects to compare to upon the next check.
  75. *
  76. * @readonly
  77. * @protected
  78. * @member {Map.<HTMLElement,module:utils/dom/rect~Rect>}
  79. */
  80. this._previousRects = new Map();
  81. /**
  82. * An UID of the current timeout upon which the observed elements rects
  83. * will be compared to the {@link #_previousRects previous rects} from the past.
  84. *
  85. * @readonly
  86. * @protected
  87. * @member {Map.<HTMLElement,module:utils/dom/rect~Rect>}
  88. */
  89. this._periodicCheckTimeout = null;
  90. }
  91. /**
  92. * Starts observing a DOM element.
  93. *
  94. * Learn more in the
  95. * [native method documentation](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/observe).
  96. *
  97. * @param {HTMLElement} element
  98. */
  99. observe( element ) {
  100. this._elements.add( element );
  101. if ( this._elements.size === 1 ) {
  102. this._startPeriodicCheck();
  103. }
  104. }
  105. /**
  106. * Stops observing a DOM element.
  107. *
  108. * Learn more in the
  109. * [native method documentation](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/unobserve).
  110. *
  111. * @param {HTMLElement} element
  112. */
  113. unobserve( element ) {
  114. this._elements.delete( element );
  115. this._previousRects.delete( element );
  116. if ( !this._elements.size ) {
  117. this._stopPeriodicCheck();
  118. }
  119. }
  120. /**
  121. * Stops observing all observed DOM elements.
  122. *
  123. * Learn more in the
  124. * [native method documentation](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/disconnect).
  125. *
  126. * @param {HTMLElement} element
  127. */
  128. disconnect() {
  129. this._elements.forEach( element => this.unobserve( element ) );
  130. }
  131. /**
  132. * When called, the observer calls the {@link #_callback resize callback} for all observed
  133. * {@link #_elements elements} but also starts checking periodically for changes in the elements' geometry.
  134. * If some are detected, {@link #_callback resize callback} is called for relevant elements that were resized.
  135. *
  136. * @protected
  137. */
  138. _startPeriodicCheck() {
  139. const periodicCheck = () => {
  140. this._checkElementRectsAndExecuteCallback();
  141. this._periodicCheckTimeout = setTimeout( periodicCheck, RESIZE_CHECK_INTERVAL );
  142. };
  143. this.listenTo( global.window, 'resize', () => {
  144. this._checkElementRectsAndExecuteCallback();
  145. } );
  146. periodicCheck();
  147. }
  148. /**
  149. * Stops checking for changes in all observed {@link #_elements elements} geometry.
  150. *
  151. * @protected
  152. */
  153. _stopPeriodicCheck() {
  154. clearTimeout( this._periodicCheckTimeout );
  155. this.stopListening();
  156. this._previousRects.clear();
  157. }
  158. /**
  159. * Checks if the geometry of any of the {@link #_elements element} has changed. If so, executes
  160. * the {@link #_callback resize callback} with element geometry data.
  161. *
  162. * @protected
  163. */
  164. _checkElementRectsAndExecuteCallback() {
  165. const entries = [];
  166. for ( const element of this._elements ) {
  167. if ( this._hasRectChanged( element ) ) {
  168. entries.push( {
  169. target: element,
  170. contentRect: this._previousRects.get( element )
  171. } );
  172. }
  173. }
  174. if ( entries.length ) {
  175. this._callback( entries );
  176. }
  177. }
  178. /**
  179. * Compares the DOM element geometry to the {@link #_previousRects cached geometry} from the past.
  180. * Returns `true` if geometry has changed or the element is checked for the first time.
  181. *
  182. * @protected
  183. * @param {HTMLElement} element
  184. * @returns {Boolean}
  185. */
  186. _hasRectChanged( element ) {
  187. if ( !element.ownerDocument.body.contains( element ) ) {
  188. return false;
  189. }
  190. const currentRect = new Rect( element );
  191. const previousRect = this._previousRects.get( element );
  192. // The first check should always yield true despite no Previous rect to compare to.
  193. // The native ResizeObserver does that and... that makes sense. Sort of.
  194. const hasChanged = !previousRect || !previousRect.isEqual( currentRect );
  195. this._previousRects.set( element, currentRect );
  196. return hasChanged;
  197. }
  198. }
  199. mix( ResizeObserverPolyfill, DomEmitterMixin );
  200. /**
  201. * A resize observer object (either native or {@link module:utils/dom/getresizeobserver~getResizeObserver polyfilled})
  202. * offering the [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) API.
  203. *
  204. * @typedef {Function} module:utils/dom/getresizeobserver~ResizeObserver
  205. */