mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
refactor(scroll): merge into ion-content
This commit is contained in:
@@ -44,6 +44,7 @@ A list of the breaking changes introduced to each component in Ionic Angular v4.
|
||||
- [Radio](#radio)
|
||||
- [Range](#range)
|
||||
- [Refresher](#refresher)
|
||||
- [Scroll](#scroll)
|
||||
- [Segment](#segment)
|
||||
- [Select](#select)
|
||||
- [Spinner](#spinner)
|
||||
@@ -1113,6 +1114,23 @@ The `enabled` property (with a default value of `true`) has been renamed to `dis
|
||||
</ion-refresher>
|
||||
```
|
||||
|
||||
## Scroll
|
||||
|
||||
`ion-scroll` has been removed, fortunatelly `ion-content` can work as a drop-in replacement:
|
||||
|
||||
```diff
|
||||
- <ion-scroll scrollX="true">
|
||||
+ <ion-content scrollX="true">
|
||||
```
|
||||
|
||||
Another very good option is to style a `div` to become scrollable using CSS:
|
||||
|
||||
```css
|
||||
div.scrollable {
|
||||
overflow: scroll
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Segment
|
||||
|
||||
|
||||
131
core/src/components.d.ts
vendored
131
core/src/components.d.ts
vendored
@@ -619,15 +619,35 @@ declare global {
|
||||
* If true, the content will scroll behind the headers and footers. This effect can easily be seen by setting the toolbar to transparent.
|
||||
*/
|
||||
'fullscreen': boolean;
|
||||
'getScrollElement': () => HTMLIonScrollElement;
|
||||
'getScrollElement': () => HTMLElement;
|
||||
/**
|
||||
* By default `ion-content` uses an `ion-scroll` under the hood to implement scrolling, if you want to disable the content scrolling for a given page, set this property to `false`.
|
||||
* Scroll by a specified X/Y distance in the component
|
||||
*/
|
||||
'scrollEnabled': boolean;
|
||||
'scrollByPoint': (x: number, y: number, duration: number) => Promise<any>;
|
||||
/**
|
||||
* Because of performance reasons, ionScroll events are disabled by default, in order to enable them and start listening from (ionScroll), set this property to `true`.
|
||||
*/
|
||||
'scrollEvents': boolean;
|
||||
/**
|
||||
* Scroll to the bottom of the component
|
||||
*/
|
||||
'scrollToBottom': (duration?: number) => Promise<void>;
|
||||
/**
|
||||
* Scroll to a specified X/Y location in the component
|
||||
*/
|
||||
'scrollToPoint': (x: number | undefined, y: number | undefined, duration?: number) => Promise<void>;
|
||||
/**
|
||||
* Scroll to the top of the component
|
||||
*/
|
||||
'scrollToTop': (duration?: number) => Promise<void>;
|
||||
/**
|
||||
* If you want to enable the content scrolling in the X axis, set this property to `true`.
|
||||
*/
|
||||
'scrollX': boolean;
|
||||
/**
|
||||
* If you want to disable the content scrolling in the Y axis, set this property to `false`.
|
||||
*/
|
||||
'scrollY': boolean;
|
||||
}
|
||||
|
||||
interface IonDatetime {
|
||||
@@ -1946,37 +1966,6 @@ declare global {
|
||||
|
||||
}
|
||||
|
||||
interface IonScroll {
|
||||
/**
|
||||
* If true and the content does not cause an overflow scroll, the scroll interaction will cause a bounce. If the content exceeds the bounds of ionScroll, nothing will change. Note, the does not disable the system bounce on iOS. That is an OS level setting.
|
||||
*/
|
||||
'forceOverscroll': boolean;
|
||||
/**
|
||||
* The mode for component.
|
||||
*/
|
||||
'mode': Mode;
|
||||
/**
|
||||
* Scroll by a specified X/Y distance in the component
|
||||
*/
|
||||
'scrollByPoint': (x: number, y: number, duration: number) => Promise<any>;
|
||||
/**
|
||||
* If true, the component will emit scroll events.
|
||||
*/
|
||||
'scrollEvents': boolean;
|
||||
/**
|
||||
* Scroll to the bottom of the component
|
||||
*/
|
||||
'scrollToBottom': (duration: number) => Promise<void>;
|
||||
/**
|
||||
* Scroll to a specified X/Y location in the component
|
||||
*/
|
||||
'scrollToPoint': (x: number, y: number, duration: number) => Promise<void>;
|
||||
/**
|
||||
* Scroll to the top of the component
|
||||
*/
|
||||
'scrollToTop': (duration: number) => Promise<void>;
|
||||
}
|
||||
|
||||
interface IonSearchbar {
|
||||
/**
|
||||
* If true, enable searchbar animation. Default `false`.
|
||||
@@ -2398,10 +2387,6 @@ declare global {
|
||||
* Set the position of the tabbar, relative to the content. Available options: `"top"`, `"bottom"`.
|
||||
*/
|
||||
'placement': TabbarPlacement;
|
||||
/**
|
||||
* If true, the tabs will be scrollable when there are enough tabs to overflow the width of the screen.
|
||||
*/
|
||||
'scrollable': boolean;
|
||||
/**
|
||||
* The selected tab component
|
||||
*/
|
||||
@@ -2434,10 +2419,6 @@ declare global {
|
||||
* A unique name for the tabs.
|
||||
*/
|
||||
'name': string;
|
||||
/**
|
||||
* If true, the tabs will be scrollable when there are enough tabs to overflow the width of the screen.
|
||||
*/
|
||||
'scrollable': boolean;
|
||||
/**
|
||||
* Index or the Tab instance, of the tab to select.
|
||||
*/
|
||||
@@ -3342,14 +3323,6 @@ declare global {
|
||||
};
|
||||
|
||||
|
||||
interface HTMLIonScrollElement extends StencilComponents.IonScroll, HTMLStencilElement {}
|
||||
|
||||
var HTMLIonScrollElement: {
|
||||
prototype: HTMLIonScrollElement;
|
||||
new (): HTMLIonScrollElement;
|
||||
};
|
||||
|
||||
|
||||
interface HTMLIonSearchbarElement extends StencilComponents.IonSearchbar, HTMLStencilElement {}
|
||||
|
||||
var HTMLIonSearchbarElement: {
|
||||
@@ -3630,7 +3603,6 @@ declare global {
|
||||
'ion-router-outlet': JSXElements.IonRouterOutletAttributes;
|
||||
'ion-router': JSXElements.IonRouterAttributes;
|
||||
'ion-row': JSXElements.IonRowAttributes;
|
||||
'ion-scroll': JSXElements.IonScrollAttributes;
|
||||
'ion-searchbar': JSXElements.IonSearchbarAttributes;
|
||||
'ion-segment-button': JSXElements.IonSegmentButtonAttributes;
|
||||
'ion-segment': JSXElements.IonSegmentAttributes;
|
||||
@@ -4218,13 +4190,29 @@ declare global {
|
||||
*/
|
||||
'fullscreen'?: boolean;
|
||||
/**
|
||||
* By default `ion-content` uses an `ion-scroll` under the hood to implement scrolling, if you want to disable the content scrolling for a given page, set this property to `false`.
|
||||
* Emitted while scrolling. This event is disabled by default. Look at the property: `scrollEvents`
|
||||
*/
|
||||
'scrollEnabled'?: boolean;
|
||||
'onIonScroll'?: (event: CustomEvent<ScrollDetail>) => void;
|
||||
/**
|
||||
* Emitted when the scroll has ended.
|
||||
*/
|
||||
'onIonScrollEnd'?: (event: CustomEvent<ScrollBaseDetail>) => void;
|
||||
/**
|
||||
* Emitted when the scroll has started.
|
||||
*/
|
||||
'onIonScrollStart'?: (event: CustomEvent<ScrollBaseDetail>) => void;
|
||||
/**
|
||||
* Because of performance reasons, ionScroll events are disabled by default, in order to enable them and start listening from (ionScroll), set this property to `true`.
|
||||
*/
|
||||
'scrollEvents'?: boolean;
|
||||
/**
|
||||
* If you want to enable the content scrolling in the X axis, set this property to `true`.
|
||||
*/
|
||||
'scrollX'?: boolean;
|
||||
/**
|
||||
* If you want to disable the content scrolling in the Y axis, set this property to `false`.
|
||||
*/
|
||||
'scrollY'?: boolean;
|
||||
}
|
||||
|
||||
export interface IonDatetimeAttributes extends HTMLAttributes {
|
||||
@@ -5510,33 +5498,6 @@ declare global {
|
||||
|
||||
}
|
||||
|
||||
export interface IonScrollAttributes extends HTMLAttributes {
|
||||
/**
|
||||
* If true and the content does not cause an overflow scroll, the scroll interaction will cause a bounce. If the content exceeds the bounds of ionScroll, nothing will change. Note, the does not disable the system bounce on iOS. That is an OS level setting.
|
||||
*/
|
||||
'forceOverscroll'?: boolean;
|
||||
/**
|
||||
* The mode for component.
|
||||
*/
|
||||
'mode'?: Mode;
|
||||
/**
|
||||
* Emitted while scrolling. This event is disabled by default. Look at the property: `scrollEvents`
|
||||
*/
|
||||
'onIonScroll'?: (event: CustomEvent<ScrollDetail>) => void;
|
||||
/**
|
||||
* Emitted when the scroll has ended.
|
||||
*/
|
||||
'onIonScrollEnd'?: (event: CustomEvent<ScrollBaseDetail>) => void;
|
||||
/**
|
||||
* Emitted when the scroll has started.
|
||||
*/
|
||||
'onIonScrollStart'?: (event: CustomEvent<ScrollBaseDetail>) => void;
|
||||
/**
|
||||
* If true, the component will emit scroll events.
|
||||
*/
|
||||
'scrollEvents'?: boolean;
|
||||
}
|
||||
|
||||
export interface IonSearchbarAttributes extends HTMLAttributes {
|
||||
/**
|
||||
* If true, enable searchbar animation. Default `false`.
|
||||
@@ -6033,10 +5994,6 @@ declare global {
|
||||
* Set the position of the tabbar, relative to the content. Available options: `"top"`, `"bottom"`.
|
||||
*/
|
||||
'placement'?: TabbarPlacement;
|
||||
/**
|
||||
* If true, the tabs will be scrollable when there are enough tabs to overflow the width of the screen.
|
||||
*/
|
||||
'scrollable'?: boolean;
|
||||
/**
|
||||
* The selected tab component
|
||||
*/
|
||||
@@ -6076,10 +6033,6 @@ declare global {
|
||||
* Emitted when the navigation will load a component.
|
||||
*/
|
||||
'onIonNavWillLoad'?: (event: CustomEvent<void>) => void;
|
||||
/**
|
||||
* If true, the tabs will be scrollable when there are enough tabs to overflow the width of the screen.
|
||||
*/
|
||||
'scrollable'?: boolean;
|
||||
/**
|
||||
* If true, the tabbar will be hidden. Defaults to `false`.
|
||||
*/
|
||||
@@ -6470,7 +6423,6 @@ declare global {
|
||||
'ion-router-outlet': HTMLIonRouterOutletElement
|
||||
'ion-router': HTMLIonRouterElement
|
||||
'ion-row': HTMLIonRowElement
|
||||
'ion-scroll': HTMLIonScrollElement
|
||||
'ion-searchbar': HTMLIonSearchbarElement
|
||||
'ion-segment-button': HTMLIonSegmentButtonElement
|
||||
'ion-segment': HTMLIonSegmentElement
|
||||
@@ -6576,7 +6528,6 @@ declare global {
|
||||
'ion-router-outlet': HTMLIonRouterOutletElement;
|
||||
'ion-router': HTMLIonRouterElement;
|
||||
'ion-row': HTMLIonRowElement;
|
||||
'ion-scroll': HTMLIonScrollElement;
|
||||
'ion-searchbar': HTMLIonSearchbarElement;
|
||||
'ion-segment-button': HTMLIonSegmentButtonElement;
|
||||
'ion-segment': HTMLIonSegmentElement;
|
||||
|
||||
@@ -19,14 +19,9 @@
|
||||
|
||||
flex: 1;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
/* stylelint-disable */
|
||||
/* TODO: find a better solution in padding.css, that does not require !important, */
|
||||
margin: 0 !important;
|
||||
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
/* stylelint-enable */
|
||||
|
||||
background-color: #{current-color(base)};
|
||||
@@ -35,23 +30,64 @@
|
||||
contain: layout size style;
|
||||
}
|
||||
|
||||
:host(.scroll-disabled),
|
||||
ion-scroll {
|
||||
:host(.ion-color-outer),
|
||||
:host(.outer-content) {
|
||||
--ion-color-base: #{$background-color-step-50};
|
||||
}
|
||||
|
||||
.inner-scroll {
|
||||
@include position(
|
||||
calc(var(--offset-top) * -1), 0,
|
||||
calc(var(--offset-bottom) * -1), 0
|
||||
);
|
||||
@include padding(
|
||||
calc(var(--padding-top) + var(--offset-top)),
|
||||
var(--padding-end),
|
||||
calc(var(--padding-bottom) + var(--keyboard-offset) + var(--offset-bottom)),
|
||||
var(--padding-start)
|
||||
);
|
||||
|
||||
position: absolute;
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:host(.ion-color-outer),
|
||||
:host(.outer-content) {
|
||||
--ion-color-base: #{$background-color-step-50};
|
||||
.scroll-x.scroll.y {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
will-change: scroll-position;
|
||||
}
|
||||
|
||||
:host(.content-size) ion-scroll {
|
||||
position: relative;
|
||||
|
||||
contain: none;
|
||||
.scroll-y {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.scroll-x {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.scroll-enabled {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
will-change: scroll-position;
|
||||
}
|
||||
|
||||
.overscroll::before,
|
||||
.overscroll::after {
|
||||
position: absolute;
|
||||
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
|
||||
content: "";
|
||||
}
|
||||
|
||||
.overscroll::before {
|
||||
bottom: -1px;
|
||||
}
|
||||
|
||||
.overscroll::after {
|
||||
top: -1px;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, Element, Listen, Method, Prop, QueueApi } from '@stencil/core';
|
||||
import { Component, Element, Event, EventEmitter, Listen, Method, Prop, QueueApi } from '@stencil/core';
|
||||
|
||||
import { Color, Config, Mode } from '../../interface';
|
||||
import { createColorClasses, hostContext } from '../../utils/theme';
|
||||
import { Color, Config, Mode, ScrollBaseDetail, ScrollDetail } from '../../interface';
|
||||
import { createColorClasses } from '../../utils/theme';
|
||||
|
||||
@Component({
|
||||
tag: 'ion-content',
|
||||
@@ -13,9 +13,35 @@ import { createColorClasses, hostContext } from '../../utils/theme';
|
||||
})
|
||||
export class Content {
|
||||
|
||||
private watchDog: any;
|
||||
private isScrolling = false;
|
||||
private lastScroll = 0;
|
||||
private queued = false;
|
||||
private cTop = -1;
|
||||
private cBottom = -1;
|
||||
private scrollEl?: HTMLIonScrollElement;
|
||||
private scrollEl!: HTMLElement;
|
||||
|
||||
// Detail is used in a hot loop in the scroll event, by allocating it here
|
||||
// V8 will be able to inline any read/write to it since it's a monomorphic class.
|
||||
// https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html
|
||||
private detail: ScrollDetail = {
|
||||
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,
|
||||
isScrolling: true,
|
||||
};
|
||||
|
||||
mode!: Mode;
|
||||
@Prop() color?: Color;
|
||||
@@ -24,6 +50,7 @@ export class Content {
|
||||
|
||||
@Prop({ context: 'config' }) config!: Config;
|
||||
@Prop({ context: 'queue' }) queue!: QueueApi;
|
||||
@Prop({ context: 'window' }) win!: Window;
|
||||
|
||||
/**
|
||||
* If true, the content will scroll behind the headers
|
||||
@@ -37,13 +64,17 @@ export class Content {
|
||||
* If the content exceeds the bounds of ionContent, nothing will change.
|
||||
* Note, the does not disable the system bounce on iOS. That is an OS level setting.
|
||||
*/
|
||||
@Prop() forceOverscroll?: boolean;
|
||||
@Prop({ mutable: true }) forceOverscroll?: boolean;
|
||||
|
||||
/**
|
||||
* By default `ion-content` uses an `ion-scroll` under the hood to implement scrolling,
|
||||
* if you want to disable the content scrolling for a given page, set this property to `false`.
|
||||
* If you want to enable the content scrolling in the X axis, set this property to `true`.
|
||||
*/
|
||||
@Prop() scrollEnabled = true;
|
||||
@Prop() scrollX = false;
|
||||
|
||||
/**
|
||||
* If you want to disable the content scrolling in the Y axis, set this property to `false`.
|
||||
*/
|
||||
@Prop() scrollY = true;
|
||||
|
||||
/**
|
||||
* Because of performance reasons, ionScroll events are disabled by default, in order to enable them
|
||||
@@ -51,24 +82,44 @@ export class Content {
|
||||
*/
|
||||
@Prop() scrollEvents = false;
|
||||
|
||||
/**
|
||||
* Emitted when the scroll has started.
|
||||
*/
|
||||
@Event() ionScrollStart!: EventEmitter<ScrollBaseDetail>;
|
||||
|
||||
/**
|
||||
* Emitted while scrolling. This event is disabled by default.
|
||||
* Look at the property: `scrollEvents`
|
||||
*/
|
||||
@Event() ionScroll!: EventEmitter<ScrollDetail>;
|
||||
|
||||
/**
|
||||
* Emitted when the scroll has ended.
|
||||
*/
|
||||
@Event() ionScrollEnd!: EventEmitter<ScrollBaseDetail>;
|
||||
|
||||
@Listen('body:ionNavDidChange')
|
||||
onNavChanged() {
|
||||
this.resize();
|
||||
}
|
||||
|
||||
componentWillLoad() {
|
||||
if (this.forceOverscroll === undefined) {
|
||||
this.forceOverscroll = this.mode === 'ios' && ('ontouchstart' in this.win);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
this.resize();
|
||||
}
|
||||
|
||||
@Method()
|
||||
getScrollElement(): HTMLIonScrollElement {
|
||||
return this.scrollEl!;
|
||||
componentDidUnload() {
|
||||
if (this.watchDog) {
|
||||
clearInterval(this.watchDog);
|
||||
}
|
||||
}
|
||||
|
||||
private resize() {
|
||||
if (!this.scrollEl) {
|
||||
return;
|
||||
}
|
||||
if (this.fullscreen) {
|
||||
this.queue.read(this.readDimensions.bind(this));
|
||||
} else if (this.cTop !== 0 || this.cBottom !== 0) {
|
||||
@@ -89,35 +140,173 @@ export class Content {
|
||||
}
|
||||
}
|
||||
|
||||
private onScroll(ev: UIEvent) {
|
||||
const timeStamp = Date.now();
|
||||
const didStart = !this.isScrolling;
|
||||
this.lastScroll = timeStamp;
|
||||
if (didStart) {
|
||||
this.onScrollStart();
|
||||
}
|
||||
if (!this.queued && this.scrollEvents) {
|
||||
this.queued = true;
|
||||
this.queue.read(ts => {
|
||||
this.queued = false;
|
||||
this.detail.event = ev;
|
||||
updateScrollDetail(this.detail, this.el, ts, didStart);
|
||||
this.ionScroll.emit(this.detail);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Method()
|
||||
getScrollElement(): HTMLElement {
|
||||
return this.scrollEl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to the top of the component
|
||||
*/
|
||||
@Method()
|
||||
scrollToTop(duration = 100): Promise<void> {
|
||||
return this.scrollToPoint(0, undefined, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to the bottom of the component
|
||||
*/
|
||||
@Method()
|
||||
scrollToBottom(duration = 100): Promise<void> {
|
||||
const y = this.scrollEl.scrollHeight - this.scrollEl.clientHeight;
|
||||
return this.scrollToPoint(undefined, y, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll by a specified X/Y distance in the component
|
||||
*/
|
||||
@Method()
|
||||
scrollByPoint(x: number, y: number, duration: number): Promise<any> {
|
||||
return this.scrollToPoint(x + this.scrollEl.scrollLeft, y + this.scrollEl.scrollTop, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to a specified X/Y location in the component
|
||||
*/
|
||||
@Method()
|
||||
async scrollToPoint(x: number | undefined, y: number | undefined, duration = 0): Promise<void> {
|
||||
const el = this.scrollEl;
|
||||
if (duration < 32) {
|
||||
if (y != null) {
|
||||
el.scrollTop = y;
|
||||
}
|
||||
if (x != null) {
|
||||
el.scrollLeft = x;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let resolve!: () => void;
|
||||
let startTime: number;
|
||||
const promise = new Promise<void>(r => resolve = r);
|
||||
const fromY = el.scrollTop;
|
||||
const fromX = el.scrollLeft;
|
||||
|
||||
const deltaY = y != null ? y - fromY : 0;
|
||||
const deltaX = x != null ? x - fromX : 0;
|
||||
|
||||
// scroll loop
|
||||
const step = (timeStamp: number) => {
|
||||
let linearTime = Math.min(1, ((timeStamp - startTime) / duration));
|
||||
const easedT = (--linearTime) * linearTime * linearTime + 1;
|
||||
|
||||
if (deltaY !== 0) {
|
||||
el.scrollTop = (easedT * deltaY) + fromY;
|
||||
}
|
||||
if (deltaX !== 0) {
|
||||
el.scrollLeft = Math.floor((easedT * deltaX) + fromX);
|
||||
}
|
||||
|
||||
if (easedT < 1) {
|
||||
// do not use DomController here
|
||||
// must use nativeRaf in order to fire in the next frame
|
||||
// TODO: remove as any
|
||||
requestAnimationFrame(step);
|
||||
|
||||
} else {
|
||||
this.isScrolling = false;
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
// start scroll loop
|
||||
this.isScrolling = true;
|
||||
|
||||
// chill out for a frame first
|
||||
requestAnimationFrame(ts => {
|
||||
startTime = ts;
|
||||
step(ts);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
private onScrollStart() {
|
||||
this.isScrolling = true;
|
||||
this.ionScrollStart.emit({
|
||||
isScrolling: true
|
||||
});
|
||||
|
||||
if (this.watchDog) {
|
||||
clearInterval(this.watchDog);
|
||||
}
|
||||
// watchdog
|
||||
this.watchDog = setInterval(() => {
|
||||
if (this.lastScroll < Date.now() - 120) {
|
||||
this.onScrollEnd();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
private onScrollEnd() {
|
||||
|
||||
clearInterval(this.watchDog);
|
||||
this.watchDog = null;
|
||||
this.isScrolling = false;
|
||||
this.ionScrollEnd.emit({
|
||||
isScrolling: false
|
||||
});
|
||||
}
|
||||
|
||||
hostData() {
|
||||
return {
|
||||
class: {
|
||||
...createColorClasses(this.color),
|
||||
'content-size': hostContext('ion-popover', this.el),
|
||||
'scroll-disabled': !this.scrollEnabled,
|
||||
'overscroll': this.forceOverscroll,
|
||||
},
|
||||
style: {
|
||||
'--offset-top': `${this.cTop}px`,
|
||||
'--offset-bottom': `${this.cBottom}px`,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { scrollX, scrollY, forceOverscroll } = this;
|
||||
const scrollEnabled = scrollX || scrollY;
|
||||
|
||||
this.resize();
|
||||
|
||||
return [
|
||||
this.scrollEnabled ? (
|
||||
<ion-scroll
|
||||
ref={el => this.scrollEl = el as any}
|
||||
mode={this.mode}
|
||||
scrollEvents={this.scrollEvents}
|
||||
forceOverscroll={this.forceOverscroll}
|
||||
style={{
|
||||
'top': `${-this.cTop}px`,
|
||||
'bottom': `${-this.cBottom}px`,
|
||||
'--offset-top': `${this.cTop}px`,
|
||||
'--offset-bottom': `${this.cBottom}px`,
|
||||
}}>
|
||||
<slot></slot>
|
||||
</ion-scroll>
|
||||
) : <slot></slot>,
|
||||
<div
|
||||
class={{
|
||||
'inner-scroll': true,
|
||||
'scroll-x': scrollX,
|
||||
'scroll-y': scrollY,
|
||||
'scroll-enabled': scrollEnabled,
|
||||
'overscroll': scrollEnabled && !!forceOverscroll
|
||||
}}
|
||||
ref={el => this.scrollEl = el!}
|
||||
onScroll={ev => this.onScroll(ev)}>
|
||||
<slot></slot>
|
||||
</div>,
|
||||
<slot name="fixed"></slot>
|
||||
];
|
||||
}
|
||||
@@ -146,3 +335,37 @@ function getPageElement(el: HTMLElement) {
|
||||
}
|
||||
return getParentElement(el);
|
||||
}
|
||||
|
||||
// ******** DOM READ ****************
|
||||
function updateScrollDetail(
|
||||
detail: ScrollDetail,
|
||||
el: HTMLElement,
|
||||
timestamp: number,
|
||||
didStart: boolean
|
||||
) {
|
||||
const prevX = detail.currentX;
|
||||
const prevY = detail.currentY;
|
||||
const prevT = detail.timeStamp;
|
||||
const currentX = el.scrollLeft;
|
||||
const currentY = el.scrollTop;
|
||||
if (didStart) {
|
||||
// remember the start positions
|
||||
detail.startTimeStamp = timestamp;
|
||||
detail.startX = currentX;
|
||||
detail.startY = currentY;
|
||||
detail.velocityX = detail.velocityY = 0;
|
||||
}
|
||||
detail.timeStamp = timestamp;
|
||||
detail.currentX = detail.scrollLeft = currentX;
|
||||
detail.currentY = detail.scrollTop = currentY;
|
||||
detail.deltaX = currentX - detail.startX;
|
||||
detail.deltaY = currentY - detail.startY;
|
||||
|
||||
const timeDelta = timestamp - prevT;
|
||||
if (timeDelta > 0 && timeDelta < 100) {
|
||||
const velocityX = (currentX - prevX) / timeDelta;
|
||||
const velocityY = (currentY - prevY) / timeDelta;
|
||||
detail.velocityX = velocityX * 0.7 + detail.velocityX * 0.3;
|
||||
detail.velocityY = velocityY * 0.7 + detail.velocityY * 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,14 +32,6 @@ and footers. This effect can easily be seen by setting the toolbar
|
||||
to transparent.
|
||||
|
||||
|
||||
#### scrollEnabled
|
||||
|
||||
boolean
|
||||
|
||||
By default `ion-content` uses an `ion-scroll` under the hood to implement scrolling,
|
||||
if you want to disable the content scrolling for a given page, set this property to `false`.
|
||||
|
||||
|
||||
#### scrollEvents
|
||||
|
||||
boolean
|
||||
@@ -48,6 +40,20 @@ Because of performance reasons, ionScroll events are disabled by default, in ord
|
||||
and start listening from (ionScroll), set this property to `true`.
|
||||
|
||||
|
||||
#### scrollX
|
||||
|
||||
boolean
|
||||
|
||||
If you want to enable the content scrolling in the X axis, set this property to `true`.
|
||||
|
||||
|
||||
#### scrollY
|
||||
|
||||
boolean
|
||||
|
||||
If you want to disable the content scrolling in the Y axis, set this property to `false`.
|
||||
|
||||
|
||||
## Attributes
|
||||
|
||||
#### color
|
||||
@@ -73,14 +79,6 @@ and footers. This effect can easily be seen by setting the toolbar
|
||||
to transparent.
|
||||
|
||||
|
||||
#### scroll-enabled
|
||||
|
||||
boolean
|
||||
|
||||
By default `ion-content` uses an `ion-scroll` under the hood to implement scrolling,
|
||||
if you want to disable the content scrolling for a given page, set this property to `false`.
|
||||
|
||||
|
||||
#### scroll-events
|
||||
|
||||
boolean
|
||||
@@ -89,11 +87,63 @@ Because of performance reasons, ionScroll events are disabled by default, in ord
|
||||
and start listening from (ionScroll), set this property to `true`.
|
||||
|
||||
|
||||
#### scroll-x
|
||||
|
||||
boolean
|
||||
|
||||
If you want to enable the content scrolling in the X axis, set this property to `true`.
|
||||
|
||||
|
||||
#### scroll-y
|
||||
|
||||
boolean
|
||||
|
||||
If you want to disable the content scrolling in the Y axis, set this property to `false`.
|
||||
|
||||
|
||||
## Events
|
||||
|
||||
#### ionScroll
|
||||
|
||||
Emitted while scrolling. This event is disabled by default.
|
||||
Look at the property: `scrollEvents`
|
||||
|
||||
|
||||
#### ionScrollEnd
|
||||
|
||||
Emitted when the scroll has ended.
|
||||
|
||||
|
||||
#### ionScrollStart
|
||||
|
||||
Emitted when the scroll has started.
|
||||
|
||||
|
||||
## Methods
|
||||
|
||||
#### getScrollElement()
|
||||
|
||||
|
||||
#### scrollByPoint()
|
||||
|
||||
Scroll by a specified X/Y distance in the component
|
||||
|
||||
|
||||
#### scrollToBottom()
|
||||
|
||||
Scroll to the bottom of the component
|
||||
|
||||
|
||||
#### scrollToPoint()
|
||||
|
||||
Scroll to a specified X/Y location in the component
|
||||
|
||||
|
||||
#### scrollToTop()
|
||||
|
||||
Scroll to the top of the component
|
||||
|
||||
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
<div style="display: flex; flex: 1">
|
||||
<ion-content padding style="text-align: center; flex: 2" id="content">
|
||||
<f></f>
|
||||
<f style="height: 50%"></f>
|
||||
<ion-button onclick="toggleFullscreen()">Toggle content.fullscreen</ion-button>
|
||||
<p>
|
||||
<ion-button onclick="toggleDisplay('header')" color="secondary">Toggle header</ion-button>
|
||||
@@ -104,10 +104,6 @@
|
||||
animation: ani1 5s infinite;
|
||||
}
|
||||
|
||||
#content2 ion-scroll {
|
||||
background: black;
|
||||
}
|
||||
|
||||
@keyframes ani1 {
|
||||
0% {
|
||||
transform: translateX(0)
|
||||
|
||||
@@ -8,7 +8,7 @@ export class InfiniteScroll {
|
||||
|
||||
private thrPx = 0;
|
||||
private thrPc = 0;
|
||||
private scrollEl?: HTMLIonScrollElement;
|
||||
private scrollEl?: HTMLElement;
|
||||
private didFire = false;
|
||||
private isBusy = false;
|
||||
|
||||
@@ -81,9 +81,13 @@ export class InfiniteScroll {
|
||||
}
|
||||
this.thresholdChanged(this.threshold);
|
||||
this.enableScrollEvents(!this.disabled);
|
||||
if (this.position === 'top') {
|
||||
this.queue.write(() => this.scrollEl && this.scrollEl.scrollToBottom(0));
|
||||
}
|
||||
// if (this.position === 'top') {
|
||||
// this.queue.write(() => {
|
||||
// if (this.scrollEl) {
|
||||
// this.scrollEl.scrollTop = this.scrollEl.scrollHeight - this.scrollEl.clientHeight;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
componentDidUnload() {
|
||||
@@ -143,45 +147,45 @@ export class InfiniteScroll {
|
||||
}
|
||||
this.isLoading = false;
|
||||
|
||||
if (this.position === 'top') {
|
||||
/**
|
||||
* New content is being added at the top, but the scrollTop position stays the same,
|
||||
* which causes a scroll jump visually. This algorithm makes sure to prevent this.
|
||||
* (Frame 1)
|
||||
* - complete() is called, but the UI hasn't had time to update yet.
|
||||
* - Save the current content dimensions.
|
||||
* - Wait for the next frame using _dom.read, so the UI will be updated.
|
||||
* (Frame 2)
|
||||
* - Read the new content dimensions.
|
||||
* - Calculate the height difference and the new scroll position.
|
||||
* - Delay the scroll position change until other possible dom reads are done using _dom.write to be performant.
|
||||
* (Still frame 2, if I'm correct)
|
||||
* - Change the scroll position (= visually maintain the scroll position).
|
||||
* - Change the state to re-enable the InfiniteScroll.
|
||||
* - This should be after changing the scroll position, or it could
|
||||
* cause the InfiniteScroll to be triggered again immediately.
|
||||
* (Frame 3)
|
||||
* Done.
|
||||
*/
|
||||
this.isBusy = true;
|
||||
// ******** DOM READ ****************
|
||||
// Save the current content dimensions before the UI updates
|
||||
const prev = scrollEl.scrollHeight - scrollEl.scrollTop;
|
||||
// if (this.position === 'top') {
|
||||
// /**
|
||||
// * New content is being added at the top, but the scrollTop position stays the same,
|
||||
// * which causes a scroll jump visually. This algorithm makes sure to prevent this.
|
||||
// * (Frame 1)
|
||||
// * - complete() is called, but the UI hasn't had time to update yet.
|
||||
// * - Save the current content dimensions.
|
||||
// * - Wait for the next frame using _dom.read, so the UI will be updated.
|
||||
// * (Frame 2)
|
||||
// * - Read the new content dimensions.
|
||||
// * - Calculate the height difference and the new scroll position.
|
||||
// * - Delay the scroll position change until other possible dom reads are done using _dom.write to be performant.
|
||||
// * (Still frame 2, if I'm correct)
|
||||
// * - Change the scroll position (= visually maintain the scroll position).
|
||||
// * - Change the state to re-enable the InfiniteScroll.
|
||||
// * - This should be after changing the scroll position, or it could
|
||||
// * cause the InfiniteScroll to be triggered again immediately.
|
||||
// * (Frame 3)
|
||||
// * Done.
|
||||
// */
|
||||
// this.isBusy = true;
|
||||
// // ******** DOM READ ****************
|
||||
// // Save the current content dimensions before the UI updates
|
||||
// const prev = scrollEl.scrollHeight - scrollEl.scrollTop;
|
||||
|
||||
// ******** DOM READ ****************
|
||||
this.queue.read(() => {
|
||||
// UI has updated, save the new content dimensions
|
||||
const scrollHeight = scrollEl.scrollHeight;
|
||||
// New content was added on top, so the scroll position should be changed immediately to prevent it from jumping around
|
||||
const newScrollTop = scrollHeight - prev;
|
||||
// // ******** DOM READ ****************
|
||||
// this.queue.read(() => {
|
||||
// // UI has updated, save the new content dimensions
|
||||
// const scrollHeight = scrollEl.scrollHeight;
|
||||
// // New content was added on top, so the scroll position should be changed immediately to prevent it from jumping around
|
||||
// const newScrollTop = scrollHeight - prev;
|
||||
|
||||
// ******** DOM WRITE ****************
|
||||
this.queue.write(() => {
|
||||
scrollEl.scrollTop = newScrollTop;
|
||||
this.isBusy = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
// // ******** DOM WRITE ****************
|
||||
// this.queue.write(() => {
|
||||
// scrollEl.scrollTop = newScrollTop;
|
||||
// this.isBusy = false;
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,10 +38,6 @@
|
||||
</ion-app>
|
||||
|
||||
<script>
|
||||
let items = [];
|
||||
for (var i = 0; i < 30; i++) {
|
||||
items.push(i + 1);
|
||||
}
|
||||
const list = document.getElementById('list');
|
||||
const infiniteScroll = document.getElementById('infinite-scroll');
|
||||
|
||||
@@ -51,35 +47,30 @@
|
||||
|
||||
infiniteScroll.addEventListener('ionInfinite', async function () {
|
||||
console.log('Loading data...');
|
||||
const data = await getAsyncData();
|
||||
items = items.concat(data);
|
||||
await wait(500);
|
||||
infiniteScroll.complete();
|
||||
render();
|
||||
appendItems();
|
||||
|
||||
console.log('Done');
|
||||
});
|
||||
|
||||
function render() {
|
||||
let html = '';
|
||||
for (let item of items) {
|
||||
html += `<ion-item>${item}</ion-item>`;
|
||||
function appendItems() {
|
||||
for (var i = 0; i < 30; i++) {
|
||||
const el = document.createElement('ion-item');
|
||||
el.textContent = `${1 + i}`;
|
||||
list.appendChild(el);
|
||||
}
|
||||
list.innerHTML = html;
|
||||
}
|
||||
|
||||
function getAsyncData() {
|
||||
// async return mock data
|
||||
function wait(time) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
let data = [];
|
||||
for (var i = 0; i < 30; i++) {
|
||||
data.push(i);
|
||||
}
|
||||
|
||||
resolve(data);
|
||||
}, 500);
|
||||
resolve();
|
||||
}, time);
|
||||
});
|
||||
}
|
||||
render();
|
||||
|
||||
appendItems();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -30,13 +30,10 @@ ion-popover {
|
||||
z-index: $z-index-overlay-wrapper;
|
||||
}
|
||||
|
||||
.popover-content ion-content,
|
||||
.popover-content ion-scroll {
|
||||
contain: none;
|
||||
}
|
||||
|
||||
.popover-content ion-scroll {
|
||||
.popover-content ion-content {
|
||||
position: relative;
|
||||
|
||||
contain: none;
|
||||
}
|
||||
|
||||
ion-popover-controller {
|
||||
|
||||
@@ -6,7 +6,6 @@ ion-refresher {
|
||||
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
width: 100%;
|
||||
height: $refresher-height;
|
||||
|
||||
@@ -15,7 +15,7 @@ export class Refresher {
|
||||
private appliedStyles = false;
|
||||
private didStart = false;
|
||||
private progress = 0;
|
||||
private scrollEl?: HTMLIonScrollElement;
|
||||
private scrollEl?: HTMLElement;
|
||||
private gesture?: Gesture;
|
||||
|
||||
mode!: Mode;
|
||||
@@ -333,7 +333,7 @@ export class Refresher {
|
||||
this.queue.write(() => {
|
||||
if (this.scrollEl) {
|
||||
const style = this.scrollEl.style;
|
||||
style.transform = ((y > 0) ? 'translateY(' + y + 'px) translateZ(0px)' : 'translateZ(0px)');
|
||||
style.transform = ((y > 0) ? `translateY(${y}px) translateZ(0px)` : 'translateZ(0px)');
|
||||
style.transitionDuration = duration;
|
||||
style.transitionDelay = delay;
|
||||
style.overflow = (overflowVisible ? 'hidden' : '');
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.reorder-enabled {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.reorder-enabled ion-reorder {
|
||||
display: block;
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export class ReorderGroup {
|
||||
private selectedItemHeight!: number;
|
||||
private lastToIndex!: number;
|
||||
private cachedHeights: number[] = [];
|
||||
private scrollEl?: HTMLIonScrollElement;
|
||||
private scrollEl?: HTMLElement;
|
||||
private gesture?: Gesture;
|
||||
|
||||
private scrollElTop = 0;
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
# ion-scroll
|
||||
|
||||
Scroll is a low-level component for arbitrary scrolling areas. It's used internally by Content.
|
||||
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
#### forceOverscroll
|
||||
|
||||
boolean
|
||||
|
||||
If true and the content does not cause an overflow scroll, the scroll interaction will cause a bounce.
|
||||
If the content exceeds the bounds of ionScroll, nothing will change.
|
||||
Note, the does not disable the system bounce on iOS. That is an OS level setting.
|
||||
|
||||
|
||||
#### mode
|
||||
|
||||
string
|
||||
|
||||
The mode for component.
|
||||
|
||||
|
||||
#### scrollEvents
|
||||
|
||||
boolean
|
||||
|
||||
If true, the component will emit scroll events.
|
||||
|
||||
|
||||
## Attributes
|
||||
|
||||
#### force-overscroll
|
||||
|
||||
boolean
|
||||
|
||||
If true and the content does not cause an overflow scroll, the scroll interaction will cause a bounce.
|
||||
If the content exceeds the bounds of ionScroll, nothing will change.
|
||||
Note, the does not disable the system bounce on iOS. That is an OS level setting.
|
||||
|
||||
|
||||
#### mode
|
||||
|
||||
string
|
||||
|
||||
The mode for component.
|
||||
|
||||
|
||||
#### scroll-events
|
||||
|
||||
boolean
|
||||
|
||||
If true, the component will emit scroll events.
|
||||
|
||||
|
||||
## Events
|
||||
|
||||
#### ionScroll
|
||||
|
||||
Emitted while scrolling. This event is disabled by default.
|
||||
Look at the property: `scrollEvents`
|
||||
|
||||
|
||||
#### ionScrollEnd
|
||||
|
||||
Emitted when the scroll has ended.
|
||||
|
||||
|
||||
#### ionScrollStart
|
||||
|
||||
Emitted when the scroll has started.
|
||||
|
||||
|
||||
## Methods
|
||||
|
||||
#### scrollByPoint()
|
||||
|
||||
Scroll by a specified X/Y distance in the component
|
||||
|
||||
|
||||
#### scrollToBottom()
|
||||
|
||||
Scroll to the bottom of the component
|
||||
|
||||
|
||||
#### scrollToPoint()
|
||||
|
||||
Scroll to a specified X/Y location in the component
|
||||
|
||||
|
||||
#### scrollToTop()
|
||||
|
||||
Scroll to the top of the component
|
||||
|
||||
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built with [StencilJS](https://stenciljs.com/)*
|
||||
@@ -1,35 +0,0 @@
|
||||
@import "../../themes/ionic.globals";
|
||||
|
||||
:host {
|
||||
@include position(0, 0, 0, 0);
|
||||
|
||||
display: block;
|
||||
position: absolute;
|
||||
|
||||
contain: size style layout;
|
||||
z-index: $z-index-scroll-content;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
will-change: scroll-position;
|
||||
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:host(.overscroll)::before,
|
||||
:host(.overscroll)::after {
|
||||
position: absolute;
|
||||
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
|
||||
content: "";
|
||||
}
|
||||
|
||||
:host(.overscroll)::before {
|
||||
bottom: -1px;
|
||||
}
|
||||
|
||||
:host(.overscroll)::after {
|
||||
top: -1px;
|
||||
}
|
||||
@@ -1,286 +0,0 @@
|
||||
import { Component, Element, Event, EventEmitter, Listen, Method, Prop, QueueApi } from '@stencil/core';
|
||||
|
||||
import { Config, Mode, ScrollBaseDetail, ScrollDetail } from '../../interface';
|
||||
import { createThemedClasses } from '../../utils/theme';
|
||||
|
||||
@Component({
|
||||
tag: 'ion-scroll',
|
||||
styleUrl: 'scroll.scss',
|
||||
shadow: true
|
||||
})
|
||||
export class Scroll {
|
||||
|
||||
private watchDog: any;
|
||||
private isScrolling = false;
|
||||
private lastScroll = 0;
|
||||
private queued = false;
|
||||
|
||||
// Detail is used in a hot loop in the scroll event, by allocating it here
|
||||
// V8 will be able to inline any read/write to it since it's a monomorphic class.
|
||||
// https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html
|
||||
private detail: ScrollDetail = {
|
||||
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,
|
||||
isScrolling: true,
|
||||
};
|
||||
|
||||
@Element() el!: HTMLElement;
|
||||
|
||||
@Prop({ context: 'config' }) config!: Config;
|
||||
@Prop({ context: 'queue' }) queue!: QueueApi;
|
||||
@Prop({ context: 'window' }) win!: Window;
|
||||
|
||||
/** The mode for component. */
|
||||
@Prop() mode!: Mode;
|
||||
|
||||
/**
|
||||
* If true and the content does not cause an overflow scroll, the scroll interaction will cause a bounce.
|
||||
* If the content exceeds the bounds of ionScroll, nothing will change.
|
||||
* Note, the does not disable the system bounce on iOS. That is an OS level setting.
|
||||
*/
|
||||
@Prop({ mutable: true }) forceOverscroll?: boolean;
|
||||
|
||||
/** If true, the component will emit scroll events. */
|
||||
@Prop() scrollEvents = false;
|
||||
|
||||
/**
|
||||
* Emitted when the scroll has started.
|
||||
*/
|
||||
@Event() ionScrollStart!: EventEmitter<ScrollBaseDetail>;
|
||||
|
||||
/**
|
||||
* Emitted while scrolling. This event is disabled by default.
|
||||
* Look at the property: `scrollEvents`
|
||||
*/
|
||||
@Event() ionScroll!: EventEmitter<ScrollDetail>;
|
||||
|
||||
/**
|
||||
* Emitted when the scroll has ended.
|
||||
*/
|
||||
@Event() ionScrollEnd!: EventEmitter<ScrollBaseDetail>;
|
||||
|
||||
componentWillLoad() {
|
||||
if (this.forceOverscroll === undefined) {
|
||||
this.forceOverscroll = this.mode === 'ios' && ('ontouchstart' in this.win);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUnload() {
|
||||
if (this.watchDog) {
|
||||
clearInterval(this.watchDog);
|
||||
}
|
||||
}
|
||||
|
||||
@Listen('scroll', { passive: true })
|
||||
onScroll(ev: UIEvent) {
|
||||
const timeStamp = Date.now();
|
||||
const didStart = !this.isScrolling;
|
||||
this.lastScroll = timeStamp;
|
||||
if (didStart) {
|
||||
this.onScrollStart();
|
||||
}
|
||||
if (!this.queued && this.scrollEvents) {
|
||||
this.queued = true;
|
||||
this.queue.read(ts => {
|
||||
this.queued = false;
|
||||
this.detail.event = ev;
|
||||
updateScrollDetail(this.detail, this.el, ts, didStart);
|
||||
this.ionScroll.emit(this.detail);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Scroll to the top of the component */
|
||||
@Method()
|
||||
scrollToTop(duration: number): Promise<void> {
|
||||
return this.scrollToPoint(0, 0, duration);
|
||||
}
|
||||
|
||||
/** Scroll to the bottom of the component */
|
||||
@Method()
|
||||
scrollToBottom(duration: number): Promise<void> {
|
||||
const y = this.el.scrollHeight - this.el.clientHeight;
|
||||
return this.scrollToPoint(0, y, duration);
|
||||
}
|
||||
|
||||
/** Scroll by a specified X/Y distance in the component */
|
||||
@Method()
|
||||
scrollByPoint(x: number, y: number, duration: number): Promise<any> {
|
||||
return this.scrollToPoint(x + this.el.scrollLeft, y + this.el.scrollTop, duration);
|
||||
}
|
||||
|
||||
/** Scroll to a specified X/Y location in the component */
|
||||
@Method()
|
||||
scrollToPoint(x: number, y: number, duration: number): Promise<void> {
|
||||
// scroll animation loop w/ easing
|
||||
// credit https://gist.github.com/dezinezync/5487119
|
||||
|
||||
let resolve!: () => void;
|
||||
const promise = new Promise<void>(r => {
|
||||
resolve = r;
|
||||
});
|
||||
|
||||
const self = this;
|
||||
const el = self.el;
|
||||
if (!el) {
|
||||
// invalid element
|
||||
resolve();
|
||||
return promise;
|
||||
}
|
||||
|
||||
if (duration < 32) {
|
||||
el.scrollTop = y;
|
||||
el.scrollLeft = x;
|
||||
resolve();
|
||||
return promise;
|
||||
}
|
||||
|
||||
const fromY = el.scrollTop;
|
||||
const fromX = el.scrollLeft;
|
||||
|
||||
const maxAttempts = (duration / 16) + 100;
|
||||
|
||||
let startTime: number;
|
||||
let attempts = 0;
|
||||
let stopScroll = false;
|
||||
|
||||
// scroll loop
|
||||
function step(timeStamp: number) {
|
||||
attempts++;
|
||||
|
||||
if (!self.el || stopScroll || attempts > maxAttempts) {
|
||||
self.isScrolling = false;
|
||||
el.style.transform = el.style.webkitTransform = '';
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
const easedT = (--time) * time * time + 1;
|
||||
|
||||
if (fromY !== y) {
|
||||
el.scrollTop = (easedT * (y - fromY)) + fromY;
|
||||
}
|
||||
|
||||
if (fromX !== x) {
|
||||
el.scrollLeft = 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
|
||||
// TODO: remove as any
|
||||
self.queue.read(step as any);
|
||||
|
||||
} else {
|
||||
stopScroll = true;
|
||||
self.isScrolling = false;
|
||||
el.style.transform = el.style.webkitTransform = '';
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
// start scroll loop
|
||||
self.isScrolling = true;
|
||||
|
||||
// chill out for a frame first
|
||||
this.queue.write(() => {
|
||||
this.queue.write(timeStamp => {
|
||||
startTime = timeStamp;
|
||||
step(timeStamp);
|
||||
});
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
private onScrollStart() {
|
||||
this.isScrolling = true;
|
||||
this.ionScrollStart.emit({
|
||||
isScrolling: true
|
||||
});
|
||||
|
||||
if (this.watchDog) {
|
||||
clearInterval(this.watchDog);
|
||||
}
|
||||
// watchdog
|
||||
this.watchDog = setInterval(() => {
|
||||
if (this.lastScroll < Date.now() - 120) {
|
||||
this.onScrollEnd();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
private onScrollEnd() {
|
||||
|
||||
clearInterval(this.watchDog);
|
||||
this.watchDog = null;
|
||||
this.isScrolling = false;
|
||||
this.ionScrollEnd.emit({
|
||||
isScrolling: false
|
||||
});
|
||||
}
|
||||
|
||||
hostData() {
|
||||
return {
|
||||
class: {
|
||||
...createThemedClasses(this.mode, 'scroll'),
|
||||
overscroll: this.forceOverscroll
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
|
||||
// ******** DOM READ ****************
|
||||
function updateScrollDetail(
|
||||
detail: ScrollDetail,
|
||||
el: HTMLElement,
|
||||
timestamp: number,
|
||||
didStart: boolean
|
||||
) {
|
||||
const prevX = detail.currentX;
|
||||
const prevY = detail.currentY;
|
||||
const prevT = detail.timeStamp;
|
||||
const currentX = el.scrollLeft;
|
||||
const currentY = el.scrollTop;
|
||||
if (didStart) {
|
||||
// remember the start positions
|
||||
detail.startTimeStamp = timestamp;
|
||||
detail.startX = currentX;
|
||||
detail.startY = currentY;
|
||||
detail.velocityX = detail.velocityY = 0;
|
||||
}
|
||||
detail.timeStamp = timestamp;
|
||||
detail.currentX = detail.scrollLeft = currentX;
|
||||
detail.currentY = detail.scrollTop = currentY;
|
||||
detail.deltaX = currentX - detail.startX;
|
||||
detail.deltaY = currentY - detail.startY;
|
||||
|
||||
const timeDelta = timestamp - prevT;
|
||||
if (timeDelta > 0 && timeDelta < 100) {
|
||||
const velocityX = (currentX - prevX) / timeDelta;
|
||||
const velocityY = (currentY - prevY) / timeDelta;
|
||||
detail.velocityX = velocityX * 0.7 + detail.velocityX * 0.3;
|
||||
detail.velocityY = velocityY * 0.7 + detail.velocityY * 0.3;
|
||||
}
|
||||
}
|
||||
@@ -39,13 +39,6 @@ string
|
||||
Set the position of the tabbar, relative to the content. Available options: `"top"`, `"bottom"`.
|
||||
|
||||
|
||||
#### scrollable
|
||||
|
||||
boolean
|
||||
|
||||
If true, the tabs will be scrollable when there are enough tabs to overflow the width of the screen.
|
||||
|
||||
|
||||
#### selectedTab
|
||||
|
||||
HTMLIonTabElement
|
||||
@@ -100,13 +93,6 @@ string
|
||||
Set the position of the tabbar, relative to the content. Available options: `"top"`, `"bottom"`.
|
||||
|
||||
|
||||
#### scrollable
|
||||
|
||||
boolean
|
||||
|
||||
If true, the tabs will be scrollable when there are enough tabs to overflow the width of the screen.
|
||||
|
||||
|
||||
#### selected-tab
|
||||
|
||||
|
||||
|
||||
@@ -13,20 +13,9 @@
|
||||
contain: strict;
|
||||
}
|
||||
|
||||
ion-scroll {
|
||||
@include margin(0, 8px);
|
||||
|
||||
max-width: 650px;
|
||||
}
|
||||
|
||||
// TODO: REVIEW
|
||||
:host(.placement-top) .tab-button.tab-selected .tab-button-icon,
|
||||
:host(.placement-top) .tab-button.tab-selected .tab-button-text {
|
||||
transform: inherit;
|
||||
}
|
||||
|
||||
.scrollable .tab-button {
|
||||
flex: 1 0 auto;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -72,22 +72,3 @@
|
||||
:host(.placement-bottom) .tabbar-highlight {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
|
||||
// Overflow Scrolling
|
||||
// --------------------------------------------------
|
||||
|
||||
ion-scroll {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scroll-inner {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
ion-button.inactive {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@ import { createColorClasses } from '../../utils/theme';
|
||||
})
|
||||
export class Tabbar {
|
||||
|
||||
private scrollEl?: HTMLIonScrollElement;
|
||||
|
||||
@Prop() mode!: Mode;
|
||||
@Prop() color?: Color;
|
||||
|
||||
@@ -40,17 +38,11 @@ export class Tabbar {
|
||||
/** The selected tab component */
|
||||
@Prop() selectedTab?: HTMLIonTabElement;
|
||||
|
||||
/**
|
||||
* If true, the tabs will be scrollable when there are enough tabs to overflow the width of the screen.
|
||||
*/
|
||||
@Prop() scrollable = false;
|
||||
|
||||
/** The tabs to render */
|
||||
@Prop() tabs: HTMLIonTabElement[] = [];
|
||||
|
||||
@Watch('selectedTab')
|
||||
selectedTabChanged() {
|
||||
this.scrollToSelectedButton();
|
||||
this.updateHighlight();
|
||||
}
|
||||
|
||||
@@ -85,92 +77,14 @@ export class Tabbar {
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
this.updateBoundaries();
|
||||
this.updateHighlight();
|
||||
}
|
||||
|
||||
protected analyzeTabs() {
|
||||
const tabs: HTMLIonTabButtonElement[] = Array.from(this.doc.querySelectorAll('ion-tab-button'));
|
||||
const scrollLeft = this.scrollEl!.scrollLeft;
|
||||
const tabsWidth = this.scrollEl!.clientWidth;
|
||||
let previous: {tab: HTMLIonTabButtonElement, amount: number} | undefined;
|
||||
let next: {tab: HTMLIonTabButtonElement, amount: number} | undefined;
|
||||
|
||||
for (const tab of tabs) {
|
||||
const left = tab.offsetLeft;
|
||||
const right = left + tab.offsetWidth;
|
||||
|
||||
if (left < scrollLeft) {
|
||||
previous = { tab, amount: left };
|
||||
}
|
||||
|
||||
if (!next && right > (tabsWidth + scrollLeft)) {
|
||||
const amount = right - tabsWidth;
|
||||
next = { tab, amount };
|
||||
}
|
||||
}
|
||||
|
||||
return { previous, next };
|
||||
}
|
||||
|
||||
private getSelectedButton(): HTMLIonTabButtonElement | undefined {
|
||||
return Array.from(this.el.querySelectorAll('ion-tab-button'))
|
||||
.find(btn => btn.selected);
|
||||
}
|
||||
|
||||
protected scrollToSelectedButton() {
|
||||
if (!this.scrollEl || !this.scrollable) {
|
||||
return;
|
||||
}
|
||||
this.queue.read(() => {
|
||||
const activeTabButton = this.getSelectedButton();
|
||||
|
||||
if (activeTabButton) {
|
||||
const scrollLeft = this.scrollEl!.scrollLeft;
|
||||
const tabsWidth = this.scrollEl!.clientWidth;
|
||||
const left = activeTabButton.offsetLeft;
|
||||
const right = left + activeTabButton.offsetWidth;
|
||||
|
||||
let amount = 0;
|
||||
|
||||
if (right > (tabsWidth + scrollLeft)) {
|
||||
amount = right - tabsWidth;
|
||||
} else if (left < scrollLeft) {
|
||||
amount = left;
|
||||
}
|
||||
|
||||
if (amount !== 0) {
|
||||
this.queue.write(() => {
|
||||
this.scrollEl!.scrollToPoint(amount, 0, 250).then(() => {
|
||||
this.updateBoundaries();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private scrollByTab(direction: 'left' | 'right') {
|
||||
this.queue.read(() => {
|
||||
const { previous, next } = this.analyzeTabs();
|
||||
const info = direction === 'right' ? next : previous;
|
||||
const amount = info && info.amount;
|
||||
|
||||
if (info && amount) {
|
||||
this.scrollEl!.scrollToPoint(amount, 0, 250).then(() => {
|
||||
this.updateBoundaries();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private updateBoundaries() {
|
||||
if (this.scrollEl && this.scrollable) {
|
||||
this.canScrollLeft = this.scrollEl.scrollLeft !== 0;
|
||||
this.canScrollRight = this.scrollEl.scrollLeft < (this.scrollEl.scrollWidth - this.scrollEl.offsetWidth);
|
||||
}
|
||||
}
|
||||
|
||||
private updateHighlight() {
|
||||
if (!this.highlight) {
|
||||
return;
|
||||
@@ -185,7 +99,7 @@ export class Tabbar {
|
||||
}
|
||||
|
||||
hostData() {
|
||||
const { color, translucent, layout, placement, keyboardVisible, scrollable } = this;
|
||||
const { color, translucent, layout, placement, keyboardVisible } = this;
|
||||
return {
|
||||
role: 'tablist',
|
||||
'aria-hidden': keyboardVisible ? 'true' : null,
|
||||
@@ -195,56 +109,38 @@ export class Tabbar {
|
||||
[`layout-${layout}`]: true,
|
||||
[`placement-${placement}`]: true,
|
||||
'tabbar-hidden': keyboardVisible,
|
||||
'scrollable': scrollable
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const selectedTab = this.selectedTab;
|
||||
const ionTabbarHighlight = this.highlight ? <div class="animated tabbar-highlight" /> as HTMLElement : null;
|
||||
const tabButtons = this.tabs.map(tab => <ion-tab-button
|
||||
id={tab.btnId}
|
||||
label={tab.label}
|
||||
icon={tab.icon}
|
||||
badge={tab.badge}
|
||||
disabled={tab.disabled}
|
||||
badgeColor={tab.badgeColor}
|
||||
href={tab.href}
|
||||
selected={selectedTab === tab}
|
||||
mode={this.mode}
|
||||
color={this.color}
|
||||
aria-hidden={ !tab.show ? 'true' : null }
|
||||
class={{ 'tab-hidden': !tab.show }}
|
||||
onClick={ev => {
|
||||
if (!tab.disabled) {
|
||||
this.ionTabbarClick.emit(tab);
|
||||
}
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}}
|
||||
/>);
|
||||
|
||||
if (this.scrollable) {
|
||||
return [
|
||||
<ion-button onClick={() => this.scrollByTab('left')} fill="clear" class={{ inactive: !this.canScrollLeft }}>
|
||||
<ion-icon name="arrow-dropleft" lazy={false}/>
|
||||
</ion-button>,
|
||||
|
||||
<ion-scroll forceOverscroll={false} ref={scrollEl => this.scrollEl = scrollEl as HTMLIonScrollElement}>
|
||||
{tabButtons}
|
||||
{ionTabbarHighlight}
|
||||
</ion-scroll>,
|
||||
|
||||
<ion-button onClick={() => this.scrollByTab('right')} fill="clear" class={{ inactive: !this.canScrollRight }}>
|
||||
<ion-icon name="arrow-dropright" lazy={false}/>
|
||||
</ion-button>
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
...tabButtons,
|
||||
ionTabbarHighlight
|
||||
];
|
||||
}
|
||||
return [
|
||||
this.tabs.map(tab => (
|
||||
<ion-tab-button
|
||||
id={tab.btnId}
|
||||
label={tab.label}
|
||||
icon={tab.icon}
|
||||
badge={tab.badge}
|
||||
disabled={tab.disabled}
|
||||
badgeColor={tab.badgeColor}
|
||||
href={tab.href}
|
||||
selected={selectedTab === tab}
|
||||
mode={this.mode}
|
||||
color={this.color}
|
||||
aria-hidden={ !tab.show ? 'true' : null }
|
||||
class={{ 'tab-hidden': !tab.show }}
|
||||
onClick={ev => {
|
||||
if (!tab.disabled) {
|
||||
this.ionTabbarClick.emit(tab);
|
||||
}
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}}
|
||||
/>
|
||||
)),
|
||||
this.highlight && <div class="animated tabbar-highlight" />
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,13 +25,6 @@ string
|
||||
A unique name for the tabs.
|
||||
|
||||
|
||||
#### scrollable
|
||||
|
||||
boolean
|
||||
|
||||
If true, the tabs will be scrollable when there are enough tabs to overflow the width of the screen.
|
||||
|
||||
|
||||
#### tabbarHidden
|
||||
|
||||
boolean
|
||||
@@ -95,13 +88,6 @@ string
|
||||
A unique name for the tabs.
|
||||
|
||||
|
||||
#### scrollable
|
||||
|
||||
boolean
|
||||
|
||||
If true, the tabs will be scrollable when there are enough tabs to overflow the width of the screen.
|
||||
|
||||
|
||||
#### tabbar-hidden
|
||||
|
||||
boolean
|
||||
|
||||
@@ -21,7 +21,6 @@ export class Tabs implements NavOutlet {
|
||||
@State() selectedTab?: HTMLIonTabElement;
|
||||
|
||||
@Prop({ context: 'config' }) config!: Config;
|
||||
|
||||
@Prop({ context: 'document' }) doc!: Document;
|
||||
|
||||
/**
|
||||
@@ -64,11 +63,6 @@ export class Tabs implements NavOutlet {
|
||||
*/
|
||||
@Prop() translucent = false;
|
||||
|
||||
/**
|
||||
* If true, the tabs will be scrollable when there are enough tabs to overflow the width of the screen.
|
||||
*/
|
||||
@Prop() scrollable = false;
|
||||
|
||||
/**
|
||||
* If true, the tabs will use the router and `selectedTab` will not do anything.
|
||||
*/
|
||||
@@ -307,14 +301,12 @@ export class Tabs implements NavOutlet {
|
||||
}
|
||||
|
||||
render() {
|
||||
const dom = [
|
||||
return [
|
||||
<div class="tabs-inner">
|
||||
<slot></slot>
|
||||
</div>
|
||||
];
|
||||
</div>,
|
||||
|
||||
if (!this.tabbarHidden) {
|
||||
dom.push(
|
||||
!this.tabbarHidden && (
|
||||
<ion-tabbar
|
||||
tabs={this.tabs.slice()}
|
||||
color={this.color}
|
||||
@@ -322,12 +314,10 @@ export class Tabs implements NavOutlet {
|
||||
highlight={this.tabbarHighlight}
|
||||
placement={this.tabbarPlacement}
|
||||
layout={this.tabbarLayout}
|
||||
translucent={this.translucent}
|
||||
scrollable={this.scrollable}>
|
||||
translucent={this.translucent}>
|
||||
</ion-tabbar>
|
||||
);
|
||||
}
|
||||
return dom;
|
||||
)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html dir="ltr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Tab - Scroll</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||
<script src="/dist/ionic.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/css/ionic.min.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<ion-app>
|
||||
<ion-tabs scrollable="true" tabbar-placement="top" tabbar-highlight="true">
|
||||
<ion-tab label="Plain List" icon="star"></ion-tab>
|
||||
<ion-tab label="Schedule Long Text" icon="globe"></ion-tab>
|
||||
<ion-tab label="Stopwatch" icon="logo-facebook"></ion-tab>
|
||||
<ion-tab label="Messages" icon="chatboxes"></ion-tab>
|
||||
<ion-tab label="Messages 2" icon="chatboxes"></ion-tab>
|
||||
<ion-tab label="Messages 3" icon="chatboxes"></ion-tab>
|
||||
<ion-tab label="Messages 4" icon="chatboxes"></ion-tab>
|
||||
<ion-tab label="Messages 5" icon="chatboxes"></ion-tab>
|
||||
<ion-tab label="Messages 6" icon="chatboxes"></ion-tab>
|
||||
<ion-tab label="Messages 7" icon="chatboxes"></ion-tab>
|
||||
<ion-tab label="Messages 8" icon="chatboxes"></ion-tab>
|
||||
<ion-tab label="Messages 9" icon="chatboxes"></ion-tab>
|
||||
<ion-tab label="Messages 10" icon="chatboxes"></ion-tab>
|
||||
<ion-tab label="Messages 11" icon="chatboxes"></ion-tab>
|
||||
<ion-tab label="Messages 12" icon="chatboxes"></ion-tab>
|
||||
</ion-tabs>
|
||||
|
||||
<ion-nav-controller></ion-nav-controller>
|
||||
</ion-app>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -11,7 +11,7 @@ import { Range, calcCells, calcHeightIndex, doRender, findCellIndex, getRange, g
|
||||
})
|
||||
export class VirtualScroll {
|
||||
|
||||
private scrollEl?: HTMLIonScrollElement;
|
||||
private scrollEl?: HTMLElement;
|
||||
private range: Range = { offset: 0, length: 0 };
|
||||
private timerUpdate: any;
|
||||
private heightIndex?: Uint32Array;
|
||||
@@ -113,17 +113,17 @@ export class VirtualScroll {
|
||||
this.calcCells();
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
async componentDidLoad() {
|
||||
const contentEl = this.el.closest('ion-content');
|
||||
if (!contentEl) {
|
||||
console.error('virtual-scroll must be used inside ion-scroll/ion-content');
|
||||
console.error('virtual-scroll must be used inside ion-content');
|
||||
return;
|
||||
}
|
||||
contentEl.componentOnReady().then(() => {
|
||||
this.scrollEl = contentEl.getScrollElement();
|
||||
this.calcCells();
|
||||
this.updateState();
|
||||
});
|
||||
await contentEl.componentOnReady();
|
||||
|
||||
this.scrollEl = contentEl.getScrollElement();
|
||||
this.calcCells();
|
||||
this.updateState();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
|
||||
2
core/src/interface.d.ts
vendored
2
core/src/interface.d.ts
vendored
@@ -11,7 +11,7 @@ export * from './components/popover/popover-interface';
|
||||
export * from './components/nav/nav-interface';
|
||||
export * from './components/router/utils/interface';
|
||||
export * from './components/range/range-interface';
|
||||
export * from './components/scroll/scroll-interface';
|
||||
export * from './components/content/content-interface';
|
||||
export * from './components/select/select-interface';
|
||||
export * from './components/select-popover/select-popover-interface';
|
||||
export * from './components/tabbar/tabbar-interface';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isFocused, relocateInput } from './common';
|
||||
|
||||
export function enableHideCaretOnScroll(componentEl: HTMLElement, inputEl: HTMLInputElement | undefined, scrollEl: HTMLIonScrollElement | undefined) {
|
||||
export function enableHideCaretOnScroll(componentEl: HTMLElement, inputEl: HTMLInputElement | undefined, scrollEl: HTMLIonContentElement | undefined) {
|
||||
if (!scrollEl || !inputEl) {
|
||||
return () => { return; };
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ function jsSetFocus(
|
||||
inputEl.focus();
|
||||
|
||||
// scroll the input into place
|
||||
contentEl.getScrollElement().scrollByPoint(0, scrollData.scrollAmount, scrollData.scrollDuration).then(() => {
|
||||
contentEl.scrollByPoint(0, scrollData.scrollAmount, scrollData.scrollDuration).then(() => {
|
||||
// the scroll view is in the correct position now
|
||||
// give the native text input focus
|
||||
relocateInput(componentEl, inputEl, false, scrollData.inputSafeY);
|
||||
|
||||
@@ -26,8 +26,7 @@ export function startInputShims(
|
||||
|
||||
function registerInput(componentEl: HTMLElement) {
|
||||
const inputEl = (componentEl.shadowRoot || componentEl).querySelector('input');
|
||||
const contentEl = componentEl.closest('ion-content');
|
||||
const scrollEl = contentEl && contentEl.getScrollElement();
|
||||
const scrollEl = componentEl.closest('ion-content');
|
||||
|
||||
if (!inputEl) {
|
||||
return;
|
||||
@@ -38,8 +37,8 @@ export function startInputShims(
|
||||
hideCaretMap.set(componentEl, rmFn);
|
||||
}
|
||||
|
||||
if (SCROLL_ASSIST && contentEl && scrollAssist && !scrollAssistMap.has(componentEl)) {
|
||||
const rmFn = enableScrollAssist(componentEl, inputEl, contentEl, keyboardHeight);
|
||||
if (SCROLL_ASSIST && scrollEl && scrollAssist && !scrollAssistMap.has(componentEl)) {
|
||||
const rmFn = enableScrollAssist(componentEl, inputEl, scrollEl, keyboardHeight);
|
||||
scrollAssistMap.set(componentEl, rmFn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export function startStatusTap(win: Window, queue: QueueApi) {
|
||||
if (contentEl) {
|
||||
await contentEl.componentOnReady();
|
||||
queue.write(() => {
|
||||
contentEl.getScrollElement().scrollToTop(300);
|
||||
contentEl.scrollToTop(300);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ exports.config = {
|
||||
{ components: ['ion-alert', 'ion-alert-controller'] },
|
||||
{ components: ['ion-anchor', 'ion-back-button'] },
|
||||
{ components: ['ion-animation-controller'] },
|
||||
{ components: ['ion-app', 'ion-buttons', 'ion-content', 'ion-footer', 'ion-header', 'ion-scroll', 'ion-title', 'ion-toolbar'] },
|
||||
{ components: ['ion-app', 'ion-buttons', 'ion-content', 'ion-footer', 'ion-header', 'ion-title', 'ion-toolbar'] },
|
||||
{ components: ['ion-avatar', 'ion-badge', 'ion-thumbnail'] },
|
||||
{ components: ['ion-backdrop'] },
|
||||
{ components: ['ion-button', 'ion-icon'] },
|
||||
|
||||
Reference in New Issue
Block a user