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

Commit

Permalink
Merge pull request #270 from ckeditor/t/263
Browse files Browse the repository at this point in the history
Other: Implemented public `show()` and `hide()` methods in the `ContextualToolbar` plugin. Closes #263.
  • Loading branch information
Reinmar authored Jul 12, 2017
2 parents 9da5bf7 + 7a5e6de commit eb4caab
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 63 deletions.
38 changes: 17 additions & 21 deletions src/toolbar/contextual/contextualtoolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,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 Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';

/**
* The contextual toolbar.
Expand Down Expand Up @@ -104,7 +105,7 @@ export default class ContextualToolbar extends Plugin {
// Hide the panel View when editor loses focus but no the other way around.
this.listenTo( editor.ui.focusTracker, 'change:isFocused', ( evt, name, isFocused ) => {
if ( this._balloon.visibleView === this.toolbarView && !isFocused ) {
this._hidePanel();
this.hide();
}
} );
}
Expand All @@ -120,30 +121,34 @@ export default class ContextualToolbar extends Plugin {
*/
_handleSelectionChange() {
const selection = this.editor.document.selection;
const editingView = this.editor.editing.view;

this.listenTo( selection, 'change:range', ( evt, data ) => {
// When the selection is not changed by a collaboration and when is not collapsed.
if ( data.directChange || selection.isCollapsed ) {
// Hide the toolbar when the selection starts changing.
this._hidePanel();
this.hide();
}

// Fire internal `_selectionChangeDebounced` when the selection stops changing.
this._fireSelectionChangeDebounced();
} );

// Hide the toolbar when the selection stops changing.
this.listenTo( this, '_selectionChangeDebounced', () => this._showPanel() );
this.listenTo( this, '_selectionChangeDebounced', () => {
// This implementation assumes that only non–collapsed selections gets the contextual toolbar.
if ( editingView.isFocused && !editingView.selection.isCollapsed ) {
this.show();
}
} );
}

