Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Added beforeShow event to the ContextualToolbar plugin #226

Merged
merged 11 commits into from
May 10, 2017
72 changes: 61 additions & 11 deletions src/toolbar/contextual/contextualtoolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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._showPanelPromiseResolver = spy();

// Attach lifecycle actions.
this._handleSelectionChange();
this._handleFocusChange();
Expand Down Expand Up @@ -128,11 +138,25 @@ export default class ContextualToolbar extends Plugin {
this.listenTo( this, '_selectionChangeDebounced', () => this._showPanel() );
}

/**
* 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._showPanelPromiseResolver();
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;
Expand All @@ -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._showPanelPromiseResolver = 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 external code then panel will be displayed.
this.listenTo( this, 'beforeShow', ( evt ) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't once() mean that evt.off() at the end is gone?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

once() uses on() but on() is not cleared on stopListening() (https://github.com/ckeditor/ckeditor5-utils/issues/144). That's why I used evt.off().

// 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'
} )
);

// Stop listening on `beforeShow` event.
evt.off();
} );
}, { priority: 'lowest' } );

// Fire this event to inform interested plugins that `ContextualToolbar` is going to be shown.
this.fire( 'beforeShow' );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we pass the stopping function along this event to avoid the additional stop method?


return showPromise;
}

/**
Expand Down Expand Up @@ -210,6 +252,14 @@ export default class ContextualToolbar extends Plugin {
super.destroy();
}

/**
* This event is fired just before balloon shows.
* It makes possible to listen to this event by an external code 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`.
Expand Down
64 changes: 63 additions & 1 deletion tests/toolbar/contextual/contextualtoolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe( 'ContextualToolbar', () => {

afterEach( () => {
sandbox.restore();
editorElement.remove();

return editor.destroy();
} );
Expand Down Expand Up @@ -234,7 +235,7 @@ describe( 'ContextualToolbar', () => {
let removeBalloonSpy;

beforeEach( () => {
removeBalloonSpy = sandbox.spy( balloon, 'remove' );
removeBalloonSpy = sandbox.stub( balloon, 'remove', () => {} );
editor.editing.view.isFocused = true;
} );

Expand Down Expand Up @@ -388,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` is 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;
Expand Down