diff --git a/src/view/document.js b/src/view/document.js index 370d5551a..116fed298 100644 --- a/src/view/document.js +++ b/src/view/document.js @@ -140,6 +140,12 @@ export default class Document { } } while ( wasFixed ); } + + /** + * @TODO. + * + * @event module:engine/view/document~Document#event:layoutChanged + */ } mix( Document, ObservableMixin ); diff --git a/src/view/view.js b/src/view/view.js index 01682e03f..adf7ec5c9 100644 --- a/src/view/view.js +++ b/src/view/view.js @@ -22,6 +22,7 @@ import CompositionObserver from './observer/compositionobserver'; import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; import log from '@ckeditor/ckeditor5-utils/src/log'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; +import throttle from '@ckeditor/ckeditor5-utils/src/lib/lodash/throttle'; import { scrollViewportToShowTarget } from '@ckeditor/ckeditor5-utils/src/dom/scroll'; import { injectUiElementHandling } from './uielement'; import { injectQuirksHandling } from './filler'; @@ -134,6 +135,14 @@ export default class View { */ this._writer = new Writer( this.document ); + /** + * Fires throttled {@link module:engine/view/document~Document#event:layoutChanged} event. + * + * @protected + * @type {Function} + */ + this._throttledLayoutChange = throttle( () => this.document.fire( 'layoutChanged' ), 200 ); + // Add default observers. this.addObserver( MutationObserver ); this.addObserver( SelectionObserver ); @@ -149,6 +158,9 @@ export default class View { // Use 'normal' priority so that rendering is performed as first when using that priority. this.on( 'render', () => { this._render(); + + // Informs that layout has changed after render. + this._throttledLayoutChange(); } ); } @@ -378,6 +390,7 @@ export default class View { } this.stopListening(); + this._throttledLayoutChange.cancel(); } /** diff --git a/tests/view/view/view.js b/tests/view/view/view.js index 4d287bde1..bc649f6b6 100644 --- a/tests/view/view/view.js +++ b/tests/view/view/view.js @@ -45,6 +45,7 @@ describe( 'view', () => { this.enable = sinon.spy(); this.disable = sinon.spy(); this.observe = sinon.spy(); + this.destroy = sinon.spy(); } }; @@ -379,6 +380,26 @@ describe( 'view', () => { sinon.assert.callOrder( observerMock.disable, renderStub, observerMock.enable ); } ); + + it( 'should fire throttled view.document.layoutChanged event after each render', () => { + const spy = sinon.spy(); + + view.document.on( 'layoutChanged', spy ); + + view.render(); + + sinon.assert.calledOnce( spy ); + + view.render(); + + // Still once because of throttle. + sinon.assert.calledOnce( spy ); + + view._throttledLayoutChange.flush(); + + // Twice after flushing throttled event. + sinon.assert.calledTwice( spy ); + } ); } ); describe( 'view and DOM integration', () => { @@ -588,6 +609,36 @@ describe( 'view', () => { } ); } ); + describe( 'destroy()', () => { + it( 'should destroy all observers', () => { + const observerMock = view.addObserver( ObserverMock ); + + view.destroy(); + + sinon.assert.calledOnce( observerMock.destroy ); + } ); + + it( 'should cancel throttled layoutChanged event', () => { + const view = new View(); + const spy = sinon.spy(); + + view.document.on( 'layoutChanged', spy ); + + view.render(); + + sinon.assert.calledOnce( spy ); + + view.render(); + + view.destroy(); + + view._throttledLayoutChange.flush(); + + // Still once because second call was canceled. + sinon.assert.calledOnce( spy ); + } ); + } ); + function createRoot( name, rootName, viewDoc ) { const viewRoot = new RootEditableElement( name );