/**
* Adds panel view to the {@link: #_balloon} and attaches panel to the selection.
* Shows the toolbar and attaches it to the selection.
*
* Fires {@link #event:beforeShow} event just before displaying the panel.
*
* @protected
*/
_showPanel() {
show() {
const editingView = this.editor.editing.view;
let isStopped = false;

Expand All @@ -152,11 +157,6 @@ export default class ContextualToolbar extends Plugin {
return;
}

// This implementation assumes that only non–collapsed selections gets the contextual toolbar.
if ( !editingView.isFocused || editingView.selection.isCollapsed ) {
return;
}

// If `beforeShow` event is not stopped by any external code then panel will be displayed.
this.once( 'beforeShow', () => {
if ( isStopped ) {
Expand Down Expand Up @@ -185,11 +185,9 @@ export default class ContextualToolbar extends Plugin {
}

/**
* Removes panel from the {@link: #_balloon}.
*
* @private
* Hides the toolbar.
*/
_hidePanel() {
hide() {
if ( this._balloon.hasView( this.toolbarView ) ) {
this.stopListening( this.editor.editing.view, 'render' );
this._balloon.remove( this.toolbarView );
Expand All @@ -215,13 +213,11 @@ export default class ContextualToolbar extends Plugin {
// computed and hence, the target is defined as a function instead of a static value.
// https://github.com/ckeditor/ckeditor5-ui/issues/195
target: () => {
// getBoundingClientRect() makes no sense when the selection spans across number
// of lines of text. Using getClientRects() allows us to browse micro–ranges
// that would normally make up the bounding client rect.
const rangeRects = editingView.domConverter.viewRangeToDom( editingView.selection.getFirstRange() ).getClientRects();
const range = editingView.selection.getFirstRange();
const rangeRects = Rect.getDomRangeRects( editingView.domConverter.viewRangeToDom( range ) );

// Select the proper range rect depending on the direction of the selection.
return isBackward ? rangeRects.item( 0 ) : rangeRects.item( rangeRects.length - 1 );
return rangeRects[ isBackward ? 0 : rangeRects.length - 1 ];
},
limiter: this.editor.ui.view.editable.element,
positions: getBalloonPositions( isBackward )
Expand Down
18 changes: 17 additions & 1 deletion src/toolbar/enabletoolbarkeyboardfocus.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,18 @@
* for `options.origin`.
* @param {module:ui/toolbar/toolbarview~ToolbarView} options.toolbar A toolbar which is to gain
* focus when `Alt+F10` is pressed.
* @param {Function} [options.beforeFocus] A callback executed before the `options.toolbar` gains focus
* upon the `Alt+F10` keystroke.
* @param {Function} [options.afterBlur] A callback executed after `options.toolbar` loses focus upon
* `Esc` keystroke but before the focus goes back to `options.origin`.
*/
export default function enableToolbarKeyboardFocus( {
origin,
originKeystrokeHandler,
originFocusTracker,
toolbar
toolbar,
beforeFocus,
afterBlur
} ) {
// Because toolbar items can get focus, the overall state of the toolbar must
// also be tracked.
Expand All @@ -33,7 +39,12 @@ export default function enableToolbarKeyboardFocus( {
// Focus the toolbar on the keystroke, if not already focused.
originKeystrokeHandler.set( 'Alt+F10', ( data, cancel ) => {
if ( originFocusTracker.isFocused && !toolbar.focusTracker.isFocused ) {
if ( beforeFocus ) {
beforeFocus();
}

toolbar.focus();

cancel();
}
} );
Expand All @@ -42,6 +53,11 @@ export default function enableToolbarKeyboardFocus( {
toolbar.keystrokes.set( 'Esc', ( data, cancel ) => {
if ( toolbar.focusTracker.isFocused ) {
origin.focus();

if ( afterBlur ) {
afterBlur();
}

cancel();
}
} );
Expand Down
92 changes: 53 additions & 39 deletions tests/toolbar/contextual/contextualtoolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ describe( 'ContextualToolbar', () => {
} );
} );

describe( '_showPanel()', () => {
describe( 'show()', () => {
let balloonAddSpy, forwardSelectionRect, backwardSelectionRect;

beforeEach( () => {
Expand All @@ -127,9 +127,9 @@ describe( 'ContextualToolbar', () => {
};

backwardSelectionRect = {
top: 100,
top: 200,
height: 10,
bottom: 110,
bottom: 210,
left: 200,
width: 50,
right: 250
Expand All @@ -146,13 +146,13 @@ describe( 'ContextualToolbar', () => {

const defaultPositions = BalloonPanelView.defaultPositions;

contextualToolbar._showPanel();
contextualToolbar.show();

sinon.assert.calledWithExactly( balloonAddSpy, {
sinon.assert.calledWith( balloonAddSpy, {
view: contextualToolbar.toolbarView,
balloonClassName: 'ck-toolbar-container ck-editor-toolbar-container',
position: {
target: sinon.match( value => value() == backwardSelectionRect ),
target: sinon.match.func,
limiter: editor.ui.view.editable.element,
positions: [
defaultPositions.southEastArrowNorth,
Expand All @@ -164,20 +164,22 @@ describe( 'ContextualToolbar', () => {
]
}
} );

expect( balloonAddSpy.firstCall.args[ 0 ].position.target() ).to.deep.equal( backwardSelectionRect );
} );

it( 'should add #toolbarView to the #_balloon and attach the #_balloon to the selection for the backward selection', () => {
setData( editor.document, '<paragraph>b[a]r</paragraph>', { lastRangeBackward: true } );

const defaultPositions = BalloonPanelView.defaultPositions;

contextualToolbar._showPanel();
contextualToolbar.show();

sinon.assert.calledWithExactly( balloonAddSpy, {
view: contextualToolbar.toolbarView,
balloonClassName: 'ck-toolbar-container ck-editor-toolbar-container',
position: {
target: sinon.match( value => value() == forwardSelectionRect ),
target: sinon.match.func,
limiter: editor.ui.view.editable.element,
positions: [
defaultPositions.northWestArrowSouth,
Expand All @@ -189,6 +191,8 @@ describe( 'ContextualToolbar', () => {
]
}
} );

expect( balloonAddSpy.firstCall.args[ 0 ].position.target() ).to.deep.equal( forwardSelectionRect );
} );

it( 'should update balloon position on ViewDocument#render event while balloon is added to the #_balloon', () => {
Expand All @@ -198,7 +202,7 @@ describe( 'ContextualToolbar', () => {

editor.editing.view.fire( 'render' );

contextualToolbar._showPanel();
contextualToolbar.show();
sinon.assert.notCalled( spy );

editor.editing.view.fire( 'render' );
Expand All @@ -208,28 +212,44 @@ describe( 'ContextualToolbar', () => {
it( 'should not add #toolbarView to the #_balloon more than once', () => {
setData( editor.document, '<paragraph>b[a]r</paragraph>' );

contextualToolbar._showPanel();
contextualToolbar._showPanel();
contextualToolbar.show();
contextualToolbar.show();
sinon.assert.calledOnce( balloonAddSpy );
} );

it( 'should not add #toolbarView to the #_balloon when editor is not focused', () => {
setData( editor.document, '<paragraph>b[a]r</paragraph>' );
editor.editing.view.isFocused = false;
describe( 'on #_selectionChangeDebounced event', () => {
let showSpy;

contextualToolbar._showPanel();
sinon.assert.notCalled( balloonAddSpy );
} );
beforeEach( () => {
showSpy = sinon.spy( contextualToolbar, 'show' );
} );

it( 'should not add #toolbarView to the #_balloon when selection is collapsed', () => {
setData( editor.document, '<paragraph>b[]ar</paragraph>' );
it( 'should not be called when the editor is not focused', () => {
setData( editor.document, '<paragraph>b[a]r</paragraph>' );
editor.editing.view.isFocused = false;

contextualToolbar._showPanel();
sinon.assert.notCalled( balloonAddSpy );
contextualToolbar.fire( '_selectionChangeDebounced' );
sinon.assert.notCalled( showSpy );
} );

it( 'should not be called when the selection is collapsed', () => {
setData( editor.document, '<paragraph>b[]ar</paragraph>' );

contextualToolbar.fire( '_selectionChangeDebounced' );
sinon.assert.notCalled( showSpy );
} );

it( 'should be called when the selection is not collapsed and editor is focused', () => {
setData( editor.document, '<paragraph>b[a]r</paragraph>' );
editor.editing.view.isFocused = true;

contextualToolbar.fire( '_selectionChangeDebounced' );
sinon.assert.calledOnce( showSpy );
} );
} );
} );

describe( '_hidePanel()', () => {
describe( 'hide()', () => {
let removeBalloonSpy;

beforeEach( () => {
Expand All @@ -240,9 +260,9 @@ describe( 'ContextualToolbar', () => {
it( 'should remove #toolbarView from the #_balloon', () => {
setData( editor.document, '<paragraph>b[a]r</paragraph>' );

contextualToolbar._showPanel();
contextualToolbar.show();

contextualToolbar._hidePanel();
contextualToolbar.hide();
sinon.assert.calledWithExactly( removeBalloonSpy, contextualToolbar.toolbarView );
} );

Expand All @@ -251,15 +271,15 @@ describe( 'ContextualToolbar', () => {

const spy = sandbox.spy( balloon, 'updatePosition' );

contextualToolbar._showPanel();
contextualToolbar._hidePanel();
contextualToolbar.show();
contextualToolbar.hide();

editor.editing.view.fire( 'render' );
sinon.assert.notCalled( spy );
} );

it( 'should not remove #ttolbarView when is not added to the #_balloon', () => {
contextualToolbar._hidePanel();
contextualToolbar.hide();

sinon.assert.notCalled( removeBalloonSpy );
} );
Expand Down Expand Up @@ -295,8 +315,8 @@ describe( 'ContextualToolbar', () => {
beforeEach( () => {
setData( editor.document, '<paragraph>[bar]</paragraph>' );

showPanelSpy = sandbox.spy( contextualToolbar, '_showPanel' );
hidePanelSpy = sandbox.spy( contextualToolbar, '_hidePanel' );
showPanelSpy = sandbox.spy( contextualToolbar, 'show' );
hidePanelSpy = sandbox.spy( contextualToolbar, 'hide' );
} );

it( 'should open when selection stops changing', () => {
Expand Down Expand Up @@ -395,7 +415,7 @@ describe( 'ContextualToolbar', () => {
contextualToolbar.on( 'beforeShow', spy );
setData( editor.document, '<paragraph>b[a]r</paragraph>' );

contextualToolbar._showPanel();
contextualToolbar.show();
sinon.assert.calledOnce( spy );
} );

Expand All @@ -408,7 +428,7 @@ describe( 'ContextualToolbar', () => {
stop();
} );

contextualToolbar._showPanel();
contextualToolbar.show();
sinon.assert.notCalled( balloonAddSpy );
} );
} );
Expand All @@ -421,14 +441,8 @@ describe( 'ContextualToolbar', () => {
sandbox.stub( editingView.domConverter, 'viewRangeToDom', ( ...args ) => {
const domRange = originalViewRangeToDom.apply( editingView.domConverter, args );

sandbox.stub( domRange, 'getClientRects', () => {
return {
length: 2,
item( id ) {
return id === 0 ? forwardSelectionRect : backwardSelectionRect;
}
};
} );
sandbox.stub( domRange, 'getClientRects' )
.returns( [ forwardSelectionRect, backwardSelectionRect ] );

return domRange;
} );
Expand Down
Loading

0 comments on commit eb4caab

Please sign in to comment.