Skip to content

Commit ff4c1a4

Browse files
committed
# 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'
1 parent 4d31485 commit ff4c1a4

File tree

2 files changed

+326
-0
lines changed

2 files changed

+326
-0
lines changed

src/demo-app/demo-app-module.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ import {
8080
} from '@angular/material';
8181
import {CdkTableModule} from '@angular/cdk';
8282
import {TableHeaderDemo} from './table/table-header-demo';
83+
import {StickyHeaderDemo} from './sticky-header/sticky-header-demo';
8384

8485
/**
8586
* NgModule that includes all Material modules that are required to serve the demo-app.
@@ -119,6 +120,8 @@ import {TableHeaderDemo} from './table/table-header-demo';
119120
MdNativeDateModule,
120121
CdkTableModule,
121122
StyleModule
123+
CdkDataTableModule,
124+
StyleModule
122125
]
123126
})
124127
export class DemoMaterialModule {}
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import {Component, Directive, Input, Output,
9+
OnDestroy, AfterViewInit, ElementRef, Injectable, Optional} from '@angular/core';
10+
import {Scrollable} from '../core/overlay/scroll/scrollable';
11+
import {extendObject} from '../core/util/object-extend';
12+
13+
14+
/**
15+
* Directive that marks an element as a "sticky region", meant to contain exactly one sticky-header
16+
* along with the content associated with that header. The sticky-header inside of the region will
17+
* "stick" to the top of the scrolling container as long as this region is within the scrolling
18+
* viewport.
19+
*
20+
* If a user does not explicitly define a sticky-region for a sticky-header, the direct
21+
* parent node of the sticky-header will be used as the sticky-region.
22+
*/
23+
@Directive({
24+
selector: '[cdkStickyRegion]',
25+
})
26+
export class CdkStickyRegion {
27+
constructor(public readonly _elementRef: ElementRef) { }
28+
}
29+
30+
31+
const STICK_START_CLASS = 'mat-stick-start';
32+
const STICK_END_CLASS = 'mat-stick-end';
33+
@Directive({
34+
selector: '[cdkStickyHeader]',
35+
})
36+
/**
37+
* Directive that marks an element as a sticky-header. Inside of a scrolling container (marked with
38+
* cdkScrollable), this header will "stick" to the top of the scrolling viewport while its sticky
39+
* region (see cdkStickyRegion) is in view.
40+
*/
41+
export class CdkStickyHeader implements OnDestroy, AfterViewInit {
42+
43+
/**
44+
* Set the sticky-header's z-index as 10 in default. Make it as an input
45+
* variable to make user be able to customize the zIndex when
46+
* the sticky-header's zIndex is not the largest in current page.
47+
* Because if the sticky-header's zIndex is not the largest in current page,
48+
* it may be sheltered by other element when being stuck.
49+
*/
50+
@Input('cdkStickyHeaderZIndex') zIndex: number = 10;
51+
@Input('cdkStickyParentRegion') parentRegion: HTMLElement;
52+
@Input('cdkStickyScrollableRegion') scrollableRegion: HTMLElement;
53+
54+
private _onScrollBind: EventListener = this.onScroll.bind(this);
55+
private _onResizeBind: EventListener = this.onResize.bind(this);
56+
private _onTouchMoveBind: EventListener = this.onTouchMove.bind(this);
57+
isStuck: boolean = false;
58+
/**
59+
* The element with the 'cdkStickyHeader' tag
60+
*/
61+
element: HTMLElement;
62+
/**
63+
* The upper container element with the 'cdkStickyRegion' tag
64+
*/
65+
stickyParent: HTMLElement | null;
66+
/**
67+
* The upper scrollable container
68+
*/
69+
upperScrollableContainer: HTMLElement;
70+
71+
/**
72+
* The original css of the sticky element, used to reset the sticky element
73+
* when it is being unstuck
74+
*/
75+
originalCss: any;
76+
77+
/**
78+
* 'getBoundingClientRect().top' of CdkStickyRegion of current sticky header.
79+
* It is used with '_scrollFinish' to judge whether the current header
80+
* need to be stuck.
81+
*/
82+
private _containerStart: number;
83+
/**
84+
* It is 'The bottom of CdkStickyRegion of current sticky header - the height
85+
* of current header', which is used with '_containerStart' to judge whether the current header
86+
* need to be stuck.
87+
*/
88+
private _scrollFinish: number;
89+
/**
90+
* The width of the sticky-header when it is stuck.
91+
*/
92+
private _scrollingWidth: number;
93+
94+
constructor(_element: ElementRef,
95+
scrollable: Scrollable,
96+
@Optional() public parentReg: CdkStickyRegion) {
97+
this.element = _element.nativeElement;
98+
this.upperScrollableContainer = scrollable.getElementRef().nativeElement;
99+
this.scrollableRegion = scrollable.getElementRef().nativeElement;
100+
}
101+
102+
ngAfterViewInit(): void {
103+
this.stickyParent = this.parentReg != null ?
104+
this.parentReg._elementRef.nativeElement : this.element.parentElement;
105+
this.originalCss = {
106+
zIndex: this.getCssValue(this.element, 'zIndex'),
107+
position: this.getCssValue(this.element, 'position'),
108+
top: this.getCssValue(this.element, 'top'),
109+
right: this.getCssValue(this.element, 'right'),
110+
left: this.getCssValue(this.element, 'left'),
111+
bottom: this.getCssValue(this.element, 'bottom'),
112+
width: this.getCssValue(this.element, 'width'),
113+
};
114+
this.attach();
115+
this.defineRestrictionsAndStick();
116+
}
117+
118+
ngOnDestroy(): void {
119+
this.upperScrollableContainer.removeEventListener('scroll', this._onScrollBind);
120+
this.upperScrollableContainer.removeEventListener('resize', this._onResizeBind);
121+
this.upperScrollableContainer.removeEventListener('touchmove', this._onTouchMoveBind);
122+
}
123+
124+
attach() {
125+
this.upperScrollableContainer.addEventListener('scroll', this._onScrollBind, false);
126+
this.upperScrollableContainer.addEventListener('resize', this._onResizeBind, false);
127+
128+
// Have to add a 'onTouchMove' listener to make sticky header work on mobile phones
129+
this.upperScrollableContainer.addEventListener('touchmove', this._onTouchMoveBind, false);
130+
}
131+
132+
onScroll(): void {
133+
this.defineRestrictionsAndStick();
134+
}
135+
136+
onTouchMove(): void {
137+
this.defineRestrictionsAndStick();
138+
}
139+
140+
onResize(): void {
141+
this.defineRestrictionsAndStick();
142+
// If there's already a header being stick when the page is
143+
// resized. The CSS style of the cdkStickyHeader element may be not fit
144+
// the resized window. So we need to unstuck it then re-stick it.
145+
// unstuck() can set 'isStuck' to FALSE. Then stickElement() can work.
146+
if (this.isStuck) {
147+
this.unstuckElement();
148+
this.stickElement();
149+
}
150+
}
151+
152+
/**
153+
* define the restrictions of the sticky header(including stickyWidth,
154+
* when to start, when to finish)
155+
*/
156+
defineRestrictions(): void {
157+
if(this.stickyParent == null) {
158+
return;
159+
}
160+
let containerTop: any = this.stickyParent.getBoundingClientRect();
161+
let elemHeight: number = this.element.offsetHeight;
162+
let containerHeight: number = this.getCssNumber(this.stickyParent, 'height');
163+
this._containerStart = containerTop.top;
164+
165+
// the padding of the element being sticked
166+
let elementPadding: any = this.getCssValue(this.element, 'padding');
167+
168+
let paddingNumber: any = Number(elementPadding.slice(0, -2));
169+
this._scrollingWidth = this.upperScrollableContainer.clientWidth -
170+
paddingNumber - paddingNumber;
171+
172+
this._scrollFinish = this._containerStart + (containerHeight - elemHeight);
173+
}
174+
175+
/**
176+
* Reset element to its original CSS
177+
*/
178+
resetElement(): void {
179+
this.element.classList.remove(STICK_START_CLASS);
180+
extendObject(this.element.style, this.originalCss);
181+
}
182+
183+
/**
184+
* Stuck element, make the element stick to the top of the scrollable container.
185+
*/
186+
stickElement(): void {
187+
this.isStuck = true;
188+
189+
this.element.classList.remove(STICK_END_CLASS);
190+
this.element.classList.add(STICK_START_CLASS);
191+
192+
/**
193+
* Have to add the translate3d function for the sticky element's css style.
194+
* Because iPhone and iPad's browser is using its owning rendering engine. And
195+
* even if you are using Chrome on an iPhone, you are just using Safari with
196+
* a Chrome skin around it.
197+
*
198+
* Safari on iPad and Safari on iPhone do not have resizable windows.
199+
* In Safari on iPhone and iPad, the window size is set to the size of
200+
* the screen (minus Safari user interface controls), and cannot be changed
201+
* by the user. To move around a webpage, the user changes the zoom level and position
202+
* of the viewport as they double tap or pinch to zoom in or out, or by touching
203+
* and dragging to pan the page. As a user changes the zoom level and position of the
204+
* viewport they are doing so within a viewable content area of fixed size
205+
* (that is, the window). This means that webpage elements that have their position
206+
* "fixed" to the viewport can end up outside the viewable content area, offscreen.
207+
*
208+
* So the 'position: fixed' does not work on iPhone and iPad. To make it work,
209+
* 'translate3d(0,0,0)' needs to be used to force Safari re-rendering the sticky element.
210+
**/
211+
this.element.style.transform = 'translate3d(0px,0px,0px)';
212+
213+
let stuckRight: any = this.upperScrollableContainer.getBoundingClientRect().right;
214+
215+
let stickyCss:any = {
216+
zIndex: this.zIndex,
217+
position: 'fixed',
218+
top: this.upperScrollableContainer.offsetTop + 'px',
219+
right: stuckRight + 'px',
220+
left: this.upperScrollableContainer.offsetLeft + 'px',
221+
bottom: 'auto',
222+
width: this._scrollingWidth + 'px',
223+
};
224+
extendObject(this.element.style, stickyCss);
225+
}
226+
227+
/**
228+
* Unstuck element: When an element reaches the bottom of its cdkStickyRegion,
229+
* It should be unstuck. And its position will be set as 'relative', its bottom
230+
* will be set as '0'. So it will be stick at the bottom of its cdkStickyRegion and
231+
* will be scrolled up with its cdkStickyRegion element. In this way, the sticky header
232+
* can be changed smoothly when two sticky header meet and the later one need to replace
233+
* the former one.
234+
*/
235+
unstuckElement(): void {
236+
this.isStuck = false;
237+
238+
if(this.stickyParent == null) {
239+
return;
240+
}
241+
242+
this.element.classList.add(STICK_END_CLASS);
243+
this.stickyParent.style.position = 'relative';
244+
let unstuckCss: any = {
245+
position: 'absolute',
246+
top: 'auto',
247+
right: '0',
248+
left: 'auto',
249+
bottom: '0',
250+
width: this.originalCss.width,
251+
};
252+
extendObject(this.element.style, unstuckCss);
253+
}
254+
255+
256+
/**
257+
* 'sticker()' function contains the main logic of sticky-header. It decides when
258+
* a header should be stick and when should it be unstuck. It will first get
259+
* the offsetTop of the upper scrollable container. And then get the Start and End
260+
* of the sticky-header's stickyRegion.
261+
* The header will be stick if 'stickyRegion Start < container offsetTop < stickyRegion End'.
262+
* And when 'stickyRegion End < container offsetTop', the header will be unstuck. It will be
263+
* stick to the bottom of its stickyRegion container and being scrolled up with its stickyRegion
264+
* container.
265+
* When 'stickyRegion Start > container offsetTop', which means the header come back to the
266+
* middle of the scrollable container, the header will be reset to its
267+
* original CSS.
268+
* A flag, isStuck. is used in this function. When a header is stick, isStuck = true.
269+
* And when the 'isStuck' flag is TRUE, the sticky-header will not be repaint, which
270+
* decreases the times on repainting sticky-header.
271+
*/
272+
sticker(): void {
273+
let currentPosition: number = this.upperScrollableContainer.offsetTop;
274+
275+
// unstuck when the element is scrolled out of the sticky region
276+
if (this.isStuck &&
277+
(currentPosition < this._containerStart || currentPosition > this._scrollFinish) ||
278+
currentPosition >= this._scrollFinish) {
279+
this.resetElement();
280+
if (currentPosition >= this._scrollFinish) {
281+
this.unstuckElement();
282+
}
283+
this.isStuck = false; // stick when the element is within the sticky region
284+
} else if ( this.isStuck === false &&
285+
currentPosition > this._containerStart && currentPosition < this._scrollFinish) {
286+
this.stickElement();
287+
}
288+
}
289+
290+
defineRestrictionsAndStick(): void {
291+
this.defineRestrictions();
292+
this.sticker();
293+
}
294+
295+
generateCssStyle(zIndex:any, position:any, top:any, right:any,
296+
left:any, bottom:any, width:any): any {
297+
let curCSS = {
298+
zIndex: zIndex,
299+
position: position,
300+
top: top,
301+
right: right,
302+
left: left,
303+
bottom: bottom,
304+
width: width,
305+
};
306+
return curCSS;
307+
}
308+
309+
310+
private getCssValue(element: any, property: string): any {
311+
let result: any = '';
312+
if (typeof window.getComputedStyle !== 'undefined') {
313+
result = window.getComputedStyle(element, '').getPropertyValue(property);
314+
} else if (typeof element.currentStyle !== 'undefined') {
315+
result = element.currentStyle.property;
316+
}
317+
return result;
318+
}
319+
320+
private getCssNumber(element: any, property: string): number {
321+
return parseInt(this.getCssValue(element, property), 10) || 0;
322+
}
323+
}

0 commit comments

Comments
 (0)