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

Commit eb4caab

Browse files
authored
Merge pull request #270 from ckeditor/t/263
Other: Implemented public `show()` and `hide()` methods in the `ContextualToolbar` plugin. Closes #263.
2 parents 9da5bf7 + 7a5e6de commit eb4caab

File tree

4 files changed

+139
-63
lines changed

4 files changed

+139
-63
lines changed

src/toolbar/contextual/contextualtoolbar.js

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import ContextualBalloon from '../../panel/balloon/contextualballoon';
1313
import ToolbarView from '../toolbarview';
1414
import BalloonPanelView from '../../panel/balloon/balloonpanelview.js';
1515
import debounce from '@ckeditor/ckeditor5-utils/src/lib/lodash/debounce';
16+
import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
1617

1718
/**
1819
* The contextual toolbar.
@@ -104,7 +105,7 @@ export default class ContextualToolbar extends Plugin {
104105
// Hide the panel View when editor loses focus but no the other way around.
105106
this.listenTo( editor.ui.focusTracker, 'change:isFocused', ( evt, name, isFocused ) => {
106107
if ( this._balloon.visibleView === this.toolbarView && !isFocused ) {
107-
this._hidePanel();
108+
this.hide();
108109
}
109110
} );
110111
}
@@ -120,30 +121,34 @@ export default class ContextualToolbar extends Plugin {
120121
*/
121122
_handleSelectionChange() {
122123
const selection = this.editor.document.selection;
124+
const editingView = this.editor.editing.view;
123125

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

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

135137
// Hide the toolbar when the selection stops changing.
136-
this.listenTo( this, '_selectionChangeDebounced', () => this._showPanel() );
138+
this.listenTo( this, '_selectionChangeDebounced', () => {
139+
// This implementation assumes that only non–collapsed selections gets the contextual toolbar.
140+
if ( editingView.isFocused && !editingView.selection.isCollapsed ) {
141+
this.show();
142+
}
143+
} );
137144
}
138145

