| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384 |
- /**
- * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
- /**
- * @module utils/dom/resizeobserver
- */
- /* globals setTimeout, clearTimeout */
- import mix from '../mix';
- import global from './global';
- import Rect from './rect';
- import DomEmitterMixin from './emittermixin';
- const RESIZE_CHECK_INTERVAL = 100;
- /**
- * A helper class which instances allow performing custom actions when native DOM elements are resized.
- *
- * const editableElement = editor.editing.view.getDomRoot();
- *
- * const observer = new ResizeObserver( editableElement, entry => {
- * console.log( 'The editable element has been resized in DOM.' );
- * console.log( entry.target ); // -> editableElement
- * console.log( entry.contentRect.width ); // -> e.g. '423px'
- * } );
- *
- * By default, it uses the [native DOM resize observer](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
- * under the hood and in browsers that do not support the native API yet, a polyfilled observer is
- * used instead.
- */
- export default class ResizeObserver {
- /**
- * Creates an instance of the `ResizeObserver` class.
- *
- * @param {HTMLElement} element A DOM element that is to be observed for resizing. Note that
- * the element must be visible (i.e. not detached from DOM) for the observer to work.
- * @param {Function} callback A function called when the observed element was resized. It passes
- * the [`ResizeObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry)
- * object with information about the resize event.
- */
- constructor( element, callback ) {
- // **Note**: For the maximum performance, this class ensures only a single instance of the native
- // (or polyfilled) observer is used no matter how many instances of this class were created.
- if ( !ResizeObserver._observerInstance ) {
- ResizeObserver._createObserver();
- }
- /**
- * The element observer by this observer.
- *
- * @readonly
- * @private
- * @member {HTMLElement}
- */
- this._element = element;
- /**
- * The callback executed each time {@link #_element} is resized.
- *
- * @readonly
- * @private
- * @member {Function}
- */
- this._callback = callback;
- ResizeObserver._addElementCallback( element, callback );
- ResizeObserver._observerInstance.observe( element );
- }
- /**
- * Destroys the observer which disables the `callback` passed to the {@link #constructor}.
- */
- destroy() {
- ResizeObserver._deleteElementCallback( this._element, this._callback );
- }
- /**
- * Registers a new resize callback for the DOM element.
- *
- * @private
- * @static
- * @param {HTMLElement} element
- * @param {Function} callback
- */
- static _addElementCallback( element, callback ) {
- if ( !ResizeObserver._elementCallbacks ) {
- ResizeObserver._elementCallbacks = new Map();
- }
- let callbacks = ResizeObserver._elementCallbacks.get( element );
- if ( !callbacks ) {
- callbacks = new Set();
- ResizeObserver._elementCallbacks.set( element, callbacks );
- }
- callbacks.add( callback );
- }
- /**
- * Removes a resize callback from the DOM element. If no callbacks are left
- * for the element, it removes the element from the native observer.
- *
- * @private
- * @static
- * @param {HTMLElement} element
- * @param {Function} callback
- */
- static _deleteElementCallback( element, callback ) {
- const callbacks = ResizeObserver._getElementCallbacks( element );
- // Remove the element callback. Check if exist first in case someone
- // called destroy() twice.
- if ( callbacks ) {
- callbacks.delete( callback );
- // If no callbacks left for the element, also remove the element.
- if ( !callbacks.size ) {
- ResizeObserver._elementCallbacks.delete( element );
- ResizeObserver._observerInstance.unobserve( element );
- }
- }
- if ( ResizeObserver._elementCallbacks && !ResizeObserver._elementCallbacks.size ) {
- ResizeObserver._observerInstance = null;
- ResizeObserver._elementCallbacks = null;
- }
- }
- /**
- * Returns are registered resize callbacks for the DOM element.
- *
- * @private
- * @static
- * @param {HTMLElement} element
- * @returns {Set.<HTMLElement>|null}
- */
- static _getElementCallbacks( element ) {
- if ( !ResizeObserver._elementCallbacks ) {
- return null;
- }
- return ResizeObserver._elementCallbacks.get( element );
- }
- /**
- * Creates the single native observer shared across all `ResizeObserver` instances.
- * If the browser does not support the native API, it creates a polyfill.
- *
- * @private
- * @static
- */
- static _createObserver() {
- let ObserverConstructor;
- // TODO: One day, the `ResizeObserver` API will be supported in all modern web browsers.
- // When it happens, this module will no longer make sense and should be removed and
- // the native implementation should be used across the project to save bytes.
- // Check out https://caniuse.com/#feat=resizeobserver.
- if ( typeof global.window.ResizeObserver === 'function' ) {
- ObserverConstructor = global.window.ResizeObserver;
- } else {
- ObserverConstructor = ResizeObserverPolyfill;
- }
- ResizeObserver._observerInstance = new ObserverConstructor( entries => {
- for ( const entry of entries ) {
- // Do not execute callbacks for elements that are invisible.
- // https://github.com/ckeditor/ckeditor5/issues/6570
- if ( !entry.target.offsetParent ) {
- continue;
- }
- const callbacks = ResizeObserver._getElementCallbacks( entry.target );
- if ( callbacks ) {
- for ( const callback of callbacks ) {
- callback( entry );
- }
- }
- }
- } );
- }
- }
- /**
- * The single native observer instance (or polyfill in browsers that do not support the API)
- * shared across all {@link module:utils/dom/resizeobserver~ResizeObserver} instances.
- *
- * @static
- * @protected
- * @readonly
- * @property {Object|null} module:utils/dom/resizeobserver~ResizeObserver#_observerInstance
- */
- ResizeObserver._observerInstance = null;
- /**
- * A mapping of native DOM elements and their callbacks shared across all
- * {@link module:utils/dom/resizeobserver~ResizeObserver} instances.
- *
- * @static
- * @private
- * @readonly
- * @property {Map.<HTMLElement,Set>|null} module:utils/dom/resizeobserver~ResizeObserver#_elementCallbacks
- */
- ResizeObserver._elementCallbacks = null;
- /**
- * A polyfill class for the native [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver).
- *
- * @private
- * @mixes module:utils/domemittermixin~DomEmitterMixin
- */
- class ResizeObserverPolyfill {
- /**
- * Creates an instance of the {@link module:utils/dom/resizeobserver~ResizeObserverPolyfill} class.
- *
- * It synchronously reacts to resize of the window to check if observed elements' geometry changed.
- *
- * Additionally, the polyfilled observer uses a timeout to check if observed elements' geometry has changed
- * in some other way (dynamic layouts, scrollbars showing up, etc.), so its response can also be asynchronous.
- *
- * @param {Function} callback A function called when any observed element was resized. Refer to the
- * native [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) API to
- * learn more.
- */
- constructor( callback ) {
- /**
- * A function called when any observed {@link #_elements element} was resized.
- *
- * @readonly
- * @protected
- * @member {Function}
- */
- this._callback = callback;
- /**
- * DOM elements currently observed by the observer instance.
- *
- * @readonly
- * @protected
- * @member {Set}
- */
- this._elements = new Set();
- /**
- * Cached DOM {@link #_elements elements} bounding rects to compare to upon the next check.
- *
- * @readonly
- * @protected
- * @member {Map.<HTMLElement,module:utils/dom/rect~Rect>}
- */
- this._previousRects = new Map();
- /**
- * An UID of the current timeout upon which the observed elements rects
- * will be compared to the {@link #_previousRects previous rects} from the past.
- *
- * @readonly
- * @protected
- * @member {Map.<HTMLElement,module:utils/dom/rect~Rect>}
- */
- this._periodicCheckTimeout = null;
- }
- /**
- * Starts observing a DOM element.
- *
- * Learn more in the
- * [native method documentation](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/observe).
- *
- * @param {HTMLElement} element
- */
- observe( element ) {
- this._elements.add( element );
- this._checkElementRectsAndExecuteCallback();
- if ( this._elements.size === 1 ) {
- this._startPeriodicCheck();
- }
- }
- /**
- * Stops observing a DOM element.
- *
- * Learn more in the
- * [native method documentation](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/unobserve).
- *
- * @param {HTMLElement} element
- */
- unobserve( element ) {
- this._elements.delete( element );
- this._previousRects.delete( element );
- if ( !this._elements.size ) {
- this._stopPeriodicCheck();
- }
- }
- /**
- * When called, the observer calls the {@link #_callback resize callback} for all observed
- * {@link #_elements elements} but also starts checking periodically for changes in the elements' geometry.
- * If some are detected, {@link #_callback resize callback} is called for relevant elements that were resized.
- *
- * @protected
- */
- _startPeriodicCheck() {
- const periodicCheck = () => {
- this._checkElementRectsAndExecuteCallback();
- this._periodicCheckTimeout = setTimeout( periodicCheck, RESIZE_CHECK_INTERVAL );
- };
- this.listenTo( global.window, 'resize', () => {
- this._checkElementRectsAndExecuteCallback();
- } );
- this._periodicCheckTimeout = setTimeout( periodicCheck, RESIZE_CHECK_INTERVAL );
- }
- /**
- * Stops checking for changes in all observed {@link #_elements elements} geometry.
- *
- * @protected
- */
- _stopPeriodicCheck() {
- clearTimeout( this._periodicCheckTimeout );
- this.stopListening();
- this._previousRects.clear();
- }
- /**
- * Checks if the geometry of any of the {@link #_elements element} has changed. If so, executes
- * the {@link #_callback resize callback} with element geometry data.
- *
- * @protected
- */
- _checkElementRectsAndExecuteCallback() {
- const entries = [];
- for ( const element of this._elements ) {
- if ( this._hasRectChanged( element ) ) {
- entries.push( {
- target: element,
- contentRect: this._previousRects.get( element )
- } );
- }
- }
- if ( entries.length ) {
- this._callback( entries );
- }
- }
- /**
- * Compares the DOM element geometry to the {@link #_previousRects cached geometry} from the past.
- * Returns `true` if geometry has changed or the element is checked for the first time.
- *
- * @protected
- * @param {HTMLElement} element
- * @returns {Boolean}
- */
- _hasRectChanged( element ) {
- if ( !element.ownerDocument.body.contains( element ) ) {
- return false;
- }
- const currentRect = new Rect( element );
- const previousRect = this._previousRects.get( element );
- // The first check should always yield true despite no Previous rect to compare to.
- // The native ResizeObserver does that and... that makes sense. Sort of.
- const hasChanged = !previousRect || !previousRect.isEqual( currentRect );
- this._previousRects.set( element, currentRect );
- return hasChanged;
- }
- }
- mix( ResizeObserverPolyfill, DomEmitterMixin );
|