@zebra-ui/swiper
Version:
专为多端设计的高性能swiper轮播组件库,支持多种复杂的 3D swiper轮播效果。
876 lines (783 loc) • 23.5 kB
text/typescript
import {
extend,
deleteProps,
createElement,
elementChildren,
elementStyle,
elementIndex,
isWeb
} from '../shared/utils'
import { getSupport } from '../shared/get-support'
import { getDevice } from '../shared/get-device'
import { getBrowser } from '../shared/get-browser'
import Resize from './modules/resize/resize'
import Observer from './modules/observer/observer'
import eventsEmitter from './events-emitter'
import update from './update'
import translate from './translate'
import transition from './transition'
import slide from './slide'
import loop from './loop'
import grabCursor from './grab-cursor'
import events from './events'
import breakpoints from './breakpoints'
import checkOverflow from './check-overflow'
import classes from './classes'
import defaults from './defaults'
import moduleExtendParams from './module-extend-params'
import { processLazyPreloader, preload } from '../shared/process-lazy-preloader'
import type { SwiperClass } from '../../types/components/core/core'
import type { SwiperOptions } from '../../types/swiper-options'
import type { SwiperModule } from '../../types/shared'
import type { SwiperItemInstance } from '../../types/swiper-item-instance'
import type { SwiperInterface } from '../../types/swiper-class'
import type { VirtualOptions } from '../../types/modules/virtual'
const prototypes = {
eventsEmitter,
update,
translate,
transition,
slide,
loop,
grabCursor,
events,
breakpoints,
checkOverflow,
classes
}
const extendedDefaults: SwiperOptions = {}
class Swiper {
[x: string]: any
constructor(...args: [HTMLElement | string | SwiperOptions, SwiperOptions?]) {
let el: HTMLElement | string | undefined
let params: SwiperOptions
if (
args.length === 1 &&
args[0].constructor &&
Object.prototype.toString.call(args[0]).slice(8, -1) === 'Object'
) {
params = args[0] as SwiperOptions
} else {
;[el, params] = args as [HTMLElement | string, SwiperOptions]
}
if (!params) params = {}
params = extend({}, params)
if (el && !params.el) params.el = el
if (
params.el &&
typeof params.el === 'string' &&
document.querySelectorAll(params.el).length > 1
) {
const swipers: Swiper[] = []
document.querySelectorAll(params.el).forEach((containerEl) => {
const newParams = extend({}, params, { el: containerEl as HTMLElement })
swipers.push(new Swiper(newParams))
})
// eslint-disable-next-line no-constructor-return
// @ts-ignore
return swipers
}
const swiper = this
swiper.__swiper__ = true
swiper.support = getSupport()
swiper.device = getDevice({ userAgent: params.userAgent as string })
swiper.browser = getBrowser()
swiper.eventsListeners = {}
swiper.eventsAnyListeners = []
if (typeof swiper.modules === 'undefined') {
swiper.modules = []
}
swiper.modules = [...(swiper as any).__modules__]
if (params.modules && Array.isArray(params.modules)) {
swiper.modules.push(...params.modules)
}
const allModulesParams: Record<string, any> = {}
swiper.modules.forEach((mod: any) => {
mod({
params,
swiper,
extendParams: moduleExtendParams(params, allModulesParams),
on: swiper.on.bind(swiper),
once: swiper.once.bind(swiper),
off: swiper.off.bind(swiper),
emit: swiper.emit.bind(swiper)
})
})
// Extend defaults with modules params
const swiperParams = extend({}, defaults, allModulesParams)
// Extend defaults with passed params
swiper.params = extend({}, swiperParams, extendedDefaults, params)
swiper.originalParams = extend({}, swiper.params)
swiper.passedParams = extend({}, params)
// add event listeners
if (swiper.params && swiper.params.on) {
Object.keys(swiper.params.on).forEach((eventName) => {
// @ts-ignore
swiper.on(eventName, swiper.params.on[eventName])
})
}
if (swiper.params && swiper.params.onAny) {
swiper.onAny(swiper.params.onAny)
}
// Extend Swiper
Object.assign(swiper, {
enabled: swiper.params.enabled,
el,
// Classes
classNames: [],
// Slides
slides: [],
slidesGrid: [],
snapGrid: [],
slidesSizesGrid: [],
// isDirection
isHorizontal() {
return swiper.params.direction === 'horizontal'
},
isVertical() {
return swiper.params.direction === 'vertical'
},
// Indexes
activeIndex: 0,
realIndex: 0,
//
isBeginning: true,
isEnd: false,
// Props
translate: 0,
previousTranslate: 0,
progress: 0,
velocity: 0,
animating: false,
cssOverflowAdjustment() {
// Returns 0 unless `translate` is > 2**23
// Should be subtracted from css values to prevent overflow
return Math.trunc(swiper.translate / 2 ** 23) * 2 ** 23
},
// Locks
allowSlideNext: swiper.params.allowSlideNext,
allowSlidePrev: swiper.params.allowSlidePrev,
// Touch Events
touchEventsData: {
isTouched: undefined,
isMoved: undefined,
allowTouchCallbacks: undefined,
touchStartTime: undefined,
isScrolling: undefined,
currentTranslate: undefined,
startTranslate: undefined,
allowThresholdMove: undefined,
// Form elements to match
focusableElements: swiper.params.focusableElements,
// Last click time
lastClickTime: 0,
clickTimeout: undefined,
// Velocities
velocities: [],
allowMomentumBounce: undefined,
startMoving: undefined,
pointerId: null,
touchId: null
},
// Clicks
allowClick: true,
// Touches
allowTouchMove: swiper.params.allowTouchMove,
touches: {
startX: 0,
startY: 0,
currentX: 0,
currentY: 0,
diff: 0
},
// Images
imagesToLoad: [],
imagesLoaded: 0
})
swiper.emit('_swiper')
// Init
if (swiper.params.init) {
swiper.init(el as HTMLElement)
}
return swiper
}
getDirectionLabel(property: string): string {
const swiper = this
if (swiper.isHorizontal()) {
return property
}
// prettier-ignore
return {
'width': 'height',
'margin-top': 'margin-left',
'margin-bottom ': 'margin-right',
'margin-left': 'margin-top',
'margin-right': 'margin-bottom',
'padding-left': 'padding-top',
'padding-right': 'padding-bottom',
'marginRight': 'marginBottom',
}[property] as string
}
getSlideIndex(slideEl: HTMLElement): number {
const swiper = this
const { slidesEl, params } = swiper
const slides = elementChildren(
slidesEl,
`.${params.slideClass}, swiper-slide`
)
const firstSlideIndex = elementIndex(slides[0], slidesEl)
// @ts-ignore
return elementIndex(slideEl, slidesEl) - Number(firstSlideIndex)
}
getSlideIndexByData(index: number): number {
if (isWeb()) {
return this.getSlideIndex(
this.slides.filter(
(slideEl: HTMLElement) =>
Number(slideEl.getAttribute('data-swiper-slide-index')) * 1 ===
index
)[0]
)
}
return this.getSlideIndex(
this.slides.filter(
(slideEl: HTMLElement) =>
Number(slideEl.getAttribute('data-swiper-slide-index')) * 1 === index
)[0]
)
}
recalcSlides(): void {
const swiper = this
const { slidesEl, params } = swiper
swiper.slides = elementChildren(
slidesEl,
`.${params.slideClass}, swiper-slide`
)
}
enable(): void {
const swiper = this
if (swiper.enabled) return
swiper.enabled = true
if (swiper.params.grabCursor) {
swiper.setGrabCursor()
}
swiper.emit('enable')
}
disable(): void {
const swiper = this
if (!swiper.enabled) return
swiper.enabled = false
if (swiper.params.grabCursor) {
swiper.unsetGrabCursor()
}
swiper.emit('disable')
}
setProgress(progress: number, speed?: number): void {
const swiper = this
progress = Math.min(Math.max(progress, 0), 1)
const min = swiper.minTranslate()
const max = swiper.maxTranslate()
const current = (max - min) * progress + min
swiper.translateTo(current, typeof speed === 'undefined' ? 0 : speed)
swiper.updateActiveIndex()
swiper.updateSlidesClasses()
}
emitContainerClasses(): void {
const swiper = this
if (!swiper.params._emitClasses || !swiper.el) return
const cls = (swiper.el as HTMLElement).className
.split(' ')
.filter((className) => {
return (
className.indexOf('swiper') === 0 ||
className.indexOf(swiper.params.containerModifierClass as string) ===
0
)
})
swiper.emit('_containerClasses', cls.join(' '))
}
getSlideClasses(slideEl: HTMLElement): string {
const swiper = this
if (swiper.destroyed) return ''
return slideEl.className
.split(' ')
.filter((className) => {
return (
className.indexOf('swiper-slide') === 0 ||
className.indexOf(swiper.params.slideClass as string) === 0
)
})
.join(' ')
}
emitSlidesClasses(): void {
const swiper = this
if (!swiper.params._emitClasses || !swiper.el) return
const updates: Array<{ slideEl: HTMLElement; classNames: string }> = []
swiper.slides.forEach((slideEl: HTMLElement) => {
const classNames = swiper.getSlideClasses(slideEl)
updates.push({ slideEl, classNames })
swiper.emit('_slideClass', slideEl, classNames)
})
swiper.emit('_slideClasses', updates)
}
slidesPerViewDynamic(view = 'current', exact = false): number {
const swiper = this
const {
params,
slides,
slidesGrid,
slidesSizesGrid,
size: swiperSize,
activeIndex
} = swiper
let spv = 1
if (typeof params.slidesPerView === 'number') return params.slidesPerView
if (params.centeredSlides) {
let slideSize = slides[activeIndex]
? Math.ceil(
(slides[activeIndex] as unknown as SwiperItemInstance)
.swiperSlideSize
)
: 0
let breakLoop
for (let i = activeIndex + 1; i < slides.length; i += 1) {
if (slides[i] && !breakLoop) {
slideSize += Math.ceil(
(slides[i] as unknown as SwiperItemInstance).swiperSlideSize
)
spv += 1
if (slideSize > swiperSize) breakLoop = true
}
}
for (let i = activeIndex - 1; i >= 0; i -= 1) {
if (slides[i] && !breakLoop) {
slideSize += (slides[i] as unknown as SwiperItemInstance)
.swiperSlideSize
spv += 1
if (slideSize > swiperSize) breakLoop = true
}
}
} else {
// eslint-disable-next-line
if (view === 'current') {
for (let i = activeIndex + 1; i < slides.length; i += 1) {
const slideInView = exact
? slidesGrid[i] + slidesSizesGrid[i] - slidesGrid[activeIndex] <
swiperSize
: slidesGrid[i] - slidesGrid[activeIndex] < swiperSize
if (slideInView) {
spv += 1
}
}
} else {
// previous
for (let i = activeIndex - 1; i >= 0; i -= 1) {
const slideInView =
slidesGrid[activeIndex] - slidesGrid[i] < swiperSize
if (slideInView) {
spv += 1
}
}
}
}
return spv
}
update() {
const swiper = this
if (!swiper || swiper.destroyed) return
const { snapGrid, params } = swiper
// Breakpoints
if (params.breakpoints) {
swiper.setBreakpoint()
}
if (isWeb()) {
Array.from(
(swiper.el as HTMLElement).querySelectorAll('[loading="lazy"]')
).forEach((imageEl) => {
if ((imageEl as HTMLImageElement).complete) {
processLazyPreloader(
swiper as unknown as SwiperInterface,
imageEl as HTMLElement
)
}
})
}
swiper.updateSize()
swiper.updateSlides()
swiper.updateProgress()
swiper.updateSlidesClasses()
function setTranslate() {
const translateValue = swiper.rtlTranslate
? swiper.translate * -1
: swiper.translate
const newTranslate = Math.min(
Math.max(translateValue, swiper.maxTranslate()),
swiper.minTranslate()
)
swiper.setTranslate(newTranslate)
swiper.updateActiveIndex()
swiper.updateSlidesClasses()
}
let translated: boolean | undefined
if (
params.freeMode &&
typeof params.freeMode === 'object' &&
params.freeMode.enabled &&
!params.cssMode
) {
setTranslate()
if (params.autoHeight) {
swiper.updateAutoHeight()
}
} else {
if (
(params.slidesPerView === 'auto' ||
(params.slidesPerView && params.slidesPerView > 1)) &&
swiper.isEnd &&
!params.centeredSlides
) {
const slides =
swiper.virtual &&
typeof params.virtual === 'object' &&
params.virtual.enabled
? (swiper.virtual as VirtualOptions<any>).slides
: swiper.slides
translated = swiper.slideTo((slides || []).length - 1, 0, false, true)
} else {
translated = swiper.slideTo(swiper.activeIndex, 0, false, true)
}
if (!translated) {
setTranslate()
}
}
if (params.watchOverflow && snapGrid !== swiper.snapGrid) {
swiper.checkOverflow()
}
swiper.emit('update')
}
changeDirection(
newDirection: 'horizontal' | 'vertical',
needUpdate = true
): SwiperClass {
const swiper = this
const currentDirection = swiper.params.direction
if (!newDirection) {
// eslint-disable-next-line
newDirection =
currentDirection === 'horizontal' ? 'vertical' : 'horizontal'
}
if (
newDirection === currentDirection ||
(newDirection !== 'horizontal' && newDirection !== 'vertical')
) {
return swiper as unknown as SwiperClass
}
;(swiper.el as HTMLElement).classList.remove(
`${swiper.params.containerModifierClass}${currentDirection}`
)
;(swiper.el as HTMLElement).classList.add(
`${swiper.params.containerModifierClass}${newDirection}`
)
swiper.emitContainerClasses()
swiper.params.direction = newDirection
swiper.slides.forEach((slideEl: HTMLElement) => {
if (newDirection === 'vertical') {
slideEl.style.width = ''
} else {
slideEl.style.height = ''
}
})
swiper.emit('changeDirection')
if (needUpdate) swiper.update()
return swiper as unknown as SwiperClass
}
changeLanguageDirection(direction: 'rtl' | 'ltr'): void {
const swiper = this
if (
(swiper.rtl && direction === 'rtl') ||
(!swiper.rtl && direction === 'ltr')
)
return
swiper.rtl = direction === 'rtl'
swiper.rtlTranslate = swiper.params.direction === 'horizontal' && swiper.rtl
if (swiper.rtl) {
;(swiper.el as HTMLElement).classList.add(
`${swiper.params.containerModifierClass}rtl`
)
;(swiper.el as HTMLElement).dir = 'rtl'
} else {
;(swiper.el as HTMLElement).classList.remove(
`${swiper.params.containerModifierClass}rtl`
)
;(swiper.el as HTMLElement).dir = 'ltr'
}
swiper.update()
}
mount(element?: HTMLElement | string, adapterEl?: any): boolean {
const swiper = this
if (swiper.mounted) return true
// Find el
let el = element || swiper.params.el
if (isWeb() && typeof el === 'string') {
el = document.querySelector(el) as HTMLElement
}
if (!el) {
return false
}
// @ts-ignore
el.swiper = swiper
if (
isWeb() &&
(el as HTMLElement).parentNode &&
// @ts-ignore
(el as HTMLElement).parentNode?.host &&
// @ts-ignore
(el as HTMLElement).parentNode?.host.nodeName ===
swiper.params.swiperElementNodeName?.toUpperCase()
) {
swiper.isElement = true
}
const getWrapperSelector = () => {
return `.${(swiper.params.wrapperClass || '').trim().split(' ').join('.')}`
}
const getWrapper = () => {
if (isWeb()) {
if (
el &&
(el as HTMLElement).shadowRoot &&
!!(el as HTMLElement).shadowRoot?.querySelector
) {
const res = (el as HTMLElement).shadowRoot?.querySelector(
getWrapperSelector()
)
// Children needs to return slot items
return res
}
return elementChildren(el, getWrapperSelector())[0]
}
// @ts-ignore
return el.wrapperEl
}
// Find Wrapper
let wrapperEl = getWrapper()
if (isWeb() && !wrapperEl && swiper.params.createElements) {
wrapperEl = createElement('div', swiper.params.wrapperClass)
;(el as HTMLElement).append(wrapperEl)
elementChildren(el, `.${swiper.params.slideClass}`).forEach((slideEl) => {
wrapperEl.append(slideEl)
})
}
Object.assign(swiper, {
el,
wrapperEl,
adapterEl,
slidesEl:
swiper.isElement &&
// @ts-ignore
!(el as HTMLElement).parentNode.host.slideSlots
? // @ts-ignore
(el as HTMLElement).parentNode?.host
: wrapperEl,
hostEl: swiper.isElement
? // @ts-ignore
(el as HTMLElement).parentNode?.host
: el,
mounted: true,
// RTL
rtl:
isWeb() &&
// @ts-ignore
(el.dir.toLowerCase() === 'rtl' ||
elementStyle(el, 'direction') === 'rtl'),
rtlTranslate:
isWeb() &&
swiper.params.direction === 'horizontal' &&
// @ts-ignore
(el.dir.toLowerCase() === 'rtl' ||
elementStyle(el, 'direction') === 'rtl'),
wrongRTL: isWeb() && elementStyle(wrapperEl, 'display') === '-webkit-box'
})
return true
}
init(el?: HTMLElement, adapterEl?: any): SwiperClass {
const swiper = this
if (swiper.initialized) return swiper as unknown as SwiperClass
const mounted = swiper.mount(el, adapterEl)
if (mounted === false) return swiper as unknown as SwiperClass
swiper.emit('beforeInit')
// Set breakpoint
if (swiper.params.breakpoints) {
swiper.setBreakpoint()
}
// Add Classes
swiper.addClasses()
// Update size
swiper.updateSize()
// Update slides
swiper.updateSlides()
if (swiper.params.watchOverflow) {
swiper.checkOverflow()
}
// Set Grab Cursor
if (swiper.params.grabCursor && swiper.enabled) {
swiper.setGrabCursor()
}
// Slide To Initial Slide
if (
swiper.params.loop &&
swiper.virtual &&
typeof swiper.params.virtual === 'object' &&
swiper.params.virtual.enabled
) {
swiper.slideTo(
swiper.params.initialSlide +
// @ts-ignore
(swiper.virtual as VirtualOptions<any>).slidesBefore,
0,
swiper.params.runCallbacksOnInit,
false,
true
)
} else {
swiper.slideTo(
Number(swiper.params.initialSlide),
0,
swiper.params.runCallbacksOnInit,
false,
true
)
}
// Create loop
if (swiper.params.loop) {
swiper.loopCreate()
}
// Attach events
swiper.attachEvents()
if (isWeb()) {
const lazyElements = Array.from(
(swiper.el as HTMLElement).querySelectorAll('[loading="lazy"]')
)
if (swiper.isElement) {
lazyElements.push(
...Array.from(
(swiper.hostEl as HTMLElement).querySelectorAll('[loading="lazy"]')
)
)
}
lazyElements.forEach((imageEl) => {
if ((imageEl as HTMLImageElement).complete) {
processLazyPreloader(
swiper as unknown as SwiperInterface,
imageEl as HTMLElement
)
} else {
;(imageEl as HTMLImageElement).addEventListener('load', (e) => {
processLazyPreloader(
swiper as unknown as SwiperInterface,
e.target as HTMLElement
)
})
}
})
preload(swiper as unknown as SwiperInterface)
}
// Init Flag
swiper.initialized = true
if (isWeb()) {
preload(swiper as unknown as SwiperInterface)
}
// Emit
swiper.emit('init')
swiper.emit('afterInit')
return swiper as unknown as SwiperClass
}
destroy(deleteInstance = true, cleanStyles = true): null {
const swiper = this
const { params, el, wrapperEl, slides } = swiper
if (typeof swiper.params === 'undefined' || swiper.destroyed) {
return null
}
swiper.emit('beforeDestroy')
// Init Flag
swiper.initialized = false
// Detach events
swiper.detachEvents()
// Destroy loop
if (params.loop) {
swiper.loopDestroy()
}
// Cleanup styles
if (cleanStyles) {
swiper.removeClasses()
if (el && typeof el !== 'string') {
el.removeAttribute('style')
}
if (wrapperEl) {
wrapperEl.removeAttribute('style')
}
if (slides && slides.length) {
slides.forEach((slideEl: HTMLElement) => {
slideEl.classList.remove(
params.slideVisibleClass as string,
params.slideFullyVisibleClass as string,
params.slideActiveClass as string,
params.slideNextClass as string,
params.slidePrevClass as string
)
slideEl.removeAttribute('style')
slideEl.removeAttribute('data-swiper-slide-index')
})
}
}
swiper.emit('destroy')
// Detach emitter events
Object.keys(swiper.eventsListeners).forEach((eventName) => {
swiper.off(eventName)
})
if (deleteInstance !== false) {
if (swiper.el && typeof swiper.el !== 'string') {
// @ts-ignore
swiper.el.swiper = null
}
deleteProps(swiper)
}
swiper.destroyed = true
return null
}
static extendDefaults(newDefaults: SwiperOptions): void {
extend(extendedDefaults, newDefaults)
}
static get extendedDefaults(): SwiperOptions {
return extendedDefaults
}
static get defaults(): SwiperOptions {
// @ts-ignore
return defaults
}
static installModule(mod: SwiperModule): void {
if (!Swiper.prototype.__modules__) Swiper.prototype.__modules__ = []
const modules = Swiper.prototype.__modules__
if (typeof mod === 'function' && modules.indexOf(mod) < 0) {
modules.push(mod)
}
}
static use(module: SwiperModule | SwiperModule[]): typeof Swiper {
if (Array.isArray(module)) {
module.forEach((m) => Swiper.installModule(m))
return Swiper
}
Swiper.installModule(module)
return Swiper
}
}
// Add prototypes
Object.keys(prototypes).forEach((prototypeGroup) => {
// @ts-ignore
Object.keys(prototypes[prototypeGroup]).forEach((protoMethod) => {
// @ts-ignore
Swiper.prototype[protoMethod] = prototypes[prototypeGroup][protoMethod]
})
})
// @ts-ignore
Swiper.use([Resize, Observer])
export default Swiper