mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-22 21:48:42 +08:00
perf(scroll): watchdog + simplification
This commit is contained in:
17
packages/core/src/components.d.ts
vendored
17
packages/core/src/components.d.ts
vendored
@ -42,20 +42,16 @@ import {
|
|||||||
GestureCallback,
|
GestureCallback,
|
||||||
GestureDetail,
|
GestureDetail,
|
||||||
} from './components/gesture/gesture';
|
} from './components/gesture/gesture';
|
||||||
import {
|
|
||||||
App,
|
|
||||||
FrameworkDelegate as FrameworkDelegate2,
|
|
||||||
} from '.';
|
|
||||||
import {
|
import {
|
||||||
PickerButton,
|
PickerButton,
|
||||||
PickerColumn as PickerColumn2,
|
PickerColumn as PickerColumn2,
|
||||||
} from './components/picker/picker';
|
} from './components/picker/picker';
|
||||||
import {
|
|
||||||
ScrollCallback,
|
|
||||||
} from './components/scroll/scroll';
|
|
||||||
import {
|
import {
|
||||||
SelectPopoverOption,
|
SelectPopoverOption,
|
||||||
} from './components/select-popover/select-popover';
|
} from './components/select-popover/select-popover';
|
||||||
|
import {
|
||||||
|
FrameworkDelegate as FrameworkDelegate2,
|
||||||
|
} from '.';
|
||||||
import {
|
import {
|
||||||
DomRenderFn,
|
DomRenderFn,
|
||||||
HeaderFn,
|
HeaderFn,
|
||||||
@ -770,6 +766,7 @@ declare global {
|
|||||||
export interface IonContentAttributes extends HTMLAttributes {
|
export interface IonContentAttributes extends HTMLAttributes {
|
||||||
forceOverscroll?: boolean;
|
forceOverscroll?: boolean;
|
||||||
fullscreen?: boolean;
|
fullscreen?: boolean;
|
||||||
|
scrollEvents?: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1240,7 +1237,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
namespace JSXElements {
|
namespace JSXElements {
|
||||||
export interface IonInputShimsAttributes extends HTMLAttributes {
|
export interface IonInputShimsAttributes extends HTMLAttributes {
|
||||||
app?: App;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2706,9 +2703,7 @@ declare global {
|
|||||||
export interface IonScrollAttributes extends HTMLAttributes {
|
export interface IonScrollAttributes extends HTMLAttributes {
|
||||||
forceOverscroll?: boolean;
|
forceOverscroll?: boolean;
|
||||||
mode?: string;
|
mode?: string;
|
||||||
onionScroll?: ScrollCallback;
|
scrollEvents?: boolean;
|
||||||
onionScrollEnd?: ScrollCallback;
|
|
||||||
onionScrollStart?: ScrollCallback;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import { Config, NavEvent, OverlayController, PublicNav, PublicViewController }
|
|||||||
import { getOrAppendElement } from '../../utils/helpers';
|
import { getOrAppendElement } from '../../utils/helpers';
|
||||||
|
|
||||||
const rootNavs = new Map<number, HTMLIonNavElement>();
|
const rootNavs = new Map<number, HTMLIonNavElement>();
|
||||||
const ACTIVE_SCROLLING_TIME = 100;
|
|
||||||
let backButtonActions: BackButtonAction[] = [];
|
let backButtonActions: BackButtonAction[] = [];
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -21,11 +20,9 @@ export class App {
|
|||||||
|
|
||||||
private isDevice = false;
|
private isDevice = false;
|
||||||
private deviceHacks = false;
|
private deviceHacks = false;
|
||||||
private scrollTime = 0;
|
|
||||||
|
|
||||||
externalNavPromise: void | Promise<any> = null;
|
externalNavPromise: void | Promise<any> = null;
|
||||||
externalNavOccuring = false;
|
externalNavOccuring = false;
|
||||||
didScroll = false;
|
|
||||||
|
|
||||||
@Element() element: HTMLElement;
|
@Element() element: HTMLElement;
|
||||||
@Event() exitApp: EventEmitter<ExitAppEventDetail>;
|
@Event() exitApp: EventEmitter<ExitAppEventDetail>;
|
||||||
@ -102,29 +99,6 @@ export class App {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Boolean if the app is actively scrolling or not.
|
|
||||||
* @return {boolean} returns true or false
|
|
||||||
*/
|
|
||||||
@Method()
|
|
||||||
isScrolling(): boolean {
|
|
||||||
const scrollTime = this.scrollTime;
|
|
||||||
if (scrollTime === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (scrollTime < Date.now()) {
|
|
||||||
this.scrollTime = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Method()
|
|
||||||
setScrolling() {
|
|
||||||
this.scrollTime = Date.now() + ACTIVE_SCROLLING_TIME;
|
|
||||||
this.didScroll = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Method()
|
@Method()
|
||||||
getTopNavs(rootNavId = -1): PublicNav[] {
|
getTopNavs(rootNavId = -1): PublicNav[] {
|
||||||
return getTopNavsImpl(rootNavId);
|
return getTopNavsImpl(rootNavId);
|
||||||
@ -142,7 +116,6 @@ export class App {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The back button event is triggered when the user presses the native
|
* The back button event is triggered when the user presses the native
|
||||||
* platform's back button, also referred to as the "hardware" back button.
|
* platform's back button, also referred to as the "hardware" back button.
|
||||||
@ -244,7 +217,7 @@ export class App {
|
|||||||
render() {
|
render() {
|
||||||
return [
|
return [
|
||||||
<ion-platform />,
|
<ion-platform />,
|
||||||
this.deviceHacks && <ion-input-shims app={this} />,
|
this.deviceHacks && <ion-input-shims />,
|
||||||
this.isDevice && <ion-tap-click />,
|
this.isDevice && <ion-tap-click />,
|
||||||
this.isDevice && <ion-status-tap />,
|
this.isDevice && <ion-status-tap />,
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
@ -42,11 +42,6 @@ Returns an array of top level Navs
|
|||||||
Returns whether the application is enabled or not
|
Returns whether the application is enabled or not
|
||||||
|
|
||||||
|
|
||||||
#### isScrolling()
|
|
||||||
|
|
||||||
Boolean if the app is actively scrolling or not.
|
|
||||||
|
|
||||||
|
|
||||||
#### registerBackButtonAction()
|
#### registerBackButtonAction()
|
||||||
|
|
||||||
The back button event is triggered when the user presses the native
|
The back button event is triggered when the user presses the native
|
||||||
@ -69,9 +64,6 @@ This API is not meant for public usage and could
|
|||||||
change at any time
|
change at any time
|
||||||
|
|
||||||
|
|
||||||
#### setScrolling()
|
|
||||||
|
|
||||||
|
|
||||||
#### updateExternalNavOccuring()
|
#### updateExternalNavOccuring()
|
||||||
|
|
||||||
Updates whether an external navigation event is occuring
|
Updates whether an external navigation event is occuring
|
||||||
|
@ -35,6 +35,9 @@ export class Content {
|
|||||||
*/
|
*/
|
||||||
@Prop() forceOverscroll: boolean;
|
@Prop() forceOverscroll: boolean;
|
||||||
|
|
||||||
|
|
||||||
|
@Prop() scrollEvents = false;
|
||||||
|
|
||||||
@Listen('body:ionNavChanged')
|
@Listen('body:ionNavChanged')
|
||||||
onNavChanged() {
|
onNavChanged() {
|
||||||
this.resize();
|
this.resize();
|
||||||
@ -128,7 +131,10 @@ export class Content {
|
|||||||
this.resize();
|
this.resize();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
<ion-scroll mode={this.mode} forceOverscroll={this.forceOverscroll}>
|
<ion-scroll
|
||||||
|
mode={this.mode}
|
||||||
|
scrollEvents={this.scrollEvents}
|
||||||
|
forceOverscroll={this.forceOverscroll}>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</ion-scroll>,
|
</ion-scroll>,
|
||||||
<slot name='fixed'></slot>
|
<slot name='fixed'></slot>
|
||||||
|
@ -34,6 +34,11 @@ and footers. This effect can easily be seen by setting the toolbar
|
|||||||
to transparent.
|
to transparent.
|
||||||
|
|
||||||
|
|
||||||
|
#### scrollEvents
|
||||||
|
|
||||||
|
boolean
|
||||||
|
|
||||||
|
|
||||||
## Attributes
|
## Attributes
|
||||||
|
|
||||||
#### force-overscroll
|
#### force-overscroll
|
||||||
@ -54,6 +59,11 @@ and footers. This effect can easily be seen by setting the toolbar
|
|||||||
to transparent.
|
to transparent.
|
||||||
|
|
||||||
|
|
||||||
|
#### scroll-events
|
||||||
|
|
||||||
|
boolean
|
||||||
|
|
||||||
|
|
||||||
## Methods
|
## Methods
|
||||||
|
|
||||||
#### scrollByPoint()
|
#### scrollByPoint()
|
||||||
|
@ -69,6 +69,13 @@
|
|||||||
</ion-footer>
|
</ion-footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const content = document.getElementById('content');
|
||||||
|
content.scrollEvents = true;
|
||||||
|
content.addEventListener('ionScrollStart', () => console.log('scroll start'));
|
||||||
|
content.addEventListener('ionScroll', (ev) => console.log('scroll', ev.detail));
|
||||||
|
content.addEventListener('ionScrollEnd', () => console.log('scroll end'));
|
||||||
|
|
||||||
|
|
||||||
function toggleFullscreen() {
|
function toggleFullscreen() {
|
||||||
const content = document.getElementById('content');
|
const content = document.getElementById('content');
|
||||||
content.fullscreen = !content.fullscreen;
|
content.fullscreen = !content.fullscreen;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Component, Element, Event, EventEmitter, EventListenerEnable, Listen, Method, Prop, State, Watch } from '@stencil/core';
|
import { Component, Element, Event, EventEmitter, EventListenerEnable, Listen, Method, Prop, State, Watch } from '@stencil/core';
|
||||||
import { DomController, ScrollDetail } from '../../index';
|
import { DomController } from '../../index';
|
||||||
|
|
||||||
const enum Position {
|
const enum Position {
|
||||||
Top = 'top',
|
Top = 'top',
|
||||||
@ -106,10 +106,9 @@ export class InfiniteScroll {
|
|||||||
this.scrollEl = null;
|
this.scrollEl = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Listen('ionScroll', {enabled: false})
|
@Listen('scroll', {enabled: false})
|
||||||
protected onScroll(ev: CustomEvent) {
|
protected onScroll() {
|
||||||
const scrollEl = this.scrollEl;
|
const scrollEl = this.scrollEl;
|
||||||
const detail = ev.detail as ScrollDetail;
|
|
||||||
if (!scrollEl || !this.canStart()) {
|
if (!scrollEl || !this.canStart()) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@ -119,7 +118,7 @@ export class InfiniteScroll {
|
|||||||
// if there is no height of this element then do nothing
|
// if there is no height of this element then do nothing
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
const scrollTop = detail.scrollTop;
|
const scrollTop = scrollEl.scrollTop;
|
||||||
const scrollHeight = scrollEl.scrollHeight;
|
const scrollHeight = scrollEl.scrollHeight;
|
||||||
const height = scrollEl.offsetHeight;
|
const height = scrollEl.offsetHeight;
|
||||||
const threshold = this.thrPc ? (height * this.thrPc) : this.thrPx;
|
const threshold = this.thrPc ? (height * this.thrPc) : this.thrPx;
|
||||||
@ -128,7 +127,7 @@ export class InfiniteScroll {
|
|||||||
? scrollHeight - infiniteHeight - scrollTop - threshold - height
|
? scrollHeight - infiniteHeight - scrollTop - threshold - height
|
||||||
: scrollTop - infiniteHeight - threshold;
|
: scrollTop - infiniteHeight - threshold;
|
||||||
|
|
||||||
if (distanceFromInfinite < 0) {
|
if (distanceFromInfinite < 0) {
|
||||||
if (!this.didFire) {
|
if (!this.didFire) {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.didFire = true;
|
this.didFire = true;
|
||||||
@ -221,7 +220,7 @@ export class InfiniteScroll {
|
|||||||
|
|
||||||
private enableScrollEvents(shouldListen: boolean) {
|
private enableScrollEvents(shouldListen: boolean) {
|
||||||
if (this.scrollEl) {
|
if (this.scrollEl) {
|
||||||
this.enableListener(this, 'ionScroll', shouldListen, this.scrollEl);
|
this.enableListener(this, 'scroll', shouldListen, this.scrollEl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
import { App } from '../../..';
|
|
||||||
|
|
||||||
const SKIP_BLURRING = ['INPUT', 'TEXTAREA', 'ION-INPUT', 'ION-TEXTAREA'];
|
const SKIP_BLURRING = ['INPUT', 'TEXTAREA', 'ION-INPUT', 'ION-TEXTAREA'];
|
||||||
|
|
||||||
export default function enableInputBlurring(app: App) {
|
export default function enableInputBlurring() {
|
||||||
console.debug('Input: enableInputBlurring');
|
console.debug('Input: enableInputBlurring');
|
||||||
|
|
||||||
let focused = true;
|
let focused = true;
|
||||||
|
let didScroll = false;
|
||||||
|
|
||||||
|
function onScroll() {
|
||||||
|
didScroll = true;
|
||||||
|
}
|
||||||
|
|
||||||
function onFocusin() {
|
function onFocusin() {
|
||||||
focused = true;
|
focused = true;
|
||||||
@ -13,8 +17,8 @@ export default function enableInputBlurring(app: App) {
|
|||||||
|
|
||||||
function onTouchend(ev: any) {
|
function onTouchend(ev: any) {
|
||||||
// if app did scroll return early
|
// if app did scroll return early
|
||||||
if (app.didScroll) {
|
if (didScroll) {
|
||||||
app.didScroll = false;
|
didScroll = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const active = document.activeElement as HTMLElement;
|
const active = document.activeElement as HTMLElement;
|
||||||
@ -49,10 +53,12 @@ export default function enableInputBlurring(app: App) {
|
|||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.addEventListener('ionScrollStart', onScroll);
|
||||||
document.addEventListener('focusin', onFocusin, true);
|
document.addEventListener('focusin', onFocusin, true);
|
||||||
document.addEventListener('touchend', onTouchend, false);
|
document.addEventListener('touchend', onTouchend, false);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
document.removeEventListener('ionScrollStart', onScroll, true);
|
||||||
document.removeEventListener('focusin', onFocusin, true);
|
document.removeEventListener('focusin', onFocusin, true);
|
||||||
document.removeEventListener('touchend', onTouchend, false);
|
document.removeEventListener('touchend', onTouchend, false);
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Component, Listen, Prop } from '@stencil/core';
|
import { Component, Listen, Prop } from '@stencil/core';
|
||||||
import { App, Config } from '../..';
|
import { Config } from '../..';
|
||||||
|
|
||||||
import enableHideCaretOnScroll from './hacks/hide-caret';
|
import enableHideCaretOnScroll from './hacks/hide-caret';
|
||||||
import enableInputBlurring from './hacks/input-blurring';
|
import enableInputBlurring from './hacks/input-blurring';
|
||||||
@ -24,7 +24,6 @@ export class InputShims {
|
|||||||
private scrollAssistMap = new WeakMap<HTMLElement, Function>();
|
private scrollAssistMap = new WeakMap<HTMLElement, Function>();
|
||||||
|
|
||||||
@Prop({context: 'config'}) config: Config;
|
@Prop({context: 'config'}) config: Config;
|
||||||
@Prop() app: App;
|
|
||||||
|
|
||||||
componentDidLoad() {
|
componentDidLoad() {
|
||||||
this.keyboardHeight = this.config.getNumber('keyboardHeight', 290);
|
this.keyboardHeight = this.config.getNumber('keyboardHeight', 290);
|
||||||
@ -33,7 +32,7 @@ export class InputShims {
|
|||||||
|
|
||||||
const inputBlurring = this.config.getBoolean('inputBlurring', true);
|
const inputBlurring = this.config.getBoolean('inputBlurring', true);
|
||||||
if (inputBlurring && INPUT_BLURRING) {
|
if (inputBlurring && INPUT_BLURRING) {
|
||||||
enableInputBlurring(this.app);
|
enableInputBlurring();
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollPadding = this.config.getBoolean('scrollPadding', true);
|
const scrollPadding = this.config.getBoolean('scrollPadding', true);
|
||||||
|
@ -5,20 +5,6 @@
|
|||||||
<!-- Auto Generated Below -->
|
<!-- Auto Generated Below -->
|
||||||
|
|
||||||
|
|
||||||
## Properties
|
|
||||||
|
|
||||||
#### app
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Attributes
|
|
||||||
|
|
||||||
#### app
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
----------------------------------------------
|
----------------------------------------------
|
||||||
|
|
||||||
|
@ -21,19 +21,9 @@ Note, the does not disable the system bounce on iOS. That is an OS level setting
|
|||||||
string
|
string
|
||||||
|
|
||||||
|
|
||||||
#### onionScroll
|
#### scrollEvents
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### onionScrollEnd
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### onionScrollStart
|
|
||||||
|
|
||||||
|
|
||||||
|
boolean
|
||||||
|
|
||||||
|
|
||||||
## Attributes
|
## Attributes
|
||||||
@ -52,26 +42,17 @@ Note, the does not disable the system bounce on iOS. That is an OS level setting
|
|||||||
string
|
string
|
||||||
|
|
||||||
|
|
||||||
#### onion-scroll
|
#### scroll-events
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### onion-scroll-end
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### onion-scroll-start
|
|
||||||
|
|
||||||
|
|
||||||
|
boolean
|
||||||
|
|
||||||
|
|
||||||
## Events
|
## Events
|
||||||
|
|
||||||
#### ionScroll
|
#### ionScroll
|
||||||
|
|
||||||
Emitted while scrolling.
|
Emitted while scrolling. This event is disabled by default.
|
||||||
|
Look at the property: `scrollEvents`
|
||||||
|
|
||||||
|
|
||||||
#### ionScrollEnd
|
#### ionScrollEnd
|
||||||
|
@ -14,9 +14,6 @@ ion-scroll {
|
|||||||
|
|
||||||
contain: size style layout;
|
contain: size style layout;
|
||||||
|
|
||||||
margin: 0 !important; // scss-lint:disable all
|
|
||||||
padding: 0 !important; // scss-lint:disable all
|
|
||||||
|
|
||||||
.focus-input {
|
.focus-input {
|
||||||
@include margin(0);
|
@include margin(0);
|
||||||
@include padding(0);
|
@include padding(0);
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Component, Element, Event, EventEmitter, EventListenerEnable, Listen, Method, Prop } from '@stencil/core';
|
import { Component, Element, Event, EventEmitter, Listen, Method, Prop } from '@stencil/core';
|
||||||
import { Config, DomController, GestureDelegate, GestureDetail } from '../../index';
|
import { Config, DomController, GestureDetail } from '../../index';
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
tag: 'ion-scroll',
|
tag: 'ion-scroll',
|
||||||
@ -14,19 +13,14 @@ import { Config, DomController, GestureDelegate, GestureDetail } from '../../ind
|
|||||||
})
|
})
|
||||||
export class Scroll {
|
export class Scroll {
|
||||||
|
|
||||||
private gesture: GestureDelegate;
|
private watchDog: any;
|
||||||
private positions: number[] = [];
|
|
||||||
private tmr: any;
|
|
||||||
private queued = false;
|
|
||||||
private app: HTMLIonAppElement;
|
|
||||||
private isScrolling = false;
|
private isScrolling = false;
|
||||||
private detail: ScrollDetail = {};
|
private lastScroll = 0;
|
||||||
|
private detail: ScrollDetail;
|
||||||
|
private queued = false;
|
||||||
@Element() private el: HTMLElement;
|
@Element() private el: HTMLElement;
|
||||||
|
|
||||||
@Prop({ connect: 'ion-gesture-controller'}) gestureCtrl: HTMLIonGestureControllerElement;
|
|
||||||
@Prop({ context: 'config'}) config: Config;
|
@Prop({ context: 'config'}) config: Config;
|
||||||
@Prop({ context: 'enableListener'}) enableListener: EventListenerEnable;
|
|
||||||
@Prop({ context: 'dom' }) dom: DomController;
|
@Prop({ context: 'dom' }) dom: DomController;
|
||||||
@Prop({ context: 'isServer' }) isServer: boolean;
|
@Prop({ context: 'isServer' }) isServer: boolean;
|
||||||
|
|
||||||
@ -40,69 +34,76 @@ export class Scroll {
|
|||||||
*/
|
*/
|
||||||
@Prop({mutable: true}) forceOverscroll: boolean;
|
@Prop({mutable: true}) forceOverscroll: boolean;
|
||||||
|
|
||||||
@Prop() onionScrollStart: ScrollCallback;
|
@Prop() scrollEvents = false;
|
||||||
@Prop() onionScroll: ScrollCallback;
|
|
||||||
@Prop() onionScrollEnd: ScrollCallback;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted when the scroll has started.
|
* Emitted when the scroll has started.
|
||||||
*/
|
*/
|
||||||
@Event() ionScrollStart: EventEmitter;
|
@Event() ionScrollStart: EventEmitter<ScrollBaseDetail>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted while scrolling.
|
* Emitted while scrolling. This event is disabled by default.
|
||||||
|
* Look at the property: `scrollEvents`
|
||||||
*/
|
*/
|
||||||
@Event({bubbles: false}) ionScroll: EventEmitter;
|
@Event({bubbles: false}) ionScroll: EventEmitter<ScrollDetail>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted when the scroll has ended.
|
* Emitted when the scroll has ended.
|
||||||
*/
|
*/
|
||||||
@Event() ionScrollEnd: EventEmitter;
|
@Event() ionScrollEnd: EventEmitter<ScrollBaseDetail>;
|
||||||
|
|
||||||
componentWillLoad() {
|
constructor() {
|
||||||
return this.gestureCtrl.create({
|
// Detail is used in a hot loop in the scroll event, by allocating it here
|
||||||
name: 'scroll',
|
// V8 will be able to inline any read/write to it since it's a monomorphic class.
|
||||||
priority: 100,
|
// https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html
|
||||||
disableScroll: false,
|
this.detail = {
|
||||||
}).then((gesture) => {
|
positions: [],
|
||||||
this.gesture = gesture;
|
scrollTop: 0,
|
||||||
});
|
scrollLeft: 0,
|
||||||
|
type: 'scroll',
|
||||||
|
event: undefined,
|
||||||
|
startX: 0,
|
||||||
|
startY: 0,
|
||||||
|
startTimeStamp: 0,
|
||||||
|
currentX: 0,
|
||||||
|
currentY: 0,
|
||||||
|
velocityX: 0,
|
||||||
|
velocityY: 0,
|
||||||
|
deltaX: 0,
|
||||||
|
deltaY: 0,
|
||||||
|
timeStamp: 0,
|
||||||
|
data: undefined
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidLoad() {
|
componentWillLoad() {
|
||||||
if (this.isServer) {
|
if (this.isServer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.forceOverscroll === undefined) {
|
if (this.forceOverscroll === undefined) {
|
||||||
this.forceOverscroll = this.mode === 'ios' && ('ontouchstart' in window);
|
this.forceOverscroll = this.mode === 'ios' && ('ontouchstart' in window);
|
||||||
}
|
}
|
||||||
this.app = this.el.closest('ion-app');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUnload() {
|
|
||||||
this.gesture && this.gesture.destroy();
|
|
||||||
this.gesture = this.detail = this.detail.event = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Native Scroll *************************
|
|
||||||
|
|
||||||
@Listen('scroll', { passive: true })
|
@Listen('scroll', { passive: true })
|
||||||
onNativeScroll() {
|
onScroll(ev: UIEvent) {
|
||||||
if (!this.queued) {
|
const timeStamp = Date.now();
|
||||||
|
const didStart = !this.isScrolling;
|
||||||
|
this.lastScroll = timeStamp;
|
||||||
|
if (didStart) {
|
||||||
|
this.onScrollStart();
|
||||||
|
}
|
||||||
|
if (!this.queued && this.scrollEvents) {
|
||||||
this.queued = true;
|
this.queued = true;
|
||||||
|
|
||||||
this.dom.read(timeStamp => {
|
this.dom.read(timeStamp => {
|
||||||
this.queued = false;
|
this.queued = false;
|
||||||
this.onScroll(timeStamp);
|
this.detail.event = ev;
|
||||||
|
updateScrollDetail(this.detail, this.el, timeStamp, didStart);
|
||||||
|
this.ionScroll.emit(this.detail);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Listen('touchend', {passive: true, capture: true })
|
|
||||||
onTouchEnd(_ev: TouchEvent) {
|
|
||||||
this.refocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Method()
|
@Method()
|
||||||
scrollToTop(duration: number): Promise<void> {
|
scrollToTop(duration: number): Promise<void> {
|
||||||
return this.scrollToPoint(0, 0, duration);
|
return this.scrollToPoint(0, 0, duration);
|
||||||
@ -212,115 +213,29 @@ export class Scroll {
|
|||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
private onScroll(timeStamp: number) {
|
|
||||||
const detail = this.detail;
|
|
||||||
const positions = this.positions;
|
|
||||||
const el = this.el;
|
|
||||||
if (this.app) {
|
|
||||||
this.app.setScrolling();
|
|
||||||
}
|
|
||||||
|
|
||||||
detail.timeStamp = timeStamp;
|
private onScrollStart() {
|
||||||
|
this.isScrolling = true;
|
||||||
|
this.ionScrollStart.emit({
|
||||||
|
isScrolling: true
|
||||||
|
});
|
||||||
|
|
||||||
// get the current scrollTop
|
// watchdog
|
||||||
// ******** DOM READ ****************
|
this.watchDog = setInterval(() => {
|
||||||
detail.scrollTop = el.scrollTop;
|
if (this.lastScroll < Date.now() - 120) {
|
||||||
|
this.onScrollEnd();
|
||||||
// get the current scrollLeft
|
|
||||||
// ******** DOM READ ****************
|
|
||||||
detail.scrollLeft = el.scrollLeft;
|
|
||||||
|
|
||||||
|
|
||||||
if (!this.isScrolling) {
|
|
||||||
// currently not scrolling, so this is a scroll start
|
|
||||||
this.isScrolling = true;
|
|
||||||
|
|
||||||
// remember the start positions
|
|
||||||
detail.startY = detail.scrollTop;
|
|
||||||
detail.startX = detail.scrollLeft;
|
|
||||||
|
|
||||||
// new scroll, so do some resets
|
|
||||||
detail.velocityY = detail.velocityX = detail.deltaY = detail.deltaX = positions.length = 0;
|
|
||||||
|
|
||||||
// emit only on the first scroll event
|
|
||||||
if (this.onionScrollStart) {
|
|
||||||
this.onionScrollStart(detail);
|
|
||||||
}
|
}
|
||||||
this.gesture.capture();
|
}, 100);
|
||||||
this.ionScrollStart.emit(detail);
|
|
||||||
}
|
|
||||||
detail.deltaY = (detail.scrollTop - detail.startY);
|
|
||||||
detail.deltaX = (detail.scrollLeft - detail.startX);
|
|
||||||
|
|
||||||
// actively scrolling
|
|
||||||
positions.push(detail.scrollTop, detail.scrollLeft, detail.timeStamp);
|
|
||||||
|
|
||||||
// move pointer to position measured 100ms ago
|
|
||||||
const timeRange = timeStamp - 100;
|
|
||||||
let startPos = positions.length - 1;
|
|
||||||
|
|
||||||
while (startPos > 0 && positions[startPos] > timeRange) {
|
|
||||||
startPos -= 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (startPos > 3) {
|
|
||||||
// compute relative movement between these two points
|
|
||||||
const frequency = 1 / (positions[startPos] - timeStamp);
|
|
||||||
const movedY = positions[startPos - 1] - detail.scrollLeft;
|
|
||||||
const movedX = positions[startPos - 2] - detail.scrollTop;
|
|
||||||
|
|
||||||
// based on XXms compute the movement to apply for each render step
|
|
||||||
// velocity = space/time = s*(1/t) = s*frequency
|
|
||||||
detail.velocityX = movedX * frequency;
|
|
||||||
detail.velocityY = movedY * frequency;
|
|
||||||
} else {
|
|
||||||
detail.velocityX = 0;
|
|
||||||
detail.velocityY = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearTimeout(this.tmr);
|
|
||||||
this.tmr = setTimeout(() => {
|
|
||||||
|
|
||||||
// haven't scrolled in a while, so it's a scrollend
|
|
||||||
this.isScrolling = false;
|
|
||||||
|
|
||||||
this.dom.read(timeStamp => {
|
|
||||||
if (!this.isScrolling) {
|
|
||||||
this.onEnd(timeStamp);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 80);
|
|
||||||
|
|
||||||
// emit on each scroll event
|
|
||||||
if (this.onionScroll) {
|
|
||||||
this.onionScroll(detail);
|
|
||||||
} else {
|
|
||||||
this.ionScroll.emit(detail);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onEnd(timeStamp: number) {
|
private onScrollEnd() {
|
||||||
const detail = this.detail;
|
|
||||||
|
|
||||||
detail.timeStamp = timeStamp;
|
clearInterval(this.watchDog);
|
||||||
|
this.watchDog = null;
|
||||||
// emit that the scroll has ended
|
this.isScrolling = false;
|
||||||
this.gesture.release();
|
this.ionScrollEnd.emit({
|
||||||
|
isScrolling: false
|
||||||
if (this.onionScrollEnd) {
|
});
|
||||||
this.onionScrollEnd(detail);
|
|
||||||
}
|
|
||||||
this.ionScrollEnd.emit(detail);
|
|
||||||
this.refocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
private refocus() {
|
|
||||||
// TODO: renable when desktop testing is done
|
|
||||||
// setTimeout(() => {
|
|
||||||
// if (document.activeElement === document.body) {
|
|
||||||
// (this.el.querySelector('.focus-input') as HTMLElement).focus();
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hostData() {
|
hostData() {
|
||||||
@ -339,18 +254,67 @@ export class Scroll {
|
|||||||
</div>
|
</div>
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScrollDetail extends GestureDetail {
|
// ******** DOM READ ****************
|
||||||
|
function updateScrollDetail(
|
||||||
|
detail: ScrollDetail,
|
||||||
|
el: HTMLElement,
|
||||||
|
timeStamp: number,
|
||||||
|
didStart: boolean
|
||||||
|
) {
|
||||||
|
const scrollTop = el.scrollTop;
|
||||||
|
const scrollLeft = el.scrollLeft;
|
||||||
|
const positions = detail.positions;
|
||||||
|
if (didStart) {
|
||||||
|
// remember the start positions
|
||||||
|
detail.startTimeStamp = timeStamp;
|
||||||
|
detail.startY = scrollTop;
|
||||||
|
detail.startX = scrollLeft;
|
||||||
|
positions.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
detail.timeStamp = timeStamp;
|
||||||
|
detail.currentY = detail.scrollTop = scrollTop;
|
||||||
|
detail.currentX = detail.scrollLeft = scrollLeft;
|
||||||
|
detail.deltaY = scrollTop - detail.startY;
|
||||||
|
detail.deltaX = scrollLeft - detail.startX;
|
||||||
|
|
||||||
|
// actively scrolling
|
||||||
|
positions.push(scrollTop, scrollLeft, timeStamp);
|
||||||
|
|
||||||
|
// move pointer to position measured 100ms ago
|
||||||
|
const timeRange = timeStamp - 100;
|
||||||
|
let startPos = positions.length - 1;
|
||||||
|
|
||||||
|
while (startPos > 0 && positions[startPos] > timeRange) {
|
||||||
|
startPos -= 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startPos > 3) {
|
||||||
|
// compute relative movement between these two points
|
||||||
|
const frequency = 1 / (positions[startPos] - timeStamp);
|
||||||
|
const movedX = positions[startPos - 1] - scrollLeft;
|
||||||
|
const movedY = positions[startPos - 2] - scrollTop;
|
||||||
|
|
||||||
|
// based on XXms compute the movement to apply for each render step
|
||||||
|
// velocity = space/time = s*(1/t) = s*frequency
|
||||||
|
detail.velocityX = movedX * frequency;
|
||||||
|
detail.velocityY = movedY * frequency;
|
||||||
|
} else {
|
||||||
|
detail.velocityX = 0;
|
||||||
|
detail.velocityY = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScrollDetail extends GestureDetail, ScrollBaseDetail {
|
||||||
|
positions?: number[];
|
||||||
scrollTop?: number;
|
scrollTop?: number;
|
||||||
scrollLeft?: number;
|
scrollLeft?: number;
|
||||||
scrollHeight?: number;
|
}
|
||||||
scrollWidth?: number;
|
|
||||||
contentHeight?: number;
|
export interface ScrollBaseDetail {
|
||||||
contentWidth?: number;
|
isScrolling?: boolean;
|
||||||
contentTop?: number;
|
|
||||||
contentBottom?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScrollCallback {
|
export interface ScrollCallback {
|
||||||
|
2
packages/core/src/index.d.ts
vendored
2
packages/core/src/index.d.ts
vendored
@ -80,7 +80,7 @@ export {
|
|||||||
} from './components/router/router-utils';
|
} from './components/router/router-utils';
|
||||||
export { Row } from './components/row/row';
|
export { Row } from './components/row/row';
|
||||||
export { Reorder } from './components/reorder/reorder';
|
export { Reorder } from './components/reorder/reorder';
|
||||||
export { Scroll, ScrollCallback, ScrollDetail } from './components/scroll/scroll';
|
export { Scroll } from './components/scroll/scroll';
|
||||||
export { Searchbar } from './components/searchbar/searchbar';
|
export { Searchbar } from './components/searchbar/searchbar';
|
||||||
export { Segment } from './components/segment/segment';
|
export { Segment } from './components/segment/segment';
|
||||||
export { SegmentButton } from './components/segment-button/segment-button';
|
export { SegmentButton } from './components/segment-button/segment-button';
|
||||||
|
@ -44,9 +44,12 @@ export function isCheckedProperty(a: any, b: any): boolean {
|
|||||||
return (a == b); // tslint:disable-line
|
return (a == b); // tslint:disable-line
|
||||||
}
|
}
|
||||||
|
|
||||||
export function assert(bool: boolean, msg: string) {
|
export function assert(actual: any, reason: string) {
|
||||||
if (!bool) {
|
if (!actual) {
|
||||||
console.error(msg);
|
const message = 'ASSERT: ' + reason;
|
||||||
|
console.error(message);
|
||||||
|
debugger; // tslint:disable-line
|
||||||
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,7 +146,7 @@ export function getPageElement(el: HTMLElement) {
|
|||||||
if (tabs) {
|
if (tabs) {
|
||||||
return tabs;
|
return tabs;
|
||||||
}
|
}
|
||||||
const page = el.closest('ion-page,.ion-page,page-inner');
|
const page = el.closest('ion-app,ion-page,.ion-page,page-inner');
|
||||||
if (page) {
|
if (page) {
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user