瀏覽代碼

[Tests, Docs, Code refac] for FloatingToolbarView.

Aleksander Nowodzinski 8 年之前
父節點
當前提交
eead918746

+ 56 - 29
packages/ckeditor5-ui/src/toolbar/floating/floatingtoolbarview.js

@@ -7,8 +7,6 @@
  * @module ui/toolbar/floating/floatingtoolbarview
  */
 
-/* globals document */
-
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 import Template from '../../template';
 import ToolbarView from '../toolbarview';
@@ -18,7 +16,8 @@ import { getOptimalPosition } from '@ckeditor/ckeditor5-utils/src/dom/position';
 const toPx = toUnit( 'px' );
 
 /**
- * The floating toolbar view class.
+ * The floating toolbar view class. It floats around the {@link #targetElement}
+ * to remain visible in the viewport.
  *
  * @extends module:ui/toolbar/toolbarview~ToolbarView
  */
@@ -32,7 +31,7 @@ export default class FloatingToolbarView extends ToolbarView {
 		const bind = this.bindTemplate;
 
 		/**
-		 * Controls whether the floating toolbar should be active. When any editable
+		 * Controls whether the floating toolbar is active. When any editable
 		 * is focused in the editor, toolbar becomes active.
 		 *
 		 * @readonly
@@ -42,7 +41,7 @@ export default class FloatingToolbarView extends ToolbarView {
 		this.set( 'isActive', false );
 
 		/**
-		 * The absolute top position of the balloon panel in pixels.
+		 * The absolute top position of the toolbar, in pixels.
 		 *
 		 * @observable
 		 * @default 0
@@ -51,22 +50,22 @@ export default class FloatingToolbarView extends ToolbarView {
 		this.set( 'top', 0 );
 
 		/**
-		 * TODO
+		 * The absolute left position of the toolbar, in pixels.
 		 *
-		 * @readonly
 		 * @observable
-		 * @member {HTMLElement} #limiterElement
+		 * @default 0
+		 * @member {Number} #left
 		 */
-		this.set( 'targetElement', null );
+		this.set( 'left', 0 );
 
 		/**
-		 * The absolute left position of the balloon panel in pixels.
+		 * An element with respect to which the toolbar is positioned.
 		 *
+		 * @readonly
 		 * @observable
-		 * @default 0
-		 * @member {Number} #left
+		 * @member {HTMLElement} #targetElement
 		 */
-		this.set( 'left', 0 );
+		this.set( 'targetElement', null );
 
 		Template.extend( this.template, {
 			attributes: {
@@ -93,13 +92,6 @@ export default class FloatingToolbarView extends ToolbarView {
 	}
 
 	/**
-	 * Destroys the toolbar and removes the {@link #_elementPlaceholder}.
-	 */
-	destroy() {
-		return super.destroy();
-	}
-
-	/**
 	 * Analyzes the environment to decide where the toolbar should
 	 * be positioned.
 	 *
@@ -110,18 +102,12 @@ export default class FloatingToolbarView extends ToolbarView {
 			return;
 		}
 
-		const defaultPositions = FloatingToolbarView.defaultPositions;
-
+		const { nw, sw, ne, se } = FloatingToolbarView.defaultPositions;
 		const { top, left } = getOptimalPosition( {
 			element: this.element,
 			target: this.targetElement,
-			positions: [
-				defaultPositions.nw,
-				defaultPositions.sw,
-				defaultPositions.ne,
-				defaultPositions.se
-			],
-			limiter: document.body,
+			positions: [ nw, sw, ne, se ],
+			limiter: global.document.body,
 			fitInViewport: true
 		} );
 
@@ -129,6 +115,47 @@ export default class FloatingToolbarView extends ToolbarView {
 	}
 }
 
+/**
+ * A default set of positioning functions used by the toolbar view to float
+ * around {@link targetElement}.
+ *
+ * The available positioning functions are as follows:
+ *
+ * * South east:
+ *
+ *		+----------------+
+ *		| #targetElement |
+ *		+----------------+
+ *		       [ Toolbar ]
+ *
+ * * South west:
+ *
+ *		+----------------+
+ *		| #targetElement |
+ *		+----------------+
+ *		[ Toolbar ]
+ *
+ * * North east:
+ *
+ *		       [ Toolbar ]
+ *		+----------------+
+ *		| #targetElement |
+ *		+----------------+
+ *
+ *
+ * * North west:
+ *
+ *		[ Toolbar ]
+ *		+----------------+
+ *		| #targetElement |
+ *		+----------------+
+ *
+ * See {@link #_updatePosition}.
+ *
+ * Positioning functions must be compatible with {@link module:utils/dom/position~Position}.
+ *
+ * @member {Object} module:ui/toolbar/floating/floatingtoolbarview~FloatingToolbarView#defaultPositions
+ */
 FloatingToolbarView.defaultPositions = {
 	nw: ( targetRect, toolbarRect ) => ( {
 		top: targetRect.top - toolbarRect.height,

+ 212 - 0
packages/ckeditor5-ui/tests/toolbar/floating/floatingtoolbarview.js

@@ -0,0 +1,212 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global Event */
+
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import FloatingToolbarView from '../../../src/toolbar/floating/floatingtoolbarview';
+import ToolbarView from '../../../src/toolbar/toolbarview';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import * as positionUtils from '@ckeditor/ckeditor5-utils/src/dom/position';
+
+testUtils.createSinonSandbox();
+
+describe( 'FloatingToolbarView', () => {
+	let locale, view, target;
+
+	beforeEach( () => {
+		locale = {};
+		view = new FloatingToolbarView( 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 ToolbarView', () => {
+			expect( view ).to.be.instanceOf( ToolbarView );
+		} );
+
+		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 FloatingToolbarView( locale );
+
+			expect( view.targetElement ).to.be.null;
+		} );
+	} );
+
+	describe( 'template', () => {
+		it( 'should create element from template', () => {
+			expect( view.element.classList.contains( 'ck-toolbar_floating' ) ).to.be.true;
+		} );
+
+		describe( 'bindings', () => {
+			describe( 'class', () => {
+				it( 'reacts on #isActive', () => {
+					view.isActive = false;
+					expect( view.element.classList.contains( 'ck-toolbar_active' ) ).to.be.false;
+
+					view.isActive = true;
+					expect( view.element.classList.contains( 'ck-toolbar_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( '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 } = FloatingToolbarView.defaultPositions;
+
+			view.isActive = true;
+
+			const spy = testUtils.sinon.stub( positionUtils, 'getOptimalPosition' ).returns( {
+				top: 5,
+				left: 10
+			} );
+
+			view._updatePosition();
+
+			sinon.assert.calledWithMatch( spy, sinon.match( {
+				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, toolbarRect, defaultPositions;
+
+		beforeEach( () => {
+			defaultPositions = FloatingToolbarView.defaultPositions;
+
+			targetRect = {
+				top: 10,
+				width: 100,
+				left: 10,
+				height: 10,
+				bottom: 20
+			};
+
+			toolbarRect = {
+				width: 20,
+				height: 10
+			};
+		} );
+
+		it( 'should provide "nw" position', () => {
+			expect( defaultPositions.nw( targetRect, toolbarRect ) ).to.deep.equal( {
+				top: 0,
+				left: 10,
+				name: 'nw'
+			} );
+		} );
+
+		it( 'should provide "sw" position', () => {
+			expect( defaultPositions.sw( targetRect, toolbarRect ) ).to.deep.equal( {
+				top: 20,
+				left: 10,
+				name: 'sw'
+			} );
+		} );
+
+		it( 'should provide "ne" position', () => {
+			expect( defaultPositions.ne( targetRect, toolbarRect ) ).to.deep.equal( {
+				top: 0,
+				left: 90,
+				name: 'ne'
+			} );
+		} );
+
+		it( 'should provide "se" position', () => {
+			expect( defaultPositions.se( targetRect, toolbarRect ) ).to.deep.equal( {
+				top: 20,
+				left: 90,
+				name: 'se'
+			} );
+		} );
+	} );
+} );