Bladeren bron

Merge pull request #800 from ckeditor/t/791

Feature: Introduced `view.Document#selectionChangeDone` event. Closes #791.
Piotrek Koszuliński 8 jaren geleden
bovenliggende
commit
ce1f92ac5b

+ 33 - 4
packages/ckeditor5-engine/src/view/observer/fakeselectionobserver.js

@@ -10,6 +10,7 @@
 import Observer from './observer';
 import ViewSelection from '../selection';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
+import debounce from '@ckeditor/ckeditor5-utils/src/lib/lodash/debounce';
 
 /**
  * Fake selection observer class. If view selection is fake it is placed in dummy DOM container. This observer listens
@@ -28,6 +29,15 @@ export default class FakeSelectionObserver extends Observer {
 	 */
 	constructor( document ) {
 		super( document );
+
+		/**
+		 * Fires debounced event `selectionChangeDone`. It uses `lodash#debounce` method to delay function call.
+		 *
+		 * @private
+		 * @param {Object} data Selection change data.
+		 * @method #_fireSelectionChangeDoneDebounced
+		 */
+		this._fireSelectionChangeDoneDebounced = debounce( data => this.document.fire( 'selectionChangeDone', data ), 200 );
 	}
 
 	/**
@@ -49,15 +59,26 @@ export default class FakeSelectionObserver extends Observer {
 	}
 
 	/**
+	 * @inheritDoc
+	 */
+	destroy() {
+		super.destroy();
+
+		this._fireSelectionChangeDoneDebounced.cancel();
+	}
+
+	/**
 	 * Handles collapsing view selection according to given key code. If left or up key is provided - new selection will be
 	 * collapsed to left. If right or down key is pressed - new selection will be collapsed to right.
 	 *
-	 * This method fires {@link module:engine/view/document~Document#event:selectionChange} event imitating behaviour of
+	 * This method fires {@link module:engine/view/document~Document#event:selectionChange} and
+	 * {@link module:engine/view/document~Document#event:selectionChangeDone} events imitating behaviour of
 	 * {@link module:engine/view/observer/selectionobserver~SelectionObserver}.
 	 *
 	 * @private
 	 * @param {Number} keyCode
 	 * @fires module:engine/view/document~Document#event:selectionChange
+	 * @fires module:engine/view/document~Document#event:selectionChangeDone
 	 */
 	_handleSelectionMove( keyCode ) {
 		const selection = this.document.selection;
@@ -74,12 +95,20 @@ export default class FakeSelectionObserver extends Observer {
 			newSelection.collapseToEnd();
 		}
 
-		// Fire dummy selection change event.
-		this.document.fire( 'selectionChange', {
+		const data = {
 			oldSelection: selection,
 			newSelection: newSelection,
 			domSelection: null
-		} );
+		};
+
+		// Fire dummy selection change event.
+		this.document.fire( 'selectionChange', data );
+
+		// Call` #_fireSelectionChangeDoneDebounced` every time when `selectionChange` event is fired.
+		// This function is debounced what means that `selectionChangeDone` event will be fired only when
+		// defined int the function time will elapse since the last time the function was called.
+		// So `selectionChangeDone` will be fired when selection will stop changing.
+		this._fireSelectionChangeDoneDebounced( data );
 	}
 }
 

+ 42 - 4
packages/ckeditor5-engine/src/view/observer/selectionobserver.js

@@ -12,6 +12,7 @@
 import Observer from './observer';
 import MutationObserver from './mutationobserver';
 import log from '@ckeditor/ckeditor5-utils/src/log';
+import debounce from '@ckeditor/ckeditor5-utils/src/lib/lodash/debounce';
 
 /**
  * Selection observer class observes selection changes in the document. If selection changes on the document this
@@ -73,6 +74,15 @@ export default class SelectionObserver extends Observer {
 		 */
 		this._documents = new WeakSet();
 
