From 2848f2c369fc3429fe7a19ff379ab8a2cc64fb6b Mon Sep 17 00:00:00 2001 From: Lin Sun <2808411862@qq.com> Date: Wed, 19 Jul 2017 13:39:36 -0700 Subject: [PATCH] # This is a combination of 16 commits. # This is the 1st commit message: # This is a combination of 10 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 fix delete sticky-header demo part from this branch revert firebase file change code according to comments in PR revert firbaserc revert demo-app.ts revert routes.ts revert demo-app-module.ts change fix the problem of : 'this.stickyParent' might be 'null' change doc Change the constructor of 'cdkStickyRegion' to 'constructor(public readonly _elementRef: ElementRef) { }' Added prefix 'mat-' for CSS class Delete 'public' before variables Object.assign isn't supported in IE11; use extendObject from src/lib/core/util. IE11 will have trouble with `translate3d(0, 0, 0);', change to `translate3d(0px, 0px, 0px);' Added docs for all variables extract 'generate CSS style' created a generateStyleCSS() function, let it be responsible for generating all those CSS styles. reformat add debounce to solve 'getBoundingClientRect() cause slow down' problem. add position:sticky and check whether browser support it. If not , use the naive implementation removed unused import Removed unused 'scrollableRegion' and 'parentRegion' removed commented lines default public Add comments about why setting style top and position for iPhone and not IE. And extract detectBrowser() as a new function format consider all circumstances of browser. use "===" instead of '==' make 'navigator.userAgent.toLocaleLowerCase()' a local variable optimize Added comments on const 'STICK_START_CLASS' and 'STICK_END_CLASS'. change their content to cdk-sticky-header-start and cdk-sticky-header-end Added comments for STICK_START_CLASS and STICK_END_CLASS. Changed the format of one-line JsDoc unsubscribe sbscriptions onDestory Use what modernizr does on compatibility instead of get the browser version directly. add 'padding' and 'stickyRegionHeight' variables to avoid calling 'getComputedStyle()' too many times (which is expensive). move docs above @Directive removed the underscore in'_element: ElementRef', expand 'reg' to 'region' use 'if (this.isIE)' instead of 'if(this.isIE === true)' added more newlines between params in 'generateCssStyle()' function to make it easier to understand. Added reference link to Modernizer in docs of getSupportList() Deleted "_supportList" variable renamed 'isIE' to 'isStickyPositionSupported', and removed extra space before Observable Set debounce time as a const variable Added docs for 'const DEBOUNCE_TIME: number = 5;' Changed ' if(this.stickyParent == null)' to ' if(!this.stickyParent)' Removed the @param and @returns and make sure the types are correct in the function signature in 'generateCssStyle(...)' function Added docs for `isStickyPositionSupported` variable changed '+=' to '=' of 'stickyText' in getSupportList() function nit added " " between 'if' and '(' nit Added comments deleted unused import change comments optimize comments deleted unnecessary global variables(padding and stickyRegionHeight) Added check whether we are on browser Array to string[] test? try to reopen the old PR fix after rebase revert list.ts test test 222 revert demo revert list.ts second time Move code to 'src/cdk' revert 'move code to 'src/cdk'' , it should be done in a new PR revert avoid calling 'getComputedStyle()' too many times. rename as sticky-header.ts imported PlatformModule Add blank lines between these top-level symbol make '_isStickyPositionSupported' private Changed the originalCSS to private and use '{} as CSSStyleDeclaration' instead of ''any. Rename '_containerStart' to '_stickyRegionTop' # This is the commit message #2: rename # This is the commit message #3: optimize discription for '_stickyRegionBottomThreshold' # This is the commit message #4: private _originalStyles = { position: '', top: '', right: '', left: '', bottom: '', width: '', zIndex: ''}; # This is the commit message #5: Deleted 'generateCssStyle()' and 'getCssNumber()' function # This is the commit message #6: Deleted 'getCssValue()' function # This is the commit message #7: fix CSSStyleDeclaration # This is the commit message #8: change sticky width to 'this.upperScrollableContainer.clientWidth' # This is the commit message #9: fix # This is the commit message #10: nit # This is the commit message #2: Added the 'isPositionStickySupported() ' function to src/cdk/platform/features.ts. Consume that function in this component and just always use both the webkit and unprefixed styles. # This is the commit message #3: nit # This is the commit message #4: nit # This is the commit message #5: update doc 'Debounce time in milliseconds for events that affect the sticky positioning (e.g. scroll, resize, touch move). Set as 5 milliseconds which is the highest delay that doesn't drastically affect the positioning adversely.' # This is the commit message #6: changed the doc to '/** z-index to be applied to the sticky header (default is 10). */' # This is the commit message #7: fix tslint error # This is the commit message #8: for comment 'Can you evaluate each method to make sure their accessor privacy is right? E.g. see which functions need to be public, private, static, etc' # This is the commit message #9: Deleted variable 'elemHeight' # This is the commit message #10: Chaned to 'if (!this.stickyParent)' # This is the commit message #11: Simplified Docs for 'sticker()'. # This is the commit message #12: set 'defineRestriction()' function to private # This is the commit message #13: use 'RxChain' # This is the commit message #14: deleted unused 'tableModule' in modules.ts # This is the commit message #15: rename to '_isPositionStickySupported' # This is the commit message #16: Use // for comments, /* */ for docs --- src/lib/sticky-header/sticky-header.ts | 300 ++++++++----------------- 1 file changed, 99 insertions(+), 201 deletions(-) diff --git a/src/lib/sticky-header/sticky-header.ts b/src/lib/sticky-header/sticky-header.ts index cbeb63787379..a2d9c040724f 100644 --- a/src/lib/sticky-header/sticky-header.ts +++ b/src/lib/sticky-header/sticky-header.ts @@ -10,10 +10,10 @@ import {Directive, Input, import {Platform} from '../core/platform'; import {Scrollable} from '../core/overlay/scroll/scrollable'; import {extendObject} from '../core/util/object-extend'; -import {Observable} from 'rxjs/Observable'; import {Subscription} from 'rxjs/Subscription'; -import 'rxjs/add/observable/fromEvent'; -import 'rxjs/add/operator/debounceTime'; +import {fromEvent} from 'rxjs/observable/fromEvent'; +import {RxChain, debounceTime} from '../core/rxjs/index'; +import {isPositionStickySupported} from '../../cdk/platform/features'; /** @@ -40,9 +40,9 @@ const STICK_START_CLASS = 'cdk-sticky-header-start'; const STICK_END_CLASS = 'cdk-sticky-header-end'; /** - * Set a debounce time which is used in debounce() function when adding event listeners. - * Set is as 5. Because if the DEBOUNCE_TIME is set as a too large number. The sticky effect - * during scroll will become vary strange and can not be scrolled smoothly. + * Debounce time in milliseconds for events that affect the sticky positioning (e.g. scroll, resize, + * touch move). Set as 5 milliseconds which is the highest delay that doesn't drastically affect the + * positioning adversely. */ const DEBOUNCE_TIME: number = 5; @@ -56,19 +56,13 @@ const DEBOUNCE_TIME: number = 5; }) 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. - */ + /** z-index to be applied to the sticky header (default is 10). */ @Input('cdkStickyHeaderZIndex') zIndex: number = 10; + /** boolean value to mark whether the current header is stuck*/ isStuck: boolean = false; /** Whether the browser support CSS sticky positioning. */ - private _isStickyPositionSupported: boolean = true; - + private _isPositionStickySupported: boolean = true; /** The element with the 'cdkStickyHeader' tag. */ element: HTMLElement; @@ -83,14 +77,16 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { private _originalStyles = {} as CSSStyleDeclaration; /** * 'getBoundingClientRect().top' of CdkStickyRegion of current sticky header. - * It is used with '_scrollFinish' to judge whether the current header + * It is used with '_stickyRegionBottomThreshold' to judge whether the current header * need to be stuck. */ - private _containerStart: number; + private _stickyRegionTop: number; /** - * `_scrollFinish` is the place from where the stuck element should be unstuck + * Bottom of the sticky region offset by the height of the sticky header. + * Once the sticky header is scrolled to this position it will stay in place + * so that it will scroll naturally out of view with the rest of the sticky region. */ - private _scrollFinish: number; + private _stickyRegionBottomThreshold: number; private _onScrollSubscription: Subscription; @@ -105,23 +101,26 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { if (platform.isBrowser) { this.element = element.nativeElement; this.upperScrollableContainer = scrollable.getElementRef().nativeElement; - this.setStrategyAccordingToCompatibility(); + this._setStrategyAccordingToCompatibility(); } } ngAfterViewInit(): void { - if (!this._isStickyPositionSupported) { + if (!this._isPositionStickySupported) { + this.stickyParent = this.parentRegion != null ? this.parentRegion._elementRef.nativeElement : this.element.parentElement; + let values = window.getComputedStyle(this.element, ''); - this._originalStyles = this.generateCssStyle( - values.getPropertyValue('position'), - values.getPropertyValue('top'), - values.getPropertyValue('right'), - values.getPropertyValue('left'), - values.getPropertyValue('bottom'), - values.getPropertyValue('width'), - values.getPropertyValue('zIndex')); + this._originalStyles = { + position: values.position, + top: values.top, + right: values.right, + left: values.left, + bottom: values.bottom, + width: values.width, + zIndex: values.zIndex} as CSSStyleDeclaration; + this.attach(); this.defineRestrictionsAndStick(); } @@ -142,98 +141,40 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { } /** - * getSupportList() is used to get a list of string which can be set to - * sticky-header's style.position and make Sticky positioning work. - * It returns a list of string. - * - * According to the "Position:sticky Browser compatibility" in - * "https://developer.mozilla.org/en-US/docs/Web/CSS/position". - * - * For Desktop: Sticky positioning works well on Chrome, Edge, Firefox and Opera. And can - * also work well on Safari with a "-webkit-" prefix. It only does not work on IE. - * - * For Mobile: Sticky positioning works well on Android Webview, Chrome for Android, Edge, - * Firefox Mobile, Opera Mobile. And can also work well on Safari Mobile with a "-webkit-" prefix. - * It won't always work on IE phone. - * - * The implementation references the compatibility checking in Modernizer - * (https://github.com/Modernizr/Modernizr/blob/master/feature-detects/css/positionsticky.js). + * Check if current browser supports sticky positioning. If yes, apply + * sticky positioning. If not, use the original implementation. */ - getSupportList(): string[] { - let prefixTestList = ['', '-webkit-', '-ms-', '-moz-', '-o-']; - let supportList: Array = new Array(); - let stickyText = ''; - for (let i = 0; i < prefixTestList.length; i++ ) { - stickyText = 'position:' + prefixTestList[i] + 'sticky;'; - // Create a DOM to check if the browser support current prefix for sticky-position. - let div = document.createElement('div'); - let body = document.body; - div.style.cssText = 'display:none;' + stickyText; - body.appendChild(div); - let isSupport = /sticky/i.test(this.getCssValue(div, 'position')); - body.removeChild(div); - if (isSupport == true) { - supportList.push(prefixTestList[i]); - } - } - return supportList; - } - - /** - * Get the first element from this._supportList. Set it as a prefix of - * sticky positioning. - * - * If the this._supportList is empty, which means the browser does not support - * sticky positioning. Set isStickyPositionSupported as 'true' and use the original - * implementation of sticky-header. - */ - setStrategyAccordingToCompatibility(): void { - let supportList = this.getSupportList(); - if (supportList.length === 0) { - this._isStickyPositionSupported = false; - } else { - // Only need supportList[0], Because supportList contains all the prefix - // that can make sticky positioning work in the current browser. - // We only need to get one prefix and make position: prefix + 'sticky', - // then sticky position will work. - let prefix: string = supportList[0]; - + private _setStrategyAccordingToCompatibility(): void { + this._isPositionStickySupported = isPositionStickySupported(); + if (this._isPositionStickySupported) { this.element.style.top = '0px'; - this.element.style.position = prefix + 'sticky'; + this.element.style.cssText += 'position: -webkit-sticky; position: sticky; '; + // TODO add css class with both 'sticky' and '-webkit-sticky' on position + // when @Directory supports adding CSS class } } attach() { - this._onScrollSubscription = Observable.fromEvent(this.upperScrollableContainer, 'scroll') - .debounceTime(DEBOUNCE_TIME).subscribe(() => this.defineRestrictionsAndStick()); + this._onScrollSubscription = RxChain.from(fromEvent(this.upperScrollableContainer, 'scroll')) + .call(debounceTime, DEBOUNCE_TIME).subscribe(() => this.defineRestrictionsAndStick()); // Have to add a 'onTouchMove' listener to make sticky header work on mobile phones - this._onTouchSubscription = Observable.fromEvent(this.upperScrollableContainer, 'touchmove') - .debounceTime(DEBOUNCE_TIME).subscribe(() => this.defineRestrictionsAndStick()); - - this._onResizeSubscription = Observable.fromEvent(this.upperScrollableContainer, 'resize') - .debounceTime(DEBOUNCE_TIME).subscribe(() => this.onResize()); - } + this._onTouchSubscription = RxChain.from(fromEvent(this.upperScrollableContainer, 'touchmove')) + .call(debounceTime, DEBOUNCE_TIME).subscribe(() => this.defineRestrictionsAndStick()); - onScroll(): void { - this.defineRestrictionsAndStick(); - } - - onTouchMove(): void { - this.defineRestrictionsAndStick(); + this._onResizeSubscription = RxChain.from(fromEvent(this.upperScrollableContainer, 'resize')) + .call(debounceTime, DEBOUNCE_TIME).subscribe(() => this.onResize()); } 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 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(); + this._unstuckElement(); + this._stickElement(); } } @@ -241,16 +182,16 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { * define the restrictions of the sticky header(including stickyWidth, * when to start, when to finish) */ - defineRestrictions(): void { + private _defineRestrictions(): void { if (!this.stickyParent) { return; } - let boundingClientRect: any = this.stickyParent.getBoundingClientRect(); - let elemHeight: number = this.element.offsetHeight; - this._containerStart = boundingClientRect.top; + const boundingClientRect: any = this.stickyParent.getBoundingClientRect(); + this._stickyRegionTop = boundingClientRect.top; let stickRegionHeight = boundingClientRect.height; - this._scrollFinish = this._containerStart + (stickRegionHeight - elemHeight); + this._stickyRegionBottomThreshold = this._stickyRegionTop + + (stickRegionHeight - this.element.offsetHeight); } /** Reset element to its original CSS. */ @@ -260,43 +201,41 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { } /** Stuck element, make the element stick to the top of the scrollable container. */ - stickElement(): void { + private _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. - **/ + // 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 = this.generateCssStyle( - 'fixed', - this.upperScrollableContainer.offsetTop + 'px', - stuckRight + 'px', - this.upperScrollableContainer.offsetLeft + 'px', - 'auto', - this._originalStyles.width, - this.zIndex + '',); + let stickyCss = { + position: 'fixed', + top: this.upperScrollableContainer.offsetTop + 'px', + right: stuckRight + 'px', + left: this.upperScrollableContainer.offsetLeft + 'px', + bottom: 'auto', + width: this._originalStyles.width, + zIndex: this.zIndex + '',}; extendObject(this.element.style, stickyCss); } @@ -308,94 +247,53 @@ export class CdkStickyHeader implements OnDestroy, AfterViewInit { * can be changed smoothly when two sticky header meet and the later one need to replace * the former one. */ - unstuckElement(): void { + private _unstuckElement(): void { this.isStuck = false; - if (this.stickyParent == null) { + if (!this.stickyParent) { return; } this.element.classList.add(STICK_END_CLASS); this.stickyParent.style.position = 'relative'; - let unstuckCss: any = this.generateCssStyle( - 'absolute', - 'auto', - '0', - 'auto', - '0', - this._originalStyles.width); + let unstuckCss = { + position: 'absolute', + top: 'auto', + right: '0', + left: 'auto', + bottom: '0', + width: this._originalStyles.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. + * a header should be stick and when should it be unstuck by comparing the offsetTop + * of scrollable container with the top and bottom of the sticky region. */ 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) { + (currentPosition < this._stickyRegionTop || + currentPosition > this._stickyRegionBottomThreshold) + || currentPosition >= this._stickyRegionBottomThreshold) { this.resetElement(); - if (currentPosition >= this._scrollFinish) { - this.unstuckElement(); + if (currentPosition >= this._stickyRegionBottomThreshold) { + 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(); + currentPosition > this._stickyRegionTop && + currentPosition < this._stickyRegionBottomThreshold) { + this._stickElement(); } } defineRestrictionsAndStick(): void { - this.defineRestrictions(); + this._defineRestrictions(); this.sticker(); } - - /** - * This function is used to generate a variable which contains 7 css styles. - */ - generateCssStyle(position:string, top:string, right:string, - left:string, bottom:string, width:string | null, zIndex?:string, ): any { - let targetCSS = { - position: position, - top: top, - right: right, - left: left, - bottom: bottom, - width: width, - zIndex: zIndex - }; - return targetCSS; -} - - - 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; - } }