Răsfoiți Sursa

Merge branch 'master' into t/182

Aleksander Nowodzinski 8 ani în urmă
părinte
comite
b2ce5851ea

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

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

+ 195 - 0
packages/ckeditor5-ui/src/notification/notification.js

@@ -0,0 +1,195 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/notification/notification
+ */
+
+/* globals window */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+
+/**
+ * The Notification plugin.
+ *
+ * This plugin sends few base types of notifications: `success`, `info` and `warning`. This notifications need to be
+ * handled and displayed by plugin responsible for showing UI of the notifications. Using this plugin for dispatching
+ * 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~Notification#showWarning}.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class Notification extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'ui/notification';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		// Each unhandled and not stopped `show:warning` event is displayed as system alert.
+		this.on( 'show:warning', ( evt, data ) => {
+			window.alert( data.message );
+		}, { priority: 'lowest' } );
+	}
+
+	/**
+	 * Shows success notification.
+	 *
+	 * At default it fires `show:success` event with given data but event namespace can be extended
+	 * by `data.namespace` option e.g.
+	 *
+	 * 		showSuccess( 'Image is uploaded.', {
+	 * 			namespace: 'upload:image'
+	 * 		} );
+	 *
+	 * will fire `show:success:upload:image` event.
+	 *
+	 * @param {String} message Content of the notification.
+	 * @param {Object} [data={}] Additional data.
+	 * @param {String} [data.namespace] Additional event namespace.
+	 */
+	showSuccess( message, data = {} ) {
+		this._showNotification( {
+			message: message,
+			type: 'success',
+			namespace: data.namespace
+		} );
+	}
+
+	/**
+	 * Shows info notification.
+	 *
+	 * At default it fires `show:info` event with given data but event namespace can be extended
+	 * by `data.namespace` option e.g.
+	 *
+	 * 		showInfo( 'Editor is offline.', {
+	 * 			namespace: 'editor:status'
+	 * 		} );
+	 *
+	 * will fire `show:info:editor:status` event.
+	 *
+	 * @param {String} message Content of the notification.
+	 * @param {Object} [data={}] Additional data.
+	 * @param {String} [data.namespace] Additional event namespace.
+	 */
+	showInfo( message, data = {} ) {
+		this._showNotification( {
+			message: message,
+			type: 'info',
+			namespace: data.namespace
+		} );
+	}
+
+	/**
+	 * Shows warning notification.
+	 *
+	 * At default it fires `show:warning` event with given data but event namespace can be extended
+	 * by `data.namespace` option e.g.
+	 *
+	 * 		showWarning( 'Image upload error.', {
+	 * 			namespace: 'upload:image'
+	 * 		} );
+	 *
+	 * will fire `show:warning:upload:image` event.
+	 *
+	 * Note that each unhandled and not stopped `warning` notification will be displayed as system alert.
+	 * Plugin responsible for displaying warnings should `stop()` the event to prevent of displaying it as alert:
+	 *
+	 * 		notifications.on( 'show:warning', ( evt, data ) => {
+	 * 			// Do something with data.
+	 *
+	 * 			// Stop this event to prevent of displaying as alert.
+	 * 			evt.stop();
+	 * 		} );
+	 *
+	 * You can attach many listeners to the same event and `stop()` this event in the listener with the low priority:
+	 *
+	 * 		notifications.on( 'show:warning', ( evt, data ) => {
+	 * 			// Show warning in the UI, but not stop it.
+	 * 		} );
+	 *
+	 * 		notifications.on( 'show:warning', ( evt, data ) => {
+	 * 			// Log warning to some error tracker.
+	 *
+	 * 			// Stop this event to prevent of displaying as alert.
+	 * 			evt.stop();
+	 * 		}, { priority: 'low' } );
+	 *
+	 * @param {String} message Content of the notification.
+	 * @param {Object} [data={}] Additional data.
+	 * @param {String} [data.namespace] Additional event namespace.
+	 */
+	showWarning( message, data = {} ) {
+		this._showNotification( {
+			message: message,
+			type: 'warning',
+			namespace: data.namespace
+		} );
+	}
+
+	/**
+	 * Fires `show` event with specified type, namespace and message.
+	 *
+	 * @private
+	 * @param {Object} data Message data.
+	 * @param {String} data.message Content of the notification.
+	 * @param {'success'|'info'|'warning'} data.type Type of message.
+	 * @param {String} [data.namespace] Additional event namespace.
+	 */
+	_showNotification( data ) {
+		const event = `show:${ data.type }` + ( data.namespace ? `:${ data.namespace }` : '' );
+
+		this.fire( event, {
+			message: data.message,
+			type: data.type
+		} );
+	}
+
+	/**
+	 * Fired when one of `showSuccess`, `showInfo`, `showWarning` methods is called.
+	 *
+	 * @event show
+	 * @param {Object} data Notification data.
+	 * @param {String} data.message Content of the notification.
+	 * @param {'success'|'info'|'warning'} data.type Type of notification.
+	 */
+
+	/**
+	 * Fired when `showSuccess` method is called.
+	 *
+	 * @event show:success
+	 * @param {Object} data Notification data.
+	 * @param {String} data.message Content of the notification.
+	 * @param {'success'} data.type Type of notification.
+	 */
+
+	/**
+	 * Fired when `showInfo` method is called.
+	 *
+	 * @event show:info
+	 * @param {Object} data Notification data.
+	 * @param {String} data.message Content of the notification.
+	 * @param {'info'} data.type Type of notification.
+	 */
+
+	/**
+	 * Fired when `showWarning` method is called.
+	 *
+	 * When this event won't be handled and stopped by `event.stop()` then data.message of this event will
+	 * be automatically displayed as system alert.
+	 *
+	 * @event show:warning
+	 * @param {Object} data Notification data.
+	 * @param {String} data.message Content of the notification.
+	 * @param {'warning'} data.type Type of 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
 		 * 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
 		 * 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.
 		 * of the "arrow". To support a new position, an additional CSS must be created.
 		 *
 		 *
 		 * Default position names correspond with
 		 * Default position names correspond with
 		 * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions}.
 		 * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions}.
 		 *
 		 *
+		 * See {@link #attachTo} to learn about custom balloon positions.
+		 *
+		 * See {@link #withArrow}.
+		 *
 		 * @observable
 		 * @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.
 		 * Controls whether the balloon panel is visible or not.
@@ -74,6 +78,16 @@ export default class BalloonPanelView extends View {
 		 */
 		 */
 		this.set( 'isVisible', false );
 		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.
 		 * Max width of the balloon panel, as in CSS.
 		 *
 		 *