139146
/**
140-
* Adds panel view to the {@link: #_balloon} and attaches panel to the selection.
147+
* Shows the toolbar and attaches it to the selection.
141148
*
142149
* Fires {@link #event:beforeShow} event just before displaying the panel.
143-
*
144-
* @protected
145150
*/
146-
_showPanel() {
151+
show() {
147152
const editingView = this.editor.editing.view;
148153
let isStopped = false;
149154

@@ -152,11 +157,6 @@ export default class ContextualToolbar extends Plugin {
152157
return;
153158
}
154159

155-
// This implementation assumes that only non–collapsed selections gets the contextual toolbar.
156-
if ( !editingView.isFocused || editingView.selection.isCollapsed ) {
157-
return;
158-
}
159-
160160
// If `beforeShow` event is not stopped by any external code then panel will be displayed.
161161
this.once( 'beforeShow', () => {
162162
if ( isStopped ) {
@@ -185,11 +185,9 @@ export default class ContextualToolbar extends Plugin {
185185
}
186186

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

223219
// Select the proper range rect depending on the direction of the selection.
224-
return isBackward ? rangeRects.item( 0 ) : rangeRects.item( rangeRects.length - 1 );
220+
return rangeRects[ isBackward ? 0 : rangeRects.length - 1 ];
225221
},
226222
limiter: this.editor.ui.view.editable.element,
227223
positions: getBalloonPositions( isBackward )

src/toolbar/enabletoolbarkeyboardfocus.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,18 @@
1919
* for `options.origin`.
2020
* @param {module:ui/toolbar/toolbarview~ToolbarView} options.toolbar A toolbar which is to gain
2121
* focus when `Alt+F10` is pressed.
22+
* @param {Function} [options.beforeFocus] A callback executed before the `options.toolbar` gains focus
23+
* upon the `Alt+F10` keystroke.
24+
* @param {Function} [options.afterBlur] A callback executed after `options.toolbar` loses focus upon
25+
* `Esc` keystroke but before the focus goes back to `options.origin`.
2226
*/
2327
export default function enableToolbarKeyboardFocus( {
2428
origin,
2529
originKeystrokeHandler,
2630
originFocusTracker,
27-
toolbar
31+
toolbar,
32+
beforeFocus,
33+
afterBlur
2834
} ) {
2935
// Because toolbar items can get focus, the overall state of the toolbar must
3036
// also be tracked.
@@ -33,7 +39,12 @@ export default function enableToolbarKeyboardFocus( {
3339
// Focus the toolbar on the keystroke, if not already focused.
3440
originKeystrokeHandler.set( 'Alt+F10', ( data, cancel ) => {
3541
if ( originFocusTracker.isFocused && !toolbar.focusTracker.isFocused ) {
42+
if ( beforeFocus ) {
43+
beforeFocus();
44+
}
45+
3646
toolbar.focus();
47+
3748
cancel();
3849
}
3950
} );
@@ -42,6 +53,11 @@ export default function enableToolbarKeyboardFocus( {
4253
toolbar.keystrokes.set( 'Esc', ( data, cancel ) => {
4354
if ( toolbar.focusTracker.isFocused ) {
4455
origin.focus();
56+
57+
if ( afterBlur ) {
58+
afterBlur();
59+
}
60+
4561
cancel();
4662
}
4763
} );

tests/toolbar/contextual/contextualtoolbar.js

Lines changed: 53 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ describe( 'ContextualToolbar', () => {
113113
} );
114114
} );
115115

116-
describe( '_showPanel()', () => {
116+
describe( 'show()', () => {
117117
let balloonAddSpy, forwardSelectionRect, backwardSelectionRect;
118118

119119
beforeEach( () => {
@@ -127,9 +127,9 @@ describe( 'ContextualToolbar', () => {
127127
};
128128

129129
backwardSelectionRect = {
130-
top: 100,
130+
top: 200,
131131
height: 10,
132-
bottom: 110,
132+
bottom: 210,
133133
left: 200,
134134
width: 50,
135135
right: 250
@@ -146,13 +146,13 @@ describe( 'ContextualToolbar', () => {
146146

147147
const defaultPositions = BalloonPanelView.defaultPositions;
148148

149-
contextualToolbar._showPanel();
149+
contextualToolbar.show();
150150

151-
sinon.assert.calledWithExactly( balloonAddSpy, {
151+
sinon.assert.calledWith( balloonAddSpy, {
152152
view: contextualToolbar.toolbarView,
153153
balloonClassName: 'ck-toolbar-container ck-editor-toolbar-container',
154154
position: {
155-
target: sinon.match( value => value() == backwardSelectionRect ),
155+
target: sinon.match.func,
156156
limiter: editor.ui.view.editable.element,
157157
positions: [
158158
defaultPositions.southEastArrowNorth,
@@ -164,20 +164,22 @@ describe( 'ContextualToolbar', () => {
164164
]
165165
}
166166
} );
167+
168+
expect( balloonAddSpy.firstCall.args[ 0 ].position.target() ).to.deep.equal( backwardSelectionRect );
167169
} );
168170

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

172174
const defaultPositions = BalloonPanelView.defaultPositions;
173175

174-
contextualToolbar._showPanel();
176+
contextualToolbar.show();
175177

176178
sinon.assert.calledWithExactly( balloonAddSpy, {
177179
view: contextualToolbar.toolbarView,
178180
balloonClassName: 'ck-toolbar-container ck-editor-toolbar-container',
179181
position: {
180-
target: sinon.match( value => value() == forwardSelectionRect ),
182+
target: sinon.match.func,
181183
limiter: editor.ui.view.editable.element,
182184
positions: [
183185
defaultPositions.northWestArrowSouth,
@@ -189,6 +191,8 @@ describe( 'ContextualToolbar', () => {
189191
]
190192
}
191193
} );
194+
195+
expect( balloonAddSpy.firstCall.args[ 0 ].position.target() ).to.deep.equal( forwardSelectionRect );
192196
} );
193197

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

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

201-
contextualToolbar._showPanel();
205+
contextualToolbar.show();
202206
sinon.assert.notCalled( spy );
203207

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

211-
contextualToolbar._showPanel();
212-
contextualToolbar._showPanel();
215+
contextualToolbar.show();
216+
contextualToolbar.show();
213217
sinon.assert.calledOnce( balloonAddSpy );
214218
} );
215219

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

220-
contextualToolbar._showPanel();
221-
sinon.assert.notCalled( balloonAddSpy );
222-
} );
223+
beforeEach( () => {
224+
showSpy = sinon.spy( contextualToolbar, 'show' );
225+
} );
223226

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

227-
contextualToolbar._showPanel();
228-
sinon.assert.notCalled( balloonAddSpy );
231+
contextualToolbar.fire( '_selectionChangeDebounced' );
232+
sinon.assert.notCalled( showSpy );
233+
} );
234+
235+
it( 'should not be called when the selection is collapsed', () => {
236+
setData( editor.document, '<paragraph>b[]ar</paragraph>' );
237+
238+
contextualToolbar.fire( '_selectionChangeDebounced' );
239+
sinon.assert.notCalled( showSpy );
240+
} );
241+
242+
it( 'should be called when the selection is not collapsed and editor is focused', () => {
243+
setData( editor.document, '<paragraph>b[a]r</paragraph>' );
244+
editor.editing.view.isFocused = true;
245+
246+
contextualToolbar.fire( '_selectionChangeDebounced' );
247+
sinon.assert.calledOnce( showSpy );
248+
} );
229249
} );
230250
} );
231251

232-
describe( '_hidePanel()', () => {
252+
describe( 'hide()', () => {
233253
let removeBalloonSpy;
234254

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

243-
contextualToolbar._showPanel();
263+
contextualToolbar.show();
244264

245-
contextualToolbar._hidePanel();
265+
contextualToolbar.hide();
246266
sinon.assert.calledWithExactly( removeBalloonSpy, contextualToolbar.toolbarView );
247267
} );
248268

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

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

254-
contextualToolbar._showPanel();
255-
contextualToolbar._hidePanel();
274+
contextualToolbar.show();
275+
contextualToolbar.hide();
256276

257277
editor.editing.view.fire( 'render' );
258278
sinon.assert.notCalled( spy );
259279
} );
260280

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

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

298-
showPanelSpy = sandbox.spy( contextualToolbar, '_showPanel' );
299-
hidePanelSpy = sandbox.spy( contextualToolbar, '_hidePanel' );
318+
showPanelSpy = sandbox.spy( contextualToolbar, 'show' );
319+
hidePanelSpy = sandbox.spy( contextualToolbar, 'hide' );
300320
} );
301321

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

398-
contextualToolbar._showPanel();
418+
contextualToolbar.show();
399419
sinon.assert.calledOnce( spy );
400420
} );
401421

@@ -408,7 +428,7 @@ describe( 'ContextualToolbar', () => {
408428
stop();
409429
} );
410430

411-
contextualToolbar._showPanel();
431+
contextualToolbar.show();
412432
sinon.assert.notCalled( balloonAddSpy );
413433
} );
414434
} );
@@ -421,14 +441,8 @@ describe( 'ContextualToolbar', () => {
421441
sandbox.stub( editingView.domConverter, 'viewRangeToDom', ( ...args ) => {
422442
const domRange = originalViewRangeToDom.apply( editingView.domConverter, args );
423443

424-
sandbox.stub( domRange, 'getClientRects', () => {
425-
return {
426-
length: 2,
427-
item( id ) {
428-
return id === 0 ? forwardSelectionRect : backwardSelectionRect;
429-
}
430-
};
431-
} );
444+
sandbox.stub( domRange, 'getClientRects' )
445+
.returns( [ forwardSelectionRect, backwardSelectionRect ] );
432446

433447
return domRange;
434448
} );

0 commit comments

Comments
 (0)