8
0
Pārlūkot izejas kodu

Flattened the UI structure of the Link feature using the new Template/View API.

Aleksander Nowodzinski 9 gadi atpakaļ
vecāks
revīzija
93553bb642

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

@@ -12,11 +12,18 @@ 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 Button from '../ui/button/button.js';
 import ButtonView from '../ui/button/buttonview.js';
 
-import LinkBalloonPanel from './ui/linkballoonpanel.js';
-import LinkBalloonPanelView from './ui/linkballoonpanelview.js';
+import BalloonPanel from '../ui/balloonpanel/balloonpanel.js';
+import BalloonPanelView from '../ui/balloonpanel/balloonpanelview.js';
+
+import LinkForm from './ui/linkform.js';
+import LinkFormView from './ui/linkformview.js';
+
+import LabeledInput from '../ui/labeledinput/labeledinput.js';
+import InputText from '../ui/inputtext/inputtext.js';
+import InputTextView from '../ui/inputtext/inputtextview.js';
 
 /**
  * The link feature. It introduces the Link and Unlink buttons and the <kbd>Ctrl+K</kbd> keystroke.
@@ -41,15 +48,29 @@ export default class Link extends Feature {
 		this.editor.editing.view.addObserver( ClickObserver );
 
 		/**
-		 * Link balloon panel component.
+		 * Balloon panel component to display the main UI.
 		 *
 		 * @member {link.ui.LinkBalloonPanel} link.Link#balloonPanel
 		 */
 		this.balloonPanel = this._createBalloonPanel();
 
+		/**
+		 * The form component inside {@link link.Link#balloonPanel}.
+		 *
+		 * @member {link.ui.LinkForm} link.Link#form
+		 */
+		this.form = this._createForm();
+
 		// Create toolbar buttons.
 		this._createToolbarLinkButton();
 		this._createToolbarUnlinkButton();
