8
0
Просмотр исходного кода

Added test + minor refactor of Link feature.

Oskar Wrobel 9 лет назад
Родитель
Сommit
84950d6897
2 измененных файлов с 256 добавлено и 36 удалено
  1. 56 36
      packages/ckeditor5-link/src/link.js
  2. 200 0
      packages/ckeditor5-link/tests/link.js

+ 56 - 36
packages/ckeditor5-link/src/link.js

@@ -35,31 +35,29 @@ export default class Link extends Feature {
 	 * @inheritDoc
 	 */
 	init() {
-		const editor = this.editor;
-		const viewDocument = editor.editing.view;
-		const linkCommand = editor.commands.get( 'link' );
+		const viewDocument = this.editor.editing.view;
+
+		// Register document click observer.
+		viewDocument.addObserver( ClickObserver );
 
 		/**
-		 * @TODO
+		 * Link balloon panel component.
+		 *
+		 * @member {link.ui.LinkBalloonPanel}
 		 */
 		this.balloonPanel = this._createBalloonPanel();
+
+		// Create toolbar buttons.
 		this._createToolbarLinkButton();
 		this._createToolbarUnlinkButton();
-
-		// Register document click observer
-		viewDocument.addObserver( ClickObserver );
-
-		// Handle click on document and show panel when click target is a link element.
-		viewDocument.on( 'click', () => {
-			if ( viewDocument.selection.isCollapsed && linkCommand.value !== undefined ) {
-				this._attachPanelToElement();
-			}
-		} );
-
-		// Handle Ctrl+l keystroke and show panel.
-		editor.keystrokes.set( 'CTRL+l', () => this._attachPanelToElement() );
 	}
 
+	/**
+	 * Create toolbar link button. Click on button will show
+	 * {@link link.ui.LinkBalloonPanel LinkBalloonPanel} attached to the selection.
+	 *
+	 * @private
+	 */
 	_createToolbarLinkButton() {
 		const editor = this.editor;
 		const viewDocument = editor.editing.view;
@@ -74,7 +72,7 @@ export default class Link extends Feature {
 			icon: 'link'
 		} );
 
-		linkButtonModel.bind( 'url', 'isEnabled' ).to( linkCommand, 'value', 'isEnabled' );
+		linkButtonModel.bind( 'isEnabled' ).to( linkCommand, 'isEnabled' );
 
 		// Show Balloon Panel on button click.
 		this.listenTo( linkButtonModel, 'execute', () => {
@@ -89,6 +87,11 @@ export default class Link extends Feature {
 		editor.ui.featureComponents.add( 'link', ButtonController, ButtonView, linkButtonModel );
 	}
 
+	/**
+	 * Create toolbar unlink button. Click on button will unlink selected link.
+	 *
+	 * @private
+	 */
 	_createToolbarUnlinkButton() {
 		const editor = this.editor;
 		const t = editor.t;
@@ -96,7 +99,7 @@ export default class Link extends Feature {
 
 		// Create button model.
 		const unlinkButtonModel = new Model( {
-			isEnabled: true,
+			isEnabled: false,
 			isOn: false,
 			label: t( 'Unlink' ),
 			icon: 'unlink'
@@ -119,6 +122,9 @@ export default class Link extends Feature {
 	}
 
 	/**
+	 * Create {@link link.ui.LinkBalloonPanel LinkBalloonPanel} instance
+	 * and attach link command to LinkBalloonPanelModel#execute event.
+	 *
 	 *	                       +------------------------------------+
 	 *	                       | <a href="http://foo.com">[foo]</a> |
 	 *	                       +------------------------------------+
@@ -151,11 +157,12 @@ export default class Link extends Feature {
 	 *	                            |                   |Save| |
 	 *	                            |                   +----+ |
 	 *	                            +--------------------------+
+	 * @private
+	 * @returns {link.ui.LinkBalloonPanel} Link balloon panel instance.
 	 */
 	_createBalloonPanel() {
 		const editor = this.editor;
 		const viewDocument = editor.editing.view;
-		const t = editor.t;
 		const linkCommand = editor.commands.get( 'link' );
 
 		// Create the model of the panel.
@@ -170,36 +177,45 @@ export default class Link extends Feature {
 		// Create balloon Panel instance.
 		const balloonPanel = new LinkBalloonPanel( panelModel, new LinkBalloonPanelView( editor.locale ) );
 
-		// Observe #execute event from within the model of the panel, which means that the "Save" button has been clicked.
+		// Observe `LinkBalloonPanelMode#execute` event from within the model of the panel,
+		// which means that the "Save" button has been clicked.
 		this.listenTo( panelModel, 'execute', () => {
-			const urlValue = balloonPanel.urlInput.value;
-
-			// TODO: Validate panelModel#url with some RegExp imported from v4.
-			if ( urlValue ) {
-				editor.execute( 'link', urlValue );
-				balloonPanel.view.hide();
-			} else {
-				window.alert( t( `"${ urlValue }" URL address is incorrect.` ) );
-				balloonPanel.urlInput.view.focus();
-			}
+			editor.execute( 'link', balloonPanel.urlInput.value );
+			balloonPanel.view.hide();
 		} );
 
 		// Always focus editor on panel hide.
 		this.listenTo( panelModel, 'hide', () => viewDocument.focus() );
 
-		// TODO: Create real focus manager.
+		// Hide panel on editor focus.
+		// @TODO replace it by some FocusManager.
 		viewDocument.on( 'focus', () => balloonPanel.view.hide() );
-		viewDocument.on( 'blur', ( evt, domEvtData ) => {
-			if ( domEvtData.domEvent.relatedTarget === balloonPanel.urlInput.input.view.element ) {
-				domEvtData.domEvent.preventDefault();
+
+		// Handle click on document and show panel when selection is placed in in link element.
+		viewDocument.on( 'click', () => {
+			if ( viewDocument.selection.isCollapsed && linkCommand.value !== undefined ) {
+				this._attachPanelToElement();
 			}
 		} );
 
+		// Handle `Ctrl+l` keystroke and show panel.
+		editor.keystrokes.set( 'CTRL+l', () => this._attachPanelToElement() );
+
+		// Append panel element to body.
 		editor.ui.add( 'body', balloonPanel );
 
 		return balloonPanel;
 	}
 
+	/**
+	 * Show {@link link.ui.LinkBalloonPanel 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.
+	 *
+	 * Input inside panel will be focused.
+	 *
+	 * @private
+	 */
 	_attachPanelToElement() {
 		const viewDocument = this.editor.editing.view;
 		const editableViewElement = this.editor.ui.editable.view.element;
@@ -207,18 +223,22 @@ export default class Link extends Feature {
 		const viewSelectionParentAncestors = viewSelectionParent.getAncestors();
 		const linkElement = viewSelectionParentAncestors.find( ( ancestor ) => ancestor.name === 'a' );
 
+		// When selection is inside link element, then attach panel to this element.
 		if ( linkElement ) {
 			this.balloonPanel.view.attachTo(
 				viewDocument.domConverter.getCorrespondingDomElement( linkElement ),
 				editableViewElement
 			);
-		} else {
+		}
+		// Otherwise attach panel to the selection.
+		else {
 			this.balloonPanel.view.attachTo(
 				viewDocument.domConverter.viewRangeToDom( viewDocument.selection.getFirstRange() ),
 				editableViewElement
 			);
 		}
 
+		// Set focus to the panel input.
 		this.balloonPanel.urlInput.view.focus();
 	}
 }

+ 200 - 0
packages/ckeditor5-link/tests/link.js

@@ -0,0 +1,200 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals document */
+/* bender-tags: link */
+
+import ClassicTestEditor from '/tests/core/_utils/classictesteditor.js';
+import testUtils from '/tests/core/_utils/utils.js';
+import { keyCodes } from '/ckeditor5/utils/keyboard.js';
+
+import Link from '/ckeditor5/link/link.js';
+import LinkEngine from '/ckeditor5/link/linkengine.js';
+import Button from '/ckeditor5/ui/button/button.js';
+import LinkBalloonPanel from '/ckeditor5/link/ui/linkballoonpanel.js';
+
+import Range from '/ckeditor5/engine/model/range.js';
+
+import ClickObserver from '/ckeditor5/engine/view/observer/clickobserver.js';
+
+testUtils.createSinonSandbox();
+
+describe( 'Link', () => {
+	let editor, linkFeature, linkButton, unlinkButton, balloonPanel;
+
+	beforeEach( () => {
+		const editorElement = document.createElement( 'div' );
+		document.body.appendChild( editorElement );
+
+		return ClassicTestEditor.create( editorElement, {
+			features: [ Link ]
+		} )
+			.then( newEditor => {
+				editor = newEditor;
+
+				linkFeature = editor.plugins.get( Link );
+				linkButton = editor.ui.featureComponents.create( 'link' );
+				unlinkButton = editor.ui.featureComponents.create( 'unlink' );
+				balloonPanel = linkFeature.balloonPanel;
+			} );
+	} );
+
+	afterEach( () => {
+		return editor.destroy();
+	} );
+
+	it( 'should be loaded', () => {
+		expect( linkFeature ).to.instanceOf( Link );
+	} );
+
+	it( 'should load LinkEngine', () => {
+		expect( editor.plugins.get( LinkEngine ) ).to.instanceOf( LinkEngine );
+	} );
+
+	it( 'should register click observer', () => {
+		expect( editor.editing.view.getObserver( ClickObserver ) ).to.instanceOf( ClickObserver );
+	} );
+
+	describe( 'link toolbar button', () => {
+		it( 'should register link feature component', () => {
+			expect( linkButton ).to.instanceOf( Button );
+		} );
+
+		it( 'should bind linkButton#model to link command', () => {
+			const model = linkButton.model;
+			const command = editor.commands.get( 'link' );
+
+			expect( model.isEnabled ).to.be.true;
+
+			command.isEnabled = false;
+			expect( model.isEnabled ).to.be.false;
+		} );
+
+		it( 'should open panel on linkButton#model execute event, when editor is focused', () => {
+			editor.editing.view.focus();
+
+			linkButton.model.fire( 'execute' );
+
+			expect( linkFeature.balloonPanel.view.isVisible ).to.true;
+		} );
+
+		it( 'should not open panel on linkButton#model execute event, when editor is not focused', () => {
+			linkButton.model.fire( 'execute' );
+
+			expect( linkFeature.balloonPanel.view.isVisible ).to.false;
+		} );
+	} );
+
+	describe( 'unlink toolbar button', () => {
+		it( 'should register unlink feature component', () => {
+			expect( unlinkButton ).to.instanceOf( Button );
+		} );
+
+		it( 'should bind unlinkButton#model to unlink command', () => {
+			const model = unlinkButton.model;
+			const command = editor.commands.get( 'unlink' );
+
+			expect( model.isEnabled ).to.false;
+
+			command.hasValue = true;
+			expect( model.isEnabled ).to.true;
+		} );
+
+		it( 'should execute unlink command on unlinkButton#model execute event', () => {
+			const executeSpy = testUtils.sinon.spy( editor, 'execute' );
+
+			unlinkButton.model.fire( 'execute' );
+
+			expect( executeSpy.calledOnce ).to.true;
+			expect( executeSpy.calledWithExactly( 'unlink' ) ).to.true;
+		} );
+
+		it( 'should hide panel on unlinkButton#model execute event', () => {
+			balloonPanel.view.model.isVisible = true;
+
+			unlinkButton.model.fire( 'execute' );
+
+			expect( balloonPanel.view.model.isVisible ).to.false;
+		} );
+	} );
+
+	describe( 'link panel', () => {
+		it( 'should create LinkBalloonPanel component', () => {
+			expect( balloonPanel ).to.instanceOf( LinkBalloonPanel );
+		} );
+
+		it( 'should bind balloonPanel#model to link command', () => {
+			const model = balloonPanel.model;
+			const command = editor.commands.get( 'link' );
+
+			expect( model.url ).to.undefined;
+
+			command.value = 'http://cksource.com';
+
+			expect( model.url ).to.equal( 'http://cksource.com' );
+		} );
+
+		it( 'should execute link command on balloonPanel#model execute event', () => {
+			const executeSpy = testUtils.sinon.spy( editor, 'execute' );
+
+			balloonPanel.model.url = 'http://cksource.com';
+			balloonPanel.model.fire( 'execute' );
+
+			expect( executeSpy.calledOnce ).to.true;
+			expect( executeSpy.calledWithExactly( 'link', 'http://cksource.com' ) ).to.true;
+		} );
+
+		it( 'should append panel element to the body', () => {
+			expect( document.body.contains( balloonPanel.view.element ) );
+		} );
+
+		it( 'should open panel on `CTRL+L` keystroke', () => {
+			editor.keystrokes.press( { keyCode: keyCodes.l, ctrlKey: true } );
+
+			expect( balloonPanel.view.model.isVisible ).to.true;
+		} );
+
+		it( 'should focus editor on balloonPanel#model hide event', () => {
+			balloonPanel.model.fire( 'hide' );
+
+			expect( editor.editing.view.isFocused ).to.true;
+		} );
+
+		it( 'should hide panel on editor focus event', () => {
+			balloonPanel.view.model.isVisible = true;
+
+			editor.editing.view.focus();
+
+			expect( balloonPanel.view.model.isVisible ).to.false;
+		} );
+
+		it( 'should show panel on editor click, when selection is inside link element', () => {
+			const modelRoot = editor.document.getRoot();
+			const observer = editor.editing.view.getObserver( ClickObserver );
+			const linkCommand = editor.commands.get( 'link' );
+
+			// Insert link element to document and put selection inside this element `<a href="url">so{}me url</a>`.
+			editor.document.schema.allow( { name: '$text', inside: '$root' } );
+			editor.setData( '<a href="url">some url</a>' );
+			editor.document.selection.setRanges( [ Range.createFromParentsAndOffsets( modelRoot, 2, modelRoot, 2 ) ] );
+			linkCommand.value = 'url';
+
+			observer.fire( 'click', { target: document.body } );
+
+			expect( balloonPanel.view.model.isVisible ).to.true;
+		} );
+
+		it( 'should not show panel on editor click, when selection is not inside link element', () => {
+			const modelRoot = editor.document.getRoot();
+			const observer = editor.editing.view.getObserver( ClickObserver );
+
+			editor.document.selection.setRanges( [ Range.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 0 ) ] );
+
+			observer.fire( 'click', { target: document.body } );
+
+			expect( balloonPanel.view.model.isVisible ).to.false;
+		} );
+	} );
+} );