Sfoglia il codice sorgente

Merge branch 'master' into t/266

Aleksander Nowodzinski 8 anni fa
parent
commit
e8159c0fca

+ 14 - 6
packages/ckeditor5-ui/src/bindings/clickoutsidehandler.js

@@ -10,7 +10,7 @@
 /* global document */
 
 /**
- * Handles a DOM `click` event outside of specified element and fires an action.
+ * Handles a DOM `click` event outside of specified elements and fires an action.
  *
  * Note that it is not handled by a `click` event, this is to avoid situation when click on some trigger
  * opens and closes element at the same time.
@@ -18,13 +18,21 @@
  * @param {Object} options Configuration options.
  * @param {module:utils/dom/emittermixin~Emitter} options.emitter The emitter to which this behavior should be added.
  * @param {Function} options.activator Function returning a `Boolean`, to determine whether handler is active.
- * @param {HTMLElement} options.contextElement `HTMLElement` that clicking inside of which will not fire the callback.
- * @param {Function} options.callback Function fired after clicking outside of a specified element.
+ * @param {Array.<HTMLElement>} options.contextElements `HTMLElement`s that clicking inside of any of them will not fire the callback.
+ * @param {Function} options.callback Function fired after clicking outside of specified elements.
  */
-export default function clickOutsideHandler( { emitter, activator, callback, contextElement } ) {
+export default function clickOutsideHandler( { emitter, activator, callback, contextElements } ) {
 	emitter.listenTo( document, 'mouseup', ( evt, { target } ) => {
-		if ( activator() && !contextElement.contains( target ) ) {
-			callback();
+		if ( !activator() ) {
+			return;
 		}
+
+		for ( const contextElement of contextElements ) {
+			if ( contextElement.contains( target ) ) {
+				return;
+			}
+		}
+
+		callback();
 	} );
 }

+ 17 - 21
packages/ckeditor5-ui/src/toolbar/contextual/contextualtoolbar.js

@@ -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.
@@ -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();
 			}
 		} );
 	}
@@ -120,12 +121,13 @@ 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.
@@ -133,17 +135,20 @@ export default class ContextualToolbar extends Plugin {
 		} );
 
 		// 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;
 
@@ -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 ) {
@@ -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 );
@@ -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 )

+ 17 - 1
packages/ckeditor5-ui/src/toolbar/enabletoolbarkeyboardfocus.js

@@ -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.
@@ -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();
 		}
 	} );
