Răsfoiți Sursa

Other: StickyToolbarView should support a configurable vertical offset from the top of the page. Closes #277.

Aleksander Nowodzinski 8 ani în urmă
părinte
comite
35ac702d79

+ 44 - 10
packages/ckeditor5-ui/src/toolbar/sticky/stickytoolbarview.js

@@ -68,9 +68,25 @@ export default class StickyToolbarView extends ToolbarView {
 		 * @readonly
 		 * @observable
 		 * @default 50
-		 * @member {Number} #limiterOffset
+		 * @member {Number} #limiterBottomOffset
 		 */
-		this.set( 'limiterOffset', 50 );
+		this.set( 'limiterBottomOffset', 50 );
+
+		/**
+		 * The offset from the top edge of the web browser's viewport which makes the
+		 * toolbar become sticky. The default value is `0`, which means the toolbar becomes
+		 * sticky when it's upper edge touches the top of the page viewport.
+		 *
+		 * This attribute is useful when the web page has UI elements positioned to the top
+		 * either using `position: fixed` or `position: sticky`, which would cover the
+		 * sticky toolbar or vice–versa (depending on the `z-index` hierarchy).
+		 *
+		 * @readonly
+		 * @observable
+		 * @default 0
+		 * @member {Number} #viewportTopOffset
+		 */
+		this.set( 'viewportTopOffset', 0 );
 
 		/**
 		 * Controls the `margin-left` CSS style of the toolbar.
@@ -93,6 +109,18 @@ export default class StickyToolbarView extends ToolbarView {
 		 */
 		this.set( '_isStickyToTheLimiter', false );
 
+		/**
+		 * Set `true` if the sticky toolbar uses the {@link #viewportTopOffset},
+		 * i.e. not {@link #_isStickyToTheLimiter} and the {@link #viewportTopOffset}
+		 * is not `0`.
+		 *
+		 * @protected
+		 * @readonly
+		 * @observable
+		 * @member {Boolean} #_hasViewportTopOffset
+		 */
+		this.set( '_hasViewportTopOffset', false );
+
 		/**
 		 * The DOM bounding client rect of the {@link module:ui/view~View#element} of the toolbar.
 		 *
@@ -121,10 +149,14 @@ export default class StickyToolbarView extends ToolbarView {
 					} ),
 
 					bottom: bind.to( '_isStickyToTheLimiter', _isStickyToTheLimiter => {
-						return _isStickyToTheLimiter ? toPx( this.limiterOffset ) : null;
+						return _isStickyToTheLimiter ? toPx( this.limiterBottomOffset ) : null;
 					} ),
 
-					marginLeft: bind.to( '_marginLeft' )
+					marginLeft: bind.to( '_marginLeft' ),
+
+					top: bind.to( '_hasViewportTopOffset', _hasViewportTopOffset => {
+						return _hasViewportTopOffset ? toPx( this.viewportTopOffset ) : null;
+					} )
 				}
 			}
 		} );
@@ -191,23 +223,25 @@ export default class StickyToolbarView extends ToolbarView {
 
 		// The toolbar must be active to become sticky.
 		this.isSticky = this.isActive &&
-			// The limiter's top edge must be beyond the upper edge of the visible viewport.
-			limiterRect.top < 0 &&
-			// The model#limiterElement's height mustn't be smaller than the toolbar's height and model#limiterOffset.
+			// The limiter's top edge must be beyond the upper edge of the visible viewport (+the viewportTopOffset).
+			limiterRect.top < this.viewportTopOffset &&
+			// The model#limiterElement's height mustn't be smaller than the toolbar's height and model#limiterBottomOffset.
 			// There's no point in entering the sticky mode if the model#limiterElement is very, very small, because
-			// it would immediately set model#_isStickyToTheLimiter true and, given model#limiterOffset, the toolbar
+			// it would immediately set model#_isStickyToTheLimiter true and, given model#limiterBottomOffset, the toolbar
 			// would be positioned before the model#limiterElement.
-			this._toolbarRect.height + this.limiterOffset < limiterRect.height;
+			this._toolbarRect.height + this.limiterBottomOffset < limiterRect.height;
 
 		// Stick the toolbar to the top edge of the viewport simulating CSS position:sticky.
 		// TODO: Possibly replaced by CSS in the future http://caniuse.com/#feat=css-sticky
 		if ( this.isSticky ) {
-			this._isStickyToTheLimiter = limiterRect.bottom < toolbarRect.height + this.limiterOffset;
+			this._isStickyToTheLimiter = limiterRect.bottom < toolbarRect.height + this.limiterBottomOffset + this.viewportTopOffset;
+			this._hasViewportTopOffset = !this._isStickyToTheLimiter && !!this.viewportTopOffset;
 			this._marginLeft = this._isStickyToTheLimiter ? null : toPx( -global.window.scrollX );
 		}
 		// Detach the toolbar from the top edge of the viewport.
 		else {
 			this._isStickyToTheLimiter = false;
+			this._hasViewportTopOffset = false;
 			this._marginLeft = null;
 		}
 	}

+ 80 - 3
packages/ckeditor5-ui/tests/toolbar/sticky/stickytoolbarview.js

@@ -47,9 +47,11 @@ describe( 'StickyToolbarView', () => {
 		it( 'sets view attributes', () => {
 			expect( view.isSticky ).to.be.false;
 			expect( view.limiterElement ).to.be.null;
-			expect( view.limiterOffset ).to.equal( 50 );
+			expect( view.limiterBottomOffset ).to.equal( 50 );
+			expect( view.viewportTopOffset ).to.equal( 0 );
 
 			expect( view._isStickyToTheLimiter ).to.be.false;
+			expect( view._hasViewportTopOffset ).to.be.false;
 			expect( view._marginLeft ).to.be.null;
 		} );
 
@@ -83,6 +85,16 @@ describe( 'StickyToolbarView', () => {
 			expect( element.classList.contains( 'ck-toolbar_sticky_bottom-limit' ) ).to.be.true;
 		} );
 
+		it( 'update the styles.top on view#_hasViewportTopOffset change', () => {
+			view.viewportTopOffset = 100;
+
+			view._hasViewportTopOffset = false;
+			expect( element.style.top ).to.equal( '' );
+
+			view._hasViewportTopOffset = true;
+			expect( element.style.top ).to.equal( '100px' );
+		} );
+
 		it( 'update the styles.width on view#isSticky change', () => {
 			testUtils.sinon.stub( view._elementPlaceholder, 'getBoundingClientRect' ).returns( { width: 100 } );
 
@@ -236,10 +248,10 @@ describe( 'StickyToolbarView', () => {
 				expect( view.isSticky ).to.be.false;
 			} );
 
-			it( 'is false if view.limiterElement is smaller than the toolbar and view.limiterOffset (toolbar is active)', () => {
+			it( 'is false if view.limiterElement is smaller than the toolbar and view.limiterBottomOffset (toolbar is active)', () => {
 				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( { top: -10, height: 60 } );
 				view.isActive = true;
-				view.limiterOffset = 50;
+				view.limiterBottomOffset = 50;
 
 				expect( view.isSticky ).to.be.false;
 
@@ -307,6 +319,71 @@ describe( 'StickyToolbarView', () => {
 			} );
 		} );
 
+		describe( 'view._hasViewportTopOffset', () => {
+			it( 'is true if view._isStickyToTheLimiter is false and view.viewportTopOffset has been specified', () => {
+				view.viewportTopOffset = 100;
+
+				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( {
+					top: 90,
+					bottom: 190,
+					height: 100
+				} );
+
+				testUtils.sinon.stub( view.element, 'getBoundingClientRect' ).returns( {
+					height: 20
+				} );
+
+				view.isActive = true;
+
+				view._checkIfShouldBeSticky();
+				expect( view.isSticky ).to.be.true;
+				expect( view._isStickyToTheLimiter ).to.be.false;
+				expect( view._hasViewportTopOffset ).to.be.true;
+			} );
+
+			it( 'is false if view._isStickyToTheLimiter is true and view.viewportTopOffset has been specified', () => {
+				view.viewportTopOffset = 100;
+
+				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( {
+					top: 10,
+					bottom: 110,
+					height: 100
+				} );
+
+				testUtils.sinon.stub( view.element, 'getBoundingClientRect' ).returns( {
+					height: 20
+				} );
+
+				view.isActive = true;
+
+				view._checkIfShouldBeSticky();
+				expect( view.isSticky ).to.be.true;
+				expect( view._isStickyToTheLimiter ).to.be.true;
+				expect( view._hasViewportTopOffset ).to.be.false;
+			} );
+
+			it( 'is false if view._isStickyToTheLimiter is false and view.viewportTopOffset is 0', () => {
+				view.viewportTopOffset = 100;
+
+				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( {
+					top: 90,
+					bottom: 190,
+					height: 100
+				} );
+
+				testUtils.sinon.stub( view.element, 'getBoundingClientRect' ).returns( {
+					height: 20
+				} );
+
+				view.isActive = true;
+
+				view._checkIfShouldBeSticky();
+				expect( view.isSticky ).to.be.true;
+				expect( view._isStickyToTheLimiter ).to.be.false;
+				expect( view._hasViewportTopOffset ).to.be.true;
+			} );
+		} );
+
 		describe( 'view._marginLeft', () => {
 			it( 'is set if view.isSticky is true view._isStickyToTheLimiter is false', () => {
 				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( {