浏览代码

Merge branch 'master' into t/193

Aleksander Nowodzinski 8 年之前
父节点
当前提交
09fa23e949

+ 1 - 1
packages/ckeditor5-ui/package.json

@@ -10,7 +10,7 @@
     "@ckeditor/ckeditor5-utils": "^0.9.0"
   },
   "devDependencies": {
-    "@ckeditor/ckeditor5-basic-styles": "^0.7.1",
+    "@ckeditor/ckeditor5-basic-styles": "^0.8.0",
     "@ckeditor/ckeditor5-core": "^0.8.0",
     "@ckeditor/ckeditor5-dev-lint": "^2.0.2",
     "@ckeditor/ckeditor5-editor-classic": "^0.7.2",

+ 3 - 3
packages/ckeditor5-ui/src/notification/notification.js

@@ -4,7 +4,7 @@
  */
 
 /**
- * @module ui/notification
+ * @module ui/notification/notification
  */
 
 /* globals window */
@@ -19,7 +19,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  * notifications makes possible to switch the notifications UI.
  *
  * Note that every unhandled and not stopped `warning` notification will be displayed as system alert.
- * See {@link module:ui/notification~Notification#showWarning}.
+ * See {@link module:ui/notification/notification~Notification#showWarning}.
  *
  * @extends module:core/plugin~Plugin
  */
@@ -28,7 +28,7 @@ export default class Notification extends Plugin {
 	 * @inheritDoc
 	 */
 	static get pluginName() {
-		return 'notification';
+		return 'ui/notification';
 	}
 
 	/**

+ 85 - 23
packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js

@@ -52,18 +52,22 @@ export default class BalloonPanelView extends View {
 
 		/**
 		 * Balloon panel's current position. The position name is reflected in the CSS class set
-		 * to the balloon, i.e. `.ck-balloon-panel_arrow_se` for "se" position. The class
+		 * to the balloon, i.e. `.ck-balloon-panel_arrow_se` for "arrow_se" position. The class
 		 * controls the minor aspects of the balloon's visual appearance like placement
 		 * of the "arrow". To support a new position, an additional CSS must be created.
 		 *
 		 * Default position names correspond with
 		 * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions}.
 		 *
+		 * See {@link #attachTo} to learn about custom balloon positions.
+		 *
+		 * See {@link #withArrow}.
+		 *
 		 * @observable
-		 * @default 'se'
-		 * @member {'se'|'sw'|'ne'|'nw'} #position
+		 * @default 'arrow_se'
+		 * @member {'arrow_se'|'arrow_sw'|'arrow_ne'|'arrow_nw'} #position
 		 */
-		this.set( 'position', 'se' );
+		this.set( 'position', 'arrow_se' );
 
 		/**
 		 * Controls whether the balloon panel is visible or not.
@@ -75,6 +79,16 @@ export default class BalloonPanelView extends View {
 		this.set( 'isVisible', false );
 
 		/**
+		 * Controls whether the balloon panel has an arrow. The presence of the arrow
+		 * is reflected in `ck-balloon-panel_with-arrow` CSS class.
+		 *
+		 * @observable
+		 * @default true
+		 * @member {Boolean} #withArrow
+		 */
+		this.set( 'withArrow', true );
+
+		/**
 		 * Max width of the balloon panel, as in CSS.
 		 *
 		 * @observable
@@ -82,6 +96,14 @@ export default class BalloonPanelView extends View {
 		 */
 
 		/**
+		 * A callback that starts pining the panel when {@link #isVisible} gets
+		 * `true`. Used by {@link #pin}.
+		 *
+		 * @private
+		 * @member {Function} #_pinWhenIsVisibleCallback
+		 */
+
+		/**
 		 * Collection of the child views which creates balloon panel contents.
 		 *
 		 * @readonly
@@ -94,8 +116,9 @@ export default class BalloonPanelView extends View {
 			attributes: {
 				class: [
 					'ck-balloon-panel',
-					bind.to( 'position', ( value ) => `ck-balloon-panel_arrow_${ value }` ),
-					bind.if( 'isVisible', 'ck-balloon-panel_visible' )
+					bind.to( 'position', ( value ) => `ck-balloon-panel_${ value }` ),
+					bind.if( 'isVisible', 'ck-balloon-panel_visible' ),
+					bind.if( 'withArrow', 'ck-balloon-panel_with-arrow' )
 				],
 
 				style: {
@@ -170,14 +193,57 @@ export default class BalloonPanelView extends View {
 	 *
 	 * Thanks to this, the panel always sticks to the {@link module:utils/dom/position~Options#target}.
 	 *
-	 * See https://github.com/ckeditor/ckeditor5-ui/issues/170.
+	 * See: {@link #unpin}.
 	 *
 	 * @param {module:utils/dom/position~Options} options Positioning options compatible with
 	 * {@link module:utils/dom/position~getOptimalPosition}. Default `positions` array is
 	 * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions}.
 	 */
-	keepAttachedTo( options ) {
-		// First we need to attach the balloon panel to the target element.
+	pin( options ) {
+		this.unpin();
+
+		this._pinWhenIsVisibleCallback = () => {
+			if ( this.isVisible ) {
+				this._startPinning( options );
+			} else {
+				this._stopPinning();
+			}
+		};
+
+		this._startPinning( options );
+
+		// Control the state of the listeners depending on whether the panel is visible
+		// or not.
+		// TODO: Use on() (https://github.com/ckeditor/ckeditor5-utils/issues/144).
+		this.listenTo( this, 'change:isVisible', this._pinWhenIsVisibleCallback );
+	}
+
+	/**
+	 * Stops pinning the panel, as set up by {@link #pin}.
+	 */
+	unpin() {
+		if ( this._pinWhenIsVisibleCallback ) {
+			// Deactivate listeners attached by pin().
+			this._stopPinning();
+
+			// Deactivate the panel pin() control logic.
+			// TODO: Use off() (https://github.com/ckeditor/ckeditor5-utils/issues/144).
+			this.stopListening( this, 'change:isVisible', this._pinWhenIsVisibleCallback );
+
+			this._pinWhenIsVisibleCallback = null;
+
+			this.hide();
+		}
+	}
+
+	/**
+	 * Starts managing the pinned state of the panel. See {@link #pin}.
+	 *
+	 * @private
+	 * @param {module:utils/dom/position~Options} options Positioning options compatible with
+	 * {@link module:utils/dom/position~getOptimalPosition}.
+	 */
+	_startPinning( options ) {
 		this.attachTo( options );
 
 		const limiter = options.limiter || defaultLimiterElement;
@@ -199,23 +265,19 @@ export default class BalloonPanelView extends View {
 		}, { useCapture: true } );
 
 		// We need to listen on window resize event and update position.
-		this.listenTo( global.window, 'resize', () => this.attachTo( options ) );
-
-		// After all we need to clean up the listeners.
-		this.once( 'change:isVisible', () => {
-			this.stopListening( global.document, 'scroll' );
-			this.stopListening( global.window, 'resize' );
+		this.listenTo( global.window, 'resize', () => {
+			this.attachTo( options );
 		} );
 	}
 
 	/**
-	 * @inheritDoc
+	 * Stops managing the pinned state of the panel. See {@link #pin}.
+	 *
+	 * @private
 	 */
-	destroy() {
+	_stopPinning() {
 		this.stopListening( global.document, 'scroll' );
 		this.stopListening( global.window, 'resize' );
-
-		return super.destroy();
 	}
 }
 
@@ -310,24 +372,24 @@ BalloonPanelView.defaultPositions = {
 	se: ( targetRect ) => ( {
 		top: targetRect.bottom + BalloonPanelView.arrowVerticalOffset,
 		left: targetRect.left + targetRect.width / 2 - BalloonPanelView.arrowHorizontalOffset,
-		name: 'se'
+		name: 'arrow_se'
 	} ),
 
 	sw: ( targetRect, balloonRect ) => ( {
 		top: targetRect.bottom + BalloonPanelView.arrowVerticalOffset,
 		left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
-		name: 'sw'
+		name: 'arrow_sw'
 	} ),
 
 	ne: ( targetRect, balloonRect ) => ( {
 		top: targetRect.top - balloonRect.height - BalloonPanelView.arrowVerticalOffset,
 		left: targetRect.left + targetRect.width / 2 - BalloonPanelView.arrowHorizontalOffset,
-		name: 'ne'
+		name: 'arrow_ne'
 	} ),
 
 	nw: ( targetRect, balloonRect ) => ( {
 		top: targetRect.top - balloonRect.height - BalloonPanelView.arrowVerticalOffset,
 		left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
-		name: 'nw'
+		name: 'arrow_nw'
 	} )
 };

+ 21 - 11
packages/ckeditor5-ui/src/contextualballoon.js

@@ -4,11 +4,11 @@
  */
 
 /**
- * @module ui/contextualballoon
+ * @module ui/panel/balloon/contextualballoon
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import BalloonPanelView from './panel/balloon/balloonpanelview';
+import BalloonPanelView from './balloonpanelview';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
@@ -42,7 +42,7 @@ export default class ContextualBalloon extends Plugin {
 
 		/**
 		 * Stack of the views injected into the balloon. Last one in the stack is displayed
-		 * as a content of {@link module:ui/contextualballoon~ContextualBalloon#view}.
+		 * as a content of {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon#view}.
 		 *
 		 * @private
 		 * @member {Map} #_stack
@@ -52,8 +52,8 @@ export default class ContextualBalloon extends Plugin {
 		// Editor should be focused when contextual balloon is focused.
 		this.editor.ui.focusTracker.add( this.view.element );
 
-		// Add balloon panel view to editor `body` collection.
-		this.editor.ui.view.body.add( this.view );
+		// Add balloon panel view to editor `body` collection and wait until view will be ready.
+		return this.editor.ui.view.body.add( this.view );
 	}
 
 	static get pluginName() {
@@ -86,8 +86,9 @@ export default class ContextualBalloon extends Plugin {
 	 * Adds a new view to the stack and makes it visible.
 	 *
 	 * @param {Object} data Configuration of the view.
-	 * @param {module:ui/view~View} view Content of the balloon.
-	 * @param {module:utils/dom/position~Options} position Positioning options.
+	 * @param {module:ui/view~View} [data.view] Content of the balloon.
+	 * @param {module:utils/dom/position~Options} [data.position] Positioning options.
+	 * @returns {Promise} A Promise resolved when the child {@link module:ui/view~View#init} is done.
 	 */
 	add( data ) {
 		if ( this.hasView( data.view ) ) {
@@ -108,7 +109,7 @@ export default class ContextualBalloon extends Plugin {
 		// Add new view to the stack.
 		this._stack.set( data.view, data );
 		// And display it.
-		this._show( data.view );
+		return this._show( data.view );
 	}
 
 	/**
@@ -117,6 +118,7 @@ export default class ContextualBalloon extends Plugin {
 	 * When there is no view in the stack then balloon will hide.
 	 *
 	 * @param {module:ui/view~View} view A view to be removed from the balloon.
+	 * @returns {Promise} A Promise resolved when the preceding view is ready.
 	 */
 	remove( view ) {
 		if ( !this.hasView( view ) ) {
@@ -128,6 +130,9 @@ export default class ContextualBalloon extends Plugin {
 			throw new CKEditorError( 'contextualballoon-remove-view-not-exist: Cannot remove configuration of not existing view.' );
 		}
 
+		// A Promise resolved when the preceding view is ready.
+		let promise = Promise.resolve();
+
 		// When visible view is being removed.
 		if ( this.visibleView === view ) {
 			// We need to remove it from the view content.
@@ -142,7 +147,7 @@ export default class ContextualBalloon extends Plugin {
 			// If it is some other view.
 			if ( last ) {
 				// Just show it.
-				this._show( last.view );
+				promise = this._show( last.view );
 			} else {
 				// Hide the balloon panel.
 				this.view.hide();
@@ -151,6 +156,8 @@ export default class ContextualBalloon extends Plugin {
 			// Just remove given view from the stack.
 			this._stack.delete( view );
 		}
+
+		return promise;
 	}
 
 	/**
@@ -167,10 +174,12 @@ export default class ContextualBalloon extends Plugin {
 	 *
 	 * @private
 	 * @param {module:ui/view~View} view View to show in the balloon.
+	 * @returns {Promise} A Promise resolved when the child {@link module:ui/view~View#init} is done.
 	 */
 	_show( view ) {
-		this.view.content.add( view );
-		this.view.attachTo( this._getBalloonPosition() );
+		return this.view.content.add( view ).then( () => {
+			this.view.attachTo( this._getBalloonPosition() );
+		} );
 	}
 
 	/**
@@ -190,6 +199,7 @@ export default class ContextualBalloon extends Plugin {
 	destroy() {
 		this.editor.ui.view.body.remove( this.view );
 		this.view.destroy();
+		this._stack.clear();
 		super.destroy();
 	}
 }

+ 0 - 196
packages/ckeditor5-ui/src/panel/floating/floatingpanelview.js

@@ -1,196 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module ui/panel/floating/floatingpanelview
- */
-
-import global from '@ckeditor/ckeditor5-utils/src/dom/global';
-import Template from '../../template';
-import View from '../../view';
-import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit';
-import { getOptimalPosition } from '@ckeditor/ckeditor5-utils/src/dom/position';
-
-const toPx = toUnit( 'px' );
-
-/**
- * The floating panel view class. It floats around the
- * {@link module:ui/panel/floating/floatingpanelview~FloatingPanelView#targetElement} in DOM
- * to remain visible in the browser viewport.
- *
- * See {@link module:ui/panel/floating/floatingpanelview~FloatingPanelView.defaultPositions}
- * to learn about the positioning.
- *
- * @extends module:ui/view~View
- */
-export default class FloatingPanelView extends View {
-	/**
-	 * @inheritDoc
-	 */
-	constructor( locale ) {
-		super( locale );
-
-		const bind = this.bindTemplate;
-
-		/**
-		 * Controls whether the floating panel is active. When any editable
-		 * is focused in the editor, panel becomes active.
-		 *
-		 * @readonly
-		 * @observable
-		 * @member {Boolean} #isActive
-		 */
-		this.set( 'isActive', false );
-
-		/**
-		 * The absolute top position of the panel, in pixels.
-		 *
-		 * @observable
-		 * @default 0
-		 * @member {Number} #top
-		 */
-		this.set( 'top', 0 );
-
-		/**
-		 * The absolute left position of the panel, in pixels.
-		 *
-		 * @observable
-		 * @default 0
-		 * @member {Number} #left
-		 */
-		this.set( 'left', 0 );
-
-		/**
-		 * An element with respect to which the panel is positioned.
-		 *
-		 * @readonly
-		 * @observable
-		 * @member {HTMLElement} #targetElement
-		 */
-		this.set( 'targetElement', null );
-
-		/**
-		 * Collection of the child views which creates balloon panel contents.
-		 *
-		 * @readonly
-		 * @member {module:ui/viewcollection~ViewCollection}
-		 */
-		this.content = this.createCollection();
-
-		this.template = new Template( {
-			tag: 'div',
-			attributes: {
-				class: [
-					'ck-floating-panel',
-					bind.if( 'isActive', 'ck-floating-panel_active' ),
-				],
-				style: {
-					top: bind.to( 'top', toPx ),
-					left: bind.to( 'left', toPx ),
-				}
-			},
-
-			children: this.content
-		} );
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	init() {
-		this.listenTo( global.window, 'scroll', () => this._updatePosition() );
-		this.listenTo( this, 'change:isActive', () => this._updatePosition() );
-
-		return super.init();
-	}
-
-	/**
-	 * Analyzes the environment to decide where the panel should
-	 * be positioned.
-	 *
-	 * @protected
-	 */
-	_updatePosition() {
-		if ( !this.isActive ) {
-			return;
-		}
-
-		const { nw, sw, ne, se } = FloatingPanelView.defaultPositions;
-		const { top, left } = getOptimalPosition( {
-			element: this.element,
-			target: this.targetElement,
-			positions: [ nw, sw, ne, se ],
-			limiter: global.document.body,
-			fitInViewport: true
-		} );
-
-		Object.assign( this, { top, left } );
-	}
-}
-
-/**
- * A default set of positioning functions used by the panel view to float
- * around {@link module:ui/panel/floating/floatingpanelview~FloatingPanelView#targetElement}.
- *
- * The available positioning functions are as follows:
- *
- * * South east:
- *
- *		+----------------+
- *		| #targetElement |
- *		+----------------+
- *		         [ Panel ]
- *
- * * South west:
- *
- *		+----------------+
- *		| #targetElement |
- *		+----------------+
- *		[ Panel ]
- *
- * * North east:
- *
- *		         [ Panel ]
- *		+----------------+
- *		| #targetElement |
- *		+----------------+
- *
- *
- * * North west:
- *
- *		[ Panel ]
- *		+----------------+
- *		| #targetElement |
- *		+----------------+
- *
- * Positioning functions must be compatible with {@link module:utils/dom/position~Position}.
- *
- * @member {Object} module:ui/panel/floating/floatingpanelview~FloatingPanelView.defaultPositions
- */
-FloatingPanelView.defaultPositions = {
-	nw: ( targetRect, panelRect ) => ( {
-		top: targetRect.top - panelRect.height,
-		left: targetRect.left,
-		name: 'nw'
-	} ),
-
-	sw: ( targetRect ) => ( {
-		top: targetRect.bottom,
-		left: targetRect.left,
-		name: 'sw'
-	} ),
-
-	ne: ( targetRect, panelRect ) => ( {
-		top: targetRect.top - panelRect.height,
-		left: targetRect.left + targetRect.width - panelRect.width,
-		name: 'ne'
-	} ),
-
-	se: ( targetRect, panelRect ) => ( {
-		top: targetRect.bottom,
-		left: targetRect.left + targetRect.width - panelRect.width,
-		name: 'se'
-	} )
-};

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

@@ -16,7 +16,7 @@ import EssentialsPresets from '@ckeditor/ckeditor5-presets/src/essentials';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 
-import ContextualBalloon from '../../../src/contextualballoon';
+import ContextualBalloon from '../../../src/panel/balloon/contextualballoon';
 import ToolbarView from '../../../src/toolbar/toolbarview';
 import ButtonView from '../../../src/button/buttonview';
 import Template from '../../../src/template';
@@ -230,13 +230,13 @@ function createContextualToolbar( editor ) {
 		forwardSelection: ( targetRect, balloonRect ) => ( {
 			top: targetRect.bottom + arrowVOffset,
 			left: targetRect.right - balloonRect.width / 2,
-			name: 's'
+			name: 'arrow_s'
 		} ),
 
 		backwardSelection: ( targetRect, balloonRect ) => ( {
 			top: targetRect.top - balloonRect.height - arrowVOffset,
 			left: targetRect.left - balloonRect.width / 2,
-			name: 'n'
+			name: 'arrow_n'
 		} )
 	};
 

+ 2 - 2
packages/ckeditor5-ui/tests/manual/contextualtoolbar/contextualtoolbar.js

@@ -28,7 +28,7 @@ const positions = {
 	forwardSelection: ( targetRect, balloonRect ) => ( {
 		top: targetRect.bottom + arrowVOffset,
 		left: targetRect.right - balloonRect.width / 2,
-		name: 's'
+		name: 'arrow_s'
 	} ),
 
 	//	+-----------------+
@@ -39,7 +39,7 @@ const positions = {
 	backwardSelection: ( targetRect, balloonRect ) => ( {
 		top: targetRect.top - balloonRect.height - arrowVOffset,
 		left: targetRect.left - balloonRect.width / 2,
-		name: 'n'
+		name: 'arrow_n'
 	} )
 };
 

+ 1 - 1
packages/ckeditor5-ui/tests/manual/tickets/170/1.js

@@ -52,7 +52,7 @@ ClassicEditor.create( document.querySelector( '#editor-stick' ), {
 	editor.ui.view.element.querySelector( '.ck-editor__editable' ).scrollTop = 360;
 
 	panel.init().then( () => {
-		panel.keepAttachedTo( {
+		panel.pin( {
 			target: editor.ui.view.element.querySelector( '.ck-editor__editable p strong' ),
 			limiter: editor.ui.view.editableElement
 		} );

+ 1 - 1
packages/ckeditor5-ui/tests/manual/tickets/170/1.md

@@ -1,4 +1,4 @@
-## BalloonPanelView `attachTo` vs `keepAttachedTo`
+## BalloonPanelView `attachTo` vs `pin`
 
 Scroll editable elements and container (horizontally as well). Balloon in the left editor should float but balloon in the
 right editor should stick to the target element.

+ 180 - 80
packages/ckeditor5-ui/tests/panel/balloon/balloonpanelview.js

@@ -25,6 +25,12 @@ describe( 'BalloonPanelView', () => {
 		return view.init();
 	} );
 
+	afterEach( () => {
+		if ( view ) {
+			return view.destroy();
+		}
+	} );
+
 	describe( 'constructor()', () => {
 		it( 'should create element from template', () => {
 			expect( view.element.tagName ).to.equal( 'DIV' );
@@ -35,8 +41,9 @@ describe( 'BalloonPanelView', () => {
 		it( 'should set default values', () => {
 			expect( view.top ).to.equal( 0 );
 			expect( view.left ).to.equal( 0 );
-			expect( view.position ).to.equal( 'se' );
+			expect( view.position ).to.equal( 'arrow_se' );
 			expect( view.isVisible ).to.equal( false );
+			expect( view.withArrow ).to.equal( true );
 		} );
 
 		it( 'creates view#content collection', () => {
@@ -49,10 +56,18 @@ describe( 'BalloonPanelView', () => {
 			it( 'should react on view#position', () => {
 				expect( view.element.classList.contains( 'ck-balloon-panel_arrow_se' ) ).to.true;
 
-				view.position = 'sw';
+				view.position = 'arrow_sw';
 
 				expect( view.element.classList.contains( 'ck-balloon-panel_arrow_sw' ) ).to.true;
 			} );
+
+			it( 'should react on view#withArrow', () => {
+				expect( view.element.classList.contains( 'ck-balloon-panel_with-arrow' ) ).to.be.true;
+
+				view.withArrow = false;
+
+				expect( view.element.classList.contains( 'ck-balloon-panel_with-arrow' ) ).to.be.false;
+			} );
 		} );
 
 		describe( 'isVisible', () => {
@@ -96,9 +111,10 @@ describe( 'BalloonPanelView', () => {
 				expect( view.element.childNodes.length ).to.equal( 0 );
 
 				const button = new ButtonView( { t() {} } );
-				view.content.add( button );
 
-				expect( view.element.childNodes.length ).to.equal( 1 );
+				return view.content.add( button ).then( () => {
+					expect( view.element.childNodes.length ).to.equal( 1 );
+				} );
 			} );
 		} );
 	} );
@@ -203,7 +219,7 @@ describe( 'BalloonPanelView', () => {
 
 				view.attachTo( { target, limiter } );
 
-				expect( view.position ).to.equal( 'se' );
+				expect( view.position ).to.equal( 'arrow_se' );
 			} );
 
 			it( 'should put balloon on the `south east` side of the target element when target is on the top left side of the limiter', () => {
@@ -216,7 +232,7 @@ describe( 'BalloonPanelView', () => {
 
 				view.attachTo( { target, limiter } );
 
-				expect( view.position ).to.equal( 'se' );
+				expect( view.position ).to.equal( 'arrow_se' );
 			} );
 
 			it( 'should put balloon on the `south west` side of the target element when target is on the right side of the limiter', () => {
@@ -229,7 +245,7 @@ describe( 'BalloonPanelView', () => {
 
 				view.attachTo( { target, limiter } );
 
-				expect( view.position ).to.equal( 'sw' );
+				expect( view.position ).to.equal( 'arrow_sw' );
 			} );
 
 			it( 'should put balloon on the `north east` side of the target element when target is on the bottom of the limiter ', () => {
@@ -242,7 +258,7 @@ describe( 'BalloonPanelView', () => {
 
 				view.attachTo( { target, limiter } );
 
-				expect( view.position ).to.equal( 'ne' );
+				expect( view.position ).to.equal( 'arrow_ne' );
 			} );
 
 			it( 'should put balloon on the `north west` side of the target element when target is on the bottom right of the limiter', () => {
@@ -255,7 +271,7 @@ describe( 'BalloonPanelView', () => {
 
 				view.attachTo( { target, limiter } );
 
-				expect( view.position ).to.equal( 'nw' );
+				expect( view.position ).to.equal( 'arrow_nw' );
 			} );
 
 			// https://github.com/ckeditor/ckeditor5-ui-default/issues/126
@@ -336,7 +352,7 @@ describe( 'BalloonPanelView', () => {
 
 				view.attachTo( { target, limiter } );
 
-				expect( view.position ).to.equal( 'sw' );
+				expect( view.position ).to.equal( 'arrow_sw' );
 			} );
 
 			it( 'should put balloon on the `south east` position when `south west` is limited', () => {
@@ -356,7 +372,7 @@ describe( 'BalloonPanelView', () => {
 
 				view.attachTo( { target, limiter } );
 
-				expect( view.position ).to.equal( 'se' );
+				expect( view.position ).to.equal( 'arrow_se' );
 			} );
 
 			it( 'should put balloon on the `north east` position when `south east` is limited', () => {
@@ -380,7 +396,7 @@ describe( 'BalloonPanelView', () => {
 
 				view.attachTo( { target, limiter } );
 
-				expect( view.position ).to.equal( 'ne' );
+				expect( view.position ).to.equal( 'arrow_ne' );
 			} );
 
 			it( 'should put balloon on the `south east` position when `north east` is limited', () => {
@@ -400,21 +416,23 @@ describe( 'BalloonPanelView', () => {
 
 				view.attachTo( { target, limiter } );
 
-				expect( view.position ).to.equal( 'se' );
+				expect( view.position ).to.equal( 'arrow_se' );
 			} );
 		} );
 	} );
 
-	describe( 'keepAttachedTo()', () => {
+	describe( 'pin() and unpin()', () => {
 		let attachToSpy, target, targetParent, limiter, notRelatedElement;
 
 		beforeEach( () => {
-			attachToSpy = testUtils.sinon.spy( view, 'attachTo' );
+			attachToSpy = sinon.spy( view, 'attachTo' );
 			limiter = document.createElement( 'div' );
 			targetParent = document.createElement( 'div' );
 			target = document.createElement( 'div' );
 			notRelatedElement = document.createElement( 'div' );
 
+			view.show();
+
 			targetParent.appendChild( target );
 			document.body.appendChild( targetParent );
 			document.body.appendChild( limiter );
@@ -422,112 +440,194 @@ describe( 'BalloonPanelView', () => {
 		} );
 
 		afterEach( () => {
-			attachToSpy.restore();
+			targetParent.remove();
 			limiter.remove();
 			notRelatedElement.remove();
 		} );
 
-		it( 'should keep the balloon attached to the target when any of the related elements is scrolled', () => {
-			view.keepAttachedTo( { target, limiter } );
+		describe( 'pin()', () => {
+			it( 'should show the balloon', () => {
+				const spy = sinon.spy( view, 'show' );
+
+				view.hide();
 
-			sinon.assert.calledOnce( attachToSpy );
-			sinon.assert.calledWith( attachToSpy.lastCall, { target, limiter } );
+				view.pin( { target, limiter } );
+				sinon.assert.calledOnce( spy );
+			} );
 
-			targetParent.dispatchEvent( new Event( 'scroll' ) );
+			it( 'should start pinning when the balloon is visible', () => {
+				view.pin( { target, limiter } );
+				sinon.assert.calledOnce( attachToSpy );
 
-			sinon.assert.calledTwice( attachToSpy );
-			sinon.assert.calledWith( attachToSpy.lastCall, { target, limiter } );
+				view.hide();
+				targetParent.dispatchEvent( new Event( 'scroll' ) );
 
-			limiter.dispatchEvent( new Event( 'scroll' ) );
+				view.show();
+				sinon.assert.calledTwice( attachToSpy );
 
-			sinon.assert.calledThrice( attachToSpy );
-			sinon.assert.calledWith( attachToSpy.lastCall, { target, limiter } );
+				targetParent.dispatchEvent( new Event( 'scroll' ) );
+				sinon.assert.calledThrice( attachToSpy );
+			} );
 
-			notRelatedElement.dispatchEvent( new Event( 'scroll' ) );
+			it( 'should stop pinning when the balloon becomes invisible', () => {
+				view.show();
 
-			// Nothing's changed.
-			sinon.assert.calledThrice( attachToSpy );
-			sinon.assert.calledWith( attachToSpy.lastCall, { target, limiter } );
-		} );
+				view.pin( { target, limiter } );
+				sinon.assert.calledOnce( attachToSpy );
 
-		it( 'should keep the balloon attached to the target when the browser window is being resized', () => {
-			view.keepAttachedTo( { target, limiter } );
+				view.hide();
 
-			sinon.assert.calledOnce( attachToSpy );
-			sinon.assert.calledWith( attachToSpy.lastCall, { target, limiter } );
+				targetParent.dispatchEvent( new Event( 'scroll' ) );
+				sinon.assert.calledOnce( attachToSpy );
+			} );
 
-			window.dispatchEvent( new Event( 'resize' ) );
+			it( 'should unpin if already pinned', () => {
+				const unpinSpy = testUtils.sinon.spy( view, 'unpin' );
 
-			sinon.assert.calledTwice( attachToSpy );
-			sinon.assert.calledWith( attachToSpy.lastCall, { target, limiter } );
-		} );
+				view.show();
+				sinon.assert.notCalled( attachToSpy );
 
-		it( 'should stop attaching when the balloon is hidden', () => {
-			view.keepAttachedTo( { target, limiter } );
+				view.pin( { target, limiter } );
+				sinon.assert.calledOnce( attachToSpy );
 
-			sinon.assert.calledOnce( attachToSpy );
+				view.pin( { target, limiter } );
+				sinon.assert.calledTwice( unpinSpy );
 
-			view.hide();
+				targetParent.dispatchEvent( new Event( 'scroll' ) );
+				sinon.assert.calledThrice( attachToSpy );
+			} );
 
-			window.dispatchEvent( new Event( 'resize' ) );
-			window.dispatchEvent( new Event( 'scroll' ) );
+			it( 'should keep the balloon pinned to the target when any of the related elements is scrolled', () => {
+				view.pin( { target, limiter } );
 
-			// Still once.
-			sinon.assert.calledOnce( attachToSpy );
-		} );
+				sinon.assert.calledOnce( attachToSpy );
+				sinon.assert.calledWith( attachToSpy.lastCall, { target, limiter } );
 
-		it( 'should stop attaching once the view is destroyed', () => {
-			view.keepAttachedTo( { target, limiter } );
+				targetParent.dispatchEvent( new Event( 'scroll' ) );
 
-			sinon.assert.calledOnce( attachToSpy );
+				sinon.assert.calledTwice( attachToSpy );
+				sinon.assert.calledWith( attachToSpy.lastCall, { target, limiter } );
 
-			view.destroy();
+				limiter.dispatchEvent( new Event( 'scroll' ) );
 
-			window.dispatchEvent( new Event( 'resize' ) );
-			window.dispatchEvent( new Event( 'scroll' ) );
+				sinon.assert.calledThrice( attachToSpy );
+				sinon.assert.calledWith( attachToSpy.lastCall, { target, limiter } );
 
-			// Still once.
-			sinon.assert.calledOnce( attachToSpy );
-		} );
+				notRelatedElement.dispatchEvent( new Event( 'scroll' ) );
 
-		it( 'should set document.body as the default limiter', () => {
-			view.keepAttachedTo( { target } );
+				// Nothing's changed.
+				sinon.assert.calledThrice( attachToSpy );
+				sinon.assert.calledWith( attachToSpy.lastCall, { target, limiter } );
+			} );
 
-			sinon.assert.calledOnce( attachToSpy );
+			it( 'should keep the balloon pinned to the target when the browser window is being resized', () => {
+				view.pin( { target, limiter } );
 
-			document.body.dispatchEvent( new Event( 'scroll' ) );
+				sinon.assert.calledOnce( attachToSpy );
+				sinon.assert.calledWith( attachToSpy.lastCall, { target, limiter } );
 
-			sinon.assert.calledTwice( attachToSpy );
-		} );
+				window.dispatchEvent( new Event( 'resize' ) );
+
+				sinon.assert.calledTwice( attachToSpy );
+				sinon.assert.calledWith( attachToSpy.lastCall, { target, limiter } );
+			} );
+
+			it( 'should stop attaching when the balloon is hidden', () => {
+				view.pin( { target, limiter } );
+
+				sinon.assert.calledOnce( attachToSpy );
+
+				view.hide();
 
-		it( 'should work for Range as a target', () => {
-			const element = document.createElement( 'div' );
-			const range = document.createRange();
+				window.dispatchEvent( new Event( 'resize' ) );
+				window.dispatchEvent( new Event( 'scroll' ) );
 
-			element.appendChild( document.createTextNode( 'foo bar' ) );
-			document.body.appendChild( element );
-			range.selectNodeContents( element );
+				// Still once.
+				sinon.assert.calledOnce( attachToSpy );
+			} );
+
+			it( 'should stop attaching once the view is destroyed', () => {
+				view.pin( { target, limiter } );
+
+				sinon.assert.calledOnce( attachToSpy );
+
+				return view.destroy().then( () => {
+					view = null;
+
+					window.dispatchEvent( new Event( 'resize' ) );
+					window.dispatchEvent( new Event( 'scroll' ) );
+
+					// Still once.
+					sinon.assert.calledOnce( attachToSpy );
+				} );
+			} );
+
+			it( 'should set document.body as the default limiter', () => {
+				view.pin( { target } );
+
+				sinon.assert.calledOnce( attachToSpy );
+
+				document.body.dispatchEvent( new Event( 'scroll' ) );
+
+				sinon.assert.calledTwice( attachToSpy );
+			} );
 
-			view.keepAttachedTo( { target: range } );
+			it( 'should work for Range as a target', () => {
+				const element = document.createElement( 'div' );
+				const range = document.createRange();
 
-			sinon.assert.calledOnce( attachToSpy );
+				element.appendChild( document.createTextNode( 'foo bar' ) );
+				document.body.appendChild( element );
+				range.selectNodeContents( element );
 
-			element.dispatchEvent( new Event( 'scroll' ) );
+				view.pin( { target: range } );
 
-			sinon.assert.calledTwice( attachToSpy );
+				sinon.assert.calledOnce( attachToSpy );
+
+				element.dispatchEvent( new Event( 'scroll' ) );
+
+				sinon.assert.calledTwice( attachToSpy );
+			} );
+
+			it( 'should work for rect as a target', () => {
+				// Just check if this normally works without errors.
+				const rect = {};
+
+				view.pin( { target: rect, limiter } );
+
+				sinon.assert.calledOnce( attachToSpy );
+
+				limiter.dispatchEvent( new Event( 'scroll' ) );
+
+				sinon.assert.calledTwice( attachToSpy );
+			} );
 		} );
 
-		it( 'should work for rect as a target', () => {
-			// Just check if this normally works without errors.
-			const rect = {};
+		describe( 'unpin()', () => {
+			it( 'should hide the balloon if pinned', () => {
+				const spy = sinon.spy( view, 'hide' );
+
+				view.pin( { target, limiter } );
+				view.unpin();
 
-			view.keepAttachedTo( { target: rect, limiter } );
+				sinon.assert.calledOnce( spy );
+			} );
+
+			it( 'should stop attaching', () => {
+				view.pin( { target, limiter } );
+				sinon.assert.calledOnce( attachToSpy );
 
-			sinon.assert.calledOnce( attachToSpy );
+				view.unpin();
 
-			limiter.dispatchEvent( new Event( 'scroll' ) );
+				view.hide();
+				window.dispatchEvent( new Event( 'resize' ) );
+				document.dispatchEvent( new Event( 'scroll' ) );
+				view.show();
+				window.dispatchEvent( new Event( 'resize' ) );
+				document.dispatchEvent( new Event( 'scroll' ) );
 
-			sinon.assert.calledTwice( attachToSpy );
+				sinon.assert.calledOnce( attachToSpy );
+			} );
 		} );
 	} );
 } );

+ 95 - 128
packages/ckeditor5-ui/tests/contextualballoon.js

@@ -4,14 +4,13 @@
  */
 
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
-import ContextualBalloon from '../src/contextualballoon';
-import BalloonPanelView from '../src/panel/balloon/balloonpanelview';
-import View from '../src/view';
-import Template from '../src/template';
+import ContextualBalloon from '../../../src/panel/balloon/contextualballoon';
+import BalloonPanelView from '../../../src/panel/balloon/balloonpanelview';
+import View from '../../../src/view';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 
-/* global document, Event */
+/* global document, Event, setTimeout */
 
 describe( 'ContextualBalloon', () => {
 	let editor, editorElement, balloon, viewA, viewB;
@@ -27,11 +26,20 @@ describe( 'ContextualBalloon', () => {
 			editor = newEditor;
 			balloon = editor.plugins.get( ContextualBalloon );
 
-			viewA = new ViewA();
-			viewB = new ViewB();
-
 			// We don't need to test attachTo method of BalloonPanel it's enough to check if was called with proper data.
 			sinon.stub( balloon.view, 'attachTo', () => {} );
+
+			viewA = new View();
+			viewB = new View();
+
+			// Add viewA to the pane and init viewB.
+			return Promise.all( [
+				balloon.add( {
+					view: viewA,
+					position: { target: 'fake' }
+				} ),
+				viewB.init(),
+			] );
 		} );
 	} );
 
@@ -74,21 +82,11 @@ describe( 'ContextualBalloon', () => {
 
 	describe( 'hasView()', () => {
 		it( 'should return true when given view is in stack', () => {
-			balloon.add( {
-				view: viewA,
-				position: { target: 'fake' }
-			} );
-
 			expect( balloon.hasView( viewA ) ).to.true;
 		} );
 
 		it( 'should return true when given view is in stack but is not visible', () => {
 			balloon.add( {
-				view: viewA,
-				position: { target: 'fake' }
-			} );
-
-			balloon.add( {
 				view: viewB,
 				position: { target: 'fake' }
 			} );
@@ -98,17 +96,39 @@ describe( 'ContextualBalloon', () => {
 		} );
 
 		it( 'should return false when given view is not in stack', () => {
-			expect( balloon.hasView( viewA ) ).to.false;
+			expect( balloon.hasView( viewB ) ).to.false;
 		} );
 	} );
 
 	describe( 'add()', () => {
-		it( 'should add view to the stack and display in balloon', () => {
-			balloon.add( {
-				view: viewA,
+		it( 'should return promise resolved when view is ready', ( done ) => {
+			const clock = sinon.useFakeTimers();
+
+			const view = {
+				init: () => {
+					return new Promise( ( resolve ) => {
+						setTimeout( () => {
+							resolve();
+						}, 10 );
+					} );
+				},
+				destroy: () => {}
+			};
+
+			const result = balloon.add( {
+				view: view,
 				position: { target: 'fake' }
 			} );
 
+			expect( result ).to.instanceof( Promise );
+
+			result.then( done );
+
+			clock.tick( 11 );
+			clock.restore();
+		} );
+
+		it( 'should add view to the stack and display in balloon attached using given position options', () => {
 			expect( balloon.view.content.length ).to.equal( 1 );
 			expect( balloon.view.content.get( 0 ) ).to.deep.equal( viewA );
 			expect( balloon.view.attachTo.calledOnce ).to.true;
@@ -116,11 +136,6 @@ describe( 'ContextualBalloon', () => {
 		} );
 
 		it( 'should throw an error when try to add the same view more than once', () => {
-			balloon.add( {
-				view: viewA,
-				position: { target: 'fake' }
-			} );
-
 			expect( () => {
 				balloon.add( {
 					view: viewA,
@@ -131,11 +146,6 @@ describe( 'ContextualBalloon', () => {
 
 		it( 'should add multiple views to he stack and display last one', () => {
 			balloon.add( {
-				view: viewA,
-				position: { target: 'fake' }
-			} );
-
-			balloon.add( {
 				view: viewB,
 				position: { target: 'fake' }
 			} );
@@ -144,48 +154,32 @@ describe( 'ContextualBalloon', () => {
 			expect( balloon.view.content.get( 0 ) ).to.deep.equal( viewB );
 		} );
 
-		it( 'should add multiple views to the stack and keep balloon in the same position', () => {
-			balloon.add( {
-				view: viewA,
-				position: { target: 'fake', foo: 'bar' }
-			} );
-
-			balloon.add( {
+		it( 'should keep balloon at the same position after adding next view', () => {
+			return balloon.add( {
 				view: viewB,
-				position: { target: 'fake', bar: 'biz' }
-			} );
-
-			expect( balloon.view.attachTo.calledTwice ).to.true;
+				position: { target: 'other' }
+			} )
+			.then( () => {
+				expect( balloon.view.attachTo.calledTwice ).to.true;
 
-			expect( balloon.view.attachTo.firstCall.args[ 0 ] ).to.deep.equal( {
-				target: 'fake',
-				foo: 'bar'
-			} );
+				expect( balloon.view.attachTo.firstCall.args[ 0 ] ).to.deep.equal( {
+					target: 'fake'
+				} );
 
-			expect( balloon.view.attachTo.secondCall.args[ 0 ] ).to.deep.equal( {
-				target: 'fake',
-				foo: 'bar'
+				expect( balloon.view.attachTo.secondCall.args[ 0 ] ).to.deep.equal( {
+					target: 'fake'
+				} );
 			} );
 		} );
 	} );
 
 	describe( 'visibleView', () => {
 		it( 'should return data of currently visible view', () => {
-			balloon.add( {
-				view: viewA,
-				position: { target: 'fake' }
-			} );
-
 			expect( balloon.visibleView ).to.equal( viewA );
 		} );
 
 		it( 'should return data of currently visible view when there is more than one in the stack', () => {
 			balloon.add( {
-				view: viewA,
-				position: { target: 'fake' }
-			} );
-
-			balloon.add( {
 				view: viewB,
 				position: { target: 'fake' }
 			} );
@@ -194,28 +188,26 @@ describe( 'ContextualBalloon', () => {
 		} );
 
 		it( 'should return `null` when the stack is empty', () => {
+			balloon.remove( viewA );
 			expect( balloon.visibleView ).to.null;
 		} );
 	} );
 
 	describe( 'remove()', () => {
+		it( 'should return promise', () => {
+			expect( balloon.remove( viewA ) ).to.instanceof( Promise );
+		} );
+
 		it( 'should remove given view and hide balloon when there is no other view to display', () => {
-			balloon.add( {
-				view: viewA,
-				position: { target: 'fake' }
-			} );
+			balloon.view.isVisible = true;
 
 			balloon.remove( viewA );
 
 			expect( balloon.visibleView ).to.null;
+			expect( balloon.view.isVisible ).to.false;
 		} );
 
-		it( 'should remove given view and set previous in the stack as visible when removed view was visible', () => {
-			balloon.add( {
-				view: viewA,
-				position: { target: 'fake' }
-			} );
-
+		it( 'should remove given view and set preceding in the stack as visible when removed view was visible', () => {
 			balloon.add( {
 				view: viewB,
 				position: { target: 'fake' }
@@ -226,12 +218,37 @@ describe( 'ContextualBalloon', () => {
 			expect( balloon.visibleView ).to.equal( viewA );
 		} );
 
-		it( 'should remove given view from the stack when view is not visible', () => {
+		it( 'should wait for init of preceding view when was is not ready', ( done ) => {
+			const clock = sinon.useFakeTimers();
+
+			const view = {
+				init: () => {
+					return new Promise( ( resolve ) => {
+						setTimeout( () => {
+							resolve();
+						}, 10 );
+					} );
+				},
+				destroy: () => {}
+			};
+
 			balloon.add( {
-				view: viewA,
+				view: view,
+				position: { target: 'fake' }
+			} );
+
+			balloon.add( {
+				view: viewB,
 				position: { target: 'fake' }
 			} );
 
+			balloon.remove( viewB ).then( done );
+
+			clock.tick( 11 );
+			clock.restore();
+		} );
+
+		it( 'should remove given view from the stack when view is not visible', () => {
 			balloon.add( {
 				view: viewB,
 				position: { target: 'fake' }
@@ -244,18 +261,13 @@ describe( 'ContextualBalloon', () => {
 
 		it( 'should throw an error when there is no given view in the stack', () => {
 			expect( () => {
-				balloon.remove( viewA );
+				balloon.remove( viewB );
 			} ).to.throw( CKEditorError, /^contextualballoon-remove-view-not-exist/ );
 		} );
 	} );
 
 	describe( 'updatePosition()', () => {
 		it( 'should attach balloon to the target using the same position options as currently set', () => {
-			balloon.add( {
-				view: viewA,
-				position: { target: 'fake' }
-			} );
-
 			balloon.view.attachTo.reset();
 
 			balloon.updatePosition();
@@ -266,18 +278,9 @@ describe( 'ContextualBalloon', () => {
 
 		it( 'should attach balloon to the target using the same position options as currently set when there is more than one view', () => {
 			balloon.add( {
-				view: viewA,
-				position: {
-					target: 'fake',
-					foo: 'bar'
-				}
-			} );
-
-			balloon.add( {
 				view: viewB,
 				position: {
-					target: 'fake',
-					bar: 'biz'
+					target: 'other'
 				}
 			} );
 
@@ -287,59 +290,23 @@ describe( 'ContextualBalloon', () => {
 
 			expect( balloon.view.attachTo.calledOnce );
 			expect( balloon.view.attachTo.firstCall.args[ 0 ] ).to.deep.equal( {
-				target: 'fake',
-				foo: 'bar'
-			} );
-		} );
-
-		it( 'should remove given view from the stack when view is not visible', () => {
-			balloon.add( {
-				view: viewA,
-				position: { target: 'fake' }
-			} );
-
-			balloon.add( {
-				view: viewB,
-				position: { target: 'fake' }
+				target: 'fake'
 			} );
-
-			balloon.remove( viewA );
-
-			expect( balloon.visibleView ).to.equal( viewB );
 		} );
 
 		it( 'should throw an error when there is no given view in the stack', () => {
 			expect( () => {
-				balloon.remove( viewA );
+				balloon.remove( viewB );
 			} ).to.throw( CKEditorError, /^contextualballoon-remove-view-not-exist/ );
 		} );
 	} );
 
 	describe( 'destroy()', () => {
-		it( 'should balloon panel remove view from editor body collection', () => {
+		it( 'should remove balloon panel view from editor body collection and clear stack', () => {
 			balloon.destroy();
 
 			expect( editor.ui.view.body.getIndex( balloon.view ) ).to.equal( -1 );
+			expect( balloon.visibleView ).to.null;
 		} );
 	} );
 } );
-
-class ViewA extends View {
-	constructor( locale ) {
-		super( locale );
-
-		this.template = new Template( {
-			tag: 'div'
-		} );
-	}
-}
-
-class ViewB extends View {
-	constructor( locale ) {
-		super( locale );
-
-		this.template = new Template( {
-			tag: 'div'
-		} );
-	}
-}

+ 0 - 229
packages/ckeditor5-ui/tests/panel/floating/floatingpanelview.js

@@ -1,229 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/* global document, Event */
-
-import global from '@ckeditor/ckeditor5-utils/src/dom/global';
-import FloatingPanelView from '../../../src/panel/floating/floatingpanelview';
-import View from '../../../src/view';
-import ViewCollection from '../../../src/viewcollection';
-import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
-import * as positionUtils from '@ckeditor/ckeditor5-utils/src/dom/position';
-
-testUtils.createSinonSandbox();
-
-describe( 'FloatingPanelView', () => {
-	let locale, view, target;
-
-	beforeEach( () => {
-		locale = {};
-		view = new FloatingPanelView( locale );
-
-		target = global.document.createElement( 'a' );
-
-		global.document.body.appendChild( view.element );
-		global.document.body.appendChild( target );
-
-		view.targetElement = target;
-
-		return view.init();
-	} );
-
-	afterEach( () => {
-		view.element.remove();
-
-		return view.destroy();
-	} );
-
-	describe( 'constructor()', () => {
-		it( 'should inherit from View', () => {
-			expect( view ).to.be.instanceOf( View );
-		} );
-
-		it( 'should set view#locale', () => {
-			expect( view.locale ).to.equal( locale );
-		} );
-
-		it( 'should set #isActive', () => {
-			expect( view.isActive ).to.be.false;
-		} );
-
-		it( 'should set #top', () => {
-			expect( view.top ).to.equal( 0 );
-		} );
-
-		it( 'should set #left', () => {
-			expect( view.left ).to.equal( 0 );
-		} );
-
-		it( 'should set #targetElement', () => {
-			view = new FloatingPanelView( locale );
-
-			expect( view.targetElement ).to.be.null;
-		} );
-
-		it( 'creates view#content collection', () => {
-			expect( view.content ).to.be.instanceOf( ViewCollection );
-		} );
-	} );
-
-	describe( 'template', () => {
-		it( 'should create element from template', () => {
-			expect( view.element.classList.contains( 'ck-floating-panel' ) ).to.be.true;
-		} );
-
-		describe( 'bindings', () => {
-			describe( 'class', () => {
-				it( 'reacts on #isActive', () => {
-					view.isActive = false;
-					expect( view.element.classList.contains( 'ck-floating-panel_active' ) ).to.be.false;
-
-					view.isActive = true;
-					expect( view.element.classList.contains( 'ck-floating-panel_active' ) ).to.be.true;
-				} );
-			} );
-
-			describe( 'style', () => {
-				it( 'reacts on #top', () => {
-					view.top = 30;
-					expect( view.element.style.top ).to.equal( '30px' );
-				} );
-
-				it( 'reacts on #left', () => {
-					view.left = 20;
-					expect( view.element.style.left ).to.equal( '20px' );
-				} );
-			} );
-
-			describe( 'children', () => {
-				it( 'should react on view#content', () => {
-					expect( view.element.childNodes.length ).to.equal( 0 );
-
-					const item = new View();
-					item.element = document.createElement( 'div' );
-					view.content.add( item );
-
-					expect( view.element.childNodes.length ).to.equal( 1 );
-				} );
-			} );
-		} );
-	} );
-
-	describe( 'init()', () => {
-		it( 'calls #_updatePosition on window.scroll', () => {
-			const spy = sinon.spy( view, '_updatePosition' );
-
-			global.window.dispatchEvent( new Event( 'scroll', { bubbles: true } ) );
-			sinon.assert.calledOnce( spy );
-		} );
-
-		it( 'calls #_updatePosition on #change:isActive', () => {
-			view.isActive = false;
-
-			const spy = sinon.spy( view, '_updatePosition' );
-
-			view.isActive = true;
-			sinon.assert.calledOnce( spy );
-
-			view.isActive = false;
-			sinon.assert.calledTwice( spy );
-		} );
-	} );
-
-	describe( '_updatePosition()', () => {
-		it( 'does not update when not #isActive', () => {
-			const spy = testUtils.sinon.spy( positionUtils, 'getOptimalPosition' );
-
-			view.isActive = false;
-
-			view._updatePosition();
-			sinon.assert.notCalled( spy );
-
-			view.isActive = true;
-
-			view._updatePosition();
-
-			// Note: #_updatePosition() is called on #change:isActive.
-			sinon.assert.calledTwice( spy );
-		} );
-
-		it( 'uses getOptimalPosition() utility', () => {
-			const { nw, sw, ne, se } = FloatingPanelView.defaultPositions;
-
-			view.isActive = true;
-
-			const spy = testUtils.sinon.stub( positionUtils, 'getOptimalPosition' ).returns( {
-				top: 5,
-				left: 10
-			} );
-
-			view._updatePosition();
-
-			sinon.assert.calledWithExactly( spy, {
-				element: view.element,
-				target: target,
-				positions: [ nw, sw, ne, se ],
-				limiter: global.document.body,
-				fitInViewport: true
-			} );
-
-			expect( view.top ).to.equal( 5 );
-			expect( view.left ).to.equal( 10 );
-		} );
-	} );
-
-	describe( 'defaultPositions', () => {
-		let targetRect, panelRect, defaultPositions;
-
-		beforeEach( () => {
-			defaultPositions = FloatingPanelView.defaultPositions;
-
-			targetRect = {
-				top: 10,
-				width: 100,
-				left: 10,
-				height: 10,
-				bottom: 20
-			};
-
-			panelRect = {
-				width: 20,
-				height: 10
-			};
-		} );
-
-		it( 'should provide "nw" position', () => {
-			expect( defaultPositions.nw( targetRect, panelRect ) ).to.deep.equal( {
-				top: 0,
-				left: 10,
-				name: 'nw'
-			} );
-		} );
-
-		it( 'should provide "sw" position', () => {
-			expect( defaultPositions.sw( targetRect, panelRect ) ).to.deep.equal( {
-				top: 20,
-				left: 10,
-				name: 'sw'
-			} );
-		} );
-
-		it( 'should provide "ne" position', () => {
-			expect( defaultPositions.ne( targetRect, panelRect ) ).to.deep.equal( {
-				top: 0,
-				left: 90,
-				name: 'ne'
-			} );
-		} );
-
-		it( 'should provide "se" position', () => {
-			expect( defaultPositions.se( targetRect, panelRect ) ).to.deep.equal( {
-				top: 20,
-				left: 90,
-				name: 'se'
-			} );
-		} );
-	} );
-} );

+ 28 - 24
packages/ckeditor5-ui/theme/components/panel/balloonpanel.scss

@@ -9,25 +9,15 @@
 
 	z-index: ck-z( 'modal' );
 
-	&:before,
-	&:after {
-		content: "";
-		position: absolute;
-	}
-
-	&:before {
-		z-index: ck-z();
-	}
-
-	&:after {
-		z-index: ck-z( 'default', 1 );
-	}
+	&.ck-balloon-panel_with-arrow {
+		&:before,
+		&:after {
+			content: "";
+			position: absolute;
+		}
 
-	&_arrow_s,
-	&_arrow_se,
-	&_arrow_sw {
 		&:before {
-			z-index: ck-z( 'default' );
+			z-index: ck-z();
 		}
 
 		&:after {
@@ -35,15 +25,29 @@
 		}
 	}
 
-	&_arrow_n,
-	&_arrow_ne,
-	&_arrow_nw {
-		&:before {
-			z-index: ck-z( 'default' );
+	&.ck-balloon-panel_arrow {
+		&_s,
+		&_se,
+		&_sw {
+			&:before {
+				z-index: ck-z( 'default' );
+			}
+
+			&:after {
+				z-index: ck-z( 'default', 1 );
+			}
 		}
 
-		&:after {
-			z-index: ck-z( 'default', 1 );
+		&_n,
+		&_ne,
+		&_nw {
+			&:before {
+				z-index: ck-z( 'default' );
+			}
+
+			&:after {
+				z-index: ck-z( 'default', 1 );
+			}
 		}
 	}
 

+ 0 - 11
packages/ckeditor5-ui/theme/components/panel/floatingpanel.scss

@@ -1,11 +0,0 @@
-// Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
-// For licensing, see LICENSE.md or http://ckeditor.com/license
-
-.ck-floating-panel {
-	position: absolute;
-	display: none;
-
-	&_active {
-		display: block;
-	}
-}

+ 0 - 1
packages/ckeditor5-ui/theme/theme.scss

@@ -14,6 +14,5 @@
 @import 'components/list';
 @import 'components/label';
 @import 'components/panel/balloonpanel';
-@import 'components/panel/floatingpanel';
 @import 'components/editor';
 @import 'components/toolbar/stickytoolbar';