+		/**
+		 * Fires debounced event `selectionChangeDone`. It uses `lodash#debounce` method to delay function call.
+		 *
+		 * @private
+		 * @param {Object} data Selection change data.
+		 * @method #_fireSelectionChangeDoneDebounced
+		 */
+		this._fireSelectionChangeDoneDebounced = debounce( data => this.document.fire( 'selectionChangeDone', data ), 200 );
+
 		this._clearInfiniteLoopInterval = setInterval( () => this._clearInfiniteLoop(), 2000 );
 
 		/**
@@ -122,11 +132,13 @@ export default class SelectionObserver extends Observer {
 		super.destroy();
 
 		clearInterval( this._clearInfiniteLoopInterval );
+		this._fireSelectionChangeDoneDebounced.cancel();
 	}
 
 	/**
 	 * Selection change listener. {@link module:engine/view/observer/mutationobserver~MutationObserver#flush Flush} mutations, check if
-	 * selection changes and fires {@link module:engine/view/document~Document#event:selectionChange} event.
+	 * selection changes and fires {@link module:engine/view/document~Document#event:selectionChange} event on every change
+	 * and {@link module:engine/view/document~Document#event:selectionChangeDone} when selection stop changing.
 	 *
 	 * @private
 	 * @param {Document} domDocument DOM document.
@@ -163,12 +175,20 @@ export default class SelectionObserver extends Observer {
 			return;
 		}
 
-		// Should be fired only when selection change was the only document change.
-		this.document.fire( 'selectionChange', {
+		const data = {
 			oldSelection: this.selection,
 			newSelection: newViewSelection,
 			domSelection: domSelection
-		} );
+		};
+
+		// Should be fired only when selection change was the only document change.
+		this.document.fire( 'selectionChange', data );
+
+		// Call` #_fireSelectionChangeDoneDebounced` every time when `selectionChange` event is fired.
+		// This function is debounced what means that `selectionChangeDone` event will be fired only when
+		// defined int the function time will elapse since the last time the function was called.
+		// So `selectionChangeDone` will be fired when selection will stop changing.
+		this._fireSelectionChangeDoneDebounced( data );
 	}
 
 	/**
@@ -232,3 +252,21 @@ export default class SelectionObserver extends Observer {
  * @param {module:engine/view/selection~Selection} data.newSelection New View selection which is converted DOM selection.
  * @param {Selection} data.domSelection Native DOM selection.
  */
+
+/**
+ * Fired when selection stops changing.
+ *
+ * Introduced by {@link module:engine/view/observer/selectionobserver~SelectionObserver}.
+ *
+ * Note that because {@link module:engine/view/observer/selectionobserver~SelectionObserver} is attached by the
+ * {@link module:engine/view/document~Document}
+ * this event is available by default.
+ *
+ * @see module:engine/view/observer/selectionobserver~SelectionObserver
+ * @event module:engine/view/document~Document#event:selectionChangeDone
+ * @param {Object} data
+ * @param {module:engine/view/selection~Selection} data.oldSelection Old View selection which is
+ * {@link module:engine/view/document~Document#selection}.
+ * @param {module:engine/view/selection~Selection} data.newSelection New View selection which is converted DOM selection.
+ * @param {Selection} data.domSelection Native DOM selection.
+ */

+ 6 - 1
packages/ckeditor5-engine/tests/view/manual/selectionobserver.js

@@ -16,7 +16,12 @@ setData( viewDocument,
 	'<container:p>bom</container:p>' );
 
 viewDocument.on( 'selectionChange', ( evt, data ) => {
-	console.log( data );
+	console.log( 'selectionChange', data );
+	viewDocument.selection.setTo( data.newSelection );
+} );
+
+viewDocument.on( 'selectionChangeDone', ( evt, data ) => {
+	console.log( '%c selectionChangeDone ', 'background: #222; color: #bada55', data );
 	viewDocument.selection.setTo( data.newSelection );
 } );
 

+ 3 - 1
packages/ckeditor5-engine/tests/view/manual/selectionobserver.md

@@ -1,6 +1,8 @@
 ### Move selection
 
-Selection should be moved and event should be fired (see console),
+Selection should be moved and events should be fired on console:
+- `selectionChange` event should be fired after every change
+- `selectionChangeDone` event should be fired when selection stop changing
 
 ### Change document
 

+ 67 - 8
packages/ckeditor5-engine/tests/view/observer/fakeselectionobserver.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-/* globals document */
+/* globals document, setTimeout */
 
 import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
 import FakeSelectionObserver from '../../../src/view/observer/fakeselectionobserver';
@@ -100,6 +100,58 @@ describe( 'FakeSelectionObserver', () => {
 		);
 	} );
 