+
+		/**
+		 * The URL input inside {@link link.Link#form}.
+		 *
+		 * @protected
+		 * @member {ui.input.labeled.LabeledInput} think.Link#_urlInput
+		 */
 	}
 
 	/**
@@ -79,7 +100,7 @@ export default class Link extends Feature {
 		this.listenTo( linkButtonModel, 'execute', () => this._showPanel() );
 
 		// Add link button to feature components.
-		editor.ui.featureComponents.add( 'link', ButtonController, ButtonView, linkButtonModel );
+		editor.ui.featureComponents.add( 'link', Button, ButtonView, linkButtonModel );
 
 		// Handle `Ctrl+K` keystroke and show panel.
 		editor.keystrokes.set( 'CTRL+K', () => this._showPanel() );
@@ -113,13 +134,11 @@ export default class Link extends Feature {
 		} );
 
 		// Add unlink button to feature components.
-		editor.ui.featureComponents.add( 'unlink', ButtonController, ButtonView, unlinkButtonModel );
+		editor.ui.featureComponents.add( 'unlink', Button, ButtonView, unlinkButtonModel );
 	}
 
 	/**
-	 * Creates the {@link link.ui.LinkBalloonPanel} instance,
-	 * attaches {@link link.LinkBalloonPanelModel} events to the link and unlink commands
-	 * and applies behaviors specific for this panel.
+	 * Creates the {@link link.ui.LinkBalloonPanel} instance.
 	 *
 	 * @private
 	 * @returns {link.ui.LinkBalloonPanel} Link balloon panel instance.
@@ -127,38 +146,19 @@ export default class Link extends Feature {
 	_createBalloonPanel() {
 		const editor = this.editor;
 		const viewDocument = editor.editing.view;
-		const linkCommand = editor.commands.get( 'link' );
-
-		// Create the model of the panel.
-		const panelModel = new Model( {
-			maxWidth: 300
-		} );
-
-		// Bind panel model to command.
-		panelModel.bind( 'url' ).to( linkCommand, 'value' );
 
 		// Create the balloon panel instance.
-		const balloonPanel = new LinkBalloonPanel( panelModel, new LinkBalloonPanelView( editor.locale ) );
+		const balloonPanel = new BalloonPanel(
+			new Model( {
+				maxWidth: 300
+			} ),
+			new BalloonPanelView( editor.locale )
+		);
 
 		// Add balloonPanel.view#element to FocusTracker.
 		// @TODO: Do it automatically ckeditor5-core#23
 		editor.focusTracker.add( balloonPanel.view.element );
 
-		// Execute link command after clicking on balloon panel `Link` button.
-		this.listenTo( panelModel, 'executeLink', () => {
-			editor.execute( 'link', balloonPanel.urlInput.value );
-			this._hidePanel( true );
-		} );
-
-		// Execute unlink command after clicking on balloon panel `Unlink` button.
-		this.listenTo( panelModel, 'executeUnlink', () => {
-			editor.execute( 'unlink' );
-			this._hidePanel( true );
-		} );
-
-		// Hide balloon panel after clicking on balloon panel `Cancel` button.
-		this.listenTo( panelModel, 'executeCancel', () => this._hidePanel( true ) );
-
 		// 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.
 		this.listenTo( viewDocument, 'click', () => {
@@ -199,13 +199,81 @@ export default class Link extends Feature {
 			callback: () => this._hidePanel()
 		} );
 
-		// Append panel element to body.
 		editor.ui.add( 'body', balloonPanel );
 
 		return balloonPanel;
 	}
 
 	/**
+	 * Creates the {@link link.ui.LinkForm} instance.
+	 *
+	 * @private
+	 * @returns {link.ui.LinkForm} Link form instance.
+	 */
+	_createForm() {
+		const editor = this.editor;
+		const t = this.editor.t;
+		const linkCommand = editor.commands.get( 'link' );
+
+		const formView = new LinkFormView( editor.locale );
+		const form = new LinkForm( new Model(), formView );
+
+		// Create component models.
+		const urlInputModel = new Model( {
+			label: t( 'Link URL' )
+		} );
+
+		urlInputModel.bind( 'value' ).to( linkCommand, 'value' );
+
+		const saveButtonModel = new Model( {
+			isEnabled: true,
+			isOn: false,
+			label: t( 'Save' ),
+			withText: true,
+			type: 'submit'
+		} );
+
+		const cancelButtonModel = new Model( {
+			isEnabled: true,
+			isOn: false,
+			label: t( 'Cancel' ),
+			withText: true
+		} );
+
+		const unlinkButtonModel = new Model( {
+			isEnabled: true,
+			isOn: false,
+			label: t( 'Unlink' ),
+			icon: 'unlink'
+		} );
+
+		// Add components to the form.
+		form.add( this._urlInput = new LabeledInput( urlInputModel, formView.urlInputView, InputText, InputTextView, new Model() ) );
+		form.add( this._saveButton = new Button( saveButtonModel, formView.saveButtonView ) );
+		form.add( this._cancelButton = new Button( cancelButtonModel, formView.cancelButtonView ) );
+		form.add( this._unlinkButton = new Button( unlinkButtonModel, formView.unlinkButtonView ) );
+
+		// Execute link command after clicking on balloon panel `Link` button.
+		this.listenTo( form.model, 'submit', () => {
+			editor.execute( 'link', this._urlInput.value );
+			this._hidePanel( true );
+		} );
+
+		// Execute unlink command after clicking on balloon panel `Unlink` button.
+		this.listenTo( unlinkButtonModel, 'execute', () => {
+			editor.execute( 'unlink' );
+			this._hidePanel( true );
+		} );
+
+		// Hide balloon panel after clicking on balloon panel `Cancel` button.
+		this.listenTo( cancelButtonModel, 'execute', () => this._hidePanel( true ) );
+
+		this.balloonPanel.add( 'content', form );
+
+		return form;
+	}
+
+	/**
 	 * 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.
@@ -255,7 +323,7 @@ export default class Link extends Feature {
 	 */
 	_showPanel() {
 		this._attachPanelToElement();
-		this.balloonPanel.urlInput.view.select();
+		this._urlInput.view.select();
 	}
 }
 

+ 0 - 202
packages/ckeditor5-link/src/ui/linkballoonpanel.js