@@ -42,6 +53,11 @@ export default function enableToolbarKeyboardFocus( {
 	toolbar.keystrokes.set( 'Esc', ( data, cancel ) => {
 		if ( toolbar.focusTracker.isFocused ) {
 			origin.focus();
+
+			if ( afterBlur ) {
+				afterBlur();
+			}
+
 			cancel();
 		}
 	} );

+ 16 - 9
packages/ckeditor5-ui/tests/bindings/clickoutsidehandler.js

@@ -14,25 +14,28 @@ import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 testUtils.createSinonSandbox();
 
 describe( 'clickOutsideHandler', () => {
-	let activator, actionSpy, contextElement;
+	let activator, actionSpy, contextElement1, contextElement2;
 
 	beforeEach( () => {
 		activator = testUtils.sinon.stub().returns( false );
-		contextElement = document.createElement( 'div' );
+		contextElement1 = document.createElement( 'div' );
+		contextElement2 = document.createElement( 'div' );
 		actionSpy = testUtils.sinon.spy();
 
-		document.body.appendChild( contextElement );
+		document.body.appendChild( contextElement1 );
+		document.body.appendChild( contextElement2 );
 
 		clickOutsideHandler( {
 			emitter: Object.create( DomEmitterMixin ),
 			activator,
-			contextElement,
+			contextElements: [ contextElement1, contextElement2 ],
 			callback: actionSpy
 		} );
 	} );
 
 	afterEach( () => {
-		document.body.removeChild( contextElement );
+		document.body.removeChild( contextElement1 );
+		document.body.removeChild( contextElement2 );
 	} );
 
 	it( 'should fired callback after clicking out of context element when listener is active', () => {
@@ -54,16 +57,20 @@ describe( 'clickOutsideHandler', () => {
 	it( 'should not fired callback after clicking on context element when listener is active', () => {
 		activator.returns( true );
 
-		contextElement.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
+		contextElement1.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
+		sinon.assert.notCalled( actionSpy );
 
+		contextElement2.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
 		sinon.assert.notCalled( actionSpy );
 	} );
 
 	it( 'should not fired callback after clicking on context element when listener is not active', () => {
 		activator.returns( false );
 
-		contextElement.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
+		contextElement1.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
+		sinon.assert.notCalled( actionSpy );
 
+		contextElement2.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
 		sinon.assert.notCalled( actionSpy );
 	} );
 
@@ -75,7 +82,7 @@ describe( 'clickOutsideHandler', () => {
 		clickOutsideHandler( {
 			emitter: Object.create( DomEmitterMixin ),
 			activator,
-			contextElement,
+			contextElements: [ contextElement1 ],
 			callback: spy
 		} );
 
@@ -92,7 +99,7 @@ describe( 'clickOutsideHandler', () => {
 		clickOutsideHandler( {
 			emitter: Object.create( DomEmitterMixin ),
 			activator,
-			contextElement,
+			contextElements: [ contextElement1 ],
 			callback: spy
 		} );
 

+ 1 - 1
packages/ckeditor5-ui/tests/manual/contextualballoon/contextualballoon.js

@@ -137,7 +137,7 @@ class PluginGeneric extends Plugin {
 		clickOutsideHandler( {
 			emitter: this.view,
 			activator: () => this._isVisible,
-			contextElement: this.view.element,
+			contextElements: [ this.view.element ],
 			callback: () => this._hidePanel()
 		} );
 	}

+ 53 - 39
packages/ckeditor5-ui/tests/toolbar/contextual/contextualtoolbar.js

@@ -113,7 +113,7 @@ describe( 'ContextualToolbar', () => {
 		} );
 	} );
 
-	describe( '_showPanel()', () => {
+	describe( 'show()', () => {
 		let balloonAddSpy, forwardSelectionRect, backwardSelectionRect;
 
 		beforeEach( () => {
@@ -127,9 +127,9 @@ describe( 'ContextualToolbar', () => {
 			};
 
 			backwardSelectionRect = {
-				top: 100,
+				top: 200,
 				height: 10,
-				bottom: 110,
+				bottom: 210,
 				left: 200,
 				width: 50,
 				right: 250
@@ -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,
@@ -164,6 +164,8 @@ 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', () => {
@@ -171,13 +173,13 @@ describe( 'ContextualToolbar', () => {
 
 			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,
@@ -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', () => {
@@ -198,7 +202,7 @@ describe( 'ContextualToolbar', () => {
 
 			editor.editing.view.fire( 'render' );
 
-			contextualToolbar._showPanel();
+			contextualToolbar.show();
 			sinon.assert.notCalled( spy );
 
 			editor.editing.view.fire( 'render' );
@@ -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( () => {
@@ -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 );
 		} );
 
@@ -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 );
 		} );
@@ -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', () => {
@@ -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 );
 		} );
 
@@ -408,7 +428,7 @@ describe( 'ContextualToolbar', () => {
 				stop();
 			} );
 
-			contextualToolbar._showPanel();
+			contextualToolbar.show();
 			sinon.assert.notCalled( balloonAddSpy );
 		} );
 	} );
@@ -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;
 		} );

+ 52 - 2
packages/ckeditor5-ui/tests/toolbar/enabletoolbarkeyboardfocus.js

@@ -28,7 +28,7 @@ describe( 'enableToolbarKeyboardFocus()', () => {
 			toolbar
 		} );
 
-		return toolbar.init();
+		toolbar.init();
 	} );
 
 	it( 'focuses the toolbar on Alt+F10', () => {
@@ -66,7 +66,8 @@ describe( 'enableToolbarKeyboardFocus()', () => {
 	it( 're–foucuses origin on Esc', () => {
 		const spy = origin.focus = sinon.spy();
 		const toolbarFocusTracker = toolbar.focusTracker;
-		const keyEvtData = { keyCode: keyCodes.esc,
+		const keyEvtData = {
+			keyCode: keyCodes.esc,
 			preventDefault: sinon.spy(),
 			stopPropagation: sinon.spy()
 		};
@@ -84,6 +85,55 @@ describe( 'enableToolbarKeyboardFocus()', () => {
 		sinon.assert.calledOnce( keyEvtData.preventDefault );
 		sinon.assert.calledOnce( keyEvtData.stopPropagation );
 	} );
+
+	it( 'supports beforeFocus and afterBlur callbacks', () => {
+		const beforeFocus = sinon.spy();
+		const afterBlur = sinon.spy();
+
+		origin = viewCreator();
+		originFocusTracker = new FocusTracker();
+		originKeystrokeHandler = new KeystrokeHandler();
+		toolbar = new ToolbarView();
+
+		const toolbarFocusSpy = sinon.spy( toolbar, 'focus' );
+		const originFocusSpy = origin.focus = sinon.spy();
+		const toolbarFocusTracker = toolbar.focusTracker;
+
+		enableToolbarKeyboardFocus( {
+			origin,
+			originFocusTracker,
+			originKeystrokeHandler,
+			toolbar,
+			beforeFocus,
+			afterBlur
+		} );
+
+		toolbar.init();
+
+		let keyEvtData = {
+			keyCode: keyCodes.f10,
+			altKey: true,
+			preventDefault: sinon.spy(),
+			stopPropagation: sinon.spy()
+		};
+
+		toolbarFocusTracker.isFocused = false;
+		originFocusTracker.isFocused = true;
+
+		originKeystrokeHandler.press( keyEvtData );
+		sinon.assert.callOrder( beforeFocus, toolbarFocusSpy );
+
+		keyEvtData = {
+			keyCode: keyCodes.esc,
+			preventDefault: sinon.spy(),
+			stopPropagation: sinon.spy()
+		};
+
+		toolbarFocusTracker.isFocused = true;
+
+		toolbar.keystrokes.press( keyEvtData );
+		sinon.assert.callOrder( originFocusSpy, afterBlur );
+	} );
 } );
 
 function viewCreator( name ) {