+	it( 'should fire `selectionChangeDone` event after selection stop changing', ( done ) => {
+		const spy = sinon.spy();
+
+		viewDocument.on( 'selectionChangeDone', spy );
+
+		// Change selection.
+		changeFakeSelectionPressing( keyCodes.arrowdown );
+
+		// Wait 100ms.
+		// Note that it's difficult/not possible to test lodash#debounce with sinon fake timers.
+		// See: https://github.com/lodash/lodash/issues/304
+		setTimeout( () => {
+			// Check if spy was called.
+			expect( spy.notCalled ).to.true;
+
+			// Change selection one more time.
+			changeFakeSelectionPressing( keyCodes.arrowdown );
+
+			// Wait 210ms (debounced function should be called).
+			setTimeout( () => {
+				expect( spy.calledOnce ).to.true;
+
+				done();
+			}, 210 );
+		}, 100 );
+	} );
+
+	it( 'should not fire `selectionChangeDone` event when observer will be destroyed', ( done ) => {
+		const spy = sinon.spy();
+
+		viewDocument.on( 'selectionChangeDone', spy );
+
+		// Change selection.
+		changeFakeSelectionPressing( keyCodes.arrowdown );
+
+		// Wait 100ms.
+		// Note that it's difficult/not possible to test lodash#debounce with sinon fake timers.
+		// See: https://github.com/lodash/lodash/issues/304
+		setTimeout( () => {
+			// And destroy observer.
+			observer.destroy();
+
+			// Wait another 110ms.
+			setTimeout( () => {
+				// Check that event won't be called.
+				expect( spy.notCalled ).to.true;
+
+				done();
+			}, 110 );
+		}, 100 );
+	} );
+
 	// Checks if preventDefault method was called by FakeSelectionObserver for specified key code.
 	//
 	// @param {Number} keyCode
@@ -140,14 +192,21 @@ describe( 'FakeSelectionObserver', () => {
 			} );
 
 			setData( viewDocument, initialData );
-			viewDocument.selection.setFake();
+			changeFakeSelectionPressing( keyCode );
+		} );
+	}
 
-			const data = {
-				keyCode,
-				preventDefault: sinon.spy(),
-			};
+	// Sets fake selection to the document and fire `keydown` event what cause `selectionChange` event.
+	//
+	// @param {Number} keyCode
+	function changeFakeSelectionPressing( keyCode ) {
+		viewDocument.selection.setFake();
 
-			viewDocument.fire( 'keydown', new DomEventData( viewDocument, { target: document.body }, data ) );
-		} );
+		const data = {
+			keyCode,
+			preventDefault: sinon.spy(),
+		};
+
+		viewDocument.fire( 'keydown', new DomEventData( viewDocument, { target: document.body }, data ) );
 	}
 } );

+ 69 - 0
packages/ckeditor5-engine/tests/view/observer/selectionobserver.js

@@ -226,6 +226,75 @@ describe( 'SelectionObserver', () => {
 			}, 200 );
 		}, 400 );
 	} );
+
+	it( 'should fire `selectionChangeDone` event after selection stop changing', ( done ) => {
+		const spy = sinon.spy();
+
+		viewDocument.on( 'selectionChangeDone', spy );
+
+		// Change selection.
+		changeDomSelection();
+
+		// Wait 100ms.
+		// Note that it's difficult/not possible to test lodash#debounce with sinon fake timers.
+		// See: https://github.com/lodash/lodash/issues/304
+		setTimeout( () => {
+			// Check if spy was called.
+			expect( spy.notCalled ).to.true;
+
+			// Change selection one more time.
+			changeDomSelection();
+
+			// Wait 210ms (debounced function should be called).
+			setTimeout( () => {
+				const data = spy.firstCall.args[ 1 ];
+
+				expect( spy.calledOnce ).to.true;
+				expect( data ).to.have.property( 'domSelection' ).to.equal( document.getSelection() );
+
+				expect( data ).to.have.property( 'oldSelection' ).to.instanceof( ViewSelection );
+				expect( data.oldSelection.rangeCount ).to.equal( 0 );
+
+				expect( data ).to.have.property( 'newSelection' ).to.instanceof( ViewSelection );
+				expect( data.newSelection.rangeCount ).to.equal( 1 );
+
+				const newViewRange = data.newSelection.getFirstRange();
+				const viewFoo = viewDocument.getRoot().getChild( 0 ).getChild( 0 );
+
+				expect( newViewRange.start.parent ).to.equal( viewFoo );
+				expect( newViewRange.start.offset ).to.equal( 3 );
+				expect( newViewRange.end.parent ).to.equal( viewFoo );
+				expect( newViewRange.end.offset ).to.equal( 3 );
+
+				done();
+			}, 210 );
+		}, 100 );
+	} );
+
+	it( 'should not fire `selectionChangeDone` event when observer will be destroyed', ( done ) => {
+		const spy = sinon.spy();
+
+		viewDocument.on( 'selectionChangeDone', spy );
+
+		// Change selection.
+		changeDomSelection();
+
+		// Wait 100ms.
+		// Note that it's difficult/not possible to test lodash#debounce with sinon fake timers.
+		// See: https://github.com/lodash/lodash/issues/304
+		setTimeout( () => {
+			// And destroy observer.
+			selectionObserver.destroy();
+
+			// Wait another 110ms.
+			setTimeout( () => {
+				// Check that event won't be called.
+				expect( spy.notCalled ).to.true;
+
+				done();
+			}, 110 );
+		}, 100 );
+	} );
 } );
 
 function changeDomSelection() {