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

Commit e3bbccf

Browse files
authored
Merge pull request #417 from ckeditor/t/408
Fix: The `BlockToolbar` should hide when the editor is blurred. Closes #408.
2 parents f6a02d4 + 1ce1d42 commit e3bbccf

File tree

2 files changed

+157
-97
lines changed

2 files changed

+157
-97
lines changed

src/toolbar/block/blocktoolbar.js

Lines changed: 107 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import BlockButtonView from './blockbuttonview';
1414
import BalloonPanelView from '../../panel/balloon/balloonpanelview';
1515
import ToolbarView from '../toolbarview';
1616

17-
import ClickObserver from '@ckeditor/ckeditor5-engine/src/view/observer/clickobserver';
1817
import clickOutsideHandler from '../../bindings/clickoutsidehandler';
1918

2019
import { getOptimalPosition } from '@ckeditor/ckeditor5-utils/src/dom/position';
@@ -66,10 +65,8 @@ export default class BlockToolbar extends Plugin {
6665
/**
6766
* @inheritDoc
6867
*/
69-
init() {
70-
const editor = this.editor;
71-
72-
editor.editing.view.addObserver( ClickObserver );
68+
constructor( editor ) {
69+
super( editor );
7370

7471
/**
7572
* The toolbar view.
@@ -99,18 +96,39 @@ export default class BlockToolbar extends Plugin {
9996
activator: () => this.panelView.isVisible,
10097
callback: () => this._hidePanel()
10198
} );
99+
}
102100

103-
// Try to hide button when the editor switches to the read-only mode.
104-
// Do not hide when panel if already visible to avoid a confusing UX when the panel
105-
// unexpectedly disappears.
106-
this.listenTo( editor, 'change:isReadOnly', () => {
107-
if ( !this.panelView.isVisible ) {
108-
this.buttonView.isVisible = false;
101+
/**
102+
* @inheritDoc
103+
*/
104+
init() {
105+
const editor = this.editor;
106+
107+
// Hides panel on a direct selection change.
108+
this.listenTo( editor.model.document.selection, 'change:range', ( evt, data ) => {
109+
if ( data.directChange ) {
110+
this._hidePanel();
109111
}
110112
} );
111113

112-
// Enable as default.
113-
this._initListeners();
114+
this.listenTo( editor.ui, 'update', () => this._updateButton() );
115+
// `low` priority is used because of https://github.com/ckeditor/ckeditor5-core/issues/133.
116+
this.listenTo( editor, 'change:isReadOnly', () => this._updateButton(), { priority: 'low' } );
117+
this.listenTo( editor.ui.focusTracker, 'change:isFocused', () => this._updateButton() );
118+
119+
// Reposition button on resize.
120+
this.listenTo( this.buttonView, 'change:isVisible', ( evt, name, isVisible ) => {
121+
if ( isVisible ) {
122+
// Keep correct position of button and panel on window#resize.
123+
this.buttonView.listenTo( window, 'resize', () => this._updateButton() );
124+
} else {
125+
// Stop repositioning button when is hidden.
126+
this.buttonView.stopListening( window, 'resize' );
127+
128+
// Hide the panel when the button disappears.
129+
this._hidePanel();
130+
}
131+
} );
114132
}
115133

116134
/**
@@ -143,10 +161,15 @@ export default class BlockToolbar extends Plugin {
143161

144162
toolbarView.extendTemplate( {
145163
attributes: {
146-
class: [
147-
// https://github.com/ckeditor/ckeditor5-editor-inline/issues/11
148-
'ck-toolbar_floating'
149-
]
164+
// https://github.com/ckeditor/ckeditor5-editor-inline/issues/11
165+
class: [ 'ck-toolbar_floating' ]
166+
}
167+
} );
168+
169+
// When toolbar lost focus then panel should hide.
170+
toolbarView.focusTracker.on( 'change:isFocused', ( evt, name, is ) => {
171+
if ( !is ) {
172+
this._hidePanel();
150173
}
151174
} );
152175

@@ -213,93 +236,62 @@ export default class BlockToolbar extends Plugin {
213236
}
214237

215238
/**
216-
* Starts displaying the button next to allowed elements.
239+
* Shows or hides the button.
240+
* When the all conditions for displaying button are matched then shows the button. Hides otherwise.
217241
*
218242
* @private
219243
*/
220-
_initListeners() {
244+
_updateButton() {
221245
const editor = this.editor;
222246
const model = editor.model;
223247
const view = editor.editing.view;
224-
let modelTarget, domTarget;
225248

226-
// Hides panel on a direct selection change.
227-
this.listenTo( editor.model.document.selection, 'change:range', ( evt, data ) => {
228-
if ( data.directChange ) {
229-
this._hidePanel();
230-
}
231-
} );
249+
// Hides the button when the editor is not focused.
250+
if ( !editor.ui.focusTracker.isFocused ) {
251+
this._hideButton();
232252

233-
this.listenTo( editor.ui, 'update', () => {
234-
// Get first selected block, button will be attached to this element.
235-
modelTarget = Array.from( model.document.selection.getSelectedBlocks() )[ 0 ];
253+
return;
254+
}
236255

237-
// Do not attach block button when there is no enabled item in toolbar for current block element.
238-
if ( !modelTarget || Array.from( this.toolbarView.items ).every( item => !item.isEnabled ) ) {
239-
this.buttonView.isVisible = false;
256+
// Hides the button when the editor switches to the read-only mode.
257+
if ( editor.isReadOnly ) {
258+
this._hideButton();
240259

241-
return;
242-
}
260+
return;
261+
}
243262

244-
// Get DOM target element.
245-
domTarget = view.domConverter.mapViewToDom( editor.editing.mapper.toViewElement( modelTarget ) );
263+
// Get the first selected block, button will be attached to this element.
264+
const modelTarget = Array.from( model.document.selection.getSelectedBlocks() )[ 0 ];
246265

247-
// Show block button.
248-
this.buttonView.isVisible = true;
266+
// Hides the button when there is no enabled item in toolbar for the current block element.
267+
if ( !modelTarget || Array.from( this.toolbarView.items ).every( item => !item.isEnabled ) ) {
268+
this._hideButton();
249269

250-
// Attach block button to target DOM element.
251-
this._attachButtonToElement( domTarget );
270+
return;
271+
}
252272

253-
// When panel is opened then refresh it position to be properly aligned with block button.
254-
if ( this.panelView.isVisible ) {
255-
this._showPanel();
256-
}
257-
}, { priority: 'low' } );
273+
// Get DOM target element.
274+
const domTarget = view.domConverter.mapViewToDom( editor.editing.mapper.toViewElement( modelTarget ) );
258275

259-
this.listenTo( this.buttonView, 'change:isVisible', ( evt, name, isVisible ) => {
260-
if ( isVisible ) {
261-
// Keep correct position of button and panel on window#resize.
262-
this.buttonView.listenTo( window, 'resize', () => this._attachButtonToElement( domTarget ) );
263-
} else {
264-
// Stop repositioning button when is hidden.
265-
this.buttonView.stopListening( window, 'resize' );
276+
// Show block button.
277+
this.buttonView.isVisible = true;
266278

267-
// Hide the panel when the button disappears.
268-
this._hidePanel();
269-
}
270-
} );
279+
// Attach block button to target DOM element.
280+
this._attachButtonToElement( domTarget );
281+
282+
// When panel is opened then refresh it position to be properly aligned with block button.
283+
if ( this.panelView.isVisible ) {
284+
this._showPanel();
285+
}
271286
}
272287

273288
/**
274-
* Attaches the {@link #buttonView} to the target block of content.
289+
* Hides the button.
275290
*
276-
* @protected
277-
* @param {HTMLElement} targetElement Target element.
291+
* @private
278292
*/
279-
_attachButtonToElement( targetElement ) {
280-
const contentStyles = window.getComputedStyle( targetElement );
281-
282-
const editableRect = new Rect( this.editor.ui.view.editableElement );
283-
const contentPaddingTop = parseInt( contentStyles.paddingTop, 10 );
284-
// When line height is not an integer then thread it as "normal".
285-
// MDN says that 'normal' == ~1.2 on desktop browsers.
286-
const contentLineHeight = parseInt( contentStyles.lineHeight, 10 ) || parseInt( contentStyles.fontSize, 10 ) * 1.2;
287-
288-
const position = getOptimalPosition( {
289-
element: this.buttonView.element,
290-
target: targetElement,
291-
positions: [
292-
( contentRect, buttonRect ) => {
293-
return {
294-
top: contentRect.top + contentPaddingTop + ( ( contentLineHeight - buttonRect.height ) / 2 ),
295-
left: editableRect.left - buttonRect.width
296-
};
297-
}
298-
]
299-
} );
300-
301-
this.buttonView.top = position.top;
302-
this.buttonView.left = position.left;
293+
_hideButton() {
294+
this.buttonView.isVisible = false;
303295
}
304296

305297
/**
@@ -334,6 +326,38 @@ export default class BlockToolbar extends Plugin {
334326
this.editor.editing.view.focus();
335327
}
336328
}
329+
330+
/**
331+
* Attaches the {@link #buttonView} to the target block of content.
332+
*
333+
* @protected
334+
* @param {HTMLElement} targetElement Target element.
335+
*/
336+
_attachButtonToElement( targetElement ) {
337+
const contentStyles = window.getComputedStyle( targetElement );
338+
339+
const editableRect = new Rect( this.editor.ui.view.editableElement );
340+
const contentPaddingTop = parseInt( contentStyles.paddingTop, 10 );
341+
// When line height is not an integer then thread it as "normal".
342+
// MDN says that 'normal' == ~1.2 on desktop browsers.
343+
const contentLineHeight = parseInt( contentStyles.lineHeight, 10 ) || parseInt( contentStyles.fontSize, 10 ) * 1.2;
344+
345+
const position = getOptimalPosition( {
346+
element: this.buttonView.element,
347+
target: targetElement,
348+
positions: [
349+
( contentRect, buttonRect ) => {
350+
return {
351+
top: contentRect.top + contentPaddingTop + ( ( contentLineHeight - buttonRect.height ) / 2 ),
352+
left: editableRect.left - buttonRect.width
353+
};
354+
}
355+
]
356+
} );
357+
358+
this.buttonView.top = position.top;
359+
this.buttonView.left = position.left;
360+
}
337361
}
338362

339363
/**

tests/toolbar/block/blocktoolbar.js

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
/* global document, window, Event */
66

77
import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
8-
import ClickObserver from '@ckeditor/ckeditor5-engine/src/view/observer/clickobserver';
98

109
import BlockToolbar from '../../../src/toolbar/block/blocktoolbar';
1110
import ToolbarView from '../../../src/toolbar/toolbarview';
@@ -39,6 +38,7 @@ describe( 'BlockToolbar', () => {
3938
} ).then( newEditor => {
4039
editor = newEditor;
4140
blockToolbar = editor.plugins.get( BlockToolbar );
41+
editor.ui.focusTracker.isFocused = true;
4242
} );
4343
} );
4444

@@ -51,10 +51,6 @@ describe( 'BlockToolbar', () => {
5151
expect( BlockToolbar.pluginName ).to.equal( 'BlockToolbar' );
5252
} );
5353

54-
it( 'should register a click observer', () => {
55-
expect( editor.editing.view.getObserver( ClickObserver ) ).to.be.instanceOf( ClickObserver );
56-
} );
57-
5854
describe( 'child views', () => {
5955
describe( 'panelView', () => {
6056
it( 'should create a view instance', () => {
@@ -70,7 +66,7 @@ describe( 'BlockToolbar', () => {
7066
} );
7167

7268
it( 'should add the #panelView to ui.focusTracker', () => {
73-
expect( editor.ui.focusTracker.isFocused ).to.be.false;
69+
editor.ui.focusTracker.isFocused = false;
7470

7571
blockToolbar.panelView.element.dispatchEvent( new Event( 'focus' ) );
7672

@@ -137,6 +133,16 @@ describe( 'BlockToolbar', () => {
137133

138134
expect( blockToolbar.panelView.isVisible ).to.be.false;
139135
} );
136+
137+
it( 'should hide the panel on toolbar blur', () => {
138+
blockToolbar.buttonView.fire( 'execute' );
139+
140+
expect( blockToolbar.panelView.isVisible ).to.be.true;
141+
142+
blockToolbar.toolbarView.focusTracker.isFocused = false;
143+
144+
expect( blockToolbar.panelView.isVisible ).to.be.false;
145+
} );
140146
} );
141147

142148
describe( 'buttonView', () => {
@@ -149,7 +155,7 @@ describe( 'BlockToolbar', () => {
149155
} );
150156

151157
it( 'should add the #buttonView to the ui.focusTracker', () => {
152-
expect( editor.ui.focusTracker.isFocused ).to.be.false;
158+
editor.ui.focusTracker.isFocused = false;
153159

154160
blockToolbar.buttonView.element.dispatchEvent( new Event( 'focus' ) );
155161

@@ -369,36 +375,66 @@ describe( 'BlockToolbar', () => {
369375
expect( blockToolbar.panelView.isVisible ).to.be.false;
370376
} );
371377

372-
it( 'should not hide the open panel on a indirect selection change', () => {
378+
it( 'should not hide the open panel on an indirect selection change', () => {
373379
blockToolbar.panelView.isVisible = true;
374380

375381
editor.model.document.selection.fire( 'change:range', { directChange: false } );
376382

377383
expect( blockToolbar.panelView.isVisible ).to.be.true;
378384
} );
379385

380-
it( 'should hide the UI when editor switches to readonly when the panel is not visible', () => {
386+
it( 'should hide the UI when editor switched to readonly', () => {
381387
setData( editor.model, '<paragraph>foo[]bar</paragraph>' );
382388

383389
blockToolbar.buttonView.isVisible = true;
384-
blockToolbar.panelView.isVisible = false;
390+
blockToolbar.panelView.isVisible = true;
385391

386392
editor.isReadOnly = true;
387393

388394
expect( blockToolbar.buttonView.isVisible ).to.be.false;
389395
expect( blockToolbar.panelView.isVisible ).to.be.false;
390396
} );
391397

392-
it( 'should not hide button when the editor switches to readonly when the panel is visible', () => {
398+
it( 'should show the button when the editor switched back from readonly', () => {
393399
setData( editor.model, '<paragraph>foo[]bar</paragraph>' );
394400

395-
blockToolbar.buttonView.isVisible = true;
396-
blockToolbar.panelView.isVisible = true;
401+
expect( blockToolbar.buttonView.isVisible ).to.true;
397402

398403
editor.isReadOnly = true;
399404

405+
expect( blockToolbar.buttonView.isVisible ).to.false;
406+
407+
editor.isReadOnly = false;
408+
400409
expect( blockToolbar.buttonView.isVisible ).to.be.true;
401-
expect( blockToolbar.panelView.isVisible ).to.be.true;
410+
} );
411+
412+
it( 'should show/hide the button on the editor focus/blur', () => {
413+
setData( editor.model, '<paragraph>foo[]bar</paragraph>' );
414+
415+
editor.ui.focusTracker.isFocused = true;
416+
417+
expect( blockToolbar.buttonView.isVisible ).to.true;
418+
419+
editor.ui.focusTracker.isFocused = false;
420+
421+
expect( blockToolbar.buttonView.isVisible ).to.false;
422+
423+
editor.ui.focusTracker.isFocused = true;
424+
425+
expect( blockToolbar.buttonView.isVisible ).to.true;
426+
} );
427+
428+
it( 'should hide the UI when the editor switched to the readonly when the panel is not visible', () => {
429+
setData( editor.model, '<paragraph>foo[]bar</paragraph>' );
430+
431+
blockToolbar.buttonView.isVisible = true;
432+
blockToolbar.panelView.isVisible = false;
433+
434+
editor.isReadOnly = true;
435+
436+
expect( blockToolbar.buttonView.isVisible ).to.be.false;
437+
expect( blockToolbar.panelView.isVisible ).to.be.false;
402438
} );
403439

404440
it( 'should update the button position on browser resize only when the button is visible', () => {

0 commit comments

Comments
 (0)