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

Commit f6a02d4

Browse files
authoredJul 3, 2018
Merge pull request #420 from ckeditor/t/418
Fix: The `BalloonToolbar` should hide when the editable is blurred. Closes #418.
2 parents d392fe1 + 3a1e49b commit f6a02d4

File tree

5 files changed

+217
-105
lines changed

5 files changed

+217
-105
lines changed
 

‎src/toolbar/balloon/balloontoolbar.js

+70-57
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
1111
import ContextualBalloon from '../../panel/balloon/contextualballoon';
1212
import ToolbarView from '../toolbarview';
1313
import BalloonPanelView from '../../panel/balloon/balloonpanelview.js';
14+
import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
1415
import debounce from '@ckeditor/ckeditor5-utils/src/lib/lodash/debounce';
1516
import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
1617
import normalizeToolbarConfig from '../normalizetoolbarconfig';
@@ -40,31 +41,32 @@ export default class BalloonToolbar extends Plugin {
4041
/**
4142
* @inheritDoc
4243
*/
43-
init() {
44-
const editor = this.editor;
44+
constructor( editor ) {
45+
super( editor );
4546

4647
/**
4748
* The toolbar view displayed in the balloon.
4849
*
49-
* @member {module:ui/toolbar/toolbarview~ToolbarView}
50+
* @type {module:ui/toolbar/toolbarview~ToolbarView}
5051
*/
51-
this.toolbarView = new ToolbarView( editor.locale );
52-
53-
this.toolbarView.extendTemplate( {
54-
attributes: {
55-
class: [
56-
'ck-toolbar_floating'
57-
]
58-
}
59-
} );
52+
this.toolbarView = this._createToolbarView();
6053

61-
this.toolbarView.render();
54+
/**
55+
* Tracks the focus of the {@link module:ui/editableui/editableuiview~EditableUIView#editableElement}
56+
* and the {@link #toolbarView}. When both are blurred then the toolbar should hide.
57+
*
58+
* @readonly
59+
* @type {module:utils:focustracker~FocusTracker}
60+
*/
61+
this.focusTracker = new FocusTracker();
62+
this.focusTracker.add( editor.ui.view.editableElement );
63+
this.focusTracker.add( this.toolbarView.element );
6264

6365
/**
6466
* The contextual balloon plugin instance.
6567
*
6668
* @private
67-
* @member {module:ui/panel/balloon/contextualballoon~ContextualBalloon}
69+
* @type {module:ui/panel/balloon/contextualballoon~ContextualBalloon}
6870
*/
6971
this._balloon = editor.plugins.get( ContextualBalloon );
7072

@@ -75,19 +77,52 @@ export default class BalloonToolbar extends Plugin {
7577
* trailing debounced invocation on destroy.
7678
*
7779
* @private
78-
* @member {Function}
80+
* @type {Function}
7981
*/
8082
this._fireSelectionChangeDebounced = debounce( () => this.fire( '_selectionChangeDebounced' ), 200 );
8183

82-
// Attach lifecycle actions.
83-
this._handleSelectionChange();
84-
this._handleFocusChange();
85-
8684
// The appearance of the BalloonToolbar method is event–driven.
8785
// It is possible to stop the #show event and this prevent the toolbar from showing up.
8886
this.decorate( 'show' );
8987
}
9088

89+
/**
90+
* @inheritDoc
91+
*/
92+
init() {
93+
const editor = this.editor;
94+
const selection = editor.model.document.selection;
95+
96+
// Show/hide the toolbar on editable focus/blur.
97+
this.listenTo( this.focusTracker, 'change:isFocused', ( evt, name, isFocused ) => {
98+
const isToolbarVisible = this._balloon.visibleView === this.toolbarView;
99+
100+
if ( !isFocused && isToolbarVisible ) {
101+
this.hide();
102+
} else if ( isFocused ) {
103+
this.show();
104+
}
105+
} );
106+
107+
// Hide the toolbar when the selection is changed by a direct change or has changed to collapsed.
108+
this.listenTo( selection, 'change:range', ( evt, data ) => {
109+
if ( data.directChange || selection.isCollapsed ) {
110+
this.hide();
111+
}
112+
113+
// Fire internal `_selectionChangeDebounced` event to use it for showing
114+
// the toolbar after the selection stops changing.
115+
this._fireSelectionChangeDebounced();
116+
} );
117+
118+
// Show the toolbar when the selection stops changing.
119+
this.listenTo( this, '_selectionChangeDebounced', () => {
120+
if ( this.editor.editing.view.document.isFocused ) {
121+
this.show();
122+
}
123+
} );
124+
}
125+
91126
/**
92127
* Creates toolbar components based on given configuration.
93128
* This needs to be done when all plugins are ready.
@@ -102,52 +137,23 @@ export default class BalloonToolbar extends Plugin {
102137
}
103138

104139
/**
105-
* Handles the editor focus change and hides the toolbar if it's needed.
140+
* Creates the toolbar view instance.
106141
*
107142
* @private
143+
* @returns {module:ui/toolbar/toolbarview~ToolbarView}
108144
*/
109-
_handleFocusChange() {
110-
const editor = this.editor;
145+
_createToolbarView() {
146+
const toolbarView = new ToolbarView( this.editor.locale );
111147

112-
// Hide the panel View when editor loses focus but no the other way around.
113-
this.listenTo( editor.ui.focusTracker, 'change:isFocused', ( evt, name, isFocused ) => {
114-
if ( this._balloon.visibleView === this.toolbarView && !isFocused ) {
115-
this.hide();
148+
toolbarView.extendTemplate( {
149+
attributes: {
150+
class: [ 'ck-toolbar_floating' ]
116151
}
117152
} );
118-
}
119153

120-
/**
121-
* Handles {@link module:engine/model/document~Document#selection} change and show or hide toolbar.
122-
*
123-
* Note that in this case it's better to listen to {@link module:engine/model/document~Document model document}
124-
* selection instead of {@link module:engine/view/document~Document view document} selection because the first one
125-
* doesn't fire `change` event after text style change (like bold or italic) and toolbar doesn't blink.
126-
*
127-
* @private
128-
*/
129-
_handleSelectionChange() {
130-
const selection = this.editor.model.document.selection;
131-
const viewDocument = this.editor.editing.view.document;
154+
toolbarView.render();
132155

133-
this.listenTo( selection, 'change:range', ( evt, data ) => {
134-
// When the selection is not changed by a collaboration and when is not collapsed.
135-
if ( data.directChange || selection.isCollapsed ) {
136-
// Hide the toolbar when the selection starts changing.
137-
this.hide();
138-
}
139-
140-
// Fire internal `_selectionChangeDebounced` when the selection stops changing.
141-
this._fireSelectionChangeDebounced();
142-
} );
143-
144-
// Hide the toolbar when the selection stops changing.
145-
this.listenTo( this, '_selectionChangeDebounced', () => {
146-
// This implementation assumes that only non–collapsed selections gets the contextual toolbar.
147-
if ( viewDocument.isFocused && !viewDocument.selection.isCollapsed ) {
148-
this.show();
149-
}
150-
} );
156+
return toolbarView;
151157
}
152158

153159
/**
@@ -156,11 +162,18 @@ export default class BalloonToolbar extends Plugin {
156162
* Fires {@link #event:show} event which can be stopped to prevent the toolbar from showing up.
157163
*/
158164
show() {
165+
const editor = this.editor;
166+
159167
// Do not add the toolbar to the balloon stack twice.
160168
if ( this._balloon.hasView( this.toolbarView ) ) {
161169
return;
162170
}
163171

172+
// Do not show the toolbar when the selection is collapsed.
173+
if ( editor.model.document.selection.isCollapsed ) {
174+
return;
175+
}
176+
164177
// Don not show the toolbar when all components inside are disabled
165178
// see https://github.com/ckeditor/ckeditor5-ui/issues/269.
166179
if ( Array.from( this.toolbarView.items ).every( item => item.isEnabled !== undefined && !item.isEnabled ) ) {

‎tests/manual/tickets/418/1.html

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<div class="wrapper">
2+
<div id="editor">
3+
<h2>The three greatest things you learn from traveling</h2>
4+
<p>
5+
Like all the great things on earth traveling teaches us by example. Here are some of the most precious lessons
6+
I’ve learned over the years of traveling.
7+
</p>
8+
9+
<h3>Appreciation of diversity</h3>
10+
<p>
11+
Getting used to an entirely different culture can be challenging. While it’s also nice to learn about
12+
cultures online or from books, nothing comes close to experiencing cultural diversity in person.
13+
You learn to appreciate each and every single one of the differences while you become more culturally fluid.
14+
</p>
15+
</div>
16+
</div>
17+
18+
<style>
19+
#editor {
20+
margin: 0 auto;
21+
max-width: 800px;
22+
}
23+
24+
.wrapper {
25+
padding: 50px 20px;
26+
}
27+
28+
.ck-block-toolbar-button {
29+
transform: translateX( -10px );
30+
}
31+
</style>

‎tests/manual/tickets/418/1.js

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md.
4+
*/
5+
6+
/* globals window, document, console:false */
7+
8+
import BalloonEditor from '@ckeditor/ckeditor5-editor-balloon/src/ballooneditor';
9+
import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
10+
import List from '@ckeditor/ckeditor5-list/src/list';
11+
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
12+
import Heading from '@ckeditor/ckeditor5-heading/src/heading';
13+
import HeadingButtonsUI from '@ckeditor/ckeditor5-heading/src/headingbuttonsui';
14+
import ParagraphButtonUI from '@ckeditor/ckeditor5-paragraph/src/paragraphbuttonui';
15+
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
16+
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
17+
import Link from '@ckeditor/ckeditor5-link/src/link';
18+
19+
import BlockToolbar from '../../../../src/toolbar/block/blocktoolbar';
20+
import BalloonToolbar from '../../../../src/toolbar/balloon/balloontoolbar';
21+
22+
BalloonEditor
23+
.create( document.querySelector( '#editor' ), {
24+
plugins: [
25+
Essentials,
26+
List,
27+
Paragraph,
28+
Heading,
29+
HeadingButtonsUI,
30+
ParagraphButtonUI,
31+
Bold,
32+
Italic,
33+
Link,
34+
BlockToolbar,
35+
BalloonToolbar
36+
],
37+
blockToolbar: [ 'paragraph', 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList' ],
38+
balloonToolbar: [ 'bold', 'italic', 'link' ]
39+
} )
40+
.then( editor => {
41+
window.editor = editor;
42+
} )
43+
.catch( err => {
44+
console.error( err.stack );
45+
} );

‎tests/manual/tickets/418/1.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
## BlockToolbar and BalloonToolbar collision [#418](https://github.com/ckeditor/ckeditor5-ui/issues/418)
2+
3+
- select some text, balloon toolbar should show up
4+
- click block toolbar button, balloon toolbar should hide
5+
- pres `Esc` to move focus back to editable, balloon toolbar should show up

‎tests/toolbar/balloon/balloontoolbar.js

+66-48
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import BalloonToolbar from '../../../src/toolbar/balloon/balloontoolbar';
88
import ContextualBalloon from '../../../src/panel/balloon/contextualballoon';
99
import BalloonPanelView from '../../../src/panel/balloon/balloonpanelview';
1010
import ToolbarView from '../../../src/toolbar/toolbarview';
11+
import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
1112
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
1213
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
1314
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
@@ -17,7 +18,7 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
1718
import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
1819
import { stringify as viewStringify } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
1920

20-
/* global document, setTimeout, window */
21+
/* global document, setTimeout, window, Event */
2122

2223
describe( 'BalloonToolbar', () => {
2324
let sandbox, editor, model, selection, editingView, balloonToolbar, balloon, editorElement;
@@ -143,6 +144,28 @@ describe( 'BalloonToolbar', () => {
143144
} );
144145
} );
145146

147+
describe( 'focusTracker', () => {
148+
it( 'should be defined', () => {
149+
expect( balloonToolbar.focusTracker ).to.instanceof( FocusTracker );
150+
} );
151+
152+
it( 'it should track the focus of the #editableElement', () => {
153+
expect( balloonToolbar.focusTracker.isFocused ).to.false;
154+
155+
editor.ui.view.editableElement.dispatchEvent( new Event( 'focus' ) );
156+
157+
expect( balloonToolbar.focusTracker.isFocused ).to.true;
158+
} );
159+
160+
it( 'it should track the focus of the toolbarView#element', () => {
161+
expect( balloonToolbar.focusTracker.isFocused ).to.false;
162+
163+
balloonToolbar.toolbarView.element.dispatchEvent( new Event( 'focus' ) );
164+
165+
expect( balloonToolbar.focusTracker.isFocused ).to.true;
166+
} );
167+
} );
168+
146169
describe( 'show()', () => {
147170
let balloonAddSpy, backwardSelectionRect, forwardSelectionRect;
148171

@@ -275,7 +298,7 @@ describe( 'BalloonToolbar', () => {
275298
expect( targetRect ).to.deep.equal( backwardSelectionRect );
276299
} );
277300

278-
it( 'should update balloon position on ui#update event while balloon is added to the #_balloon', () => {
301+
it( 'should update balloon position on ui#update event when #toolbarView is already added to the #_balloon', () => {
279302
setData( model, '<paragraph>b[a]r</paragraph>' );
280303

281304
const spy = sandbox.spy( balloon, 'updatePosition' );
@@ -297,6 +320,13 @@ describe( 'BalloonToolbar', () => {
297320
sinon.assert.calledOnce( balloonAddSpy );
298321
} );
299322

323+
it( 'should not add the #toolbarView to the #_balloon when the selection is collapsed', () => {
324+
setData( model, '<paragraph>b[]ar</paragraph>' );
325+
326+
balloonToolbar.show();
327+
sinon.assert.notCalled( balloonAddSpy );
328+
} );
329+
300330
it( 'should not add #toolbarView to the #_balloon when all components inside #toolbarView are disabled', () => {
301331
Array.from( balloonToolbar.toolbarView.items ).forEach( item => {
302332
item.isEnabled = false;
@@ -319,37 +349,6 @@ describe( 'BalloonToolbar', () => {
319349
balloonToolbar.show();
320350
sinon.assert.calledOnce( balloonAddSpy );
321351
} );
322-
323-
describe( 'on #_selectionChangeDebounced event', () => {
324-
let showSpy;
325-
326-
beforeEach( () => {
327-
showSpy = sandbox.spy( balloonToolbar, 'show' );
328-
} );
329-
330-
it( 'should not be called when the editor is not focused', () => {
331-
setData( model, '<paragraph>b[a]r</paragraph>' );
332-
editingView.document.isFocused = false;
333-
334-
balloonToolbar.fire( '_selectionChangeDebounced' );
335-
sinon.assert.notCalled( showSpy );
336-
} );
337-
338-
it( 'should not be called when the selection is collapsed', () => {
339-
setData( model, '<paragraph>b[]ar</paragraph>' );
340-
341-
balloonToolbar.fire( '_selectionChangeDebounced' );
342-
sinon.assert.notCalled( showSpy );
343-
} );
344-
345-
it( 'should be called when the selection is not collapsed and editor is focused', () => {
346-
setData( model, '<paragraph>b[a]r</paragraph>' );
347-
editingView.document.isFocused = true;
348-
349-
balloonToolbar.fire( '_selectionChangeDebounced' );
350-
sinon.assert.calledOnce( showSpy );
351-
} );
352-
} );
353352
} );
354353

355354
describe( 'hide()', () => {
@@ -381,7 +380,7 @@ describe( 'BalloonToolbar', () => {
381380
sinon.assert.notCalled( spy );
382381
} );
383382

384-
it( 'should not remove #ttolbarView when is not added to the #_balloon', () => {
383+
it( 'should not remove #toolbarView when is not added to the #_balloon', () => {
385384
balloonToolbar.hide();
386385

387386
sinon.assert.notCalled( removeBalloonSpy );
@@ -412,7 +411,7 @@ describe( 'BalloonToolbar', () => {
412411
} );
413412
} );
414413

415-
describe( 'showing and hiding', () => {
414+
describe( 'show and hide triggers', () => {
416415
let showPanelSpy, hidePanelSpy;
417416

418417
beforeEach( () => {
@@ -422,7 +421,7 @@ describe( 'BalloonToolbar', () => {
422421
hidePanelSpy = sandbox.spy( balloonToolbar, 'hide' );
423422
} );
424423

425-
it( 'should open when selection stops changing', () => {
424+
it( 'should show when selection stops changing', () => {
426425
sinon.assert.notCalled( showPanelSpy );
427426
sinon.assert.notCalled( hidePanelSpy );
428427

@@ -432,7 +431,18 @@ describe( 'BalloonToolbar', () => {
432431
sinon.assert.notCalled( hidePanelSpy );
433432
} );
434433

435-
it( 'should close when selection starts changing by a directChange', () => {
434+
it( 'should not show when the selection stops changing when the editable is blurred', () => {
435+
sinon.assert.notCalled( showPanelSpy );
436+
sinon.assert.notCalled( hidePanelSpy );
437+
438+
editingView.document.isFocused = false;
439+
balloonToolbar.fire( '_selectionChangeDebounced' );
440+
441+
sinon.assert.notCalled( showPanelSpy );
442+
sinon.assert.notCalled( hidePanelSpy );
443+
} );
444+
445+
it( 'should hide when selection starts changing by a direct change', () => {
436446
balloonToolbar.fire( '_selectionChangeDebounced' );
437447

438448
sinon.assert.calledOnce( showPanelSpy );
@@ -444,7 +454,7 @@ describe( 'BalloonToolbar', () => {
444454
sinon.assert.calledOnce( hidePanelSpy );
445455
} );
446456

447-
it( 'should not close when selection starts changing by not a directChange', () => {
457+
it( 'should not hide when selection starts changing by an indirect change', () => {
448458
balloonToolbar.fire( '_selectionChangeDebounced' );
449459

450460
sinon.assert.calledOnce( showPanelSpy );
@@ -456,7 +466,7 @@ describe( 'BalloonToolbar', () => {
456466
sinon.assert.notCalled( hidePanelSpy );
457467
} );
458468

459-
it( 'should close when selection starts changing by not a directChange but will become collapsed', () => {
469+
it( 'should hide when selection starts changing by an indirect change but has changed to collapsed', () => {
460470
balloonToolbar.fire( '_selectionChangeDebounced' );
461471

462472
sinon.assert.calledOnce( showPanelSpy );
@@ -472,35 +482,43 @@ describe( 'BalloonToolbar', () => {
472482
sinon.assert.calledOnce( hidePanelSpy );
473483
} );
474484

475-
it( 'should hide if the editor loses focus', () => {
476-
editor.ui.focusTracker.isFocused = true;
485+
it( 'should show on #focusTracker focus', () => {
486+
balloonToolbar.focusTracker.isFocused = false;
477487

478-
balloonToolbar.fire( '_selectionChangeDebounced' );
488+
sinon.assert.notCalled( showPanelSpy );
489+
sinon.assert.notCalled( hidePanelSpy );
490+
491+
balloonToolbar.focusTracker.isFocused = true;
492+
493+
sinon.assert.calledOnce( showPanelSpy );
494+
sinon.assert.notCalled( hidePanelSpy );
495+
} );
496+
497+
it( 'should hide on #focusTracker blur', () => {
498+
balloonToolbar.focusTracker.isFocused = true;
479499

480500
const stub = sandbox.stub( balloon, 'visibleView' ).get( () => balloonToolbar.toolbarView );
481501

482502
sinon.assert.calledOnce( showPanelSpy );
483503
sinon.assert.notCalled( hidePanelSpy );
484504

485-
editor.ui.focusTracker.isFocused = false;
505+
balloonToolbar.focusTracker.isFocused = false;
486506

487507
sinon.assert.calledOnce( showPanelSpy );
488508
sinon.assert.calledOnce( hidePanelSpy );
489509

490510
stub.restore();
491511
} );
492512

493-
it( 'should not hide if the editor loses focus and #toolbarView is not visible', () => {
494-
editor.ui.focusTracker.isFocused = true;
495-
496-
balloonToolbar.fire( '_selectionChangeDebounced' );
513+
it( 'should not hide on #focusTracker blur when toolbar is not in the balloon stack', () => {
514+
balloonToolbar.focusTracker.isFocused = true;
497515

498516
const stub = sandbox.stub( balloon, 'visibleView' ).get( () => null );
499517

500518
sinon.assert.calledOnce( showPanelSpy );
501519
sinon.assert.notCalled( hidePanelSpy );
502520

503-
editor.ui.focusTracker.isFocused = false;
521+
balloonToolbar.focusTracker.isFocused = false;
504522

505523
sinon.assert.calledOnce( showPanelSpy );
506524
sinon.assert.notCalled( hidePanelSpy );

0 commit comments

Comments
 (0)
This repository has been archived.