Răsfoiți Sursa

Massive code refactoring, decoupling and polishing.

Aleksander Nowodzinski 9 ani în urmă
părinte
comite
03be50c8db

+ 1 - 2
packages/ckeditor5-link/src/link.js

@@ -101,8 +101,7 @@ export default class Link extends Feature {
 
 		editingView.on( 'blur', ( evt, domEvtData ) => {
 			// TODO: Lame FocusManager.
-			// TODO input and label as separate components.
-			if ( domEvtData.domEvent.relatedTarget === this.balloonPanel.view.element.querySelector( 'input' ) ) {
+			if ( domEvtData.domEvent.relatedTarget === this.balloonPanel.urlInput ) {
 				domEvtData.domEvent.preventDefault();
 			} else {
 				this.balloonPanel.view.hide();

+ 31 - 0
packages/ckeditor5-link/src/ui/balloonpanel.js

@@ -0,0 +1,31 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Controller from '../../ui/controller.js';
+
+/**
+ * The balloon panel controller class.
+ *
+ * @memberOf ui.balloonPanel
+ * @extends ui.Controller
+ */
+export default class BalloonPanel extends Controller {
+	/**
+	 * Creates an instance of {@link ui.balloonPanel.BalloonPanel} class.
+	 *
+	 * @param {ui.balloonPanel.BalloonPanelModel} model Model of this balloon panel.
+	 * @param {ui.View} view View of this balloon panel.
+	 */
+	constructor( model, view ) {
+		super( model, view );
+
+		view.model.set( 'top', 0 );
+		view.model.set( 'left', 0 );
+		view.model.set( 'isVisible', false );
+		view.model.bind( 'arrow', 'maxWidth', 'maxHeight' ).to( model );
+
+		this.addCollection( 'content' );
+	}
+}

+ 227 - 0
packages/ckeditor5-link/src/ui/balloonpanelview.js

@@ -0,0 +1,227 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import View from '../../ui/view.js';
+import Template from '../../ui/template.js';
+
+const arrowLeftOffset = 30;
+const arrowTopOffset = 15;
+
+/**
+ * The balloon panel view class.
+ *
+ * See {@link ui.balloonPanel.BalloonPanel}.
+ *
+ * @memberOf ui.balloonPanel
+ * @extends ui.View
+ */
+export default class BalloonPanelView extends View {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( locale ) {
+		super( locale );
+
+		const bind = this.bind;
+
+		this.template = new Template( {
+			tag: 'div',
+			attributes: {
+				class: [
+					'ck-balloon-panel',
+					'ck-link-balloon-panel',
+					bind.to( 'arrow', ( value ) =>  `ck-balloon-panel_arrow_${ value }` ),
+					bind.if( 'isVisible', 'ck-balloon-panel_visible' ),
+				],
+
+				style: {
+					top: bind.to( 'top', ( value ) => `${ value }px` ),
+					left: bind.to( 'left', ( value ) => `${ value }px` ),
+					maxWidth: bind.to( 'maxWidth', ( value ) => `${ value }px` ),
+					maxHeight: bind.to( 'maxHeight', ( value ) => `${ value }px` ),
+				}
+			}
+		} );
+
+		this.register( 'content', el => el );
+	}
+
+	show() {
+		this.model.isVisible = true;
+	}
+
+	hide() {
+		this.model.isVisible = false;
+	}
+
+	getVisibleViewportRect( limiterElement ) {
+		const limiterRect = new AbsoluteDomRect( limiterElement, 'limiter' );
+		const windowScrollX = window.scrollX;
+		const windowScrollY = window.scrollY;
+		const bodyWidth = document.body.clientWidth;
+		const bodyHeight = document.body.clientHeight;
+
+		// 	[Viewport]
+		// 	+---------------------------------------+
+		// 	|                        [Limiter]      |
+		// 	|                        +----------------------+
+		// 	|                        |##############|       |
+		// 	|                        |##############|       |
+		// 	|                        |##############|       |
+		// 	|                        +-------^--------------+
+		// 	|                                |      |
+		// 	+--------------------------------|------+
+		//                                   |
+		//                                    \- [Visible Viewport Rect]
+		//
+		return new AbsoluteDomRect( {
+			top: Math.max( limiterRect.top, windowScrollY ),
+			left: Math.max( limiterRect.left, windowScrollX ),
+			right: Math.min( limiterRect.right, bodyWidth + windowScrollX ),
+			bottom: Math.min( limiterRect.bottom, bodyHeight + windowScrollY )
+		}, 'visibleViewportRect' );
+	}
+
+	attachTo( elementOrRange, limiterElement ) {
+		this.show();
+
+		const elementOrRangeRect = new AbsoluteDomRect( elementOrRange, 'elementOrRange' );
+		const panelRect = new AbsoluteDomRect( this.element );
+		const visibleViewportRect = this.getVisibleViewportRect( limiterElement );
+
+		// visibleViewportRect.paint();
+		// elementOrRangeRect.paint();
+
+		this.smartAttachTo( [
+			// [     ]
+			//    ^
+			// +--------------+
+			// |              |
+			// +--------------+
+			panelRect.clone( 'se' ).moveTo( {
+				top: elementOrRangeRect.bottom + arrowTopOffset,
+				left: elementOrRangeRect.left + elementOrRangeRect.width / 2 - arrowLeftOffset
+			} ),
+
+			//          [     ]
+			//             ^
+			// +--------------+
+			// |              |
+			// +--------------+
+			panelRect.clone( 'sw' ).moveTo( {
+				top: elementOrRangeRect.bottom + arrowTopOffset,
+				left: elementOrRangeRect.left + elementOrRangeRect.width / 2 - panelRect.width + arrowLeftOffset
+			} ),
+
+			// +--------------+
+			// |              |
+			// +--------------+
+			//    V
+			// [     ]
+			panelRect.clone( 'ne' ).moveTo( {
+				top: elementOrRangeRect.top - panelRect.height - arrowTopOffset,
+				left: elementOrRangeRect.left + elementOrRangeRect.width / 2 - arrowLeftOffset
+			} ),
+
+			// +--------------+
+			// |              |
+			// +--------------+
+			//             V
+			//          [     ]
+			panelRect.clone( 'nw' ).moveTo( {
+				top: elementOrRangeRect.top - panelRect.height - arrowTopOffset,
+				left: elementOrRangeRect.left + elementOrRangeRect.width / 2 - panelRect.width + arrowLeftOffset
+			} )
+		], visibleViewportRect );
+	}
+
+	smartAttachTo( rects, visibleViewportRect ) {
+		let maxIntersectRect;
+		let maxIntersectArea = -1;
+
+		for ( let rect of rects ) {
+			const intersectArea = rect.getIntersectArea( visibleViewportRect );
+			// rect.paint();
+
+			if ( intersectArea > maxIntersectArea ) {
+				maxIntersectRect = rect;
+				maxIntersectArea = intersectArea;
+			}
+		}
+
+		this.model.arrow = maxIntersectRect.name;
+		this.model.top = maxIntersectRect.top;
+		this.model.left = maxIntersectRect.left;
+	}
+}
+
+// AbsoluteDomRect always corresponds with what position: absolute would behave.
+class AbsoluteDomRect {
+	constructor( elementOrRangeOrRect, name ) {
+		if ( name ) {
+			this.name = name;
+		}
+
+		if ( elementOrRangeOrRect instanceof HTMLElement || elementOrRangeOrRect instanceof Range ) {
+			const elementRect = elementOrRangeOrRect.getBoundingClientRect();
+			const bodyRect = document.body.getBoundingClientRect();
+
+			this.top = elementRect.top - bodyRect.top;
+			this.right = elementRect.right - bodyRect.left;
+			this.bottom = elementRect.bottom - bodyRect.top;
+			this.left = elementRect.left - bodyRect.left;
+			this.width = elementRect.width;
+			this.height = elementRect.height;
+		} else {
+			Object.assign( this, elementOrRangeOrRect );
+
+			if ( typeof width == 'undefined' ) {
+				this.width = this.right - this.left;
+			}
+
+			if ( typeof height == 'undefined' ) {
+				this.height = this.bottom - this.top;
+			}
+		}
+	}
+
+	clone( newName ) {
+		return new AbsoluteDomRect( this, newName );
+	}
+
+	moveTo( { top, left } ) {
+		this.top = top;
+		this.right = left + this.width;
+		this.bottom = top + this.height;
+		this.left = left;
+
+		return this;
+	}
+
+	getIntersectArea( rect ) {
+		const hOverlap = Math.max( 0, Math.min( this.right, rect.right ) - Math.max( this.left, rect.left ) );
+		const vOverlap = Math.max( 0, Math.min( this.bottom, rect.bottom ) - Math.max( this.top, rect.top ) );
+
+		return hOverlap * vOverlap;
+	}
+
+	paint() {
+		this.painter = document.createElement( 'div' );
+		this.painter.style.backgroundColor = 'rgba(255,0,0,.05)';
+		this.painter.style.color = '#000';
+		this.painter.style.position = 'absolute';
+		this.painter.style.display = 'flex';
+		this.painter.style.justifyContent = 'center';
+		this.painter.style.alignItems = 'center';
+		this.painter.style.left = this.left + 'px';
+		this.painter.style.top = this.top + 'px';
+		this.painter.style.width = this.width + 'px';
+		this.painter.style.height = this.height + 'px';
+		this.painter.style.outline = '1px solid rgba(255,0,0,.3)';
+		this.painter.innerHTML = this.name;
+
+		document.body.appendChild( this.painter );
+	}
+}

+ 28 - 0
packages/ckeditor5-link/src/ui/box.js

@@ -0,0 +1,28 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Controller from '../../ui/controller.js';
+
+/**
+ * The box controller class.
+ *
+ * @memberOf ui.box
+ * @extends ui.Controller
+ */
+export default class Box extends Controller {
+	/**
+	 * Creates an instance of {@link ui.box.Box} class.
+	 *
+	 * @param {ui.box.BoxModel} model Model of this box.
+	 * @param {ui.View} view View of this box.
+	 */
+	constructor( model, view ) {
+		super( model, view );
+
+		view.model.bind( 'alignRight' ).to( model );
+
+		this.addCollection( 'content' );
+	}
+}

+ 38 - 0
packages/ckeditor5-link/src/ui/boxview.js

@@ -0,0 +1,38 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import View from '../../ui/view.js';
+import Template from '../../ui/template.js';
+
+/**
+ * The box view class.
+ *
+ * See {@link ui.box.Box}.
+ *
+ * @memberOf ui.box
+ * @extends ui.View
+ */
+export default class BoxView extends View {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( locale ) {
+		super( locale );
+
+		const bind = this.bind;
+
+		this.template = new Template( {
+			tag: 'div',
+			attributes: {
+				class: [
+					'ck-box',
+					bind.if( 'alignRight', 'ck-box_align_right' )
+				]
+			}
+		} );
+
+		this.register( 'content', el => el );
+	}
+}

+ 26 - 0
packages/ckeditor5-link/src/ui/inputlabel.js

@@ -0,0 +1,26 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Controller from '../../ui/controller.js';
+
+/**
+ * The input label controller class.
+ *
+ * @memberOf ui.input
+ * @extends ui.Controller
+ */
+export default class InputLabel extends Controller {
+	/**
+	 * Creates an instance of {@link ui.input.InputLabel} class.
+	 *
+	 * @param {ui.input.InputLabelModel} model Model of this label.
+	 * @param {ui.View} view View of this label.
+	 */
+	constructor( model, view ) {
+		super( model, view );
+
+		view.model.bind( 'for', 'text' ).to( model );
+	}
+}

+ 41 - 0
packages/ckeditor5-link/src/ui/inputlabelview.js

@@ -0,0 +1,41 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import View from '../../ui/view.js';
+import Template from '../../ui/template.js';
+
+/**
+ * The input label view class.
+ *
+ * See {@link ui.input.InputLabel}.
+ *
+ * @memberOf ui.input
+ * @extends ui.View
+ */
+export default class InputLabelView extends View {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( locale ) {
+		super( locale );
+
+		const bind = this.bind;
+
+		this.template = new Template( {
+			tag: 'label',
+			attributes: {
+				class: [
+					'ck-labeled-input__label'
+				],
+				for: bind.to( 'for' )
+			},
+			children: [
+				{
+					text: bind.to( 'text' )
+				}
+			]
+		} );
+	}
+}

+ 28 - 0
packages/ckeditor5-link/src/ui/inputtext.js

@@ -0,0 +1,28 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import uid from '../../utils/uid.js';
+import Controller from '../../ui/controller.js';
+
+/**
+ * The text input controller class.
+ *
+ * @memberOf ui.input
+ * @extends ui.Controller
+ */
+export default class InputText extends Controller {
+	/**
+	 * Creates an instance of {@link ui.input.InputText} class.
+	 *
+	 * @param {ui.input.InputTextModel} model Model of this input.
+	 * @param {ui.View} view View of this input.
+	 */
+	constructor( model, view ) {
+		super( model, view );
+
+		view.model.bind( 'value' ).to( model );
+		view.model.set( 'uid', `ck-input-${ uid() }` );
+	}
+}

+ 39 - 0
packages/ckeditor5-link/src/ui/inputtextview.js

@@ -0,0 +1,39 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import View from '../../ui/view.js';
+import Template from '../../ui/template.js';
+
+/**
+ * The text input view class.
+ *
+ * See {@link ui.input.InputText}.
+ *
+ * @memberOf ui.input
+ * @extends ui.View
+ */
+export default class InputTextView extends View {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( locale ) {
+		super( locale );
+
+		const bind = this.bind;
+
+		this.template = new Template( {
+			tag: 'input',
+			attributes: {
+				type: 'text',
+				class: [
+					'ck-input',
+					'ck-input-text'
+				],
+				id: bind.to( 'uid' ),
+				value: bind.to( 'value' )
+			}
+		} );
+	}
+}

+ 67 - 0
packages/ckeditor5-link/src/ui/labeledinput.js

@@ -0,0 +1,67 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Model from '../../ui/model.js';
+import Controller from '../../ui/controller.js';
+
+import InputText from './inputtext.js';
+import InputTextView from './inputtextview.js';
+
+import InputLabel from './inputlabel.js';
+import InputLabelView from './inputlabelview.js';
+
+/**
+ * The labeled input controller class.
+ *
+ * @memberOf ui.input.labeled
+ * @extends ui.Controller
+ */
+export default class LabeledInput extends Controller {
+	/**
+	 * Creates an instance of {@link ui.input.labeled.LabeledInput} class.
+	 *
+	 * @param {ui.input.labeled.LabeledInputModel} model Model of this input.
+	 * @param {ui.View} view View of this input.
+	 */
+	constructor( model, view ) {
+		super( model, view );
+
+		const contentCollection = this.addCollection( 'content' );
+
+		/**
+		 * TODO
+		 *
+		 * @member {} todo
+		 */
+		this.input = this._createInput();
+
+		/**
+		 * TODO
+		 *
+		 * @member {} todo
+		 */
+		this.label = this._createLabel();
+
+		contentCollection.add( this.input );
+		contentCollection.add( this.label, 0 );
+	}
+
+	_createInput() {
+		const model = new Model();
+
+		model.bind( 'value', 'label' ).to( this.model );
+
+		return new InputText( model, new InputTextView( this.locale ) );
+	}
+
+	_createLabel() {
+		const model = new Model();
+
+		model.bind( 'for' ).to( this.input.view.model, 'uid' );
+		model.bind( 'text' ).to( this.model, 'label' );
+
+		return new InputLabel( model, new InputLabelView( this.locale ) );
+	}
+}

+ 53 - 0
packages/ckeditor5-link/src/ui/labeledinputview.js

@@ -0,0 +1,53 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import View from '../../ui/view.js';
+import Template from '../../ui/template.js';
+
+/**
+ * The labeled input view class.
+ *
+ * See {@link ui.input.labeled.LabeledInput}.
+ *
+ * @memberOf ui.input.labeled
+ * @extends ui.View
+ */
+export default class LabeledInputView extends View {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( locale ) {
+		super( locale );
+
+		const bind = this.bind;
+
+		this.template = new Template( {
+			tag: 'div',
+			attributes: {
+				class: [
+					'ck-labeled-input',
+				]
+			},
+			children: [
+				{
+					tag: 'label',
+					attributes: {
+						class: [
+							'ck-labeled-input__label'
+						],
+						for: bind.to( 'uid' )
+					},
+					children: [
+						{
+							text: bind.to( 'label' )
+						}
+					]
+				},
+			]
+		} );
+
+		this.register( 'content', el => el );
+	}
+}

+ 51 - 19
packages/ckeditor5-link/src/ui/linkballoonpanel.js

@@ -4,19 +4,21 @@
  */
 
 import Model from '../../ui/model.js';
-import Controller from '../../ui/controller.js';
 import Button from '../../ui/button/button.js';
 import ButtonView from '../../ui/button/buttonview.js';
+import BalloonPanel from './balloonpanel.js';
+import LabeledInput from './labeledinput.js';
+import LabeledInputView from './labeledinputview.js';
+import Box from './box.js';
+import BoxView from './boxview.js';
 
 /**
  * The link balloon panel controller class.
  *
- * TODO: extends BalloonPanel
- *
  * @memberOf link.ui
  * @extends ui.Controller
  */
-export default class LinkBalloonPanel extends Controller {
+export default class LinkBalloonPanel extends BalloonPanel {
 	/**
 	 * Creates an instance of {@link ui.dropdown.Dropdown} class.
 	 *
@@ -26,36 +28,66 @@ export default class LinkBalloonPanel extends Controller {
 	constructor( model, view ) {
 		super( model, view );
 
-		view.model.bind( 'arrow', 'maxWidth', 'maxHeight', 'url' ).to( model );
-		view.model.set( 'top', 0 );
-		view.model.set( 'left', 0 );
-		view.model.set( 'isVisible', false );
+		const contentCollection = this.collections.get( 'content' );
+
+		/**
+		 * TODO
+		 *
+		 * @member {} todo
+		 */
+		this.labeledInput = this._createLabeledInput();
+
+		/**
+		 * TODO
+		 *
+		 * @member {} todo
+		 */
+		this.urlInput = this.labeledInput.input;
+
+		contentCollection.add( this.labeledInput );
+		contentCollection.add( this._createButtons() );
+	}
+
+	_createLabeledInput() {
+		const t = this.view.t;
+		const labeledInputModel = new Model( {
+			label: t( 'Link URL' )
+		} );
+
+		labeledInputModel.bind( 'value' ).to( this.model, 'url' );
 
-		const buttonsCollection = this.addCollection( 'buttons' );
+		return new LabeledInput( labeledInputModel, new LabeledInputView( this.locale ) );
+	}
+
+	_createButtons() {
+		const box = new Box( new Model( {
+			alignRight: true
+		} ), new BoxView( this.locale ) );
 
 		this.saveButton = this._createSaveButton();
 		this.cancelButton = this._createCancelButton();
 
-		buttonsCollection.add( this.cancelButton );
-		buttonsCollection.add( this.saveButton );
+		box.add( 'content', this.cancelButton );
+		box.add( 'content', this.saveButton );
+
+		return box;
 	}
 
 	_createSaveButton() {
 		const t = this.view.t;
-		const saveButtonModel = new Model( {
+		const model = new Model( {
 			isEnabled: true,
 			isOn: false,
 			label: t( 'Save' ),
 			withText: true
 		} );
 
-		saveButtonModel.on( 'execute', () => {
-			// TODO input and label as separate components.
-			this.model.url = this.view.element.querySelector( 'input' ).value;
+		model.on( 'execute', () => {
+			this.model.url = this.urlInput.value;
 			this.view.hide();
 		} );
 
-		const button = new Button( saveButtonModel, new ButtonView( this.locale ) );
+		const button = new Button( model, new ButtonView( this.locale ) );
 		button.view.element.classList.add( 'ck-button-action' );
 
 		return button;
@@ -63,15 +95,15 @@ export default class LinkBalloonPanel extends Controller {
 
 	_createCancelButton() {
 		const t = this.view.t;
-		const cancelButtonModel = new Model( {
+		const model = new Model( {
 			isEnabled: true,
 			isOn: false,
 			label: t( 'Cancel' ),
 			withText: true
 		} );
 
-		cancelButtonModel.on( 'execute', () => this.view.hide() );
+		model.on( 'execute', () => this.view.hide() );
 
-		return new Button( cancelButtonModel, new ButtonView( this.locale ) );
+		return new Button( model, new ButtonView( this.locale ) );
 	}
 }

+ 7 - 246
packages/ckeditor5-link/src/ui/linkballoonpanelview.js

@@ -3,269 +3,30 @@
  * For licensing, see LICENSE.md.
  */
 
-import uid from '../../utils/uid.js';
-import View from '../../ui/view.js';
 import Template from '../../ui/template.js';
-
-const arrowLeftOffset = 30;
-const arrowTopOffset = 15;
+import BalloonPanelView from './balloonpanelview.js';
 
 /**
- * The balloon panel view class.
- *
- * See {@link ui.balloonPanel.BalloonPanel}.
+ * The link balloon panel view class.
  *
- * TODO: extends BalloonPanelView
+ * See {@link ui.balloonPanel.BalloonPanelView}.
  *
  * @memberOf link.ui
  * @extends ui.View
  */
-export default class LinkBalloonPanelView extends View {
+export default class LinkBalloonPanelView extends BalloonPanelView {
 	/**
 	 * @inheritDoc
 	 */
 	constructor( locale ) {
 		super( locale );
 
-		const t = this.t;
-		const bind = this.bind;
-		const urlFieldId = `ck-input-${ uid() }`;
-
-		this.template = new Template( {
-			tag: 'div',
-
+		Template.extend( this.template, {
 			attributes: {
 				class: [
-					'ck-balloon-panel',
 					'ck-link-balloon-panel',
-					bind.to( 'arrow', ( value ) =>  `ck-balloon-panel_arrow_${ value }` ),
-					bind.if( 'isVisible', 'ck-balloon-panel_visible' ),
-				],
-
-				style: {
-					top: bind.to( 'top', ( value ) => `${ value }px` ),
-					left: bind.to( 'left', ( value ) => `${ value }px` ),
-					maxWidth: bind.to( 'maxWidth', ( value ) => `${ value }px` ),
-					maxHeight: bind.to( 'maxHeight', ( value ) => `${ value }px` ),
-				}
-			},
-
-			children: [
-				{
-					tag: 'label',
-					attributes: {
-						class: [
-							'ck-input__label'
-						],
-						for: urlFieldId
-					},
-					children: [
-						t( 'Link URL' )
-					]
-				},
-				{
-					tag: 'input',
-					attributes: {
-						class: [
-							'ck-input',
-							'ck-input-text'
-						],
-						id: urlFieldId,
-						type: 'text',
-						value: bind.to( 'url' )
-					},
-					on: {
-						change: bind.to( 'urlChange' )
-					}
-				},
-				{
-					tag: 'div',
-					attributes: {
-						class: [
-							'ck-balloon-panel__buttons'
-						]
-					}
-				}
-			]
-		} );
-
-		this.register( 'buttons', '.ck-balloon-panel__buttons' );
-	}
-
-	show() {
-		this.model.isVisible = true;
-	}
-
-	hide() {
-		this.model.isVisible = false;
-	}
-
-	getVisibleViewportRect( limiterElement ) {
-		const limiterRect = new AbsoluteDomRect( limiterElement, 'limiter' );
-		const windowScrollX = window.scrollX;
-		const windowScrollY = window.scrollY;
-		const bodyWidth = document.body.clientWidth;
-		const bodyHeight = document.body.clientHeight;
-
-		// 	[Viewport]
-		// 	+---------------------------------------+
-		// 	|                        [Limiter]      |
-		// 	|                        +----------------------+
-		// 	|                        |##############|       |
-		// 	|                        |##############|       |
-		// 	|                        |##############|       |
-		// 	|                        +-------^--------------+
-		// 	|                                |      |
-		// 	+--------------------------------|------+
-		//                                   |
-		//                                    \- [Visible Viewport Rect]
-		//
-		return new AbsoluteDomRect( {
-			top: Math.max( limiterRect.top, windowScrollY ),
-			left: Math.max( limiterRect.left, windowScrollX ),
-			right: Math.min( limiterRect.right, bodyWidth + windowScrollX ),
-			bottom: Math.min( limiterRect.bottom, bodyHeight + windowScrollY )
-		}, 'visibleViewportRect' );
-	}
-
-	attachTo( elementOrRange, limiterElement ) {
-		this.show();
-
-		const elementOrRangeRect = new AbsoluteDomRect( elementOrRange, 'elementOrRange' );
-		const panelRect = new AbsoluteDomRect( this.element );
-		const visibleViewportRect = this.getVisibleViewportRect( limiterElement );
-
-		// visibleViewportRect.paint();
-		// elementOrRangeRect.paint();
-
-		this.smartAttachTo( [
-			// [     ]
-			//    ^
-			// +--------------+
-			// |              |
-			// +--------------+
-			panelRect.clone( 'se' ).moveTo( {
-				top: elementOrRangeRect.bottom + arrowTopOffset,
-				left: elementOrRangeRect.left + elementOrRangeRect.width / 2 - arrowLeftOffset
-			} ),
-
-			//          [     ]
-			//             ^
-			// +--------------+
-			// |              |
-			// +--------------+
-			panelRect.clone( 'sw' ).moveTo( {
-				top: elementOrRangeRect.bottom + arrowTopOffset,
-				left: elementOrRangeRect.left + elementOrRangeRect.width / 2 - panelRect.width + arrowLeftOffset
-			} ),
-
-			// +--------------+
-			// |              |
-			// +--------------+
-			//    V
-			// [     ]
-			panelRect.clone( 'ne' ).moveTo( {
-				top: elementOrRangeRect.top - panelRect.height - arrowTopOffset,
-				left: elementOrRangeRect.left + elementOrRangeRect.width / 2 - arrowLeftOffset
-			} ),
-
-			// +--------------+
-			// |              |
-			// +--------------+
-			//             V
-			//          [     ]
-			panelRect.clone( 'nw' ).moveTo( {
-				top: elementOrRangeRect.top - panelRect.height - arrowTopOffset,
-				left: elementOrRangeRect.left + elementOrRangeRect.width / 2 - panelRect.width + arrowLeftOffset
-			} )
-		], visibleViewportRect );
-	}
-
-	smartAttachTo( rects, visibleViewportRect ) {
-		let maxIntersectRect;
-		let maxIntersectArea = -1;
-
-		for ( let rect of rects ) {
-			const intersectArea = rect.getIntersectArea( visibleViewportRect );
-			// rect.paint();
-
-			if ( intersectArea > maxIntersectArea ) {
-				maxIntersectRect = rect;
-				maxIntersectArea = intersectArea;
-			}
-		}
-
-		this.model.arrow = maxIntersectRect.name;
-		this.model.top = maxIntersectRect.top;
-		this.model.left = maxIntersectRect.left;
-	}
-}
-
-// AbsoluteDomRect always corresponds with what position: absolute would behave.
-class AbsoluteDomRect {
-	constructor( elementOrRangeOrRect, name ) {
-		if ( name ) {
-			this.name = name;
-		}
-
-		if ( elementOrRangeOrRect instanceof HTMLElement || elementOrRangeOrRect instanceof Range ) {
-			const elementRect = elementOrRangeOrRect.getBoundingClientRect();
-			const bodyRect = document.body.getBoundingClientRect();
-
-			this.top = elementRect.top - bodyRect.top;
-			this.right = elementRect.right - bodyRect.left;
-			this.bottom = elementRect.bottom - bodyRect.top;
-			this.left = elementRect.left - bodyRect.left;
-			this.width = elementRect.width;
-			this.height = elementRect.height;
-		} else {
-			Object.assign( this, elementOrRangeOrRect );
-
-			if ( typeof width == 'undefined' ) {
-				this.width = this.right - this.left;
-			}
-
-			if ( typeof height == 'undefined' ) {
-				this.height = this.bottom - this.top;
+				]
 			}
-		}
-	}
-
-	clone( newName ) {
-		return new AbsoluteDomRect( this, newName );
-	}
-
-	moveTo( { top, left } ) {
-		this.top = top;
-		this.right = left + this.width;
-		this.bottom = top + this.height;
-		this.left = left;
-
-		return this;
-	}
-
-	getIntersectArea( rect ) {
-		const hOverlap = Math.max( 0, Math.min( this.right, rect.right ) - Math.max( this.left, rect.left ) );
-		const vOverlap = Math.max( 0, Math.min( this.bottom, rect.bottom ) - Math.max( this.top, rect.top ) );
-
-		return hOverlap * vOverlap;
-	}
-
-	paint() {
-		this.painter = document.createElement( 'div' );
-		this.painter.style.backgroundColor = 'rgba(255,0,0,.05)';
-		this.painter.style.color = '#000';
-		this.painter.style.position = 'absolute';
-		this.painter.style.display = 'flex';
-		this.painter.style.justifyContent = 'center';
-		this.painter.style.alignItems = 'center';
-		this.painter.style.left = this.left + 'px';
-		this.painter.style.top = this.top + 'px';
-		this.painter.style.width = this.width + 'px';
-		this.painter.style.height = this.height + 'px';
-		this.painter.style.outline = '1px solid rgba(255,0,0,.3)';
-		this.painter.innerHTML = this.name;
-
-		document.body.appendChild( this.painter );
+		} );
 	}
 }

+ 8 - 0
packages/ckeditor5-link/theme/components/box.scss

@@ -0,0 +1,8 @@
+// Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+// For licensing, see LICENSE.md or http://ckeditor.com/license
+
+.ck-box {
+	&_align_right {
+		text-align: right;
+	}
+}

+ 8 - 6
packages/ckeditor5-link/theme/components/input.scss → packages/ckeditor5-link/theme/components/labeledinput.scss

@@ -1,13 +1,15 @@
 // Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
 // For licensing, see LICENSE.md or http://ckeditor.com/license
 
-.ck-input {
-	&-text {
-		@include ck-rounded-corners();
+.ck-labeled-input {
+	.ck-input {
+		&-text {
+			@include ck-rounded-corners();
 
-		border: 1px solid ck-border-color( 'foreground' );
-		padding: ck-spacing( 'small' );
-		min-width: 200px;
+			border: 1px solid ck-border-color( 'foreground' );
+			padding: ck-spacing( 'small' );
+			min-width: 200px;
+		}
 	}
 
 	&__label {

+ 4 - 2
packages/ckeditor5-link/theme/components/linkballoonpanel.scss

@@ -10,6 +10,9 @@ $ck-balloon-arrow-half-width: 10px;
 	@include ck-unselectable();
 	@include ck-rounded-corners();
 
+	min-width: 50px;
+	min-height: 15px;
+
 	display: none;
 	position: absolute;
 	background: ck-color( 'background' );
@@ -113,8 +116,7 @@ $ck-balloon-arrow-half-width: 10px;
 		display: block;
 	}
 
-	&__buttons {
-		text-align: right;
+	.ck-box {
 		margin-top: ck-spacing( 'medium' );
 
 		* + * {

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

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