8
0
Просмотр исходного кода

Created FloatingPanelView class. Moved panels to panel/ directory.

Aleksander Nowodzinski 9 лет назад
Родитель
Сommit
bb4382e9ff

+ 3 - 3
packages/ckeditor5-ui/src/balloonpanel/balloonpanelview.js → packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js

@@ -4,13 +4,13 @@
  */
 
 /**
- * @module ui/balloonpanel/balloonpanelview
+ * @module ui/panel/balloon/balloonpanelview
  */
 
 /* globals document */
 
-import View from '../view';
-import Template from '../template';
+import View from '../../view';
+import Template from '../../template';
 import { getOptimalPosition } from '@ckeditor/ckeditor5-utils/src/dom/position';
 import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit';
 

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

@@ -0,0 +1,184 @@
+/**
+ * @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 #targetElement}
+ * to remain visible in the browser viewport.
+ *
+ * @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 );
+
+		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 ),
+				}
+			}
+		} );
+	}
+
+	/**
+	 * @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 targetElement}.
+ *
+ * The available positioning functions are as follows:
+ *
+ * * South east:
+ *
+ *		+----------------+
+ *		| #targetElement |
+ *		+----------------+
+ *		         [ Panel ]
+ *
+ * * South west:
+ *
+ *		+----------------+
+ *		| #targetElement |
+ *		+----------------+
+ *		[ Panel ]
+ *
+ * * North east:
+ *
+ *		         [ Panel ]
+ *		+----------------+
+ *		| #targetElement |
+ *		+----------------+
+ *
+ *
+ * * North west:
+ *
+ *		[ Panel ]
+ *		+----------------+
+ *		| #targetElement |
+ *		+----------------+
+ *
+ * See {@link #_updatePosition}.
+ *
+ * Positioning functions must be compatible with {@link module:utils/dom/position~Position}.
+ *
+ * @member {Object} module:ui/floatingpanel/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/balloonpanel/balloonpanelview.js → packages/ckeditor5-ui/tests/panel/balloon/balloonpanelview.js

@@ -6,9 +6,9 @@
 /* global window, document */
 
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
-import ViewCollection from '../../src/viewcollection';
-import BalloonPanelView from '../../src/balloonpanel/balloonpanelview';
-import ButtonView from '../../src/button/buttonview';
+import ViewCollection from '../../../src/viewcollection';
+import BalloonPanelView from '../../../src/panel/balloon/balloonpanelview';
+import ButtonView from '../../../src/button/buttonview';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import * as positionUtils from '@ckeditor/ckeditor5-utils/src/dom/position';
 

+ 212 - 0
packages/ckeditor5-ui/tests/panel/floating/floatingpanelview.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 FloatingPanelView from '../../../src/panel/floating/floatingpanelview';
+import View from '../../../src/view';
+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;
+		} );
+	} );
+
+	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( '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'
+			} );
+		} );
+	} );
+} );