@@ -81,6 +95,14 @@ export default class BalloonPanelView extends View {
 		 * @member {Number} #maxWidth
 		 * @member {Number} #maxWidth
 		 */
 		 */
 
 
+		/**
+		 * 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.
 		 * Collection of the child views which creates balloon panel contents.
 		 *
 		 *
@@ -94,8 +116,9 @@ export default class BalloonPanelView extends View {
 			attributes: {
 			attributes: {
 				class: [
 				class: [
 					'ck-balloon-panel',
 					'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: {
 				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}.
 	 * 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
 	 * @param {module:utils/dom/position~Options} options Positioning options compatible with
 	 * {@link module:utils/dom/position~getOptimalPosition}. Default `positions` array is
 	 * {@link module:utils/dom/position~getOptimalPosition}. Default `positions` array is
 	 * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions}.
 	 * {@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 );
 		this.attachTo( options );
 
 
 		const limiter = options.limiter || defaultLimiterElement;
 		const limiter = options.limiter || defaultLimiterElement;
@@ -199,23 +265,19 @@ export default class BalloonPanelView extends View {
 		}, { useCapture: true } );
 		}, { useCapture: true } );
 
 
 		// We need to listen on window resize event and update position.
 		// 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.document, 'scroll' );
 		this.stopListening( global.window, 'resize' );
 		this.stopListening( global.window, 'resize' );
-
-		return super.destroy();
 	}
 	}
 }
 }
 
 
@@ -310,24 +372,24 @@ BalloonPanelView.defaultPositions = {
 	se: ( targetRect ) => ( {
 	se: ( targetRect ) => ( {
 		top: targetRect.bottom + BalloonPanelView.arrowVerticalOffset,
 		top: targetRect.bottom + BalloonPanelView.arrowVerticalOffset,
 		left: targetRect.left + targetRect.width / 2 - BalloonPanelView.arrowHorizontalOffset,
 		left: targetRect.left + targetRect.width / 2 - BalloonPanelView.arrowHorizontalOffset,
-		name: 'se'
+		name: 'arrow_se'
 	} ),
 	} ),
 
 
 	sw: ( targetRect, balloonRect ) => ( {
 	sw: ( targetRect, balloonRect ) => ( {
 		top: targetRect.bottom + BalloonPanelView.arrowVerticalOffset,
 		top: targetRect.bottom + BalloonPanelView.arrowVerticalOffset,
 		left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
 		left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
-		name: 'sw'
+		name: 'arrow_sw'
 	} ),
 	} ),
 
 
 	ne: ( targetRect, balloonRect ) => ( {
 	ne: ( targetRect, balloonRect ) => ( {
 		top: targetRect.top - balloonRect.height - BalloonPanelView.arrowVerticalOffset,
 		top: targetRect.top - balloonRect.height - BalloonPanelView.arrowVerticalOffset,
 		left: targetRect.left + targetRect.width / 2 - BalloonPanelView.arrowHorizontalOffset,
 		left: targetRect.left + targetRect.width / 2 - BalloonPanelView.arrowHorizontalOffset,
-		name: 'ne'
+		name: 'arrow_ne'
 	} ),
 	} ),
 
 
 	nw: ( targetRect, balloonRect ) => ( {
 	nw: ( targetRect, balloonRect ) => ( {
 		top: targetRect.top - balloonRect.height - BalloonPanelView.arrowVerticalOffset,
 		top: targetRect.top - balloonRect.height - BalloonPanelView.arrowVerticalOffset,
 		left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
 		left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
-		name: 'nw'
+		name: 'arrow_nw'
 	} )
 	} )
 };
 };

+ 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'
-	} )
-};

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

@@ -230,13 +230,13 @@ function createContextualToolbar( editor ) {
 		forwardSelection: ( targetRect, balloonRect ) => ( {
 		forwardSelection: ( targetRect, balloonRect ) => ( {
 			top: targetRect.bottom + arrowVOffset,
 			top: targetRect.bottom + arrowVOffset,
 			left: targetRect.right - balloonRect.width / 2,
 			left: targetRect.right - balloonRect.width / 2,
-			name: 's'
+			name: 'arrow_s'
 		} ),
 		} ),
 
 
 		backwardSelection: ( targetRect, balloonRect ) => ( {
 		backwardSelection: ( targetRect, balloonRect ) => ( {
 			top: targetRect.top - balloonRect.height - arrowVOffset,
 			top: targetRect.top - balloonRect.height - arrowVOffset,
 			left: targetRect.left - balloonRect.width / 2,
 			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;
 	editor.ui.view.element.querySelector( '.ck-editor__editable' ).scrollTop = 360;
 
 
 	panel.init().then( () => {
 	panel.init().then( () => {
-		panel.keepAttachedTo( {
+		panel.pin( {
 			target: editor.ui.view.element.querySelector( '.ck-editor__editable p strong' ),
 			target: editor.ui.view.element.querySelector( '.ck-editor__editable p strong' ),
 			limiter: editor.ui.view.editableElement
 			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
 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.
 right editor should stick to the target element.

+ 152 - 0
packages/ckeditor5-ui/tests/notification/notification.js

@@ -0,0 +1,152 @@
+/**
+ * Copyright (c) 2016, CKSource - Frederico Knabben. All rights reserved.
+ */
+
+/* globals window */
+
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import Notification from '../../src/notification/notification';
+
+describe( 'Notification', () => {
+	let editor, notification;
+
+	testUtils.createSinonSandbox();
+
+	beforeEach( () => {
+		return VirtualTestEditor.create( {
+			plugins: [ Notification ]
+		} )
+		.then( newEditor => {
+			editor = newEditor;
+			notification = editor.plugins.get( Notification );
+		} );
+	} );
+
+	describe( 'init()', () => {
+		it( 'should create notification plugin', () => {
+			expect( notification ).to.instanceof( Notification );
+			expect( notification ).to.instanceof( Plugin );
+		} );
+	} );
+
+	describe( 'showSuccess()', () => {
+		it( 'should fire `show:success` event with given data', () => {
+			const spy = testUtils.sinon.spy();
+
+			notification.on( 'show:success', spy );
+
+			notification.showSuccess( 'foo bar' );
+
+			sinon.assert.calledOnce( spy );
+			expect( spy.firstCall.args[ 1 ] ).to.deep.equal( {
+				message: 'foo bar',
+				type: 'success'
+			} );
+		} );
+
+		it( 'should fire `show:success` event with additional namespace', () => {
+			const spy = testUtils.sinon.spy();
+
+			notification.on( 'show:success:something:else', spy );
+
+			notification.showSuccess( 'foo bar', {
+				namespace: 'something:else'
+			} );
+
+			sinon.assert.calledOnce( spy );
+			expect( spy.firstCall.args[ 1 ] ).to.deep.equal( {
+				message: 'foo bar',
+				type: 'success'
+			} );
+		} );
+	} );
+
+	describe( 'showInfo()', () => {
+		it( 'should fire `show:info` event with given data', () => {
+			const spy = testUtils.sinon.spy();
+
+			notification.on( 'show:info', spy );
+
+			notification.showInfo( 'foo bar' );
+
+			sinon.assert.calledOnce( spy );
+			expect( spy.firstCall.args[ 1 ] ).to.deep.equal( {
+				message: 'foo bar',
+				type: 'info'
+			} );
+		} );
+
+		it( 'should fire `show:info` event with additional namespace', () => {
+			const spy = testUtils.sinon.spy();
+
+			notification.on( 'show:info:something:else', spy );
+
+			notification.showInfo( 'foo bar', {
+				namespace: 'something:else'
+			} );
+
+			sinon.assert.calledOnce( spy );
+			expect( spy.firstCall.args[ 1 ] ).to.deep.equal( {
+				message: 'foo bar',
+				type: 'info'
+			} );
+		} );
+	} );
+
+	describe( 'showWarning()', () => {
+		let alertStub;
+
+		beforeEach( () => {
+			alertStub = testUtils.sinon.stub( window, 'alert' );
+		} );
+
+		it( 'should fire `show:warning` event with given data', () => {
+			const spy = testUtils.sinon.spy();
+
+			notification.on( 'show:warning', spy );
+
+			notification.showWarning( 'foo bar' );
+
+			sinon.assert.calledOnce( spy );
+			expect( spy.firstCall.args[ 1 ] ).to.deep.equal( {
+				message: 'foo bar',
+				type: 'warning'
+			} );
+		} );
+
+		it( 'should fire `show:warning` event with additional namespace', () => {
+			const spy = testUtils.sinon.spy();
+
+			notification.on( 'show:warning:something:else', spy );
+
+			notification.showWarning( 'foo bar', {
+				namespace: 'something:else'
+			} );
+
+			sinon.assert.calledOnce( spy );
+			expect( spy.firstCall.args[ 1 ] ).to.deep.equal( {
+				message: 'foo bar',
+				type: 'warning'
+			} );
+		} );
+
+		it( 'should display `warning` message as system alert if is not cached and stopped by other plugins', () => {
+			notification.showWarning( 'foo bar' );
+
+			sinon.assert.calledOnce( alertStub );
+			expect( alertStub.firstCall.args[ 0 ] ).to.equal( 'foo bar' );
+		} );
+
+		it( 'should not display alert when `warning` message is cached and stopped by other plugins', () => {
+			notification.on( 'show:warning', evt => {
+				evt.stop();
+			} );
+
+			notification.showWarning( 'foo bar' );
+
+			sinon.assert.notCalled( alertStub );
+		} );
+	} );
+} );

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

@@ -25,6 +25,12 @@ describe( 'BalloonPanelView', () => {
 		return view.init();
 		return view.init();
 	} );
 	} );
 
 
