Browse Source

Added beforeShow event.

Oskar Wróbel 8 years ago
parent
commit
4cc2ab7a71

+ 61 - 11
packages/ckeditor5-ui/src/toolbar/contextual/contextualtoolbar.js

@@ -12,6 +12,7 @@ import ContextualBalloon from '../../panel/balloon/contextualballoon';
 import ToolbarView from '../toolbarview';
 import BalloonPanelView from '../../panel/balloon/balloonpanelview.js';
 import debounce from '@ckeditor/ckeditor5-utils/src/lib/lodash/debounce';
+import spy from '@ckeditor/ckeditor5-utils/src/spy';
 
 const defaultPositions = BalloonPanelView.defaultPositions;
 
@@ -67,6 +68,15 @@ export default class ContextualToolbar extends Plugin {
 		 */
 		this._fireSelectionChangeDebounced = debounce( () => this.fire( '_selectionChangeDebounced' ), 200 );
 
+		/**
+		 * Resolve {@link #_showPanel} promise. When panel is prevented of being shown we need to resolve
+		 * this promise otherwise will be pending forever.
+		 *
+		 * @private
+		 * @member {Function}
+		 */
+		this._stopShowPanel = spy();
+
 		// Attach lifecycle actions.
 		this._handleSelectionChange();
 		this._handleFocusChange();
@@ -129,10 +139,24 @@ export default class ContextualToolbar extends Plugin {
 	}
 
 	/**
+	 * Prevents panel of being displayed. This should be used together with {@link #event:beforeShow}` event.
+	 *
+	 * @param {module:utils/eventinfo~EventInfo} evt Event object provided by the {@link #event:beforeShow} event.
+	 */
+	stop( evt ) {
+		evt.stop();
+		this._stopShowPanel();
+		this.stopListening( this, 'beforeShow' );
+	}
+
+	/**
 	 * Adds panel view to the {@link: #_balloon} and attaches panel to the selection.
 	 *
+	 * Fires {@link #event:beforeShow} event just before displaying the panel.
+	 *
 	 * @protected
-	 * @return {Promise} A promise resolved when the {@link #toolbarView} {@link module:ui/view~View#init} is done.
+	 * @return {Promise} A promise resolved when the {@link #toolbarView} {@link module:ui/view~View#init} is done
+	 * or rejected when panel will be prevented of being displayed.
 	 */
 	_showPanel() {
 		const editingView = this.editor.editing.view;
@@ -147,17 +171,35 @@ export default class ContextualToolbar extends Plugin {
 			return Promise.resolve();
 		}
 
-		// Update panel position when selection changes while balloon will be opened (by a collaboration).
-		this.listenTo( this.editor.editing.view, 'render', () => {
-			this._balloon.updatePosition( this._getBalloonPositionData() );
-		} );
+		const showPromise = new Promise( ( resolve ) => {
+			this._stopShowPanel = resolve;
 
-		// Add panel to the common editor contextual balloon.
-		return this._balloon.add( {
-			view: this.toolbarView,
-			position: this._getBalloonPositionData(),
-			balloonClassName: 'ck-toolbar-container'
-		} );
+			// If `beforeShow` event is not stopped by any other plugin we can display toolbar panel.
+			this.listenTo( this, 'beforeShow', ( evt ) => {
+				// Update panel position when selection changes while balloon will be opened
+				// (by an external document changes).
+				this.listenTo( editingView, 'render', () => {
+					this._balloon.updatePosition( this._getBalloonPositionData() );
+				} );
+
+				resolve(
+					// Add panel to the common editor contextual balloon.
+					this._balloon.add( {
+						view: this.toolbarView,
+						position: this._getBalloonPositionData(),
+						balloonClassName: 'ck-toolbar-container'
+					} )
+				);
+
+				// Clean up listener to be sure that won't be duplicated in the future.
+				evt.off();
+			} );
+		}, { priority: 'lowest' } );
+
+		// Fire this event to inform interested plugins that `ContextualToolbar` is going to be shown.
+		this.fire( 'beforeShow' );
+
+		return showPromise;
 	}
 
 	/**
@@ -211,6 +253,14 @@ export default class ContextualToolbar extends Plugin {
 	}
 
 	/**
+	 * This event is fired just before balloon shows.
+	 * It makes possible to listen to this event by the other plugins and prevent
+	 * ContextualToolbar of being displayed by calling {@link #stop} method.
+	 *
+	 * @event beforeShow
+	 */
+
+	/**
 	 * This is internal plugin event which is fired 200 ms after model selection last change.
 	 * This is to makes easy test debounced action without need to use `setTimeout`.
 	 *

+ 61 - 0
packages/ckeditor5-ui/tests/toolbar/contextual/contextualtoolbar.js

@@ -389,6 +389,67 @@ describe( 'ContextualToolbar', () => {
 		} );
 	} );
 
+	describe( 'beforeShow event', () => {
+		it( 'should fire `beforeShow` event just before panel shows', () => {
+			const spy = sinon.spy();
+
+			contextualToolbar.on( 'beforeShow', spy );
+			setData( editor.document, '<paragraph>b[a]r</paragraph>' );
+
+			const promise = contextualToolbar._showPanel();
+
+			sinon.assert.calledOnce( spy );
+
+			return promise;
+		} );
+
+		it( 'should not show panel when `beforeShow` event will be stopped', () => {
+			const balloonAddSpy = sandbox.spy( balloon, 'add' );
+
+			setData( editor.document, '<paragraph>b[a]r</paragraph>' );
+
+			contextualToolbar.on( 'beforeShow', ( evt ) => {
+				contextualToolbar.stop( evt );
+			} );
+
+			return contextualToolbar._showPanel().then( () => {
+				sinon.assert.notCalled( balloonAddSpy );
+			} );
+		} );
+	} );
+
+	describe( 'stop', () => {
+		it( 'should stop `beforeShow` event', () => {
+			const evtMock = {
+				stop: sinon.spy()
+			};
+
+			contextualToolbar.stop( evtMock );
+
+			sinon.assert.calledOnce( evtMock.stop );
+		} );
+
+		it( 'should resolve promise and clean up listener', () => {
+			const balloonAddSpy = sandbox.spy( balloon, 'add' );
+
+			setData( editor.document, '<paragraph>b[a]r</paragraph>' );
+
+			contextualToolbar.once( 'beforeShow', ( evt ) => {
+				contextualToolbar.stop( evt );
+			} );
+
+			return contextualToolbar._showPanel()
+				.then( () => contextualToolbar._hidePanel() )
+				.then( () => contextualToolbar._showPanel() )
+				.then( () => contextualToolbar._hidePanel() )
+				.then( () => contextualToolbar._showPanel() )
+				.then( () => {
+					// Called twice but _showPanel was called thrice.
+					sinon.assert.calledTwice( balloonAddSpy );
+				} );
+		} );
+	} );
+
 	function stubSelectionRect( forwardSelectionRect, backwardSelectionRect ) {
 		const editingView = editor.editing.view;
 		const originalViewRangeToDom = editingView.domConverter.viewRangeToDom;