forked from angular/components
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
# This is a combination of 9 commits.
# This is the 1st commit message: # This is a combination of 11 commits. # This is the 1st commit message: add lib files for sticky-header add chose parent add support to 'optional 'cdkStickyRegion' input ' add app-demo for sticky-header fix bugs and deleted unused tag id in HTML files modify fix some code according to PR review comments change some format to pass TSlint check add '_' before private elements delete @Injectable for StickyHeaderDirective. Because we do not need @Injectable refine code encapsulate 'set style for element' change @input() Delete 'Observable.fromEvent(this.upperScrollableContainer, 'scroll')' add const STICK_START_CLASS and STICK_END_CLASS Add doc for [cdkStickyRegion] and 'unstuckElement()'. Delete 'detach()' function, add its content into 'ngOnDestroy()'. change 'MdStickyHeaderModule' to 'CdkStickyHeaderModule'; encapsulate reset css style operation for sticky header. delete unnecessary gloable variables delete global variable '_width' Add doc for 'sticker()' function. explained how it works. add more doc for 'sticker()', explaining 'isStuck' flag 2 space for indent # This is the commit message angular#2: fix # This is the commit message angular#3: delete sticky-header demo part from this branch # This is the commit message angular#4: revert firebase file # This is the commit message angular#5: change code according to comments in PR # This is the commit message angular#6: revert firbaserc # This is the commit message angular#7: revert demo-app.ts # This is the commit message angular#8: revert routes.ts # This is the commit message angular#9: revert demo-app-module.ts # This is the commit message angular#10: change # This is the commit message angular#11: fix the problem of : 'this.stickyParent' might be 'null' # This is the commit message angular#2: change doc # This is the commit message angular#3: Change the constructor of 'cdkStickyRegion' to 'constructor(public readonly _elementRef: ElementRef) { }' # This is the commit message angular#4: Added prefix 'mat-' for CSS class # This is the commit message angular#5: Delete 'public' before variables # This is the commit message angular#6: Object.assign isn't supported in IE11; use extendObject from src/lib/core/util. # This is the commit message angular#7: IE11 will have trouble with `translate3d(0, 0, 0);', change to `translate3d(0px, 0px, 0px);' # This is the commit message angular#8: Added docs for all variables # This is the commit message angular#9: extract 'generate CSS style'
- Loading branch information
Showing
2 changed files
with
326 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,323 @@ | ||
/** | ||
* @license | ||
* Copyright Google Inc. All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
import {Component, Directive, Input, Output, | ||
OnDestroy, AfterViewInit, ElementRef, Injectable, Optional} from '@angular/core'; | ||
import {Scrollable} from '../core/overlay/scroll/scrollable'; | ||
import {extendObject} from '../core/util/object-extend'; | ||
|
||
|
||
/** | ||
* Directive that marks an element as a "sticky region", meant to contain exactly one sticky-header | ||
* along with the content associated with that header. The sticky-header inside of the region will | ||
* "stick" to the top of the scrolling container as long as this region is within the scrolling | ||
* viewport. | ||
* | ||
* If a user does not explicitly define a sticky-region for a sticky-header, the direct | ||
* parent node of the sticky-header will be used as the sticky-region. | ||
*/ | ||
@Directive({ | ||
selector: '[cdkStickyRegion]', | ||
}) | ||
export class CdkStickyRegion { | ||
constructor(public readonly _elementRef: ElementRef) { } | ||
} | ||
|
||
|
||
const STICK_START_CLASS = 'mat-stick-start'; | ||
const STICK_END_CLASS = 'mat-stick-end'; | ||
@Directive({ | ||
selector: '[cdkStickyHeader]', | ||
}) | ||
/** | ||
* Directive that marks an element as a sticky-header. Inside of a scrolling container (marked with | ||
* cdkScrollable), this header will "stick" to the top of the scrolling viewport while its sticky | ||
* region (see cdkStickyRegion) is in view. | ||
*/ | ||
export class CdkStickyHeader implements OnDestroy, AfterViewInit { | ||
|
||
/** | ||
* Set the sticky-header's z-index as 10 in default. Make it as an input | ||
* variable to make user be able to customize the zIndex when | ||
* the sticky-header's zIndex is not the largest in current page. | ||
* Because if the sticky-header's zIndex is not the largest in current page, | ||
* it may be sheltered by other element when being stuck. | ||
*/ | ||
@Input('cdkStickyHeaderZIndex') zIndex: number = 10; | ||
@Input('cdkStickyParentRegion') parentRegion: HTMLElement; | ||
@Input('cdkStickyScrollableRegion') scrollableRegion: HTMLElement; | ||
|
||
private _onScrollBind: EventListener = this.onScroll.bind(this); | ||
private _onResizeBind: EventListener = this.onResize.bind(this); | ||
private _onTouchMoveBind: EventListener = this.onTouchMove.bind(this); | ||
isStuck: boolean = false; | ||
/** | ||
* The element with the 'cdkStickyHeader' tag | ||
*/ | ||
element: HTMLElement; | ||
/** | ||
* The upper container element with the 'cdkStickyRegion' tag | ||
*/ | ||
stickyParent: HTMLElement | null; | ||
/** | ||
* The upper scrollable container | ||
*/ | ||
upperScrollableContainer: HTMLElement; | ||
|
||
/** | ||
* The original css of the sticky element, used to reset the sticky element | ||
* when it is being unstuck | ||
*/ | ||
originalCss: any; | ||
|
||
/** | ||
* 'getBoundingClientRect().top' of CdkStickyRegion of current sticky header. | ||
* It is used with '_scrollFinish' to judge whether the current header | ||
* need to be stuck. | ||
*/ | ||
private _containerStart: number; | ||
/** | ||
* It is 'The bottom of CdkStickyRegion of current sticky header - the height | ||
* of current header', which is used with '_containerStart' to judge whether the current header | ||
* need to be stuck. | ||
*/ | ||
private _scrollFinish: number; | ||
/** | ||
* The width of the sticky-header when it is stuck. | ||
*/ | ||
private _scrollingWidth: number; | ||
|
||
constructor(_element: ElementRef, | ||
scrollable: Scrollable, | ||
@Optional() public parentReg: CdkStickyRegion) { | ||
this.element = _element.nativeElement; | ||
this.upperScrollableContainer = scrollable.getElementRef().nativeElement; | ||
this.scrollableRegion = scrollable.getElementRef().nativeElement; | ||
} | ||
|
||
ngAfterViewInit(): void { | ||
this.stickyParent = this.parentReg != null ? | ||
this.parentReg._elementRef.nativeElement : this.element.parentElement; | ||
this.originalCss = { | ||
zIndex: this.getCssValue(this.element, 'zIndex'), | ||
position: this.getCssValue(this.element, 'position'), | ||
top: this.getCssValue(this.element, 'top'), | ||
right: this.getCssValue(this.element, 'right'), | ||
left: this.getCssValue(this.element, 'left'), | ||
bottom: this.getCssValue(this.element, 'bottom'), | ||
width: this.getCssValue(this.element, 'width'), | ||
}; | ||
this.attach(); | ||
this.defineRestrictionsAndStick(); | ||
} | ||
|
||
ngOnDestroy(): void { | ||
this.upperScrollableContainer.removeEventListener('scroll', this._onScrollBind); | ||
this.upperScrollableContainer.removeEventListener('resize', this._onResizeBind); | ||
this.upperScrollableContainer.removeEventListener('touchmove', this._onTouchMoveBind); | ||
} | ||
|
||
attach() { | ||
this.upperScrollableContainer.addEventListener('scroll', this._onScrollBind, false); | ||
this.upperScrollableContainer.addEventListener('resize', this._onResizeBind, false); | ||
|
||
// Have to add a 'onTouchMove' listener to make sticky header work on mobile phones | ||
this.upperScrollableContainer.addEventListener('touchmove', this._onTouchMoveBind, false); | ||
} | ||
|
||
onScroll(): void { | ||
this.defineRestrictionsAndStick(); | ||
} | ||
|
||
onTouchMove(): void { | ||
this.defineRestrictionsAndStick(); | ||
} | ||
|
||
onResize(): void { | ||
this.defineRestrictionsAndStick(); | ||
// If there's already a header being stick when the page is | ||
// resized. The CSS style of the cdkStickyHeader element may be not fit | ||
// the resized window. So we need to unstuck it then re-stick it. | ||
// unstuck() can set 'isStuck' to FALSE. Then stickElement() can work. | ||
if (this.isStuck) { | ||
this.unstuckElement(); | ||
this.stickElement(); | ||
} | ||
} | ||
|
||
/** | ||
* define the restrictions of the sticky header(including stickyWidth, | ||
* when to start, when to finish) | ||
*/ | ||
defineRestrictions(): void { | ||
if(this.stickyParent == null) { | ||
return; | ||
} | ||
let containerTop: any = this.stickyParent.getBoundingClientRect(); | ||
let elemHeight: number = this.element.offsetHeight; | ||
let containerHeight: number = this.getCssNumber(this.stickyParent, 'height'); | ||
this._containerStart = containerTop.top; | ||
|
||
// the padding of the element being sticked | ||
let elementPadding: any = this.getCssValue(this.element, 'padding'); | ||
|
||
let paddingNumber: any = Number(elementPadding.slice(0, -2)); | ||
this._scrollingWidth = this.upperScrollableContainer.clientWidth - | ||
paddingNumber - paddingNumber; | ||
|
||
this._scrollFinish = this._containerStart + (containerHeight - elemHeight); | ||
} | ||
|
||
/** | ||
* Reset element to its original CSS | ||
*/ | ||
resetElement(): void { | ||
this.element.classList.remove(STICK_START_CLASS); | ||
extendObject(this.element.style, this.originalCss); | ||
} | ||
|
||
/** | ||
* Stuck element, make the element stick to the top of the scrollable container. | ||
*/ | ||
stickElement(): void { | ||
this.isStuck = true; | ||
|
||
this.element.classList.remove(STICK_END_CLASS); | ||
this.element.classList.add(STICK_START_CLASS); | ||
|
||
/** | ||
* Have to add the translate3d function for the sticky element's css style. | ||
* Because iPhone and iPad's browser is using its owning rendering engine. And | ||
* even if you are using Chrome on an iPhone, you are just using Safari with | ||
* a Chrome skin around it. | ||
* | ||
* Safari on iPad and Safari on iPhone do not have resizable windows. | ||
* In Safari on iPhone and iPad, the window size is set to the size of | ||
* the screen (minus Safari user interface controls), and cannot be changed | ||
* by the user. To move around a webpage, the user changes the zoom level and position | ||
* of the viewport as they double tap or pinch to zoom in or out, or by touching | ||
* and dragging to pan the page. As a user changes the zoom level and position of the | ||
* viewport they are doing so within a viewable content area of fixed size | ||
* (that is, the window). This means that webpage elements that have their position | ||
* "fixed" to the viewport can end up outside the viewable content area, offscreen. | ||
* | ||
* So the 'position: fixed' does not work on iPhone and iPad. To make it work, | ||
* 'translate3d(0,0,0)' needs to be used to force Safari re-rendering the sticky element. | ||
**/ | ||
this.element.style.transform = 'translate3d(0px,0px,0px)'; | ||
|
||
let stuckRight: any = this.upperScrollableContainer.getBoundingClientRect().right; | ||
|
||
let stickyCss:any = { | ||
zIndex: this.zIndex, | ||
position: 'fixed', | ||
top: this.upperScrollableContainer.offsetTop + 'px', | ||
right: stuckRight + 'px', | ||
left: this.upperScrollableContainer.offsetLeft + 'px', | ||
bottom: 'auto', | ||
width: this._scrollingWidth + 'px', | ||
}; | ||
extendObject(this.element.style, stickyCss); | ||
} | ||
|
||
/** | ||
* Unstuck element: When an element reaches the bottom of its cdkStickyRegion, | ||
* It should be unstuck. And its position will be set as 'relative', its bottom | ||
* will be set as '0'. So it will be stick at the bottom of its cdkStickyRegion and | ||
* will be scrolled up with its cdkStickyRegion element. In this way, the sticky header | ||
* can be changed smoothly when two sticky header meet and the later one need to replace | ||
* the former one. | ||
*/ | ||
unstuckElement(): void { | ||
this.isStuck = false; | ||
|
||
if(this.stickyParent == null) { | ||
return; | ||
} | ||
|
||
this.element.classList.add(STICK_END_CLASS); | ||
this.stickyParent.style.position = 'relative'; | ||
let unstuckCss: any = { | ||
position: 'absolute', | ||
top: 'auto', | ||
right: '0', | ||
left: 'auto', | ||
bottom: '0', | ||
width: this.originalCss.width, | ||
}; | ||
extendObject(this.element.style, unstuckCss); | ||
} | ||
|
||
|
||
/** | ||
* 'sticker()' function contains the main logic of sticky-header. It decides when | ||
* a header should be stick and when should it be unstuck. It will first get | ||
* the offsetTop of the upper scrollable container. And then get the Start and End | ||
* of the sticky-header's stickyRegion. | ||
* The header will be stick if 'stickyRegion Start < container offsetTop < stickyRegion End'. | ||
* And when 'stickyRegion End < container offsetTop', the header will be unstuck. It will be | ||
* stick to the bottom of its stickyRegion container and being scrolled up with its stickyRegion | ||
* container. | ||
* When 'stickyRegion Start > container offsetTop', which means the header come back to the | ||
* middle of the scrollable container, the header will be reset to its | ||
* original CSS. | ||
* A flag, isStuck. is used in this function. When a header is stick, isStuck = true. | ||
* And when the 'isStuck' flag is TRUE, the sticky-header will not be repaint, which | ||
* decreases the times on repainting sticky-header. | ||
*/ | ||
sticker(): void { | ||
let currentPosition: number = this.upperScrollableContainer.offsetTop; | ||
|
||
// unstuck when the element is scrolled out of the sticky region | ||
if (this.isStuck && | ||
(currentPosition < this._containerStart || currentPosition > this._scrollFinish) || | ||
currentPosition >= this._scrollFinish) { | ||
this.resetElement(); | ||
if (currentPosition >= this._scrollFinish) { | ||
this.unstuckElement(); | ||
} | ||
this.isStuck = false; // stick when the element is within the sticky region | ||
} else if ( this.isStuck === false && | ||
currentPosition > this._containerStart && currentPosition < this._scrollFinish) { | ||
this.stickElement(); | ||
} | ||
} | ||
|
||
defineRestrictionsAndStick(): void { | ||
this.defineRestrictions(); | ||
this.sticker(); | ||
} | ||
|
||
generateCssStyle(zIndex:any, position:any, top:any, right:any, | ||
left:any, bottom:any, width:any): any { | ||
let curCSS = { | ||
zIndex: zIndex, | ||
position: position, | ||
top: top, | ||
right: right, | ||
left: left, | ||
bottom: bottom, | ||
width: width, | ||
}; | ||
return curCSS; | ||
} | ||
|
||
|
||
private getCssValue(element: any, property: string): any { | ||
let result: any = ''; | ||
if (typeof window.getComputedStyle !== 'undefined') { | ||
result = window.getComputedStyle(element, '').getPropertyValue(property); | ||
} else if (typeof element.currentStyle !== 'undefined') { | ||
result = element.currentStyle.property; | ||
} | ||
return result; | ||
} | ||
|
||
private getCssNumber(element: any, property: string): number { | ||
return parseInt(this.getCssValue(element, property), 10) || 0; | ||
} | ||
} |