@@ -1,202 +0,0 @@
-/**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import Model from '../../ui/model.js';
-import Button from '../../ui/button/button.js';
-import ButtonView from '../../ui/button/buttonview.js';
-import BalloonPanel from '../../ui/balloonpanel/balloonpanel.js';
-import LabeledInput from '../../ui/labeledinput/labeledinput.js';
-import LabeledInputView from '../../ui/labeledinput/labeledinputview.js';
-import LinkForm from './linkform.js';
-import LinkFormView from './linkformview.js';
-import InputText from '../../ui/inputtext/inputtext.js';
-import InputTextView from '../../ui/inputtext/inputtextview.js';
-
-/**
- * The link balloon panel controller class.
- *
- *		const model = new Model( {
- *			maxWidth: 300,
- *			url: 'http://ckeditor.com'
- *		} );
- *
- *		// An instance of LinkBalloonPanel.
- *		new LinkBalloonPanel( model, new LinkBalloonPanelView() );
- *
- * See {@link link.ui.LinkBalloonPanelView}.
- *
- * @memberOf link.ui
- * @extends ui.balloonPanel.BalloonPanel
- */
-export default class LinkBalloonPanel extends BalloonPanel {
-	/**
-	 * Creates an instance of {@link link.ui.LinkBalloonPanel} class.
-	 *
-	 * @param {link.ui.LinkBalloonPanelModel} model Model of this link balloon panel.
-	 * @param {ui.View} view View of this link balloon panel.
-	 */
-	constructor( model, view ) {
-		super( model, view );
-
-		this.add( 'content', this._createForm() );
-	}
-
-	/**
-	 * Initializes {@link link.ui.LinkForm} component with input and buttons.
-	 *
-	 * @private
-	 * @returns {link.ui.LinkForm} Form component.
-	 */
-	_createForm() {
-		const formModel = new Model();
-
-		formModel.on( 'execute', () => this.model.fire( 'executeLink' ) );
-
-		/**
-		 * An instance of {@link link.ui.LinkForm} component.
-		 *
-		 * @member {link.ui.LinkForm} link.ui.LinkBalloonPanel#form
-		 */
-		this.form = new LinkForm( formModel, new LinkFormView( this.locale ) );
-
-		/**
-		 * The button component for submitting form.
-		 *
-		 * @member {ui.button.Button} link.ui.LinkBalloonPanel#saveButton
-		 */
-		this.saveButton = this._createSaveButton();
-
-		/**
-		 * The button component for canceling form.
-		 *
-		 * @member {ui.button.Button} link.ui.LinkBalloonPanel#cancelButton
-		 */
-		this.cancelButton = this._createCancelButton();
-
-		/**
-		 * The button component for unlinking.
-		 *
-		 * @member {ui.button.Button} link.ui.LinkBalloonPanel#unlinkButton
-		 */
-		this.unlinkButton = this._createUnlinkButton();
-
-		// Add Input to the form content.
-		this.form.add( 'content', this._createLabeledInput() );
-
-		// Add `Save` and `Cancel` buttons to the form actions.
-		this.form.add( 'actions', this.saveButton );
-		this.form.add( 'actions', this.cancelButton );
-		this.form.add( 'actions', this.unlinkButton );
-
-		return this.form;
-	}
-
-	/**
-	 * Initializes the {@link ui.input.LabeledInput LabeledInput} which displays
-	 * and allows manipulation of the `href` attribute in edited link.
-	 *
-	 * @private
-	 * @returns {ui.input.LabeledInput} Labeled input component.
-	 */
-	_createLabeledInput() {
-		const t = this.view.t;
-		const model = new Model( {
-			label: t( 'Link URL' )
-		} );
-
-		model.bind( 'value' ).to( this.model, 'url' );
-
-		/**
-		 * The input component to display and manipulate the `href` attribute.
-		 *
-		 * @member {ui.input.LabeledInput} link.ui.LinkBalloonPanel#urlInput
-		 */
-		return ( this.urlInput = new LabeledInput( model, new LabeledInputView( this.locale ),
-			InputText, InputTextView, new Model() ) );
-	}
-
-	/**
-	 * Initializes the {@link ui.button.Button} for submitting the form.
-	 *
-	 * @private
-	 * @returns {ui.button.Button} Save button component.
-	 */
-	_createSaveButton() {
-		const t = this.view.t;
-		const saveModel = new Model( {
-			isEnabled: true,
-			isOn: false,
-			label: t( 'Save' ),
-			withText: true,
-			type: 'submit'
-		} );
-
-		const button = new Button( saveModel, new ButtonView( this.locale ) );
-
-		button.view.element.classList.add( 'ck-button-action' );
-
-		return button;
-	}
-
-	/**
-	 * Initializes the {@link ui.button.Button Button} for canceling the form.
-	 *
-	 * @private
-	 * @returns {ui.button.Button} Cancel button component.
-	 */
-	_createCancelButton() {
-		const t = this.view.t;
-		const cancelModel = new Model( {
-			isEnabled: true,
-			isOn: false,
-			label: t( 'Cancel' ),
-			withText: true
-		} );
-
-		cancelModel.on( 'execute', () => this.model.fire( 'executeCancel' ) );
-
-		return new Button( cancelModel, new ButtonView( this.locale ) );
-	}
-
-	/**
-	 * Initializes the {@link ui.button.Button Button} for the `unlink` command.
-	 *
-	 * @private
-	 * @returns {ui.button.Button} Unlink button component.
-	 */
-	_createUnlinkButton() {
-		const t = this.view.t;
-		const unlinkModel = new Model( {
-			isEnabled: true,
-			isOn: false,
-			label: t( 'Unlink' ),
-			icon: 'unlink'
-		} );
-
-		unlinkModel.on( 'execute', () => this.model.fire( 'executeUnlink' ) );
-
-		return new Button( unlinkModel, new ButtonView( this.locale ) );
-	}
-}
-
-/**
- * The link balloon panel component {@link ui.Model} interface.
- *
- * @extends ui.balloonPanel.BalloonPanelModel
- * @interface link.ui.LinkBalloonPanelModel
- */
-
-/**
- * URL of the link displayed in the {@link link.ui.LinkBalloonPanel#urlInput}.
- *
- * @observable
- * @member {String} link.ui.LinkBalloonPanelModel#url
- */
-
-/**
- * Fired when {@link link.ui.LinkBalloonPanel#saveButton} has been executed by the user.
- *
- * @event link.ui.LinkBalloonPanelModel#execute
- */

