Browse Source

Merge pull request #44 from ckeditor/t/40

Helpers for closing link balloon panel.
Piotrek Koszuliński 9 years ago
parent
commit
7d7367f972
2 changed files with 68 additions and 120 deletions
  1. 45 65
      packages/ckeditor5-link/src/link.js
  2. 23 55
      packages/ckeditor5-link/tests/link.js

+ 45 - 65
packages/ckeditor5-link/src/link.js

@@ -9,6 +9,8 @@ import LinkEngine from './linkengine.js';
 import LinkElement from './linkelement.js';
 
 import Model from '../ui/model.js';
+import clickOutsideHandler from '../ui/bindings/clickoutsidehandler.js';
+import escPressHandler from '../ui/bindings/escpresshandler.js';
 
 import ButtonController from '../ui/button/button.js';
 import ButtonView from '../ui/button/buttonview.js';
@@ -16,8 +18,6 @@ import ButtonView from '../ui/button/buttonview.js';
 import LinkBalloonPanel from './ui/linkballoonpanel.js';
 import LinkBalloonPanelView from './ui/linkballoonpanelview.js';
 
-import { keyCodes } from '../utils/keyboard.js';
-
 /**
  * The link feature. It introduces the Link and Unlink buttons and the <kbd>Ctrl+K</kbd> keystroke.
  *
@@ -76,13 +76,13 @@ export default class Link extends Feature {
 		linkButtonModel.bind( 'isEnabled' ).to( linkCommand, 'isEnabled' );
 
 		// Show the panel on button click only when editor is focused.
-		this.listenTo( linkButtonModel, 'execute', () => {
-			this._attachPanelToElement();
-			this.balloonPanel.urlInput.view.select();
-		} );
+		this.listenTo( linkButtonModel, 'execute', () => this._showPanel() );
 
 		// Add link button to feature components.
 		editor.ui.featureComponents.add( 'link', ButtonController, ButtonView, linkButtonModel );
+
+		// Handle `Ctrl+K` keystroke and show panel.
+		editor.keystrokes.set( 'CTRL+K', () => this._showPanel() );
 	}
 
 	/**
@@ -140,56 +140,24 @@ export default class Link extends Feature {
 		// Create the balloon panel instance.
 		const balloonPanel = new LinkBalloonPanel( panelModel, new LinkBalloonPanelView( editor.locale ) );
 
-		// Observe `LinkBalloonPanelMode#executeLink` event from within the model of the panel,
-		// which means that form has been submitted.
+		// Execute link command after clicking on balloon panel `Link` button.
 		this.listenTo( panelModel, 'executeLink', () => {
 			editor.execute( 'link', balloonPanel.urlInput.value );
-			this._hidePanel( { focusEditable: true } );
+			this._hidePanel( true );
 		} );
 
-		// Observe `LinkBalloonPanelMode#executeUnlink` event from within the model of the panel,
-		// which means that the `Unlink` button has been clicked.
+		// Execute unlink command after clicking on balloon panel `Unlink` button.
 		this.listenTo( panelModel, 'executeUnlink', () => {
 			editor.execute( 'unlink' );
-			this._hidePanel( { focusEditable: true } );
-		} );
-
-		// Observe `LinkBalloonPanelMode#executeCancel` event from within the model of the panel,
-		// which means that the `Cancel` button has been clicked.
-		this.listenTo( panelModel, 'executeCancel', () => this._hidePanel( { focusEditable: true } ) );
-
-		// Handle `Ctrl+K` keystroke and show panel.
-		editor.keystrokes.set( 'CTRL+K', () => {
-			this._attachPanelToElement();
-			balloonPanel.urlInput.view.select();
+			this._hidePanel( true );
 		} );
 
-		// Attach close by `Esc` press and click out of panel actions on panel show, on panel hide clean up listeners.
-		this.listenTo( balloonPanel.view.model, 'change:isVisible', ( evt, propertyName, value ) => {
-			if ( value ) {
-				// Handle close by `Esc`.
-				balloonPanel.view.listenTo( document, 'keydown', this._closePanelOnEsc.bind( this ) );
-
-				// Handle close by clicking out of the panel.
-				// Note that it is not handled by a `click` event, this is because clicking on link button or directly on link element
-				// opens and closes panel at the same time.
-				balloonPanel.view.listenTo( document, 'mouseup', ( evt, domEvt ) => {
-					// Do nothing when the panel was clicked.
-					if ( balloonPanel.view.element.contains( domEvt.target ) ) {
-						return;
-					}
-
-					// When click was out of the panel then hide it.
-					balloonPanel.view.hide();
-				} );
-			} else {
-				balloonPanel.view.stopListening( document );
-			}
-		} );
+		// Hide balloon panel after clicking on balloon panel `Cancel` button.
+		this.listenTo( panelModel, 'executeCancel', () => this._hidePanel( true ) );
 
-		// Handle click on document and show panel when selection is placed inside the link element.
+		// Handle click on view document and show panel when selection is placed inside the link element.
 		// Keep panel open until selection will be inside the same link element.
-		viewDocument.on( 'click', () => {
+		this.listenTo( viewDocument, 'click', () => {
 			const viewSelection = viewDocument.selection;
 			const parentLink = getPositionParentLink( viewSelection.getFirstPosition() );
 
@@ -206,10 +174,27 @@ export default class Link extends Feature {
 					}
 				} );
 
-				this.listenTo( balloonPanel.view.model, 'change:isVisible', () => this.stopListening( viewDocument ) );
+				this.listenTo( balloonPanel.view.model, 'change:isVisible', () => this.stopListening( viewDocument, 'render' ) );
 			}
 		} );
 
+		// Close on `ESC` press.
+		escPressHandler( {
+			controller: balloonPanel.view,
+			model: balloonPanel.view.model,
+			activeIf: 'isVisible',
+			callback: () => this._hidePanel( true )
+		} );
+
+		// Close on click outside of balloon panel element.
+		clickOutsideHandler( {
+			controller: balloonPanel.view,
+			model: balloonPanel.view.model,
+			activeIf: 'isVisible',
+			contextElement: balloonPanel.view.element,
+			callback: () => this._hidePanel()
+		} );
+
 		// Append panel element to body.
 		editor.ui.add( 'body', balloonPanel );
 
@@ -217,12 +202,12 @@ export default class Link extends Feature {
 	}
 
 	/**
-	 * Shows {@link link#balloonPanel LinkBalloonPanel} and attach to target element.
+	 * Shows {@link link.Link#balloonPanel LinkBalloonPanel} and attach to target element.
 	 * If selection is collapsed and is placed inside link element, then panel will be attached
 	 * to whole link element, otherwise will be attached to the selection.
 	 *
 	 * @private
-	 * @param {core.view.LinkElement} [parentLink] Target element.
+	 * @param {link.LinkElement} [parentLink] Target element.
 	 */
 	_attachPanelToElement( parentLink ) {
 		const viewDocument = this.editor.editing.view;
@@ -246,37 +231,32 @@ export default class Link extends Feature {
 	}
 
 	/**
-	 * Hides {@link link#balloonPanel LinkBalloonPanel}.
+	 * Hides {@link link.Link#balloonPanel LinkBalloonPanel}.
 	 *
 	 * @private
-	 * @param {Object} [options={}] Additional options.
-	 * @param {Boolean} [options.focusEditable=false] When `true` then editable focus will be restored on panel hide.
+	 * @param {Boolean} [focusEditable=false] When `true` then editable focus will be restored on panel hide.
 	 */
-	_hidePanel( options = {} ) {
+	_hidePanel( focusEditable ) {
 		this.balloonPanel.view.hide();
 
-		if ( options.focusEditable ) {
+		if ( focusEditable ) {
 			this.editor.editing.view.focus();
 		}
 	}
 
 	/**
-	 * Hides balloon panel on `ESC` key press event and restores editor focus.
-	 *
-	 * **Note**: this method is `@protected` for testing purposes only.
+	 * Shows {@link link.Link#balloonPanel LinkBalloonPanel}.
 	 *
-	 * @protected
-	 * @param {utils.EventInfo} evt Information about the event.
-	 * @param {KeyboardEvent} domEvt DOM `keydown` event.
+	 * @private
 	 */
-	_closePanelOnEsc( evt, domEvt ) {
-		if ( domEvt.keyCode == keyCodes.esc ) {
-			this._hidePanel( { focusEditable: true } );
-		}
+	_showPanel() {
+		this._attachPanelToElement();
+		this.balloonPanel.urlInput.view.select();
 	}
 }
 
-// Get position parent LinkElement.
+// Try to find if one of the position parent ancestors is a LinkElement,
+// if yes return this element.
 //
 // @private
 // @param {engine.view.Position} position

+ 23 - 55
packages/ckeditor5-link/tests/link.js

@@ -223,64 +223,28 @@ describe( 'Link', () => {
 
 		describe( 'close listeners', () => {
 			describe( 'keyboard', () => {
-				it( 'should listen keyboard events when is open', () => {
-					const escCloseSpy = testUtils.sinon.spy( linkFeature, '_closePanelOnEsc' );
-
+				it( 'should close after `ESC` press', () => {
 					balloonPanel.view.model.isVisible = true;
-					document.dispatchEvent( new Event( 'keydown' ) );
-
-					expect( escCloseSpy.calledOnce ).to.true;
-				} );
-
-				it( 'should not listen keyboard events when is closed', () => {
-					const escCloseSpy = testUtils.sinon.spy( linkFeature, '_closePanelOnEsc' );
-
-					balloonPanel.view.model.isVisible = false;
-					document.dispatchEvent( new Event( 'keydown' ) );
 
-					expect( escCloseSpy.calledOnce ).to.false;
-				} );
-
-				it( 'should stop listening keyboard events after close', () => {
-					const escCloseSpy = testUtils.sinon.spy( linkFeature, '_closePanelOnEsc' );
-
-					balloonPanel.view.model.isVisible = true;
-					balloonPanel.view.model.isVisible = false;
+					dispatchKeyboardEvent( document, 'keydown', keyCodes.esc );
 
-					document.dispatchEvent( new Event( 'keydown' ) );
-
-					expect( escCloseSpy.notCalled ).to.true;
+					expect( hidePanelSpy.calledOnce ).to.true;
+					expect( focusEditableSpy.calledOnce ).to.true;
 				} );
 			} );
 
 			describe( 'mouse', () => {
-				it( 'should not close on click inside the panel', () => {
-					balloonPanel.view.model.isVisible = true;
-					balloonPanel.view.element.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
-
-					expect( hidePanelSpy.notCalled ).to.true;
-				} );
-
 				it( 'should close and not focus editable on click outside the panel', () => {
 					balloonPanel.view.model.isVisible = true;
-					document.dispatchEvent( new Event( 'mouseup' ) );
+					document.body.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
 
 					expect( hidePanelSpy.calledOnce ).to.true;
 					expect( focusEditableSpy.notCalled ).to.true;
 				} );
 
-				it( 'should not listen mouse events before open', () => {
-					balloonPanel.view.model.isVisible = false;
-					document.dispatchEvent( new Event( 'mouseup' ) );
-
-					expect( hidePanelSpy.notCalled ).to.true;
-				} );
-
-				it( 'should stop listening mouse events after closed', () => {
+				it( 'should not close on click inside the panel', () => {
 					balloonPanel.view.model.isVisible = true;
-					balloonPanel.view.model.isVisible = false;
-
-					document.dispatchEvent( new Event( 'mouseup' ) );
+					balloonPanel.view.element.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
 
 					expect( hidePanelSpy.notCalled ).to.true;
 				} );
@@ -424,17 +388,21 @@ describe( 'Link', () => {
 				expect( balloonPanel.view.model.isVisible ).to.false;
 			} );
 		} );
-
-		describe( '_closePanelOnEsc', () => {
-			it( 'should hide panel and focus editable on ESC press', () => {
-				const eventInfo = {};
-				const domEvent = { keyCode: 27 };
-
-				linkFeature._closePanelOnEsc( eventInfo, domEvent );
-
-				expect( hidePanelSpy.calledOnce ).to.true;
-				expect( focusEditableSpy.calledOnce ).to.true;
-			} );
-		} );
 	} );
 } );
+
+// Creates and dispatches keyboard event with specified keyCode.
+//
+// @private
+// @param {EventTarget} eventTarget
+// @param {String} eventName
+// @param {Number} keyCode
+function dispatchKeyboardEvent( element, eventName, keyCode ) {
+	const event = document.createEvent( 'Events' );
+
+	event.initEvent( eventName, true, true );
+	event.which = keyCode;
+	event.keyCode = keyCode;
+
+	element.dispatchEvent( event );
+}