mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-15 01:03:03 +08:00
537 lines
14 KiB
TypeScript
537 lines
14 KiB
TypeScript
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, Watch, h } from '@stencil/core';
|
|
|
|
import { getIonMode } from '../../global/ionic-global';
|
|
import { componentOnReady } from '../../utils/helpers'
|
|
|
|
import { SwiperInterface, SwiperOptions } from './swiper/swiper-interface';
|
|
|
|
/**
|
|
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
|
|
*/
|
|
@Component({
|
|
tag: 'ion-slides',
|
|
styleUrls: {
|
|
ios: 'slides.ios.scss',
|
|
md: 'slides.md.scss'
|
|
},
|
|
assetsDirs: ['swiper'],
|
|
})
|
|
export class Slides implements ComponentInterface {
|
|
|
|
private scrollbarEl?: HTMLElement;
|
|
private paginationEl?: HTMLElement;
|
|
private swiperReady = false;
|
|
private mutationO?: MutationObserver;
|
|
private readySwiper!: (swiper: SwiperInterface) => void;
|
|
private swiper: Promise<SwiperInterface> = new Promise(resolve => { this.readySwiper = resolve; });
|
|
private syncSwiper?: SwiperInterface;
|
|
private didInit = false;
|
|
|
|
@Element() el!: HTMLIonSlidesElement;
|
|
|
|
/**
|
|
* Options to pass to the swiper instance.
|
|
* See http://idangero.us/swiper/api/ for valid options
|
|
*/
|
|
@Prop() options: any = {}; // SwiperOptions; // TODO
|
|
|
|
@Watch('options')
|
|
async optionsChanged() {
|
|
if (this.swiperReady) {
|
|
const swiper = await this.getSwiper();
|
|
Object.assign(swiper.params, this.options);
|
|
await this.update();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If `true`, show the pagination.
|
|
*/
|
|
@Prop() pager = false;
|
|
|
|
/**
|
|
* If `true`, show the scrollbar.
|
|
*/
|
|
@Prop() scrollbar = false;
|
|
|
|
/**
|
|
* Emitted after Swiper initialization
|
|
*/
|
|
@Event() ionSlidesDidLoad!: EventEmitter<void>;
|
|
|
|
/**
|
|
* Emitted when the user taps/clicks on the slide's container.
|
|
*/
|
|
@Event() ionSlideTap!: EventEmitter<void>;
|
|
|
|
/**
|
|
* Emitted when the user double taps on the slide's container.
|
|
*/
|
|
@Event() ionSlideDoubleTap!: EventEmitter<void>;
|
|
|
|
/**
|
|
* Emitted before the active slide has changed.
|
|
*/
|
|
@Event() ionSlideWillChange!: EventEmitter<void>;
|
|
|
|
/**
|
|
* Emitted after the active slide has changed.
|
|
*/
|
|
@Event() ionSlideDidChange!: EventEmitter<void>;
|
|
|
|
/**
|
|
* Emitted when the next slide has started.
|
|
*/
|
|
@Event() ionSlideNextStart!: EventEmitter<void>;
|
|
|
|
/**
|
|
* Emitted when the previous slide has started.
|
|
*/
|
|
@Event() ionSlidePrevStart!: EventEmitter<void>;
|
|
|
|
/**
|
|
* Emitted when the next slide has ended.
|
|
*/
|
|
@Event() ionSlideNextEnd!: EventEmitter<void>;
|
|
|
|
/**
|
|
* Emitted when the previous slide has ended.
|
|
*/
|
|
@Event() ionSlidePrevEnd!: EventEmitter<void>;
|
|
|
|
/**
|
|
* Emitted when the slide transition has started.
|
|
*/
|
|
@Event() ionSlideTransitionStart!: EventEmitter<void>;
|
|
|
|
/**
|
|
* Emitted when the slide transition has ended.
|
|
*/
|
|
@Event() ionSlideTransitionEnd!: EventEmitter<void>;
|
|
|
|
/**
|
|
* Emitted when the slider is actively being moved.
|
|
*/
|
|
@Event() ionSlideDrag!: EventEmitter<void>;
|
|
|
|
/**
|
|
* Emitted when the slider is at its initial position.
|
|
*/
|
|
@Event() ionSlideReachStart!: EventEmitter<void>;
|
|
|
|
/**
|
|
* Emitted when the slider is at the last slide.
|
|
*/
|
|
@Event() ionSlideReachEnd!: EventEmitter<void>;
|
|
|
|
/**
|
|
* Emitted when the user first touches the slider.
|
|
*/
|
|
@Event() ionSlideTouchStart!: EventEmitter<void>;
|
|
|
|
/**
|
|
* Emitted when the user releases the touch.
|
|
*/
|
|
@Event() ionSlideTouchEnd!: EventEmitter<void>;
|
|
|
|
connectedCallback() {
|
|
// tslint:disable-next-line: strict-type-predicates
|
|
if (typeof MutationObserver !== 'undefined') {
|
|
const mut = this.mutationO = new MutationObserver(() => {
|
|
if (this.swiperReady) {
|
|
this.update();
|
|
}
|
|
});
|
|
mut.observe(this.el, {
|
|
childList: true,
|
|
subtree: true
|
|
});
|
|
|
|
componentOnReady(this.el, () => {
|
|
if (!this.didInit) {
|
|
this.didInit = true;
|
|
this.initSwiper();
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
if (this.mutationO) {
|
|
this.mutationO.disconnect();
|
|
this.mutationO = undefined;
|
|
}
|
|
|
|
/**
|
|
* We need to synchronously destroy
|
|
* swiper otherwise it is possible
|
|
* that it will be left in a
|
|
* destroyed state if connectedCallback
|
|
* is called multiple times
|
|
*/
|
|
const swiper = this.syncSwiper;
|
|
if (swiper !== undefined) {
|
|
swiper.destroy(true, true);
|
|
this.swiper = new Promise(resolve => { this.readySwiper = resolve; });
|
|
this.swiperReady = false;
|
|
this.syncSwiper = undefined;
|
|
}
|
|
|
|
this.didInit = false;
|
|
}
|
|
|
|
/**
|
|
* Update the underlying slider implementation. Call this if you've added or removed
|
|
* child slides.
|
|
*/
|
|
@Method()
|
|
async update() {
|
|
const [swiper] = await Promise.all([
|
|
this.getSwiper(),
|
|
waitForSlides(this.el)
|
|
]);
|
|
swiper.update();
|
|
}
|
|
|
|
/**
|
|
* Force swiper to update its height (when autoHeight is enabled) for the duration
|
|
* equal to 'speed' parameter.
|
|
*
|
|
* @param speed The transition duration (in ms).
|
|
*/
|
|
@Method()
|
|
async updateAutoHeight(speed?: number) {
|
|
const swiper = await this.getSwiper();
|
|
swiper.updateAutoHeight(speed);
|
|
}
|
|
|
|
/**
|
|
* Transition to the specified slide.
|
|
*
|
|
* @param index The index of the slide to transition to.
|
|
* @param speed The transition duration (in ms).
|
|
* @param runCallbacks If true, the transition will produce [Transition/SlideChange][Start/End] transition events.
|
|
*/
|
|
@Method()
|
|
async slideTo(index: number, speed?: number, runCallbacks?: boolean) {
|
|
const swiper = await this.getSwiper();
|
|
swiper.slideTo(index, speed, runCallbacks);
|
|
}
|
|
|
|
/**
|
|
* Transition to the next slide.
|
|
*
|
|
* @param speed The transition duration (in ms).
|
|
* @param runCallbacks If true, the transition will produce [Transition/SlideChange][Start/End] transition events.
|
|
*/
|
|
@Method()
|
|
async slideNext(speed?: number, runCallbacks?: boolean) {
|
|
const swiper = await this.getSwiper();
|
|
swiper.slideNext(speed!, runCallbacks!);
|
|
}
|
|
|
|
/**
|
|
* Transition to the previous slide.
|
|
*
|
|
* @param speed The transition duration (in ms).
|
|
* @param runCallbacks If true, the transition will produce the [Transition/SlideChange][Start/End] transition events.
|
|
*/
|
|
@Method()
|
|
async slidePrev(speed?: number, runCallbacks?: boolean) {
|
|
const swiper = await this.getSwiper();
|
|
swiper.slidePrev(speed, runCallbacks);
|
|
}
|
|
|
|
/**
|
|
* Get the index of the active slide.
|
|
*/
|
|
@Method()
|
|
async getActiveIndex(): Promise<number> {
|
|
const swiper = await this.getSwiper();
|
|
return swiper.activeIndex;
|
|
}
|
|
|
|
/**
|
|
* Get the index of the previous slide.
|
|
*/
|
|
@Method()
|
|
async getPreviousIndex(): Promise<number> {
|
|
const swiper = await this.getSwiper();
|
|
return swiper.previousIndex;
|
|
}
|
|
|
|
/**
|
|
* Get the total number of slides.
|
|
*/
|
|
@Method()
|
|
async length(): Promise<number> {
|
|
const swiper = await this.getSwiper();
|
|
return swiper.slides.length;
|
|
}
|
|
|
|
/**
|
|
* Get whether or not the current slide is the last slide.
|
|
*/
|
|
@Method()
|
|
async isEnd(): Promise<boolean> {
|
|
const swiper = await this.getSwiper();
|
|
return swiper.isEnd;
|
|
}
|
|
|
|
/**
|
|
* Get whether or not the current slide is the first slide.
|
|
*/
|
|
@Method()
|
|
async isBeginning(): Promise<boolean> {
|
|
const swiper = await this.getSwiper();
|
|
return swiper.isBeginning;
|
|
}
|
|
|
|
/**
|
|
* Start auto play.
|
|
*/
|
|
@Method()
|
|
async startAutoplay() {
|
|
const swiper = await this.getSwiper();
|
|
if (swiper.autoplay) {
|
|
swiper.autoplay.start();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop auto play.
|
|
*/
|
|
@Method()
|
|
async stopAutoplay() {
|
|
const swiper = await this.getSwiper();
|
|
if (swiper.autoplay) {
|
|
swiper.autoplay.stop();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lock or unlock the ability to slide to the next slide.
|
|
*
|
|
* @param lock If `true`, disable swiping to the next slide.
|
|
*/
|
|
@Method()
|
|
async lockSwipeToNext(lock: boolean) {
|
|
const swiper = await this.getSwiper();
|
|
swiper.allowSlideNext = !lock;
|
|
}
|
|
|
|
/**
|
|
* Lock or unlock the ability to slide to the previous slide.
|
|
*
|
|
* @param lock If `true`, disable swiping to the previous slide.
|
|
*/
|
|
@Method()
|
|
async lockSwipeToPrev(lock: boolean) {
|
|
const swiper = await this.getSwiper();
|
|
swiper.allowSlidePrev = !lock;
|
|
}
|
|
|
|
/**
|
|
* Lock or unlock the ability to slide to the next or previous slide.
|
|
*
|
|
* @param lock If `true`, disable swiping to the next and previous slide.
|
|
*/
|
|
@Method()
|
|
async lockSwipes(lock: boolean) {
|
|
const swiper = await this.getSwiper();
|
|
swiper.allowSlideNext = !lock;
|
|
swiper.allowSlidePrev = !lock;
|
|
swiper.allowTouchMove = !lock;
|
|
}
|
|
|
|
/**
|
|
* Get the Swiper instance.
|
|
* Use this to access the full Swiper API.
|
|
* See https://idangero.us/swiper/api/ for all API options.
|
|
*/
|
|
@Method()
|
|
async getSwiper(): Promise<any> {
|
|
return this.swiper;
|
|
}
|
|
|
|
private async initSwiper() {
|
|
const finalOptions = this.normalizeOptions();
|
|
|
|
// init swiper core
|
|
// @ts-ignore
|
|
const { Swiper } = await import('./swiper/swiper.bundle.js');
|
|
await waitForSlides(this.el);
|
|
const swiper = new Swiper(this.el, finalOptions);
|
|
this.swiperReady = true;
|
|
this.syncSwiper = swiper;
|
|
this.readySwiper(swiper);
|
|
}
|
|
|
|
private normalizeOptions(): SwiperOptions {
|
|
// Base options, can be changed
|
|
// TODO Add interface SwiperOptions
|
|
const swiperOptions: SwiperOptions = {
|
|
effect: undefined,
|
|
direction: 'horizontal',
|
|
initialSlide: 0,
|
|
loop: false,
|
|
parallax: false,
|
|
slidesPerView: 1,
|
|
spaceBetween: 0,
|
|
speed: 300,
|
|
slidesPerColumn: 1,
|
|
slidesPerColumnFill: 'column',
|
|
slidesPerGroup: 1,
|
|
centeredSlides: false,
|
|
slidesOffsetBefore: 0,
|
|
slidesOffsetAfter: 0,
|
|
touchEventsTarget: 'container',
|
|
autoplay: false,
|
|
freeMode: false,
|
|
freeModeMomentum: true,
|
|
freeModeMomentumRatio: 1,
|
|
freeModeMomentumBounce: true,
|
|
freeModeMomentumBounceRatio: 1,
|
|
freeModeMomentumVelocityRatio: 1,
|
|
freeModeSticky: false,
|
|
freeModeMinimumVelocity: 0.02,
|
|
autoHeight: false,
|
|
setWrapperSize: false,
|
|
zoom: {
|
|
maxRatio: 3,
|
|
minRatio: 1,
|
|
toggle: false,
|
|
},
|
|
touchRatio: 1,
|
|
touchAngle: 45,
|
|
simulateTouch: true,
|
|
touchStartPreventDefault: false,
|
|
shortSwipes: true,
|
|
longSwipes: true,
|
|
longSwipesRatio: 0.5,
|
|
longSwipesMs: 300,
|
|
followFinger: true,
|
|
threshold: 0,
|
|
touchMoveStopPropagation: true,
|
|
touchReleaseOnEdges: false,
|
|
iOSEdgeSwipeDetection: false,
|
|
iOSEdgeSwipeThreshold: 20,
|
|
resistance: true,
|
|
resistanceRatio: 0.85,
|
|
watchSlidesProgress: false,
|
|
watchSlidesVisibility: false,
|
|
preventClicks: true,
|
|
preventClicksPropagation: true,
|
|
slideToClickedSlide: false,
|
|
loopAdditionalSlides: 0,
|
|
noSwiping: true,
|
|
runCallbacksOnInit: true,
|
|
coverflowEffect: {
|
|
rotate: 50,
|
|
stretch: 0,
|
|
depth: 100,
|
|
modifier: 1,
|
|
slideShadows: true
|
|
},
|
|
flipEffect: {
|
|
slideShadows: true,
|
|
limitRotation: true
|
|
},
|
|
cubeEffect: {
|
|
slideShadows: true,
|
|
shadow: true,
|
|
shadowOffset: 20,
|
|
shadowScale: 0.94
|
|
},
|
|
fadeEffect: {
|
|
crossFade: false
|
|
},
|
|
a11y: {
|
|
prevSlideMessage: 'Previous slide',
|
|
nextSlideMessage: 'Next slide',
|
|
firstSlideMessage: 'This is the first slide',
|
|
lastSlideMessage: 'This is the last slide'
|
|
}
|
|
};
|
|
|
|
if (this.pager) {
|
|
swiperOptions.pagination = {
|
|
el: this.paginationEl!,
|
|
type: 'bullets',
|
|
clickable: false,
|
|
hideOnClick: false,
|
|
};
|
|
}
|
|
|
|
if (this.scrollbar) {
|
|
swiperOptions.scrollbar = {
|
|
el: this.scrollbarEl!,
|
|
hide: true,
|
|
};
|
|
}
|
|
|
|
// Keep the event options separate, we dont want users
|
|
// overwriting these
|
|
const eventOptions: SwiperOptions = {
|
|
on: {
|
|
init: () => {
|
|
setTimeout(() => {
|
|
this.ionSlidesDidLoad.emit();
|
|
}, 20);
|
|
},
|
|
slideChangeTransitionStart: this.ionSlideWillChange.emit,
|
|
slideChangeTransitionEnd: this.ionSlideDidChange.emit,
|
|
slideNextTransitionStart: this.ionSlideNextStart.emit,
|
|
slidePrevTransitionStart: this.ionSlidePrevStart.emit,
|
|
slideNextTransitionEnd: this.ionSlideNextEnd.emit,
|
|
slidePrevTransitionEnd: this.ionSlidePrevEnd.emit,
|
|
transitionStart: this.ionSlideTransitionStart.emit,
|
|
transitionEnd: this.ionSlideTransitionEnd.emit,
|
|
sliderMove: this.ionSlideDrag.emit,
|
|
reachBeginning: this.ionSlideReachStart.emit,
|
|
reachEnd: this.ionSlideReachEnd.emit,
|
|
touchStart: this.ionSlideTouchStart.emit,
|
|
touchEnd: this.ionSlideTouchEnd.emit,
|
|
tap: this.ionSlideTap.emit,
|
|
doubleTap: this.ionSlideDoubleTap.emit
|
|
}
|
|
};
|
|
|
|
const customEvents = (!!this.options && !!this.options.on) ? this.options.on : {};
|
|
|
|
// merge "on" event listeners, while giving our event listeners priority
|
|
const mergedEventOptions = { on: { ...customEvents, ...eventOptions.on } };
|
|
|
|
// Merge the base, user options, and events together then pas to swiper
|
|
return { ...swiperOptions, ...this.options, ...mergedEventOptions };
|
|
}
|
|
|
|
render() {
|
|
const mode = getIonMode(this);
|
|
return (
|
|
<Host
|
|
class={{
|
|
[`${mode}`]: true,
|
|
|
|
// Used internally for styling
|
|
[`slides-${mode}`]: true,
|
|
|
|
'swiper-container': true
|
|
}}
|
|
>
|
|
<div class="swiper-wrapper">
|
|
<slot></slot>
|
|
</div>
|
|
{this.pager && <div class="swiper-pagination" ref={el => this.paginationEl = el}></div>}
|
|
{this.scrollbar && <div class="swiper-scrollbar" ref={el => this.scrollbarEl = el}></div>}
|
|
</Host>
|
|
);
|
|
}
|
|
}
|
|
|
|
const waitForSlides = (el: HTMLElement) => {
|
|
return Promise.all(
|
|
Array.from(el.querySelectorAll('ion-slide')).map(s => new Promise(resolve => componentOnReady(s, resolve)))
|
|
);
|
|
};
|