mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-22 21:48:42 +08:00
perf(scroll): efficient scroll events and properties
This commit is contained in:
@ -161,8 +161,8 @@ export class App {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
setScrolling() {
|
||||
this._scrollTime = Date.now() + ACTIVE_SCROLLING_TIME;
|
||||
setScrolling(timeStamp: number) {
|
||||
this._scrollTime = timeStamp + ACTIVE_SCROLLING_TIME;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,14 +1,19 @@
|
||||
import { ChangeDetectionStrategy, Component, ElementRef, Input, NgZone, Optional, Renderer, ViewEncapsulation } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, NgZone, OnDestroy, AfterViewInit, Optional, Output, Renderer, ViewEncapsulation } from '@angular/core';
|
||||
|
||||
import { App } from '../app/app';
|
||||
import { Ion } from '../ion';
|
||||
import { Config } from '../../config/config';
|
||||
import { DomController } from '../../util/dom-controller';
|
||||
import { eventOptions } from '../../util/ui-event-manager';
|
||||
import { Img } from '../img/img';
|
||||
import { Ion } from '../ion';
|
||||
import { isTrueProperty, assert, removeArrayItem } from '../../util/util';
|
||||
import { Keyboard } from '../../util/keyboard';
|
||||
import { nativeRaf, nativeTimeout, transitionEnd } from '../../util/dom';
|
||||
import { ScrollView } from '../../util/scroll-view';
|
||||
import { ScrollView, ScrollDirection } from '../../util/scroll-view';
|
||||
import { Tabs } from '../tabs/tabs';
|
||||
import { transitionEnd } from '../../util/dom';
|
||||
import { ViewController } from '../../navigation/view-controller';
|
||||
import { isTrueProperty, assert } from '../../util/util';
|
||||
|
||||
export { ScrollEvent, ScrollDirection } from '../../util/scroll-view';
|
||||
|
||||
|
||||
/**
|
||||
@ -116,32 +121,51 @@ import { isTrueProperty, assert } from '../../util/util';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class Content extends Ion {
|
||||
_paddingTop: number;
|
||||
_paddingRight: number;
|
||||
_paddingBottom: number;
|
||||
_paddingLeft: number;
|
||||
_scrollPadding: number = 0;
|
||||
_headerHeight: number;
|
||||
_footerHeight: number;
|
||||
_tabbarHeight: number;
|
||||
_tabsPlacement: string;
|
||||
_inputPolling: boolean = false;
|
||||
_scroll: ScrollView;
|
||||
_scLsn: Function;
|
||||
export class Content extends Ion implements AfterViewInit, OnDestroy {
|
||||
/* @private */
|
||||
_sbPadding: boolean;
|
||||
|
||||
/* @internal */
|
||||
_cTop: number;
|
||||
/* @internal */
|
||||
_cBottom: number;
|
||||
/* @internal */
|
||||
_pTop: number;
|
||||
/* @internal */
|
||||
_pRight: number;
|
||||
/* @internal */
|
||||
_pBottom: number;
|
||||
/* @internal */
|
||||
_pLeft: number;
|
||||
/* @internal */
|
||||
_scrollPadding: number = 0;
|
||||
/* @internal */
|
||||
_hdrHeight: number;
|
||||
/* @internal */
|
||||
_ftrHeight: number;
|
||||
/* @internal */
|
||||
_tabbarHeight: number;
|
||||
/* @internal */
|
||||
_tabsPlacement: string;
|
||||
/* @internal */
|
||||
_inputPolling: boolean = false;
|
||||
/* @internal */
|
||||
_scroll: ScrollView;
|
||||
/* @internal */
|
||||
_scLsn: Function;
|
||||
/* @internal */
|
||||
_fullscreen: boolean;
|
||||
/* @internal */
|
||||
_lazyLoadImages: boolean = true;
|
||||
/* @internal */
|
||||
_imgs: Img[] = [];
|
||||
/* @internal */
|
||||
_footerEle: HTMLElement;
|
||||
_dirty: boolean = false;
|
||||
|
||||
/*
|
||||
* @private
|
||||
*/
|
||||
/* @internal */
|
||||
_dirty: boolean;
|
||||
/* @internal */
|
||||
_scrollEle: HTMLElement;
|
||||
|
||||
/*
|
||||
* @private
|
||||
*/
|
||||
/* @internal */
|
||||
_fixedEle: HTMLElement;
|
||||
|
||||
/**
|
||||
@ -156,6 +180,100 @@ export class Content extends Ion {
|
||||
*/
|
||||
contentBottom: number;
|
||||
|
||||
/**
|
||||
* The height the content, including content not visible
|
||||
* on the screen due to overflow.
|
||||
*/
|
||||
scrollHeight: number = 0;
|
||||
|
||||
/**
|
||||
* The width the content, including content not visible
|
||||
* on the screen due to overflow.
|
||||
*/
|
||||
scrollWidth: number = 0;
|
||||
|
||||
|
||||
/**
|
||||
* The distance of the content's top to its topmost visible content.
|
||||
*/
|
||||
get scrollTop(): number {
|
||||
return this._scroll.getTop();
|
||||
}
|
||||
set scrollTop(top: number) {
|
||||
this._scroll.setTop(top);
|
||||
}
|
||||
|
||||
/**
|
||||
* The distance of the content's left to its leftmost visible content.
|
||||
*/
|
||||
get scrollLeft(): number {
|
||||
return this._scroll.getLeft();
|
||||
}
|
||||
set scrollLeft(top: number) {
|
||||
this._scroll.setLeft(top);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the scrollable area is actively scrolling or not.
|
||||
*/
|
||||
get isScrolling(): boolean {
|
||||
return this._scroll.isScrolling;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current vertical scroll velocity.
|
||||
*/
|
||||
get velocityY(): number {
|
||||
return this._scroll.ev.velocityY || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current horizontal scroll velocity.
|
||||
*/
|
||||
get velocityX(): number {
|
||||
return this._scroll.ev.velocityX || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current, or last known, vertical scroll direction.
|
||||
*/
|
||||
get directionY(): ScrollDirection {
|
||||
return this._scroll.ev.directionY;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current, or last known, horizontal scroll direction.
|
||||
*/
|
||||
get directionX(): ScrollDirection {
|
||||
return this._scroll.ev.directionX;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@Output() ionScrollStart: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@Output() ionScroll: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@Output() ionScrollEnd: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@Output() readReady: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@Output() writeReady: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
|
||||
constructor(
|
||||
config: Config,
|
||||
elementRef: ElementRef,
|
||||
@ -164,7 +282,8 @@ export class Content extends Ion {
|
||||
public _keyboard: Keyboard,
|
||||
public _zone: NgZone,
|
||||
@Optional() viewCtrl: ViewController,
|
||||
@Optional() public _tabs: Tabs
|
||||
@Optional() public _tabs: Tabs,
|
||||
private _dom: DomController
|
||||
) {
|
||||
super(config, elementRef, renderer, 'content');
|
||||
|
||||
@ -174,21 +293,43 @@ export class Content extends Ion {
|
||||
viewCtrl._setIONContent(this);
|
||||
viewCtrl._setIONContentRef(elementRef);
|
||||
}
|
||||
|
||||
this._scroll = new ScrollView(_dom);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
ngOnInit() {
|
||||
let children = this._elementRef.nativeElement.children;
|
||||
ngAfterViewInit() {
|
||||
if (this._scrollEle) return;
|
||||
|
||||
const children = this._elementRef.nativeElement.children;
|
||||
assert(children && children.length >= 2, 'content needs at least two children');
|
||||
|
||||
this._fixedEle = children[0];
|
||||
this._scrollEle = children[1];
|
||||
|
||||
this._zone.runOutsideAngular(() => {
|
||||
this._scroll = new ScrollView(this._scrollEle);
|
||||
this._scLsn = this.addScrollListener(this._app.setScrolling.bind(this._app));
|
||||
// subscribe to the scroll start
|
||||
this._scroll.scrollStart.subscribe(ev => {
|
||||
this.ionScrollStart.emit(ev);
|
||||
});
|
||||
|
||||
// subscribe to every scroll move
|
||||
this._scroll.scroll.subscribe(ev => {
|
||||
// remind the app that it's currently scrolling
|
||||
this._app.setScrolling(ev.timeStamp);
|
||||
|
||||
// emit to all of our other friends things be scrolling
|
||||
this.ionScroll.emit(ev);
|
||||
|
||||
this.imgsRefresh();
|
||||
});
|
||||
|
||||
// subscribe to the scroll end
|
||||
this._scroll.scrollEnd.subscribe(ev => {
|
||||
this.ionScrollEnd.emit(ev);
|
||||
|
||||
this.imgsRefresh();
|
||||
});
|
||||
}
|
||||
|
||||
@ -198,72 +339,67 @@ export class Content extends Ion {
|
||||
ngOnDestroy() {
|
||||
this._scLsn && this._scLsn();
|
||||
this._scroll && this._scroll.destroy();
|
||||
this._scrollEle = this._footerEle = this._scLsn = this._scroll = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
addScrollListener(handler: any) {
|
||||
return this._addListener('scroll', handler);
|
||||
this._scrollEle = this._fixedEle = this._footerEle = this._scLsn = this._scroll = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
addTouchStartListener(handler: any) {
|
||||
return this._addListener('touchstart', handler);
|
||||
return this._addListener('touchstart', handler, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
addTouchMoveListener(handler: any) {
|
||||
return this._addListener('touchmove', handler);
|
||||
return this._addListener('touchmove', handler, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
addTouchEndListener(handler: any) {
|
||||
return this._addListener('touchend', handler);
|
||||
return this._addListener('touchend', handler, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
addMouseDownListener(handler: any) {
|
||||
return this._addListener('mousedown', handler);
|
||||
return this._addListener('mousedown', handler, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
addMouseUpListener(handler: any) {
|
||||
return this._addListener('mouseup', handler);
|
||||
return this._addListener('mouseup', handler, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
addMouseMoveListener(handler: any) {
|
||||
return this._addListener('mousemove', handler);
|
||||
return this._addListener('mousemove', handler, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_addListener(type: string, handler: any): Function {
|
||||
_addListener(type: string, handler: any, usePassive: boolean): Function {
|
||||
assert(handler, 'handler must be valid');
|
||||
assert(this._scrollEle, '_scrollEle must be valid');
|
||||
|
||||
const opts = eventOptions(false, usePassive);
|
||||
|
||||
// ensure we're not creating duplicates
|
||||
this._scrollEle.removeEventListener(type, handler);
|
||||
this._scrollEle.addEventListener(type, handler);
|
||||
this._scrollEle.removeEventListener(type, handler, opts);
|
||||
this._scrollEle.addEventListener(type, handler, opts);
|
||||
|
||||
return () => {
|
||||
if (this._scrollEle) {
|
||||
this._scrollEle.removeEventListener(type, handler);
|
||||
this._scrollEle.removeEventListener(type, handler, opts);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -275,42 +411,6 @@ export class Content extends Ion {
|
||||
return this._scrollEle;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Call a method when scrolling has stopped
|
||||
* @param {Function} callback The method you want perform when scrolling has ended
|
||||
*/
|
||||
onScrollEnd(callback: Function) {
|
||||
let lastScrollTop: number = null;
|
||||
let framesUnchanged: number = 0;
|
||||
let _scrollEle = this._scrollEle;
|
||||
|
||||
function next() {
|
||||
let currentScrollTop = _scrollEle.scrollTop;
|
||||
if (lastScrollTop !== null) {
|
||||
|
||||
if (Math.round(lastScrollTop) === Math.round(currentScrollTop)) {
|
||||
framesUnchanged++;
|
||||
|
||||
} else {
|
||||
framesUnchanged = 0;
|
||||
}
|
||||
|
||||
if (framesUnchanged > 9) {
|
||||
return callback();
|
||||
}
|
||||
}
|
||||
|
||||
lastScrollTop = currentScrollTop;
|
||||
|
||||
nativeRaf(() => {
|
||||
nativeRaf(next);
|
||||
});
|
||||
}
|
||||
|
||||
nativeTimeout(next, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@ -342,23 +442,6 @@ export class Content extends Ion {
|
||||
return this._scroll.scrollToTop(duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the `scrollTop` property of the content's scrollable element.
|
||||
* @returns {number}
|
||||
*/
|
||||
getScrollTop(): number {
|
||||
return this._scroll.getTop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the `scrollTop` property of the content's scrollable element.
|
||||
* @param {number} top
|
||||
*/
|
||||
setScrollTop(top: number) {
|
||||
console.debug(`content, setScrollTop, top: ${top}`);
|
||||
this._scroll.setTop(top);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to the bottom of the content component.
|
||||
* @param {number} [duration] Duration of the scroll animation in milliseconds. Defaults to `300`.
|
||||
@ -372,8 +455,8 @@ export class Content extends Ion {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
jsScroll(onScrollCallback: Function): Function {
|
||||
return this._scroll.jsScroll(onScrollCallback);
|
||||
enableJsScroll() {
|
||||
this._scroll.enableJsScroll(this.contentTop, this.contentBottom);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -392,12 +475,43 @@ export class Content extends Ion {
|
||||
this._fullscreen = isTrueProperty(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@Input()
|
||||
get lazyLoadImages(): boolean {
|
||||
return !!this._lazyLoadImages;
|
||||
}
|
||||
set lazyLoadImages(val: boolean) {
|
||||
this._lazyLoadImages = isTrueProperty(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
addImg(img: Img) {
|
||||
this._imgs.push(img);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
removeImg(img: Img) {
|
||||
removeArrayItem(this._imgs, img);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
|
||||
/**
|
||||
* @private
|
||||
* DOM WRITE
|
||||
*/
|
||||
setScrollElementStyle(prop: string, val: any) {
|
||||
(<any>this._scrollEle.style)[prop] = val;
|
||||
this._dom.write(() => {
|
||||
(<any>this._scrollEle.style)[prop] = val;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -448,7 +562,9 @@ export class Content extends Ion {
|
||||
console.debug(`content, addScrollPadding, newPadding: ${newPadding}, this._scrollPadding: ${this._scrollPadding}`);
|
||||
|
||||
this._scrollPadding = newPadding;
|
||||
this._scrollEle.style.paddingBottom = (newPadding > 0) ? newPadding + 'px' : '';
|
||||
this._dom.write(() => {
|
||||
this._scrollEle.style.paddingBottom = (newPadding > 0) ? newPadding + 'px' : '';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -475,10 +591,8 @@ export class Content extends Ion {
|
||||
* after dynamically adding headers, footers, or tabs.
|
||||
*/
|
||||
resize() {
|
||||
nativeRaf(() => {
|
||||
this.readDimensions();
|
||||
this.writeDimensions();
|
||||
});
|
||||
this._dom.read(this.readDimensions, this);
|
||||
this._dom.write(this.writeDimensions, this);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -486,20 +600,22 @@ export class Content extends Ion {
|
||||
* DOM READ
|
||||
*/
|
||||
readDimensions() {
|
||||
let cachePaddingTop = this._paddingTop;
|
||||
let cachePaddingRight = this._paddingRight;
|
||||
let cachePaddingBottom = this._paddingBottom;
|
||||
let cachePaddingLeft = this._paddingLeft;
|
||||
let cacheHeaderHeight = this._headerHeight;
|
||||
let cacheFooterHeight = this._footerHeight;
|
||||
let cachePaddingTop = this._pTop;
|
||||
let cachePaddingRight = this._pRight;
|
||||
let cachePaddingBottom = this._pBottom;
|
||||
let cachePaddingLeft = this._pLeft;
|
||||
let cacheHeaderHeight = this._hdrHeight;
|
||||
let cacheFooterHeight = this._ftrHeight;
|
||||
let cacheTabsPlacement = this._tabsPlacement;
|
||||
|
||||
this._paddingTop = 0;
|
||||
this._paddingRight = 0;
|
||||
this._paddingBottom = 0;
|
||||
this._paddingLeft = 0;
|
||||
this._headerHeight = 0;
|
||||
this._footerHeight = 0;
|
||||
this.scrollWidth = 0;
|
||||
this.scrollHeight = 0;
|
||||
this._pTop = 0;
|
||||
this._pRight = 0;
|
||||
this._pBottom = 0;
|
||||
this._pLeft = 0;
|
||||
this._hdrHeight = 0;
|
||||
this._ftrHeight = 0;
|
||||
this._tabsPlacement = null;
|
||||
|
||||
let ele: HTMLElement = this._elementRef.nativeElement;
|
||||
@ -516,19 +632,27 @@ export class Content extends Ion {
|
||||
ele = <HTMLElement>children[i];
|
||||
tagName = ele.tagName;
|
||||
if (tagName === 'ION-CONTENT') {
|
||||
// ******** DOM READ ****************
|
||||
this.scrollWidth = ele.scrollWidth;
|
||||
// ******** DOM READ ****************
|
||||
this.scrollHeight = ele.scrollHeight;
|
||||
|
||||
if (this._fullscreen) {
|
||||
// ******** DOM READ ****************
|
||||
computedStyle = getComputedStyle(ele);
|
||||
this._paddingTop = parsePxUnit(computedStyle.paddingTop);
|
||||
this._paddingBottom = parsePxUnit(computedStyle.paddingBottom);
|
||||
this._paddingRight = parsePxUnit(computedStyle.paddingRight);
|
||||
this._paddingLeft = parsePxUnit(computedStyle.paddingLeft);
|
||||
this._pTop = parsePxUnit(computedStyle.paddingTop);
|
||||
this._pBottom = parsePxUnit(computedStyle.paddingBottom);
|
||||
this._pRight = parsePxUnit(computedStyle.paddingRight);
|
||||
this._pLeft = parsePxUnit(computedStyle.paddingLeft);
|
||||
}
|
||||
|
||||
} else if (tagName === 'ION-HEADER') {
|
||||
this._headerHeight = ele.clientHeight;
|
||||
// ******** DOM READ ****************
|
||||
this._hdrHeight = ele.clientHeight;
|
||||
|
||||
} else if (tagName === 'ION-FOOTER') {
|
||||
this._footerHeight = ele.clientHeight;
|
||||
// ******** DOM READ ****************
|
||||
this._ftrHeight = ele.clientHeight;
|
||||
this._footerEle = ele;
|
||||
}
|
||||
}
|
||||
@ -540,6 +664,7 @@ export class Content extends Ion {
|
||||
|
||||
if (ele.tagName === 'ION-TABS') {
|
||||
tabbarEle = <HTMLElement>ele.firstElementChild;
|
||||
// ******** DOM READ ****************
|
||||
this._tabbarHeight = tabbarEle.clientHeight;
|
||||
|
||||
if (this._tabsPlacement === null) {
|
||||
@ -551,15 +676,42 @@ export class Content extends Ion {
|
||||
ele = ele.parentElement;
|
||||
}
|
||||
|
||||
// Toolbar height
|
||||
this._cTop = this._hdrHeight;
|
||||
this._cBottom = this._ftrHeight;
|
||||
|
||||
// Tabs height
|
||||
if (this._tabsPlacement === 'top') {
|
||||
this._cTop += this._tabbarHeight;
|
||||
|
||||
} else if (this._tabsPlacement === 'bottom') {
|
||||
this._cBottom += this._tabbarHeight;
|
||||
}
|
||||
|
||||
// Handle fullscreen viewport (padding vs margin)
|
||||
if (this._fullscreen) {
|
||||
this._cTop += this._pTop;
|
||||
this._cBottom += this._pBottom;
|
||||
}
|
||||
|
||||
this._dirty = (
|
||||
cachePaddingTop !== this._paddingTop ||
|
||||
cachePaddingBottom !== this._paddingBottom ||
|
||||
cachePaddingLeft !== this._paddingLeft ||
|
||||
cachePaddingRight !== this._paddingRight ||
|
||||
cacheHeaderHeight !== this._headerHeight ||
|
||||
cacheFooterHeight !== this._footerHeight ||
|
||||
cacheTabsPlacement !== this._tabsPlacement
|
||||
cachePaddingTop !== this._pTop ||
|
||||
cachePaddingBottom !== this._pBottom ||
|
||||
cachePaddingLeft !== this._pLeft ||
|
||||
cachePaddingRight !== this._pRight ||
|
||||
cacheHeaderHeight !== this._hdrHeight ||
|
||||
cacheFooterHeight !== this._ftrHeight ||
|
||||
cacheTabsPlacement !== this._tabsPlacement ||
|
||||
this._cTop !== this.contentTop ||
|
||||
this._cBottom !== this.contentBottom
|
||||
);
|
||||
|
||||
this._scroll.init(this._scrollEle, this._cTop, this._cBottom);
|
||||
|
||||
// initial imgs refresh
|
||||
this.imgsRefresh();
|
||||
|
||||
this.readReady.emit();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -572,91 +724,194 @@ export class Content extends Ion {
|
||||
return;
|
||||
}
|
||||
|
||||
let scrollEle = this._scrollEle as any;
|
||||
const scrollEle = this._scrollEle as any;
|
||||
if (!scrollEle) {
|
||||
assert(false, 'this._scrollEle should be valid');
|
||||
return;
|
||||
}
|
||||
|
||||
let fixedEle = this._fixedEle;
|
||||
const fixedEle = this._fixedEle;
|
||||
if (!fixedEle) {
|
||||
assert(false, 'this._fixedEle should be valid');
|
||||
return;
|
||||
}
|
||||
|
||||
// Toolbar height
|
||||
let contentTop = this._headerHeight;
|
||||
let contentBottom = this._footerHeight;
|
||||
|
||||
// Tabs height
|
||||
if (this._tabsPlacement === 'top') {
|
||||
assert(this._tabbarHeight >= 0, '_tabbarHeight has to be positive');
|
||||
contentTop += this._tabbarHeight;
|
||||
|
||||
} else if (this._tabsPlacement === 'bottom') {
|
||||
assert(this._tabbarHeight >= 0, '_tabbarHeight has to be positive');
|
||||
contentBottom += this._tabbarHeight;
|
||||
|
||||
// Update footer position
|
||||
if (contentBottom > 0 && this._footerEle) {
|
||||
let footerPos = contentBottom - this._footerHeight;
|
||||
assert(footerPos >= 0, 'footerPos has to be positive');
|
||||
|
||||
this._footerEle.style.bottom = cssFormat(footerPos);
|
||||
}
|
||||
if (this._tabsPlacement === 'bottom' && this._cBottom > 0 && this._footerEle) {
|
||||
var footerPos = this._cBottom - this._ftrHeight;
|
||||
assert(footerPos >= 0, 'footerPos has to be positive');
|
||||
// ******** DOM WRITE ****************
|
||||
this._footerEle.style.bottom = cssFormat(footerPos);
|
||||
}
|
||||
|
||||
// Handle fullscreen viewport (padding vs margin)
|
||||
let topProperty = 'marginTop';
|
||||
let bottomProperty = 'marginBottom';
|
||||
let fixedTop: number = contentTop;
|
||||
let fixedBottom: number = contentBottom;
|
||||
let fixedTop: number = this._cTop;
|
||||
let fixedBottom: number = this._cBottom;
|
||||
|
||||
if (this._fullscreen) {
|
||||
assert(this._paddingTop >= 0, '_paddingTop has to be positive');
|
||||
assert(this._paddingBottom >= 0, '_paddingBottom has to be positive');
|
||||
assert(this._pTop >= 0, '_paddingTop has to be positive');
|
||||
assert(this._pBottom >= 0, '_paddingBottom has to be positive');
|
||||
|
||||
// adjust the content with padding, allowing content to scroll under headers/footers
|
||||
// however, on iOS you cannot control the margins of the scrollbar (last tested iOS9.2)
|
||||
// only add inline padding styles if the computed padding value, which would
|
||||
// have come from the app's css, is different than the new padding value
|
||||
contentTop += this._paddingTop;
|
||||
contentBottom += this._paddingBottom;
|
||||
topProperty = 'paddingTop';
|
||||
bottomProperty = 'paddingBottom';
|
||||
}
|
||||
|
||||
// Only update top margin if value changed
|
||||
if (contentTop !== this.contentTop) {
|
||||
assert(contentTop >= 0, 'contentTop has to be positive');
|
||||
if (this._cTop !== this.contentTop) {
|
||||
assert(this._cTop >= 0, 'contentTop has to be positive');
|
||||
assert(fixedTop >= 0, 'fixedTop has to be positive');
|
||||
|
||||
scrollEle.style[topProperty] = cssFormat(contentTop);
|
||||
// ******** DOM WRITE ****************
|
||||
scrollEle.style[topProperty] = cssFormat(this._cTop);
|
||||
// ******** DOM WRITE ****************
|
||||
fixedEle.style.marginTop = cssFormat(fixedTop);
|
||||
this.contentTop = contentTop;
|
||||
|
||||
this.contentTop = this._cTop;
|
||||
}
|
||||
|
||||
// Only update bottom margin if value changed
|
||||
if (contentBottom !== this.contentBottom) {
|
||||
assert(contentBottom >= 0, 'contentBottom has to be positive');
|
||||
if (this._cBottom !== this.contentBottom) {
|
||||
assert(this._cBottom >= 0, 'contentBottom has to be positive');
|
||||
assert(fixedBottom >= 0, 'fixedBottom has to be positive');
|
||||
|
||||
scrollEle.style[bottomProperty] = cssFormat(contentBottom);
|
||||
// ******** DOM WRITE ****************
|
||||
scrollEle.style[bottomProperty] = cssFormat(this._cBottom);
|
||||
// ******** DOM WRITE ****************
|
||||
fixedEle.style.marginBottom = cssFormat(fixedBottom);
|
||||
this.contentBottom = contentBottom;
|
||||
|
||||
this.contentBottom = this._cBottom;
|
||||
}
|
||||
|
||||
if (this._tabsPlacement !== null && this._tabs) {
|
||||
// set the position of the tabbar
|
||||
if (this._tabsPlacement === 'top') {
|
||||
this._tabs.setTabbarPosition(this._headerHeight, -1);
|
||||
// ******** DOM WRITE ****************
|
||||
this._tabs.setTabbarPosition(this._hdrHeight, -1);
|
||||
|
||||
} else {
|
||||
assert(this._tabsPlacement === 'bottom', 'tabsPlacement should be bottom');
|
||||
// ******** DOM WRITE ****************
|
||||
this._tabs.setTabbarPosition(-1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
this.writeReady.emit();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
imgsRefresh() {
|
||||
if (this._imgs.length && this.isImgsRefreshable()) {
|
||||
loadImgs(this._imgs, this.scrollTop, this.scrollHeight, this.directionY, IMG_REQUESTABLE_BUFFER, IMG_RENDERABLE_BUFFER);
|
||||
}
|
||||
}
|
||||
|
||||
isImgsRefreshable() {
|
||||
return Math.abs(this.velocityY) < 3;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function loadImgs(imgs: Img[], scrollTop: number, scrollHeight: number, scrollDirectionY: ScrollDirection, requestableBuffer: number, renderableBuffer: number) {
|
||||
const scrollBottom = (scrollTop + scrollHeight);
|
||||
const priority1: Img[] = [];
|
||||
const priority2: Img[] = [];
|
||||
let img: Img;
|
||||
|
||||
// all images should be paused
|
||||
for (var i = 0, ilen = imgs.length; i < ilen; i++) {
|
||||
img = imgs[i];
|
||||
|
||||
if (scrollDirectionY === ScrollDirection.Up) {
|
||||
// scrolling up
|
||||
if (img.top < scrollBottom && img.bottom > scrollTop - renderableBuffer) {
|
||||
// scrolling up, img is within viewable area
|
||||
// or about to be viewable area
|
||||
img.canRequest = img.canRender = true;
|
||||
priority1.push(img);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (img.bottom <= scrollTop && img.bottom > scrollTop - requestableBuffer) {
|
||||
// scrolling up, img is within requestable area
|
||||
img.canRequest = true;
|
||||
img.canRender = false;
|
||||
priority2.push(img);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (img.top >= scrollBottom && img.top < scrollBottom + renderableBuffer) {
|
||||
// scrolling up, img below viewable area
|
||||
// but it's still within renderable area
|
||||
// don't allow a reset
|
||||
img.canRequest = img.canRender = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
} else {
|
||||
// scrolling down
|
||||
|
||||
if (img.bottom > scrollTop && img.top < scrollBottom + renderableBuffer) {
|
||||
// scrolling down, img is within viewable area
|
||||
// or about to be viewable area
|
||||
img.canRequest = img.canRender = true;
|
||||
priority1.push(img);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (img.top >= scrollBottom && img.top < scrollBottom + requestableBuffer) {
|
||||
// scrolling down, img is within requestable area
|
||||
img.canRequest = true;
|
||||
img.canRender = false;
|
||||
priority2.push(img);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (img.bottom <= scrollTop && img.bottom > scrollTop - renderableBuffer) {
|
||||
// scrolling down, img above viewable area
|
||||
// but it's still within renderable area
|
||||
// don't allow a reset
|
||||
img.canRequest = img.canRender = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
img.canRequest = img.canRender = false;
|
||||
img.reset();
|
||||
}
|
||||
|
||||
// update all imgs which are viewable
|
||||
priority1.sort(sortTopToBottom).forEach(i => i.update());
|
||||
|
||||
if (scrollDirectionY === ScrollDirection.Up) {
|
||||
// scrolling up
|
||||
priority2.sort(sortTopToBottom).reverse().forEach(i => i.update());
|
||||
|
||||
} else {
|
||||
// scrolling down
|
||||
priority2.sort(sortTopToBottom).forEach(i => i.update());
|
||||
}
|
||||
}
|
||||
|
||||
const IMG_REQUESTABLE_BUFFER = 1200;
|
||||
const IMG_RENDERABLE_BUFFER = 200;
|
||||
|
||||
|
||||
function sortTopToBottom(a: Img, b: Img) {
|
||||
if (a.top < b.top) {
|
||||
return -1;
|
||||
}
|
||||
if (a.top > b.top) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function parsePxUnit(val: string): number {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Directive, ElementRef, EventEmitter, Host, Input, NgZone, Output } from '@angular/core';
|
||||
|
||||
import { Content } from '../content/content';
|
||||
import { Content, ScrollEvent } from '../content/content';
|
||||
import { DomController } from '../../util/dom-controller';
|
||||
|
||||
|
||||
/**
|
||||
@ -97,7 +98,7 @@ import { Content } from '../content/content';
|
||||
export class InfiniteScroll {
|
||||
_lastCheck: number = 0;
|
||||
_highestY: number = 0;
|
||||
_scLsn: Function;
|
||||
_scLsn: any;
|
||||
_thr: string = '15%';
|
||||
_thrPx: number = 0;
|
||||
_thrPc: number = 0.15;
|
||||
@ -156,31 +157,32 @@ export class InfiniteScroll {
|
||||
constructor(
|
||||
@Host() private _content: Content,
|
||||
private _zone: NgZone,
|
||||
private _elementRef: ElementRef
|
||||
private _elementRef: ElementRef,
|
||||
private _dom: DomController
|
||||
) {
|
||||
_content.setElementClass('has-infinite-scroll', true);
|
||||
}
|
||||
|
||||
_onScroll() {
|
||||
_onScroll(ev: ScrollEvent) {
|
||||
if (this.state === STATE_LOADING || this.state === STATE_DISABLED) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
let now = Date.now();
|
||||
|
||||
if (this._lastCheck + 32 > now) {
|
||||
if (this._lastCheck + 32 > ev.timeStamp) {
|
||||
// no need to check less than every XXms
|
||||
return 2;
|
||||
}
|
||||
this._lastCheck = now;
|
||||
this._lastCheck = ev.timeStamp;
|
||||
|
||||
let infiniteHeight = this._elementRef.nativeElement.scrollHeight;
|
||||
// ******** DOM READ ****************
|
||||
const infiniteHeight = this._elementRef.nativeElement.scrollHeight;
|
||||
if (!infiniteHeight) {
|
||||
// if there is no height of this element then do nothing
|
||||
return 3;
|
||||
}
|
||||
|
||||
let d = this._content.getContentDimensions();
|
||||
// ******** DOM READ ****************
|
||||
const d = this._content.getContentDimensions();
|
||||
|
||||
let reloadY = d.contentHeight;
|
||||
if (this._thrPc) {
|
||||
@ -189,13 +191,18 @@ export class InfiniteScroll {
|
||||
reloadY += this._thrPx;
|
||||
}
|
||||
|
||||
let distanceFromInfinite = ((d.scrollHeight - infiniteHeight) - d.scrollTop) - reloadY;
|
||||
// ******** DOM READS ABOVE / DOM WRITES BELOW ****************
|
||||
|
||||
const distanceFromInfinite = ((d.scrollHeight - infiniteHeight) - d.scrollTop) - reloadY;
|
||||
if (distanceFromInfinite < 0) {
|
||||
this._zone.run(() => {
|
||||
if (this.state !== STATE_LOADING && this.state !== STATE_DISABLED) {
|
||||
this.state = STATE_LOADING;
|
||||
this.ionInfinite.emit(this);
|
||||
}
|
||||
// ******** DOM WRITE ****************
|
||||
this._dom.write(() => {
|
||||
this._zone.run(() => {
|
||||
if (this.state !== STATE_LOADING && this.state !== STATE_DISABLED) {
|
||||
this.state = STATE_LOADING;
|
||||
this.ionInfinite.emit(this);
|
||||
}
|
||||
});
|
||||
});
|
||||
return 5;
|
||||
}
|
||||
@ -238,12 +245,12 @@ export class InfiniteScroll {
|
||||
if (this._init) {
|
||||
if (shouldListen) {
|
||||
if (!this._scLsn) {
|
||||
this._zone.runOutsideAngular(() => {
|
||||
this._scLsn = this._content.addScrollListener( this._onScroll.bind(this) );
|
||||
this._scLsn = this._content.ionScroll.subscribe((ev: ScrollEvent) => {
|
||||
this._onScroll(ev);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this._scLsn && this._scLsn();
|
||||
this._scLsn && this._scLsn.unsubscribe();
|
||||
this._scLsn = null;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Content } from '../../content/content';
|
||||
import { Content, ScrollEvent } from '../../content/content';
|
||||
import { InfiniteScroll } from '../infinite-scroll';
|
||||
import { mockConfig, mockElementRef, mockRenderer, mockZone } from '../../../util/mock-providers';
|
||||
import { mockConfig, MockDomController, mockElementRef, mockRenderer, mockZone } from '../../../util/mock-providers';
|
||||
|
||||
|
||||
describe('Infinite Scroll', () => {
|
||||
@ -17,7 +17,7 @@ describe('Infinite Scroll', () => {
|
||||
|
||||
setInfiniteScrollTop(300);
|
||||
|
||||
var result = inf._onScroll();
|
||||
var result = inf._onScroll(ev);
|
||||
expect(result).toEqual(6);
|
||||
});
|
||||
|
||||
@ -30,37 +30,38 @@ describe('Infinite Scroll', () => {
|
||||
|
||||
setInfiniteScrollTop(300);
|
||||
|
||||
var result = inf._onScroll();
|
||||
var result = inf._onScroll(ev);
|
||||
expect(result).toEqual(5);
|
||||
});
|
||||
|
||||
it('should not run if there is not infinite element height', () => {
|
||||
setInfiniteScrollTop(0);
|
||||
var result = inf._onScroll();
|
||||
var result = inf._onScroll(ev);
|
||||
expect(result).toEqual(3);
|
||||
});
|
||||
|
||||
it('should not run again if ran less than 32ms ago', () => {
|
||||
ev.timeStamp = Date.now();
|
||||
inf._lastCheck = Date.now();
|
||||
var result = inf._onScroll();
|
||||
var result = inf._onScroll(ev);
|
||||
expect(result).toEqual(2);
|
||||
});
|
||||
|
||||
it('should not run if state is disabled', () => {
|
||||
inf.state = 'disabled';
|
||||
var result = inf._onScroll();
|
||||
var result = inf._onScroll(ev);
|
||||
expect(result).toEqual(1);
|
||||
});
|
||||
|
||||
it('should not run if state is loading', () => {
|
||||
inf.state = 'loading';
|
||||
var result = inf._onScroll();
|
||||
var result = inf._onScroll(ev);
|
||||
expect(result).toEqual(1);
|
||||
});
|
||||
|
||||
it('should not run if not enabled', () => {
|
||||
inf.state = 'disabled';
|
||||
var result = inf._onScroll();
|
||||
var result = inf._onScroll(ev);
|
||||
expect(result).toEqual(1);
|
||||
});
|
||||
|
||||
@ -95,15 +96,19 @@ describe('Infinite Scroll', () => {
|
||||
let content: Content;
|
||||
let contentElementRef;
|
||||
let infiniteElementRef;
|
||||
let ev: ScrollEvent = {};
|
||||
let dom: MockDomController;
|
||||
|
||||
beforeEach(() => {
|
||||
contentElementRef = mockElementRef();
|
||||
content = new Content(config, contentElementRef, mockRenderer(), null, null, null, null, null);
|
||||
content = new Content(config, contentElementRef, mockRenderer(), null, null, null, null, null, dom);
|
||||
content._scrollEle = document.createElement('div');
|
||||
content._scrollEle.className = 'scroll-content';
|
||||
|
||||
infiniteElementRef = mockElementRef();
|
||||
inf = new InfiniteScroll(content, mockZone(), infiniteElementRef);
|
||||
dom = new MockDomController();
|
||||
|
||||
inf = new InfiniteScroll(content, mockZone(), infiniteElementRef, dom);
|
||||
});
|
||||
|
||||
function setInfiniteScrollTop(scrollTop) {
|
||||
|
@ -4,7 +4,8 @@ import { NgControl } from '@angular/forms';
|
||||
import { App } from '../app/app';
|
||||
import { copyInputAttributes, PointerCoordinates, hasPointerMoved, pointerCoord } from '../../util/dom';
|
||||
import { Config } from '../../config/config';
|
||||
import { Content, ContentDimensions } from '../content/content';
|
||||
import { Content, ContentDimensions, ScrollEvent } from '../content/content';
|
||||
import { DomController } from '../../util/dom-controller';
|
||||
import { Form, IonicFormInput } from '../../util/form';
|
||||
import { Ion } from '../ion';
|
||||
import { isTrueProperty } from '../../util/util';
|
||||
@ -21,10 +22,8 @@ import { Platform } from '../../platform/platform';
|
||||
*/
|
||||
export class InputBase extends Ion implements IonicFormInput {
|
||||
_coord: PointerCoordinates;
|
||||
_deregScroll: Function;
|
||||
_disabled: boolean = false;
|
||||
_keyboardHeight: number;
|
||||
_scrollMove: EventListener;
|
||||
_type: string = 'text';
|
||||
_useAssist: boolean;
|
||||
_usePadding: boolean;
|
||||
@ -35,6 +34,8 @@ export class InputBase extends Ion implements IonicFormInput {
|
||||
_autoCorrect: string;
|
||||
_nav: NavControllerBase;
|
||||
_native: NativeInput;
|
||||
_scrollStart: any;
|
||||
_scrollEnd: any;
|
||||
|
||||
// Whether to clear after the user returns to the input and resumes editing
|
||||
_clearOnEdit: boolean;
|
||||
@ -51,9 +52,10 @@ export class InputBase extends Ion implements IonicFormInput {
|
||||
protected _platform: Platform,
|
||||
elementRef: ElementRef,
|
||||
renderer: Renderer,
|
||||
protected _scrollView: Content,
|
||||
protected _content: Content,
|
||||
nav: NavController,
|
||||
ngControl: NgControl
|
||||
ngControl: NgControl,
|
||||
protected _dom: DomController
|
||||
) {
|
||||
super(config, elementRef, renderer, 'input');
|
||||
|
||||
@ -72,29 +74,27 @@ export class InputBase extends Ion implements IonicFormInput {
|
||||
}
|
||||
|
||||
_form.register(this);
|
||||
|
||||
this._scrollStart = _content.ionScrollStart.subscribe((ev: ScrollEvent) => {
|
||||
this.scrollHideFocus(ev, true);
|
||||
});
|
||||
this._scrollEnd = _content.ionScrollEnd.subscribe((ev: ScrollEvent) => {
|
||||
this.scrollHideFocus(ev, false);
|
||||
});
|
||||
}
|
||||
|
||||
scrollMove(ev: UIEvent) {
|
||||
// scroll move event listener this instance can reuse
|
||||
console.debug(`input-base, scrollMove`);
|
||||
scrollHideFocus(ev: ScrollEvent, shouldHideFocus: boolean) {
|
||||
// do not continue if there's no nav, or it's transitioning
|
||||
if (!this._nav) return;
|
||||
|
||||
if (!(this._nav && this._nav.isTransitioning())) {
|
||||
this.deregScrollMove();
|
||||
|
||||
if (this.hasFocus()) {
|
||||
this._native.hideFocus(true);
|
||||
|
||||
this._scrollView.onScrollEnd(() => {
|
||||
this._native.hideFocus(false);
|
||||
|
||||
if (this.hasFocus()) {
|
||||
// if it still has focus then keep listening
|
||||
this.regScrollMove();
|
||||
}
|
||||
});
|
||||
}
|
||||
// DOM READ: check if this input has focus
|
||||
if (this.hasFocus()) {
|
||||
// if it does have focus, then do the dom write
|
||||
this._dom.write(() => {
|
||||
this._native.hideFocus(shouldHideFocus);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
setItemInputControlCss() {
|
||||
let item = this._item;
|
||||
@ -322,9 +322,6 @@ export class InputBase extends Ion implements IonicFormInput {
|
||||
console.debug(`input-base, focusChange, inputHasFocus: ${inputHasFocus}, ${this._item.getNativeElement().nodeName}.${this._item.getNativeElement().className}`);
|
||||
this._item.setElementClass('input-has-focus', inputHasFocus);
|
||||
}
|
||||
if (!inputHasFocus) {
|
||||
this.deregScrollMove();
|
||||
}
|
||||
|
||||
// If clearOnEdit is enabled and the input blurred but has a value, set a flag
|
||||
if (this._clearOnEdit && !inputHasFocus && this.hasValue()) {
|
||||
@ -379,11 +376,11 @@ export class InputBase extends Ion implements IonicFormInput {
|
||||
*/
|
||||
initFocus() {
|
||||
// begin the process of setting focus to the inner input element
|
||||
const scrollView = this._scrollView;
|
||||
const content = this._content;
|
||||
|
||||
console.debug(`input-base, initFocus(), scrollView: ${!!scrollView}`);
|
||||
console.debug(`input-base, initFocus(), scrollView: ${!!content}`);
|
||||
|
||||
if (scrollView) {
|
||||
if (content) {
|
||||
// this input is inside of a scroll view
|
||||
// find out if text input should be manually scrolled into view
|
||||
|
||||
@ -391,7 +388,7 @@ export class InputBase extends Ion implements IonicFormInput {
|
||||
let ele: HTMLElement = this._elementRef.nativeElement;
|
||||
ele = <HTMLElement>ele.closest('ion-item,[ion-item]') || ele;
|
||||
|
||||
const scrollData = getScrollData(ele.offsetTop, ele.offsetHeight, scrollView.getContentDimensions(), this._keyboardHeight, this._platform.height());
|
||||
const scrollData = getScrollData(ele.offsetTop, ele.offsetHeight, content.getContentDimensions(), this._keyboardHeight, this._platform.height());
|
||||
if (Math.abs(scrollData.scrollAmount) < 4) {
|
||||
// the text input is in a safe position that doesn't
|
||||
// require it to be scrolled into view, just set focus now
|
||||
@ -400,17 +397,16 @@ export class InputBase extends Ion implements IonicFormInput {
|
||||
// all good, allow clicks again
|
||||
this._app.setEnabled(true);
|
||||
this._nav && this._nav.setTransitioning(false);
|
||||
this.regScrollMove();
|
||||
|
||||
if (this._usePadding) {
|
||||
this._scrollView.clearScrollPaddingFocusOut();
|
||||
content.clearScrollPaddingFocusOut();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._usePadding) {
|
||||
// add padding to the bottom of the scroll view (if needed)
|
||||
scrollView.addScrollPadding(scrollData.scrollPadding);
|
||||
content.addScrollPadding(scrollData.scrollPadding);
|
||||
}
|
||||
|
||||
// manually scroll the text input to the top
|
||||
@ -425,7 +421,7 @@ export class InputBase extends Ion implements IonicFormInput {
|
||||
this._native.beginFocus(true, scrollData.inputSafeY);
|
||||
|
||||
// scroll the input into place
|
||||
scrollView.scrollTo(0, scrollData.scrollTo, scrollDuration, () => {
|
||||
content.scrollTo(0, scrollData.scrollTo, scrollDuration, () => {
|
||||
console.debug(`input-base, scrollTo completed, scrollTo: ${scrollData.scrollTo}, scrollDuration: ${scrollDuration}`);
|
||||
// the scroll view is in the correct position now
|
||||
// give the native text input focus
|
||||
@ -437,17 +433,15 @@ export class InputBase extends Ion implements IonicFormInput {
|
||||
// all good, allow clicks again
|
||||
this._app.setEnabled(true);
|
||||
this._nav && this._nav.setTransitioning(false);
|
||||
this.regScrollMove();
|
||||
|
||||
if (this._usePadding) {
|
||||
this._scrollView.clearScrollPaddingFocusOut();
|
||||
content.clearScrollPaddingFocusOut();
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
// not inside of a scroll view, just focus it
|
||||
this.setFocus();
|
||||
this.regScrollMove();
|
||||
}
|
||||
}
|
||||
|
||||
@ -482,26 +476,6 @@ export class InputBase extends Ion implements IonicFormInput {
|
||||
*/
|
||||
registerOnTouched(fn: any) { this.onTouched = fn; }
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
regScrollMove() {
|
||||
// register scroll move listener
|
||||
if (this._useAssist && this._scrollView) {
|
||||
setTimeout(() => {
|
||||
this.deregScrollMove();
|
||||
this._deregScroll = this._scrollView.addScrollListener(this.scrollMove.bind(this));
|
||||
}, 80);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
deregScrollMove() {
|
||||
// deregister the scroll move listener
|
||||
this._deregScroll && this._deregScroll();
|
||||
}
|
||||
|
||||
focusNext() {
|
||||
this._form.tabFocus(this);
|
||||
|
@ -4,6 +4,7 @@ import { NgControl } from '@angular/forms';
|
||||
import { App } from '../app/app';
|
||||
import { Config } from '../../config/config';
|
||||
import { Content } from '../content/content';
|
||||
import { DomController } from '../../util/dom-controller';
|
||||
import { Form } from '../../util/form';
|
||||
import { InputBase } from './input-base';
|
||||
import { isTrueProperty } from '../../util/util';
|
||||
@ -91,9 +92,10 @@ export class TextInput extends InputBase {
|
||||
renderer: Renderer,
|
||||
@Optional() scrollView: Content,
|
||||
@Optional() nav: NavController,
|
||||
@Optional() ngControl: NgControl
|
||||
@Optional() ngControl: NgControl,
|
||||
dom: DomController
|
||||
) {
|
||||
super(config, form, item, app, platform, elementRef, renderer, scrollView, nav, ngControl);
|
||||
super(config, form, item, app, platform, elementRef, renderer, scrollView, nav, ngControl, dom);
|
||||
|
||||
this.mode = config.get('mode');
|
||||
}
|
||||
@ -238,6 +240,8 @@ export class TextInput extends InputBase {
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
this._form.deregister(this);
|
||||
this._scrollStart.unsubscribe();
|
||||
this._scrollEnd.unsubscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -314,9 +318,10 @@ export class TextArea extends InputBase {
|
||||
renderer: Renderer,
|
||||
@Optional() scrollView: Content,
|
||||
@Optional() nav: NavController,
|
||||
@Optional() ngControl: NgControl
|
||||
@Optional() ngControl: NgControl,
|
||||
dom: DomController
|
||||
) {
|
||||
super(config, form, item, app, platform, elementRef, renderer, scrollView, nav, ngControl);
|
||||
super(config, form, item, app, platform, elementRef, renderer, scrollView, nav, ngControl, dom);
|
||||
|
||||
this.mode = config.get('mode');
|
||||
}
|
||||
@ -405,6 +410,8 @@ export class TextArea extends InputBase {
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
this._form.deregister(this);
|
||||
this._scrollStart.unsubscribe();
|
||||
this._scrollEnd.unsubscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -225,7 +225,7 @@ export class ItemReorder {
|
||||
}
|
||||
|
||||
_scrollContent(scroll: number) {
|
||||
let scrollTop = this._content.getScrollTop() + scroll;
|
||||
const scrollTop = this._content.scrollTop + scroll;
|
||||
if (scroll !== 0) {
|
||||
this._content.scrollTo(0, scrollTop, 0);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Refresher } from '../refresher';
|
||||
import { Content } from '../../content/content';
|
||||
import { GestureController } from '../../../gestures/gesture-controller';
|
||||
import { mockConfig, mockElementRef, mockRenderer, mockZone } from '../../../util/mock-providers';
|
||||
import { mockConfig, MockDomController, mockElementRef, mockRenderer, mockZone } from '../../../util/mock-providers';
|
||||
|
||||
|
||||
describe('Refresher', () => {
|
||||
@ -217,12 +217,14 @@ describe('Refresher', () => {
|
||||
|
||||
let refresher: Refresher;
|
||||
let content: Content;
|
||||
let dom: MockDomController;
|
||||
|
||||
beforeEach(() => {
|
||||
let gestureController = new GestureController(null);
|
||||
let elementRef = mockElementRef();
|
||||
dom = new MockDomController();
|
||||
elementRef.nativeElement.children.push('');
|
||||
content = new Content(mockConfig(), mockElementRef(), mockRenderer(), null, null, mockZone(), null, null);
|
||||
content = new Content(mockConfig(), mockElementRef(), mockRenderer(), null, null, mockZone(), null, null, dom);
|
||||
content._scrollEle = document.createElement('div');
|
||||
content._scrollEle.className = 'scroll-content';
|
||||
|
||||
|
@ -84,7 +84,7 @@ export { Card, CardContent, CardHeader, CardTitle } from './components/card/card
|
||||
export { Checkbox } from './components/checkbox/checkbox';
|
||||
export { Chip } from './components/chip/chip';
|
||||
export { ClickBlock } from './util/click-block';
|
||||
export { Content } from './components/content/content';
|
||||
export { Content, ScrollEvent, ScrollDirection } from './components/content/content';
|
||||
export { DateTime } from './components/datetime/datetime';
|
||||
export { FabContainer, FabButton, FabList } from './components/fab/fab';
|
||||
export { Grid, Row, Col } from './components/grid/grid';
|
||||
|
@ -95,32 +95,37 @@ export class DomController {
|
||||
}
|
||||
|
||||
protected flush(timeStamp: number) {
|
||||
let err;
|
||||
|
||||
try {
|
||||
this.dispatch(timeStamp);
|
||||
} finally {
|
||||
this.q = false;
|
||||
}
|
||||
}
|
||||
|
||||
private dispatch(timeStamp: number) {
|
||||
let i: number;
|
||||
const r = this.r;
|
||||
const rLen = r.length;
|
||||
const w = this.w;
|
||||
const wLen = w.length;
|
||||
|
||||
// ******** DOM READS ****************
|
||||
for (i = 0; i < rLen; i++) {
|
||||
r[i](timeStamp);
|
||||
dispatch(timeStamp, this.r, this.w);
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
// ******** DOM WRITES ****************
|
||||
for (i = 0; i < wLen; i++) {
|
||||
w[i](timeStamp);
|
||||
this.q = false;
|
||||
|
||||
if (this.r.length || this.w.length) {
|
||||
this.queue();
|
||||
}
|
||||
|
||||
r.length = 0;
|
||||
w.length = 0;
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function dispatch(timeStamp: number, r: Function[], w: Function[]) {
|
||||
let task;
|
||||
|
||||
// ******** DOM READS ****************
|
||||
while (task = r.shift()) {
|
||||
task(timeStamp);
|
||||
}
|
||||
|
||||
// ******** DOM WRITES ****************
|
||||
while (task = w.shift()) {
|
||||
task(timeStamp);
|
||||
}
|
||||
}
|
||||
|
@ -133,21 +133,22 @@ export function setupEvents(platform: Platform, dom: DomController): Events {
|
||||
let el = <HTMLElement>document.elementFromPoint(platform.width() / 2, platform.height() / 2);
|
||||
if (!el) { return; }
|
||||
|
||||
let content = <HTMLElement>el.closest('.scroll-content');
|
||||
if (content) {
|
||||
var scroll = new ScrollView(content);
|
||||
let contentEle = <HTMLElement>el.closest('.scroll-content');
|
||||
if (contentEle) {
|
||||
var scroll = new ScrollView(dom);
|
||||
scroll.init(contentEle, 0, 0);
|
||||
// We need to stop scrolling if it's happening and scroll up
|
||||
|
||||
content.style['WebkitBackfaceVisibility'] = 'hidden';
|
||||
content.style['WebkitTransform'] = 'translate3d(0,0,0)';
|
||||
contentEle.style['WebkitBackfaceVisibility'] = 'hidden';
|
||||
contentEle.style['WebkitTransform'] = 'translate3d(0,0,0)';
|
||||
|
||||
nativeRaf(function() {
|
||||
content.style.overflow = 'hidden';
|
||||
contentEle.style.overflow = 'hidden';
|
||||
|
||||
function finish() {
|
||||
content.style.overflow = '';
|
||||
content.style['WebkitBackfaceVisibility'] = '';
|
||||
content.style['WebkitTransform'] = '';
|
||||
contentEle.style.overflow = '';
|
||||
contentEle.style['WebkitBackfaceVisibility'] = '';
|
||||
contentEle.style['WebkitTransform'] = '';
|
||||
}
|
||||
|
||||
let didScrollTimeout = setTimeout(() => {
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { Injectable, NgZone } from '@angular/core';
|
||||
|
||||
import { Config } from '../config/config';
|
||||
import { focusOutActiveElement, hasFocusedTextInput, nativeRaf, nativeTimeout, zoneRafFrames } from './dom';
|
||||
import { DomController } from './dom-controller';
|
||||
import { focusOutActiveElement, hasFocusedTextInput, nativeTimeout } from './dom';
|
||||
import { Key } from './key';
|
||||
|
||||
|
||||
@ -23,7 +24,7 @@ import { Key } from './key';
|
||||
@Injectable()
|
||||
export class Keyboard {
|
||||
|
||||
constructor(config: Config, private _zone: NgZone) {
|
||||
constructor(config: Config, private _zone: NgZone, private _dom: DomController) {
|
||||
_zone.runOutsideAngular(() => {
|
||||
this.focusOutline(config.get('focusOutline'), document);
|
||||
|
||||
@ -92,10 +93,12 @@ export class Keyboard {
|
||||
function checkKeyboard() {
|
||||
console.debug(`keyboard, isOpen: ${self.isOpen()}`);
|
||||
if (!self.isOpen() || checks > pollingChecksMax) {
|
||||
zoneRafFrames(30, () => {
|
||||
console.debug(`keyboard, closed`);
|
||||
callback();
|
||||
});
|
||||
nativeTimeout(function() {
|
||||
self._zone.run(function() {
|
||||
console.debug(`keyboard, closed`);
|
||||
callback();
|
||||
});
|
||||
}, 400);
|
||||
|
||||
} else {
|
||||
nativeTimeout(checkKeyboard, pollingInternval);
|
||||
@ -112,11 +115,13 @@ export class Keyboard {
|
||||
* Programmatically close the keyboard.
|
||||
*/
|
||||
close() {
|
||||
console.debug(`keyboard, close()`);
|
||||
nativeRaf(() => {
|
||||
this._dom.read(() => {
|
||||
if (hasFocusedTextInput()) {
|
||||
// only focus out when a text input has focus
|
||||
focusOutActiveElement();
|
||||
console.debug(`keyboard, close()`);
|
||||
this._dom.write(() => {
|
||||
focusOutActiveElement();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -141,7 +146,7 @@ export class Keyboard {
|
||||
let isKeyInputEnabled = false;
|
||||
|
||||
function cssClass() {
|
||||
nativeRaf(() => {
|
||||
this._dom.write(() => {
|
||||
document.body.classList[isKeyInputEnabled ? 'add' : 'remove']('focus-outline');
|
||||
});
|
||||
}
|
||||
|
@ -3,13 +3,16 @@ import { Location } from '@angular/common';
|
||||
|
||||
import { AnimationOptions } from '../animations/animation';
|
||||
import { App } from '../components/app/app';
|
||||
import { IonicApp } from '../components/app/app-root';
|
||||
import { Config } from '../config/config';
|
||||
import { Content } from '../components/content/content';
|
||||
import { DeepLinker } from '../navigation/deep-linker';
|
||||
import { DomController } from './dom-controller';
|
||||
import { GestureController } from '../gestures/gesture-controller';
|
||||
import { Haptic } from './haptic';
|
||||
import { IonicApp } from '../components/app/app-root';
|
||||
import { Keyboard } from './keyboard';
|
||||
import { Menu } from '../components/menu/menu';
|
||||
import { ViewState, DeepLinkConfig } from '../navigation/nav-util';
|
||||
import { NavControllerBase } from '../navigation/nav-controller-base';
|
||||
import { OverlayPortal } from '../components/nav/overlay-portal';
|
||||
import { PageTransition } from '../transitions/page-transition';
|
||||
import { Platform } from '../platform/platform';
|
||||
@ -19,10 +22,8 @@ import { Tabs } from '../components/tabs/tabs';
|
||||
import { TransitionController } from '../transitions/transition-controller';
|
||||
import { UrlSerializer } from '../navigation/url-serializer';
|
||||
import { ViewController } from '../navigation/view-controller';
|
||||
import { ViewState, DeepLinkConfig } from '../navigation/nav-util';
|
||||
|
||||
import { NavControllerBase } from '../navigation/nav-controller-base';
|
||||
import { Haptic } from './haptic';
|
||||
import { DomController } from './dom-controller';
|
||||
|
||||
export const mockConfig = function(config?: any, url: string = '/', platform?: Platform) {
|
||||
const c = new Config();
|
||||
@ -72,6 +73,10 @@ export const mockTrasitionController = function(config: Config) {
|
||||
return trnsCtrl;
|
||||
};
|
||||
|
||||
export const mockContent = function(): Content {
|
||||
return new Content(mockConfig(), mockElementRef(), mockRenderer(), null, null, mockZone(), null, null, new MockDomController());
|
||||
};
|
||||
|
||||
export const mockZone = function(): NgZone {
|
||||
return new NgZone(false);
|
||||
};
|
||||
@ -234,7 +239,9 @@ export const mockNavController = function(): NavControllerBase {
|
||||
|
||||
let zone = mockZone();
|
||||
|
||||
let keyboard = new Keyboard(config, zone);
|
||||
let dom = new MockDomController();
|
||||
|
||||
let keyboard = new Keyboard(config, zone, dom);
|
||||
|
||||
let elementRef = mockElementRef();
|
||||
|
||||
@ -248,8 +255,6 @@ export const mockNavController = function(): NavControllerBase {
|
||||
|
||||
let trnsCtrl = mockTrasitionController(config);
|
||||
|
||||
let dom = new DomController();
|
||||
|
||||
let nav = new NavControllerBase(
|
||||
null,
|
||||
app,
|
||||
@ -262,7 +267,7 @@ export const mockNavController = function(): NavControllerBase {
|
||||
gestureCtrl,
|
||||
trnsCtrl,
|
||||
linker,
|
||||
dom,
|
||||
dom
|
||||
);
|
||||
|
||||
nav._viewInit = function(enteringView: ViewController) {
|
||||
@ -285,7 +290,9 @@ export const mockNavController = function(): NavControllerBase {
|
||||
export const mockOverlayPortal = function(app: App, config: Config, platform: Platform): OverlayPortal {
|
||||
let zone = mockZone();
|
||||
|
||||
let keyboard = new Keyboard(config, zone);
|
||||
let dom = new MockDomController();
|
||||
|
||||
let keyboard = new Keyboard(config, zone, dom);
|
||||
|
||||
let elementRef = mockElementRef();
|
||||
|
||||
@ -301,8 +308,6 @@ export const mockOverlayPortal = function(app: App, config: Config, platform: Pl
|
||||
|
||||
let deepLinker = new DeepLinker(app, serializer, location);
|
||||
|
||||
let dom = new DomController();
|
||||
|
||||
return new OverlayPortal(
|
||||
app,
|
||||
config,
|
||||
@ -328,7 +333,9 @@ export const mockTab = function(parentTabs: Tabs): Tab {
|
||||
|
||||
let zone = mockZone();
|
||||
|
||||
let keyboard = new Keyboard(config, zone);
|
||||
let dom = new MockDomController();
|
||||
|
||||
let keyboard = new Keyboard(config, zone, dom);
|
||||
|
||||
let elementRef = mockElementRef();
|
||||
|
||||
@ -342,8 +349,6 @@ export const mockTab = function(parentTabs: Tabs): Tab {
|
||||
|
||||
let linker = mockDeepLinker(null, app);
|
||||
|
||||
let dom = new DomController();
|
||||
|
||||
let tab = new Tab(
|
||||
parentTabs,
|
||||
app,
|
||||
@ -402,6 +407,22 @@ export const mockHaptic = function (): Haptic {
|
||||
return new Haptic(null);
|
||||
};
|
||||
|
||||
|
||||
|
||||
export class MockDomController extends DomController {
|
||||
private timeStamp = 0;
|
||||
|
||||
protected queue() {}
|
||||
|
||||
flush(done: any) {
|
||||
setTimeout(() => {
|
||||
const timeStamp = ++this.timeStamp;
|
||||
super.flush(timeStamp);
|
||||
done(timeStamp);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
export class MockView {}
|
||||
export class MockView1 {}
|
||||
export class MockView2 {}
|
||||
|
@ -1,40 +1,388 @@
|
||||
import { CSS, pointerCoord, nativeRaf, rafFrames, cancelRaf } from '../util/dom';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
|
||||
import { assert } from './util';
|
||||
import { CSS, nativeRaf, pointerCoord, rafFrames } from './dom';
|
||||
import { DomController } from './dom-controller';
|
||||
import { eventOptions, listenEvent } from './ui-event-manager';
|
||||
|
||||
|
||||
export class ScrollView {
|
||||
private _el: HTMLElement;
|
||||
private _js: boolean = false;
|
||||
private _top: number = 0;
|
||||
private _pos: Array<number>;
|
||||
private _velocity: number;
|
||||
private _max: number;
|
||||
private _rafId: number;
|
||||
private _cb: Function;
|
||||
isPlaying: boolean;
|
||||
isScrolling = false;
|
||||
scrollStart = new Subject<ScrollEvent>();
|
||||
scroll = new Subject<ScrollEvent>();
|
||||
scrollEnd = new Subject<ScrollEvent>();
|
||||
|
||||
constructor(ele: HTMLElement) {
|
||||
this._el = ele;
|
||||
private _el: HTMLElement;
|
||||
private _js: boolean;
|
||||
private _t: number = 0;
|
||||
private _l: number = 0;
|
||||
private _lsn: Function;
|
||||
private _endTmr: Function;
|
||||
|
||||
ev: ScrollEvent = {
|
||||
directionY: ScrollDirection.Down,
|
||||
directionX: null
|
||||
};
|
||||
|
||||
|
||||
constructor(private _dom: DomController) {}
|
||||
|
||||
init(ele: HTMLElement, contentTop: number, contentBottom: number) {
|
||||
if (!this._el) {
|
||||
assert(ele, 'scroll-view, element can not be null');
|
||||
this._el = ele;
|
||||
|
||||
if (this._js) {
|
||||
this.enableJsScroll(contentTop, contentBottom);
|
||||
} else {
|
||||
this.enableNativeScrolling();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getTop(): number {
|
||||
if (this._js) {
|
||||
return this._top;
|
||||
private enableNativeScrolling() {
|
||||
this._js = false;
|
||||
if (!this._el) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this._top = this._el.scrollTop;
|
||||
console.debug(`ScrollView, enableNativeScrolling`);
|
||||
|
||||
const self = this;
|
||||
const ev = self.ev;
|
||||
const positions: number[] = [];
|
||||
|
||||
function scrollCallback(scrollEvent: UIEvent) {
|
||||
ev.timeStamp = scrollEvent.timeStamp;
|
||||
|
||||
// get the current scrollTop
|
||||
// ******** DOM READ ****************
|
||||
ev.scrollTop = self.getTop();
|
||||
|
||||
// get the current scrollLeft
|
||||
// ******** DOM READ ****************
|
||||
ev.scrollLeft = self.getLeft();
|
||||
|
||||
if (!self.isScrolling) {
|
||||
// currently not scrolling, so this is a scroll start
|
||||
self.isScrolling = true;
|
||||
|
||||
// remember the start positions
|
||||
ev.startY = ev.scrollTop;
|
||||
ev.startX = ev.scrollLeft;
|
||||
|
||||
// new scroll, so do some resets
|
||||
ev.velocityY = ev.velocityX = 0;
|
||||
ev.deltaY = ev.deltaX = 0;
|
||||
positions.length = 0;
|
||||
|
||||
// emit only on the first scroll event
|
||||
self.scrollStart.next(ev);
|
||||
}
|
||||
|
||||
// actively scrolling
|
||||
positions.push(ev.scrollTop, ev.scrollLeft, ev.timeStamp);
|
||||
|
||||
if (positions.length > 3) {
|
||||
// we've gotten at least 2 scroll events so far
|
||||
ev.deltaY = (ev.scrollTop - ev.startY);
|
||||
ev.deltaX = (ev.scrollLeft - ev.startX);
|
||||
|
||||
var endPos = (positions.length - 1);
|
||||
var startPos = endPos;
|
||||
var timeRange = (ev.timeStamp - 100);
|
||||
|
||||
// move pointer to position measured 100ms ago
|
||||
for (var i = endPos; i > 0 && positions[i] > timeRange; i -= 3) {
|
||||
startPos = i;
|
||||
}
|
||||
|
||||
if (startPos !== endPos) {
|
||||
// compute relative movement between these two points
|
||||
var timeOffset = (positions[endPos] - positions[startPos]);
|
||||
var movedTop = (positions[startPos - 2] - positions[endPos - 2]);
|
||||
var movedLeft = (positions[startPos - 1] - positions[endPos - 1]);
|
||||
|
||||
// based on XXms compute the movement to apply for each render step
|
||||
ev.velocityY = ((movedTop / timeOffset) * FRAME_MS);
|
||||
ev.velocityX = ((movedLeft / timeOffset) * FRAME_MS);
|
||||
|
||||
// figure out which direction we're scrolling
|
||||
ev.directionY = (movedTop > 0 ? ScrollDirection.Up : ScrollDirection.Down);
|
||||
ev.directionX = (movedLeft > 0 ? ScrollDirection.Left : ScrollDirection.Right);
|
||||
}
|
||||
}
|
||||
|
||||
// emit on each scroll event
|
||||
self.scroll.next(ev);
|
||||
|
||||
// debounce for a moment after the last scroll event
|
||||
self._endTmr && self._endTmr();
|
||||
self._endTmr = rafFrames(5, function scrollEnd() {
|
||||
// haven't scrolled in a while, so it's a scrollend
|
||||
self.isScrolling = false;
|
||||
|
||||
// reset velocity, do not reset the directions or deltas
|
||||
ev.velocityY = ev.velocityX = 0;
|
||||
|
||||
// emit that the scroll has ended
|
||||
self.scrollEnd.next(ev);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
// clear out any existing listeners (just to be safe)
|
||||
self._lsn && self._lsn();
|
||||
|
||||
// assign the raw scroll listener
|
||||
// note that it does not have a wrapping requestAnimationFrame on purpose
|
||||
// a scroll event callback will always be right before the raf callback
|
||||
// so there's little to no value of using raf here since it'll all ways immediately
|
||||
// call the raf if it was set within the scroll event, so this will save us some time
|
||||
const opts = eventOptions(false, false);
|
||||
self._lsn = listenEvent(self._el, 'scroll', false, opts, scrollCallback);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @private
|
||||
* JS Scrolling has been provided only as a temporary solution
|
||||
* until iOS apps can take advantage of scroll events at all times.
|
||||
* The goal is to eventually remove JS scrolling entirely. When we
|
||||
* no longer have to worry about iOS not firing scroll events during
|
||||
* inertia then this can be burned to the ground. iOS's more modern
|
||||
* WKWebView does not have this issue, only UIWebView does.
|
||||
*/
|
||||
enableJsScroll(contentTop: number, contentBottom: number) {
|
||||
const self = this;
|
||||
self._js = true;
|
||||
const ele = self._el;
|
||||
|
||||
if (!ele) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug(`ScrollView, enableJsScroll`);
|
||||
|
||||
const positions: number[] = [];
|
||||
let rafCancel: Function;
|
||||
let max: number;
|
||||
|
||||
const ev = self.ev;
|
||||
ev.scrollLeft = 0;
|
||||
ev.startX = 0;
|
||||
ev.deltaX = 0;
|
||||
ev.velocityX = 0;
|
||||
ev.directionX = null;
|
||||
|
||||
function setMax() {
|
||||
if (!max) {
|
||||
// ******** DOM READ ****************
|
||||
max = (ele.scrollHeight - ele.offsetHeight) + contentTop + contentBottom;
|
||||
}
|
||||
};
|
||||
|
||||
function jsScrollDecelerate(timeStamp: number) {
|
||||
ev.timeStamp = timeStamp;
|
||||
|
||||
console.debug(`scroll-view, decelerate, velocity: ${ev.velocityY}`);
|
||||
|
||||
if (ev.velocityY) {
|
||||
ev.velocityY *= DECELERATION_FRICTION;
|
||||
|
||||
// update top with updated velocity
|
||||
// clamp top within scroll limits
|
||||
setMax();
|
||||
self._t = Math.min(Math.max(self._t + ev.velocityY, 0), max);
|
||||
|
||||
ev.scrollTop = self._t;
|
||||
|
||||
// emit on each scroll event
|
||||
self.scroll.next(ev);
|
||||
|
||||
self._dom.write(() => {
|
||||
// ******** DOM WRITE ****************
|
||||
self.setTop(self._t);
|
||||
|
||||
if (self._t > 0 && self._t < max && Math.abs(ev.velocityY) > MIN_VELOCITY_CONTINUE_DECELERATION) {
|
||||
rafCancel = self._dom.read(rafTimeStamp => {
|
||||
jsScrollDecelerate(rafTimeStamp);
|
||||
});
|
||||
|
||||
} else {
|
||||
// haven't scrolled in a while, so it's a scrollend
|
||||
self.isScrolling = false;
|
||||
|
||||
// reset velocity, do not reset the directions or deltas
|
||||
ev.velocityY = ev.velocityX = 0;
|
||||
|
||||
// emit that the scroll has ended
|
||||
self.scrollEnd.next(ev);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function jsScrollTouchStart(touchEvent: TouchEvent) {
|
||||
positions.length = 0;
|
||||
max = null;
|
||||
positions.push(pointerCoord(touchEvent).y, touchEvent.timeStamp);
|
||||
}
|
||||
|
||||
function jsScrollTouchMove(touchEvent: TouchEvent) {
|
||||
if (!positions.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
ev.timeStamp = touchEvent.timeStamp;
|
||||
|
||||
var y = pointerCoord(touchEvent).y;
|
||||
|
||||
// ******** DOM READ ****************
|
||||
setMax();
|
||||
self._t -= (y - positions[positions.length - 2]);
|
||||
self._t = Math.min(Math.max(self._t, 0), max);
|
||||
|
||||
positions.push(y, ev.timeStamp);
|
||||
|
||||
if (!self.isScrolling) {
|
||||
// remember the start position
|
||||
ev.startY = self._t;
|
||||
|
||||
// new scroll, so do some resets
|
||||
ev.velocityY = ev.deltaY = 0;
|
||||
|
||||
self.isScrolling = true;
|
||||
|
||||
// emit only on the first scroll event
|
||||
self.scrollStart.next(ev);
|
||||
}
|
||||
|
||||
self._dom.write(() => {
|
||||
// ******** DOM WRITE ****************
|
||||
self.setTop(self._t);
|
||||
});
|
||||
}
|
||||
|
||||
function jsScrollTouchEnd(touchEvent: TouchEvent) {
|
||||
// figure out what the scroll position was about 100ms ago
|
||||
self._dom.cancel(rafCancel);
|
||||
|
||||
if (!positions.length && self.isScrolling) {
|
||||
self.isScrolling = false;
|
||||
ev.velocityY = ev.velocityX = 0;
|
||||
self.scrollEnd.next(ev);
|
||||
return;
|
||||
}
|
||||
|
||||
var y = pointerCoord(touchEvent).y;
|
||||
|
||||
positions.push(y, touchEvent.timeStamp);
|
||||
|
||||
var endPos = (positions.length - 1);
|
||||
var startPos = endPos;
|
||||
var timeRange = (touchEvent.timeStamp - 100);
|
||||
|
||||
// move pointer to position measured 100ms ago
|
||||
for (var i = endPos; i > 0 && positions[i] > timeRange; i -= 2) {
|
||||
startPos = i;
|
||||
}
|
||||
|
||||
if (startPos !== endPos) {
|
||||
// compute relative movement between these two points
|
||||
var timeOffset = (positions[endPos] - positions[startPos]);
|
||||
var movedTop = (positions[startPos - 1] - positions[endPos - 1]);
|
||||
|
||||
// based on XXms compute the movement to apply for each render step
|
||||
ev.velocityY = ((movedTop / timeOffset) * FRAME_MS);
|
||||
|
||||
// verify that we have enough velocity to start deceleration
|
||||
if (Math.abs(ev.velocityY) > MIN_VELOCITY_START_DECELERATION) {
|
||||
// ******** DOM READ ****************
|
||||
setMax();
|
||||
|
||||
rafCancel = self._dom.read((rafTimeStamp: number) => {
|
||||
jsScrollDecelerate(rafTimeStamp);
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
self.isScrolling = false;
|
||||
ev.velocityY = ev.velocityX = 0;
|
||||
self.scrollEnd.next(ev);
|
||||
}
|
||||
|
||||
positions.length = 0;
|
||||
}
|
||||
|
||||
const opts = eventOptions(false, true);
|
||||
const unRegStart = listenEvent(ele, 'touchstart', false, opts, jsScrollTouchStart);
|
||||
const unRegMove = listenEvent(ele, 'touchmove', false, opts, jsScrollTouchMove);
|
||||
const unRegEnd = listenEvent(ele, 'touchend', false, opts, jsScrollTouchEnd);
|
||||
|
||||
ele.parentElement.classList.add('js-scroll');
|
||||
|
||||
// stop listening for actual scroll events
|
||||
self._lsn && self._lsn();
|
||||
|
||||
// create an unregister for all of these events
|
||||
self._lsn = () => {
|
||||
unRegStart();
|
||||
unRegMove();
|
||||
unRegEnd();
|
||||
ele.parentElement.classList.remove('js-scroll');
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* DOM READ
|
||||
*/
|
||||
getTop() {
|
||||
if (this._js) {
|
||||
return this._t;
|
||||
}
|
||||
return this._t = this._el.scrollTop || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* DOM READ
|
||||
*/
|
||||
getLeft() {
|
||||
if (this._js) {
|
||||
return 0;
|
||||
}
|
||||
return this._l = this._el.scrollLeft || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* DOM WRITE
|
||||
*/
|
||||
setTop(top: number) {
|
||||
this._top = top;
|
||||
this._t = top;
|
||||
|
||||
if (this._js) {
|
||||
(<any>this._el.style)[CSS.transform] = `translate3d(0px,${top * -1}px,0px)`;
|
||||
(<any>this._el.style)[CSS.transform] = `translate3d(${this._l * -1}px,${top * -1}px,0px)`;
|
||||
|
||||
} else {
|
||||
this._el.scrollTop = top;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DOM WRITE
|
||||
*/
|
||||
setLeft(left: number) {
|
||||
this._l = left;
|
||||
|
||||
if (this._js) {
|
||||
(<any>this._el.style)[CSS.transform] = `translate3d(${left * -1}px,${this._t * -1}px,0px)`;
|
||||
|
||||
} else {
|
||||
this._el.scrollLeft = left;
|
||||
}
|
||||
}
|
||||
|
||||
scrollTo(x: number, y: number, duration: number, done?: Function): Promise<any> {
|
||||
// scroll animation loop w/ easing
|
||||
// credit https://gist.github.com/dezinezync/5487119
|
||||
@ -68,17 +416,17 @@ export class ScrollView {
|
||||
let attempts = 0;
|
||||
|
||||
// scroll loop
|
||||
function step() {
|
||||
function step(timeStamp: number) {
|
||||
attempts++;
|
||||
|
||||
if (!self._el || !self.isPlaying || attempts > maxAttempts) {
|
||||
self.isPlaying = false;
|
||||
if (!self._el || !self.isScrolling || attempts > maxAttempts) {
|
||||
self.isScrolling = false;
|
||||
self._el.style.transform = ``;
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
let time = Math.min(1, ((Date.now() - startTime) / duration));
|
||||
let time = Math.min(1, ((timeStamp - startTime) / duration));
|
||||
|
||||
// where .5 would be 50% of time on a linear scale easedT gives a
|
||||
// fraction based on the easing method
|
||||
@ -89,25 +437,28 @@ export class ScrollView {
|
||||
}
|
||||
|
||||
if (fromX !== x) {
|
||||
self._el.scrollLeft = Math.floor((easedT * (x - fromX)) + fromX);
|
||||
self.setLeft(Math.floor((easedT * (x - fromX)) + fromX));
|
||||
}
|
||||
|
||||
if (easedT < 1) {
|
||||
// do not use DomController here
|
||||
// must use nativeRaf in order to fire in the next frame
|
||||
nativeRaf(step);
|
||||
|
||||
} else {
|
||||
self.isScrolling = false;
|
||||
self._el.style.transform = ``;
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
// start scroll loop
|
||||
self.isPlaying = true;
|
||||
self.isScrolling = true;
|
||||
|
||||
// chill out for a frame first
|
||||
rafFrames(2, () => {
|
||||
startTime = Date.now();
|
||||
step();
|
||||
rafFrames(2, (timeStamp) => {
|
||||
startTime = timeStamp;
|
||||
step(timeStamp);
|
||||
});
|
||||
|
||||
return promise;
|
||||
@ -126,167 +477,51 @@ export class ScrollView {
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.isPlaying = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* JS Scrolling has been provided only as a temporary solution
|
||||
* until iOS apps can take advantage of scroll events at all times.
|
||||
* The goal is to eventually remove JS scrolling entirely. This
|
||||
* method may be removed in the future.
|
||||
*/
|
||||
jsScroll(onScrollCallback: Function): Function {
|
||||
this._js = true;
|
||||
this._cb = onScrollCallback;
|
||||
this._pos = [];
|
||||
|
||||
if (this._el) {
|
||||
this._el.addEventListener('touchstart', this._start.bind(this));
|
||||
this._el.addEventListener('touchmove', this._move.bind(this));
|
||||
this._el.addEventListener('touchend', this._end.bind(this));
|
||||
this._el.parentElement.classList.add('js-scroll');
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (this._el) {
|
||||
this._el.removeEventListener('touchstart', this._start.bind(this));
|
||||
this._el.removeEventListener('touchmove', this._move.bind(this));
|
||||
this._el.removeEventListener('touchend', this._end.bind(this));
|
||||
this._el.parentElement.classList.remove('js-scroll');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Used for JS scrolling. May be removed in the future.
|
||||
*/
|
||||
private _start(ev: UIEvent) {
|
||||
this._velocity = 0;
|
||||
this._pos.length = 0;
|
||||
this._max = null;
|
||||
this._pos.push(pointerCoord(ev).y, Date.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Used for JS scrolling. May be removed in the future.
|
||||
*/
|
||||
private _move(ev: UIEvent) {
|
||||
if (this._pos.length) {
|
||||
let y = pointerCoord(ev).y;
|
||||
|
||||
// ******** DOM READ ****************
|
||||
this._setMax();
|
||||
|
||||
this._top -= (y - this._pos[this._pos.length - 2]);
|
||||
|
||||
this._top = Math.min(Math.max(this._top, 0), this._max);
|
||||
|
||||
this._pos.push(y, Date.now());
|
||||
|
||||
// ******** DOM READ THEN DOM WRITE ****************
|
||||
this._cb(this._top);
|
||||
|
||||
// ******** DOM WRITE ****************
|
||||
this.setTop(this._top);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Used for JS scrolling. May be removed in the future.
|
||||
*/
|
||||
private _setMax() {
|
||||
if (!this._max) {
|
||||
// ******** DOM READ ****************
|
||||
this._max = (this._el.offsetHeight - this._el.parentElement.offsetHeight + this._el.parentElement.offsetTop);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Used for JS scrolling. May be removed in the future.
|
||||
*/
|
||||
private _end(ev: UIEvent) {
|
||||
// figure out what the scroll position was about 100ms ago
|
||||
let positions = this._pos;
|
||||
this._velocity = 0;
|
||||
cancelRaf(this._rafId);
|
||||
|
||||
if (!positions.length) return;
|
||||
|
||||
let y = pointerCoord(ev).y;
|
||||
|
||||
positions.push(y, Date.now());
|
||||
|
||||
let endPos = (positions.length - 1);
|
||||
let startPos = endPos;
|
||||
let timeRange = (Date.now() - 100);
|
||||
|
||||
// move pointer to position measured 100ms ago
|
||||
for (var i = endPos; i > 0 && positions[i] > timeRange; i -= 2) {
|
||||
startPos = i;
|
||||
}
|
||||
|
||||
if (startPos !== endPos) {
|
||||
// compute relative movement between these two points
|
||||
let timeOffset = (positions[endPos] - positions[startPos]);
|
||||
let movedTop = (positions[startPos - 1] - positions[endPos - 1]);
|
||||
|
||||
// based on XXms compute the movement to apply for each render step
|
||||
this._velocity = ((movedTop / timeOffset) * FRAME_MS);
|
||||
|
||||
// verify that we have enough velocity to start deceleration
|
||||
if (Math.abs(this._velocity) > MIN_VELOCITY_START_DECELERATION) {
|
||||
// ******** DOM READ ****************
|
||||
this._setMax();
|
||||
|
||||
this._rafId = nativeRaf(this._decelerate.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
positions.length = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Used for JS scrolling. May be removed in the future.
|
||||
*/
|
||||
private _decelerate() {
|
||||
var self = this;
|
||||
|
||||
if (self._velocity) {
|
||||
self._velocity *= DECELERATION_FRICTION;
|
||||
|
||||
// update top with updated velocity
|
||||
// clamp top within scroll limits
|
||||
self._top = Math.min(Math.max(self._top + self._velocity, 0), self._max);
|
||||
|
||||
// ******** DOM READ THEN DOM WRITE ****************
|
||||
self._cb(self._top);
|
||||
|
||||
// ******** DOM WRITE ****************
|
||||
self.setTop(self._top);
|
||||
|
||||
if (self._top > 0 && self._top < self._max && Math.abs(self._velocity) > MIN_VELOCITY_CONTINUE_DECELERATION) {
|
||||
self._rafId = nativeRaf(self._decelerate.bind(self));
|
||||
}
|
||||
}
|
||||
this.isScrolling = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
destroy() {
|
||||
this._velocity = 0;
|
||||
this.scrollStart.unsubscribe();
|
||||
this.scroll.unsubscribe();
|
||||
this.scrollEnd.unsubscribe();
|
||||
|
||||
this.stop();
|
||||
this._el = null;
|
||||
this._lsn();
|
||||
this._endTmr && this._endTmr();
|
||||
this._el = this._dom = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export interface ScrollEvent {
|
||||
scrollTop?: number;
|
||||
scrollLeft?: number;
|
||||
startY?: number;
|
||||
startX?: number;
|
||||
deltaY?: number;
|
||||
deltaX?: number;
|
||||
timeStamp?: number;
|
||||
velocityY?: number;
|
||||
velocityX?: number;
|
||||
directionY?: ScrollDirection;
|
||||
directionX?: ScrollDirection;
|
||||
}
|
||||
|
||||
|
||||
export enum ScrollDirection {
|
||||
Up, Down, Left, Right
|
||||
}
|
||||
|
||||
|
||||
export interface DomFn {
|
||||
(callback: Function): void;
|
||||
}
|
||||
|
||||
|
||||
const MIN_VELOCITY_START_DECELERATION = 4;
|
||||
const MIN_VELOCITY_CONTINUE_DECELERATION = 0.12;
|
||||
const DECELERATION_FRICTION = 0.97;
|
||||
|
@ -191,14 +191,14 @@ export class UIEventManager {
|
||||
return;
|
||||
}
|
||||
let zone = config.zone || this.zoneWrapped;
|
||||
let opts;
|
||||
let opts: any;
|
||||
if (supportsOptions) {
|
||||
opts = {};
|
||||
if (config.passive === true) {
|
||||
opts['passive'] = true;
|
||||
opts.passive = true;
|
||||
}
|
||||
if (config.capture === true) {
|
||||
opts['capture'] = true;
|
||||
opts.capture = true;
|
||||
}
|
||||
} else {
|
||||
if (config.passive === true) {
|
||||
@ -244,13 +244,13 @@ export class UIEventManager {
|
||||
}
|
||||
|
||||
export function listenEvent(ele: any, eventName: string, zoneWrapped: boolean, option: any, callback: any): Function {
|
||||
let rawEvent = (!zoneWrapped && '__zone_symbol__addEventListener' in ele);
|
||||
const rawEvent = (!zoneWrapped && !!ele.__zone_symbol__addEventListener);
|
||||
if (rawEvent) {
|
||||
ele.__zone_symbol__addEventListener(eventName, callback, option);
|
||||
assert('__zone_symbol__removeEventListener' in ele, 'native removeEventListener does not exist');
|
||||
assert(!!ele.__zone_symbol__removeEventListener, 'native removeEventListener does not exist');
|
||||
return () => ele.__zone_symbol__removeEventListener(eventName, callback, option);
|
||||
} else {
|
||||
ele.addEventListener(eventName, callback, option);
|
||||
return () => ele.removeEventListener(eventName, callback, option);
|
||||
}
|
||||
|
||||
ele.addEventListener(eventName, callback, option);
|
||||
return () => ele.removeEventListener(eventName, callback, option);
|
||||
}
|
||||
|
Reference in New Issue
Block a user