+ 0 - 32
packages/ckeditor5-link/src/ui/linkballoonpanelview.js

@@ -1,32 +0,0 @@
-/**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import Template from '../../ui/template.js';
-import BalloonPanelView from '../../ui/balloonpanel/balloonpanelview.js';
-
-/**
- * The link balloon panel view class.
- *
- * See {@link link.ui.LinkBalloonPanel}.
- *
- * @memberOf link.ui
- * @extends ui.balloonPanel.BalloonPanelView
- */
-export default class LinkBalloonPanelView extends BalloonPanelView {
-	/**
-	 * @inheritDoc
-	 */
-	constructor( locale ) {
-		super( locale );
-
-		Template.extend( this.template, {
-			attributes: {
-				class: [
-					'ck-link-balloon-panel',
-				]
-			}
-		} );
-	}
-}

+ 3 - 3
packages/ckeditor5-link/src/ui/linkform.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-import Form from '../../ui/form/form.js';
+import Controller from '../../ui/controller.js';
 
 /**
  * The link form class.
@@ -15,7 +15,7 @@ import Form from '../../ui/form/form.js';
  * @memberOf link.ui
  * @extends ui.form.Form
  */
-export default class LinkForm extends Form {
+export default class LinkForm extends Controller {
 	/**
 	 * Creates an instance of {@link link.ui.LinkForm} class.
 	 *
@@ -25,7 +25,7 @@ export default class LinkForm extends Form {
 	constructor( model, view ) {
 		super( model, view );
 
-		this.addCollection( 'actions' );
+		view.delegate( 'submit' ).to( model );
 	}
 }
 

+ 47 - 10
packages/ckeditor5-link/src/ui/linkformview.js

@@ -3,8 +3,11 @@
  * For licensing, see LICENSE.md.
  */
 
+import View from '../../ui/view.js';
 import Template from '../../ui/template.js';
-import FormView from '../../ui/form/formview.js';
+
+import ButtonView from '../../ui/button/buttonview.js';
+import LabeledInputView from '../../ui/labeledinput/labeledinputview.js';
 
 /**
  * The link form view controller class.
@@ -14,28 +17,62 @@ import FormView from '../../ui/form/formview.js';
  * @memberOf link.ui
  * @extends ui.form.FormView
  */
-export default class LinkFormView extends FormView {
+export default class LinkFormView extends View {
 	/**
 	 * @inheritDoc
 	 */
 	constructor( locale ) {
 		super( locale );
 
-		Template.extend( this.template, {
+		const bind = this.bindTemplate;
+
+		this.urlInputView = new LabeledInputView( locale );
+		this.saveButtonView = new ButtonView( locale );
+		this.cancelButtonView = new ButtonView( locale );
+		this.unlinkButtonView = new ButtonView( locale );
+
+		Template.extend( this.saveButtonView.template, {
 			attributes: {
 				class: [
-					'ck-link-form',
+					'ck-button-action'
 				]
 			}
 		} );
 
-		this.template.children.add( new Template( {
-			tag: 'div',
+		this.template = new Template( {
+			tag: 'form',
+
 			attributes: {
-				class: 'ck-link-form__actions'
-			}
-		} ) );
+				class: [
+					'ck-link-form',
+				]
+			},
+
+			children: [
+				this.urlInputView,
+				{
+					tag: 'div',
+
+					attributes: {
+						class: [
+							'ck-link-form__actions'
+						]
+					},
 
-		this.register( 'actions', 'div.ck-link-form__actions' );
+					children: [
+						this.saveButtonView,
+						this.cancelButtonView,
+						this.unlinkButtonView
+					]
+				}
+			],
+
+			on: {
+				submit: bind.to( evt => {
+					evt.preventDefault();
+					this.fire( 'submit' );
+				} )
+			}
+		} );
 	}
 }

+ 69 - 59
packages/ckeditor5-link/tests/link.js

@@ -13,7 +13,7 @@ import { setData as setModelData } from '/ckeditor5/engine/dev-utils/model.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 BalloonPanel from '/ckeditor5/ui/balloonpanel/balloonpanel.js';
 
 import Range from '/ckeditor5/engine/view/range.js';
 import ClickObserver from '/ckeditor5/engine/view/observer/clickobserver.js';
@@ -21,7 +21,7 @@ import ClickObserver from '/ckeditor5/engine/view/observer/clickobserver.js';
 testUtils.createSinonSandbox();
 
 describe( 'Link', () => {
-	let editor, linkFeature, linkButton, unlinkButton, balloonPanel, editorElement;
+	let editor, linkFeature, linkButton, unlinkButton, balloonPanel, form, editorElement;
 
 	beforeEach( () => {
 		editorElement = document.createElement( 'div' );
@@ -39,6 +39,7 @@ describe( 'Link', () => {
 			linkButton = editor.ui.featureComponents.create( 'link' );
 			unlinkButton = editor.ui.featureComponents.create( 'unlink' );
 			balloonPanel = linkFeature.balloonPanel;
+			form = linkFeature.form;
 		} );
 	} );
 
@@ -108,7 +109,7 @@ describe( 'Link', () => {
 		} );
 
 		it( 'should select panel input value when panel is opened', () => {
-			const selectUrlInputSpy = testUtils.sinon.spy( balloonPanel.urlInput.view, 'select' );
+			const selectUrlInputSpy = testUtils.sinon.spy( linkFeature._urlInput.view, 'select' );
 
 			editor.editing.view.isFocused = true;
 
@@ -152,7 +153,7 @@ describe( 'Link', () => {
 		} );
 
 		it( 'should be created', () => {
-			expect( balloonPanel ).to.instanceOf( LinkBalloonPanel );
+			expect( balloonPanel ).to.instanceOf( BalloonPanel );
 		} );
 
 		it( 'should be appended to the document body', () => {
@@ -160,7 +161,7 @@ describe( 'Link', () => {
 		} );
 
 		it( 'should open with selected url input on `CTRL+K` keystroke', () => {
-			const selectUrlInputSpy = testUtils.sinon.spy( balloonPanel.urlInput.view, 'select' );
+			const selectUrlInputSpy = testUtils.sinon.spy( linkFeature._urlInput.view, 'select' );
 
 			editor.keystrokes.press( { keyCode: keyCodes.k, ctrlKey: true } );
 
@@ -176,59 +177,6 @@ describe( 'Link', () => {
 			expect( editor.focusTracker.isFocused ).to.true;
 		} );
 
-		describe( 'binding', () => {
-			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 executeLink event', () => {
-				const executeSpy = testUtils.sinon.spy( editor, 'execute' );
-
-				balloonPanel.model.url = 'http://cksource.com';
-				balloonPanel.model.fire( 'executeLink' );
-
-				expect( executeSpy.calledOnce ).to.true;
-				expect( executeSpy.calledWithExactly( 'link', 'http://cksource.com' ) ).to.true;
-			} );
-
-			it( 'should hide and focus editable on balloonPanel#model executeLink event', () => {
-				balloonPanel.model.fire( 'executeLink' );
-
-				expect( hidePanelSpy.calledOnce ).to.true;
-				expect( focusEditableSpy.calledOnce ).to.true;
-			} );
-
-			it( 'should execute unlink command on balloonPanel#model executeUnlink event', () => {
-				const executeSpy = testUtils.sinon.spy( editor, 'execute' );
-
-				balloonPanel.model.fire( 'executeUnlink' );
-
-				expect( executeSpy.calledOnce ).to.true;
-				expect( executeSpy.calledWithExactly( 'unlink' ) ).to.true;
-			} );
-
-			it( 'should hide and focus editable on balloonPanel#model executeUnlink event', () => {
-				balloonPanel.model.fire( 'executeUnlink' );
-
-				expect( hidePanelSpy.calledOnce ).to.true;
-				expect( focusEditableSpy.calledOnce ).to.true;
-			} );
-
-			it( 'should hide and focus editable on balloonPanel#model executeCancel event', () => {
-				balloonPanel.model.fire( 'executeCancel' );
-
-				expect( hidePanelSpy.calledOnce ).to.true;
-				expect( focusEditableSpy.calledOnce ).to.true;
-			} );
-		} );
-
 		describe( 'close listeners', () => {
 			describe( 'keyboard', () => {
 				it( 'should close after `ESC` press', () => {
@@ -261,7 +209,7 @@ describe( 'Link', () => {
 
 		describe( 'click on editable', () => {
 			it( 'should open with not selected url input when collapsed selection is inside link element', () => {
-				const selectUrlInputSpy = testUtils.sinon.spy( balloonPanel.urlInput.view, 'select' );
+				const selectUrlInputSpy = testUtils.sinon.spy( linkFeature._urlInput.view, 'select' );
 				const observer = editor.editing.view.getObserver( ClickObserver );
 
 				editor.document.schema.allow( { name: '$text', inside: '$root' } );
@@ -397,6 +345,68 @@ describe( 'Link', () => {
 			} );
 		} );
 	} );
+
+	describe( 'link form', () => {
+		let hidePanelSpy, focusEditableSpy;
+
+		beforeEach( () => {
+			hidePanelSpy = testUtils.sinon.spy( balloonPanel.view, 'hide' );
+			focusEditableSpy = testUtils.sinon.spy( editor.editing.view, 'focus' );
+		} );
+
+		describe( 'binding', () => {
+			it( 'should bind _urlInput#model to link command', () => {
+				const model = linkFeature._urlInput.model;
+				const command = editor.commands.get( 'link' );
+
+				expect( model.value ).to.undefined;
+
+				command.value = 'http://cksource.com';
+
+				expect( model.value ).to.equal( 'http://cksource.com' );
+			} );
+
+			it( 'should execute link command on _saveButton#model submit event', () => {
+				const executeSpy = testUtils.sinon.spy( editor, 'execute' );
+
+				linkFeature._urlInput.model.value = 'http://cksource.com';
+				form.model.fire( 'submit' );
+
+				expect( executeSpy.calledOnce ).to.true;
+				expect( executeSpy.calledWithExactly( 'link', 'http://cksource.com' ) ).to.true;
+			} );
+
+			it( 'should hide and focus editable on form#model submit event', () => {
+				form.model.fire( 'submit' );
+
+				expect( hidePanelSpy.calledOnce ).to.true;
+				expect( focusEditableSpy.calledOnce ).to.true;
+			} );
+
+			it( 'should execute unlink command on _unlinkButton#model execute event', () => {
+				const executeSpy = testUtils.sinon.spy( editor, 'execute' );
+
+				linkFeature._unlinkButton.model.fire( 'execute' );
+
+				expect( executeSpy.calledOnce ).to.true;
+				expect( executeSpy.calledWithExactly( 'unlink' ) ).to.true;
+			} );
+
+			it( 'should hide and focus editable on _unlinkButton#model execute event', () => {
+				linkFeature._unlinkButton.model.fire( 'execute' );
+
+				expect( hidePanelSpy.calledOnce ).to.true;
+				expect( focusEditableSpy.calledOnce ).to.true;
+			} );
+
+			it( 'should hide and focus editable on _cancelButton#model cancel event', () => {
+				linkFeature._cancelButton.model.fire( 'execute' );
+
+				expect( hidePanelSpy.calledOnce ).to.true;
+				expect( focusEditableSpy.calledOnce ).to.true;
+			} );
+		} );
+	} );
 } );
 
 // Creates and dispatches keyboard event with specified keyCode.

+ 0 - 145
packages/ckeditor5-link/tests/ui/linkballoonpanel.js

@@ -1,145 +0,0 @@
-/**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/* bender-tags: ui, balloonPanel */
-/* global MouseEvent */
-
-import LinkBalloonPanel from '/ckeditor5/link/ui/linkballoonpanel.js';
-import LinkBalloonPanelView from '/ckeditor5/link/ui/linkballoonpanelview.js';
-import BalloonPanel from '/ckeditor5/ui/balloonpanel/balloonpanel.js';
-import Model from '/ckeditor5/ui/model.js';
-
-import LinkForm from '/ckeditor5/link/ui/linkform.js';
-import LabeledInput from '/ckeditor5/ui/labeledinput/labeledinput.js';
-import Button from '/ckeditor5/ui/button/button.js';
-
-import LocaleMock from '/tests/utils/_utils/locale-mock.js';
-
-describe( 'LinkBalloonPanel', () => {
-	let model, linkBalloonPanel, view;
-
-	beforeEach( () => {
-		model = new Model( {
-			maxWidth: 200,
-			url: 'http://ckeditor.com'
-		} );
-
-		view = new LinkBalloonPanelView( new LocaleMock() );
-		linkBalloonPanel = new LinkBalloonPanel( model, view );
-	} );
-
-	describe( 'constructor', () => {
-		it( 'should extend BalloonPanel class', () => {
-			expect( linkBalloonPanel ).to.be.instanceOf( BalloonPanel );
-		} );
-
-		describe( 'child components', () => {
-			describe( 'form', () => {
-				it( 'should be created', () => {
-					expect( linkBalloonPanel.form ).to.instanceof( LinkForm );
-				} );
-
-				it( 'should be appended to "content" collection', () => {
-					expect( linkBalloonPanel.collections.get( 'content' ).get( 0 ) ).to.deep.equal( linkBalloonPanel.form );
-				} );
-
-				it( 'should fire model#executeLink event on form.model#execute event', () => {
-					const executeSpy = sinon.spy();
-
-					model.on( 'executeLink', executeSpy );
-
-					linkBalloonPanel.form.model.fire( 'execute' );
-
-					expect( executeSpy.calledOnce ).to.true;
-				} );
-			} );
-
-			describe( 'urlInput', () => {
-				it( 'should be created', () => {
-					expect( linkBalloonPanel.urlInput ).to.instanceof( LabeledInput );
-				} );
-
-				it( 'should be appended to the form "content" collection', () => {
-					expect( linkBalloonPanel.form.collections.get( 'content' ).get( 0 ) ).to.deep.equal( linkBalloonPanel.urlInput );
-				} );
-
-				it( 'should bind model#url to urlInput.model#value', () => {
-					expect( linkBalloonPanel.urlInput.model.value ).to.equal( model.url ).to.equal( 'http://ckeditor.com' );
-
-					model.url = 'http://cksource.com';
-
-					expect( linkBalloonPanel.urlInput.model.value ).to.equal( 'http://cksource.com' );
-				} );
-			} );
-
-			describe( 'saveButton', () => {
-				it( 'should be created', () => {
-					expect( linkBalloonPanel.saveButton ).to.instanceof( Button );
-				} );
-
-				it( 'should be appended to the form "actions" collection', () => {
-					expect( linkBalloonPanel.form.collections.get( 'actions' ).get( 0 ) ).to.deep.equal( linkBalloonPanel.saveButton );
-				} );
-
-				it( 'should fire model#executeLink event on DOM click event', ( done ) => {
-					const executeSpy = sinon.spy();
-
-					model.on( 'executeLink', executeSpy );
-
-					linkBalloonPanel.init().then( () => {
-						linkBalloonPanel.saveButton.view.element.dispatchEvent( new MouseEvent( 'click' ) );
-
-						expect( executeSpy.calledOnce ).to.true;
-						done();
-					} );
-				} );
-
-				it( 'should be a type `submit`', () => {
-					expect( linkBalloonPanel.saveButton.model.type ).to.equal( 'submit' );
-				} );
-			} );
-
-			describe( 'cancelButton', () => {
-				it( 'should be created', () => {
-					expect( linkBalloonPanel.cancelButton ).to.instanceof( Button );
-				} );
-
-				it( 'should be appended to the form "actions" collection', () => {
-					expect( linkBalloonPanel.form.collections.get( 'actions' ).get( 1 ) ).to.deep.equal( linkBalloonPanel.cancelButton );
-				} );
-
-				it( 'should fire model#executeCancel event on cancelButton.model#execute event', () => {
-					const executeSpy = sinon.spy();
-
-					model.on( 'executeCancel', executeSpy );
-
-					linkBalloonPanel.cancelButton.model.fire( 'execute' );
-
-					expect( executeSpy.calledOnce ).to.true;
-				} );
-			} );
-
-			describe( 'unlinkButton', () => {
-				it( 'should be created', () => {
-					expect( linkBalloonPanel.unlinkButton ).to.instanceof( Button );
-				} );
-
-				it( 'should be appended to the form "actions" collection', () => {
-					expect( linkBalloonPanel.form.collections.get( 'actions' ).get( 2 ) ).to.deep.equal( linkBalloonPanel.unlinkButton );
-				} );
-
-				it( 'should fire model#executeUnlink event on unlinkButton.model#execute event', () => {
-					const executeUnlinkSpy = sinon.spy();
-
-					model.on( 'executeUnlink', executeUnlinkSpy );
-
-					linkBalloonPanel.unlinkButton.model.fire( 'execute' );
-
-					expect( executeUnlinkSpy.calledOnce ).to.true;
-				} );
-			} );
-		} );
-	} );
-} );

+ 0 - 27
packages/ckeditor5-link/tests/ui/linkballoonpanelview.js

@@ -1,27 +0,0 @@
-/**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/* bender-tags: ui, balloonPanel */
-
-import LinkBalloonPanelView from '/ckeditor5/link/ui/linkballoonpanelview.js';
-import BalloonPanelView from '/ckeditor5/ui/balloonpanel/balloonpanelview.js';
-
-describe( 'LinkBalloonPanelView', () => {
-	let view;
-
-	beforeEach( () => {
-		view = new LinkBalloonPanelView();
-	} );
-
-	describe( 'constructor', () => {
-		it( 'should extend BalloonPanelView class', () => {
-			expect( view ).to.be.instanceof( BalloonPanelView );
-		} );
-
-		it( 'should extend BalloonPanel element by additional class', () => {
-			expect( view.element.classList.contains( 'ck-link-balloon-panel' ) ).to.be.true;
-		} );
-	} );
-} );

+ 9 - 8
packages/ckeditor5-link/tests/ui/linkform.js

@@ -7,24 +7,25 @@
 
 import LinkForm from '/ckeditor5/link/ui/linkform.js';
 import LinkFormView from '/ckeditor5/link/ui/linkformview.js';
-import Form from '/ckeditor5/ui/form/form.js';
 import Model from '/ckeditor5/ui/model.js';
 
 describe( 'LinkForm', () => {
-	let linkForm, view;
+	let linkForm, view, model;
 
 	beforeEach( () => {
 		view = new LinkFormView();
-		linkForm = new LinkForm( new Model(), view );
+		model = new Model();
+		linkForm = new LinkForm( model, view );
 	} );
 
 	describe( 'constructor', () => {
-		it( 'should extend Form class', () => {
-			expect( linkForm ).to.instanceof( Form );
-		} );
+		it( 'creates view#submit -> model#submit binding', () => {
+			const spy = sinon.spy();
+
+			model.on( 'submit', spy );
 
-		it( 'should create empty "actions" collection', () => {
-			expect( linkForm.collections.get( 'actions' ) ).to.have.length( 0 );
+			view.fire( 'submit' );
+			expect( spy.calledOnce ).to.be.true;
 		} );
 	} );
 } );

+ 37 - 8
packages/ckeditor5-link/tests/ui/linkformview.js

@@ -5,8 +5,8 @@
 
 /* bender-tags: ui, form */
 
+import View from '/ckeditor5/ui/view.js';
 import LinkFormView from '/ckeditor5/link/ui/linkformview.js';
-import FormView from '/ckeditor5/ui/form/formview.js';
 
 describe( 'LinkFormView', () => {
 	let view;
@@ -18,17 +18,46 @@ describe( 'LinkFormView', () => {
 	} );
 
 	describe( 'constructor', () => {
-		it( 'should extend FormView class', () => {
-			expect( view ).to.instanceof( FormView );
-		} );
-
 		it( 'should create element from template', () => {
 			expect( view.element.classList.contains( 'ck-link-form' ) ).to.true;
 		} );
 
-		it( 'should register "actions" region', () => {
-			expect( view.regions.get( 1 ).name ).to.equal( 'actions' );
-			expect( view.regions.get( 1 ).element ).to.equal( view.element.querySelector( '.ck-link-form__actions' ) );
+		it( 'should create child views', () => {
+			expect( view.urlInputView ).to.be.instanceOf( View );
+			expect( view.saveButtonView ).to.be.instanceOf( View );
+			expect( view.cancelButtonView ).to.be.instanceOf( View );
+			expect( view.unlinkButtonView ).to.be.instanceOf( View );
+		} );
+
+		describe( 'template', () => {
+			it( 'has url input view', () => {
+				expect( view.template.children.get( 0 ) ).to.equal( view.urlInputView );
+			} );
+
+			it( 'has form actions container', () => {
+				expect( view.template.children.get( 1 ).attributes.class ).to.have.members( [ 'ck-link-form__actions' ] );
+			} );
+
+			it( 'has form action views', () => {
+				const actions = view.template.children.get( 1 ).children;
+
+				expect( actions.get( 0 ) ).to.equal( view.saveButtonView );
+				expect( actions.get( 1 ) ).to.equal( view.cancelButtonView );
+				expect( actions.get( 2 ) ).to.equal( view.unlinkButtonView );
+			} );
+		} );
+	} );
+
+	describe( 'DOM bindings', () => {
+		describe( 'submit event', () => {
+			it( 'should trigger submit event', () => {
+				const spy = sinon.spy();
+
+				view.on( 'submit', spy );
+				view.element.dispatchEvent( new Event( 'submit' ) );
+
+				expect( spy.calledOnce ).to.true;
+			} );
 		} );
 	} );
 } );

+ 12 - 11
packages/ckeditor5-link/theme/components/linkballoonpanel.scss

@@ -2,26 +2,27 @@
 // For licensing, see LICENSE.md or http://ckeditor.com/license
 
 .ck-link {
-	&-balloon-panel {
+	&-form {
 		padding: ck-spacing( 'large' );
+		overflow: hidden;
 
 		.ck-label {
 			margin-bottom: ck-spacing( 'small' );
 		}
-	}
-
-	&-form__actions {
-		clear: both;
-		padding-top: ck-spacing( 'large' );
 
-		.ck-button {
-			float: right;
+		&__actions {
+			clear: both;
+			padding-top: ck-spacing( 'large' );
 
-			& + .ck-button {
-				margin-right: ck-spacing( 'medium' );
+			.ck-button {
+				float: right;
 
 				& + .ck-button {
-					float: left;
+					margin-right: ck-spacing( 'medium' );
+
+					& + .ck-button {
+						float: left;
+					}
 				}
 			}
 		}

+ 1 - 1
packages/ckeditor5-link/theme/theme.scss

@@ -1,4 +1,4 @@
 // Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
 // For licensing, see LICENSE.md or http://ckeditor.com/license
 
-@import 'components/linkballoonpanel';
+@import 'components/linkform';