+	afterEach( () => {
+		if ( view ) {
+			return view.destroy();
+		}
+	} );
+
 	describe( 'constructor()', () => {
 	describe( 'constructor()', () => {
 		it( 'should create element from template', () => {
 		it( 'should create element from template', () => {
 			expect( view.element.tagName ).to.equal( 'DIV' );
 			expect( view.element.tagName ).to.equal( 'DIV' );
@@ -35,8 +41,9 @@ describe( 'BalloonPanelView', () => {
 		it( 'should set default values', () => {
 		it( 'should set default values', () => {
 			expect( view.top ).to.equal( 0 );
 			expect( view.top ).to.equal( 0 );
 			expect( view.left ).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.isVisible ).to.equal( false );
+			expect( view.withArrow ).to.equal( true );
 		} );
 		} );
 
 
 		it( 'creates view#content collection', () => {
 		it( 'creates view#content collection', () => {
@@ -49,10 +56,18 @@ describe( 'BalloonPanelView', () => {
 			it( 'should react on view#position', () => {
 			it( 'should react on view#position', () => {
 				expect( view.element.classList.contains( 'ck-balloon-panel_arrow_se' ) ).to.true;
 				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;
 				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', () => {
 		describe( 'isVisible', () => {
@@ -96,9 +111,10 @@ describe( 'BalloonPanelView', () => {
 				expect( view.element.childNodes.length ).to.equal( 0 );
 				expect( view.element.childNodes.length ).to.equal( 0 );
 
 
 				const button = new ButtonView( { t() {} } );
 				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 } );
 				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', () => {
 			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 } );
 				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', () => {
 			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 } );
 				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 ', () => {
 			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 } );
 				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', () => {
 			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 } );
 				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
 			// https://github.com/ckeditor/ckeditor5-ui-default/issues/126
@@ -336,7 +352,7 @@ describe( 'BalloonPanelView', () => {
 
 
 				view.attachTo( { target, limiter } );
 				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', () => {
 			it( 'should put balloon on the `south east` position when `south west` is limited', () => {
@@ -356,7 +372,7 @@ describe( 'BalloonPanelView', () => {
 
 
 				view.attachTo( { target, limiter } );
 				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', () => {
 			it( 'should put balloon on the `north east` position when `south east` is limited', () => {
@@ -380,7 +396,7 @@ describe( 'BalloonPanelView', () => {
 
 
 				view.attachTo( { target, limiter } );
 				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', () => {
 			it( 'should put balloon on the `south east` position when `north east` is limited', () => {
@@ -400,21 +416,23 @@ describe( 'BalloonPanelView', () => {
 
 
 				view.attachTo( { target, limiter } );
 				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;
 		let attachToSpy, target, targetParent, limiter, notRelatedElement;
 
 
 		beforeEach( () => {
 		beforeEach( () => {
-			attachToSpy = testUtils.sinon.spy( view, 'attachTo' );
+			attachToSpy = sinon.spy( view, 'attachTo' );
 			limiter = document.createElement( 'div' );
 			limiter = document.createElement( 'div' );
 			targetParent = document.createElement( 'div' );
 			targetParent = document.createElement( 'div' );
 			target = document.createElement( 'div' );
 			target = document.createElement( 'div' );
 			notRelatedElement = document.createElement( 'div' );
 			notRelatedElement = document.createElement( 'div' );
 
 
+			view.show();
+
 			targetParent.appendChild( target );
 			targetParent.appendChild( target );
 			document.body.appendChild( targetParent );
 			document.body.appendChild( targetParent );
 			document.body.appendChild( limiter );
 			document.body.appendChild( limiter );
@@ -422,112 +440,194 @@ describe( 'BalloonPanelView', () => {
 		} );
 		} );
 
 
 		afterEach( () => {
 		afterEach( () => {
-			attachToSpy.restore();
+			targetParent.remove();
 			limiter.remove();
 			limiter.remove();
 			notRelatedElement.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 );
+			} );
 		} );
 		} );
 	} );
 	} );
 } );
 } );

+ 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' );
 	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 {
 		&:before {
-			z-index: ck-z( 'default' );
+			z-index: ck-z();
 		}
 		}
 
 
 		&:after {
 		&: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 );
+			}
 		}
 		}
 	}
 	}