UNPKG

@wiajs/ui

Version:

wia ui packages

509 lines (508 loc) 18.9 kB
/* eslint-disable no-multi-assign */ /* * Swipe 2.0 * Brad Birdsall * https://github.com/thebird/Swipe * Copyright 2013, MIT License * * modify by wia for img swipe * load first img, then load other img! * 通过函数调用实现多实例! */ import { Event } from '@wiajs/core'; // import {Event} from 'https://cos.wia.pub/wiajs/core.mjs' // import * as styles from './index.less' // ??? /** * @typedef {Object} Opt * @prop {string} el * @prop {number} startSlide * @prop {number} speed * @prop {number} delay - 5000 * @prop {number} WHR * @prop {number} width - 指定宽度,默认满屏 * @prop {number} height - 指定高度,通过宽高比确定宽度 * @prop {boolean} auto - 自动播放 * @prop {boolean} loop - 循环 * @prop {boolean} disableScroll * @prop {boolean} stopPropagation * @prop {string[]} imgs * @prop {(index: number, view: object)=>*} onSlide - runs at slide change. * @prop {(index: number, view: object)=>*} onEnd - runs at the end slide transition. */ const def = { el: '.wia-swiper', startSlide: 0, speed: 500, delay: 3000, auto: true, loop: true, disableScroll: true, WHR: 2 }; export default class Swiper extends Event { /** * 构造函数 * @param {*} opts * @param {*} page Page 实例 */ constructor(opts = {}, page = null){ super(opts, page ? [ page ] : page), /** @type {number} */ this.index = 0, // Evnets this.onTouch = { // @ts-ignore start: (ev)=>{ this.touchStart(ev); }, // @ts-ignore move: (ev)=>{ this.touchMove(ev); }, // @ts-ignore end: (ev)=>{ this.touchEnd(ev); } }, // setup initial vars for event this.touch = { /** @type {*} */ start: {}, /** @type {*} */ delta: {}, isScrolling: false }, // @ts-ignore this.onTransEnd = (ev)=>{ const _ = this; nextTick(_.transitionEnd.bind(_)(ev)); if (_.opt.stopPropagation) ev.stopPropagation(); }, // @ts-ignore this.onResize = (ev)=>{ const _ = this; nextTick(_.setup.bind(_)); if (_.opt.stopPropagation) ev.stopPropagation(); }; const _ = this; /** @type {Opt} */ _.opt = { ...def, ...opts }; _.root = $(_.opt.el) // 容器层 ; // 样式为 class='swiper' ??? 内置样式已改名,页面调试时需屏蔽 // _.root.removeClass('swiper').addClass(`${styles.swiper} swiper`) // 容器里的图片层的封装层 /** @type {HTMLElement} */ const wrap = _.root.dom.children[0]; _.wrap = wrap; // ??? 内置样式已改名,页面调试时需屏蔽 // $(el).removeClass('swiper-wrap').addClass(`${styles['swiper-wrap']} swiper-wrap`) _.index = _.opt.startSlide; _.init() // trigger setup ; // add event listeners // set touchstart event on element wrap.addEventListener('touchstart', _.onTouch.start, false); wrap.addEventListener('transitionend', _.onTransEnd, false); // set resize event on window window.addEventListener('resize', _.onResize, false); // 左右点击切换 _.root.click((ev)=>{ const divWidth = _.root.dom.clientWidth; const clickX = ev.offsetX; // 相对于 div 的点击位置 X 坐标 if (clickX < divWidth / 2) _.prev(); else _.next(); }); } /** * walter add for dynamic create swipe img from img urls! * 根据图片数量创建图层 */ init() { const _ = this; const { opt, wrap } = _; _.stop(); // 默认宽度满屏,可指定宽度、高度 const w = opt.width || (window.innerWidth > 0 ? window.innerWidth : screen.width); const h = opt.WHR ? w / opt.WHR : opt.height // 默认通过宽高比计算,也可以直接指定 ; const imgs = opt.imgs || []; const cnt = imgs.length; // 清除原有图层!!! wrap.innerHTML = ''; // static swipe! if (!cnt) { _.setup(); } else { // 按图片数量创建图层,图片放入该层 /** @type {*} */ const dvs = []; for(let i = 0; i < cnt; i++){ const div = document.createElement('div'); $(div).addClass('swiper-slide'); dvs.push(div); wrap.appendChild(div); } // load first img! _.newImg(imgs[0], (img)=>{ _.addImg(dvs[0], img, h); if (imgs.length === 1) _.setup(); else { // load others img for(let i = 1; i < imgs.length; i++){ _.newImg(imgs[i], (pic)=>{ _.addImg(dvs[i], pic, h); // last img loaded, start swipe! if (i === imgs.length - 1) { _.setup(); } }); } } }); } } /** * * @param {*} src * @param {*} cb */ newImg(src, cb) { const img = document.createElement('img'); img.src = src; if (img.complete) cb(img); else img.onload = ()=>cb(img); } /** * * @param {HTMLElement} dv * @param {*} img * @param {number} h */ addImg(dv, img, h) { img.height = h; img.width = h * img.naturalWidth / img.naturalHeight; dv.appendChild(img); } /** * 根据屏幕宽度、参数控制容器宽度,根据容器宽度,设置每个图层宽度和位置,启动定时轮播 * 容器宽度变化,需重新调用该方法 */ setup() { const _ = this; const { opt, root, wrap, index } = _; const { WHR } = opt; // 默认宽度满屏 const w = opt.width || (window.innerWidth > 0 ? window.innerWidth : screen.width); const h = WHR ? w / WHR : opt.height; // const WHR = options.WHR || 2; root.dom.style.width = `${w}px`; root.dom.style.height = `${h}px`; root.css('visibility', 'visible'); // cache slides,所有图片层 _.slides = wrap.children; // create an array to store current positions of each slide _.dists = new Array(_.slides.length); // determine width of each slide // dom加载后无法获取,好像使用 setTimeout 可以! // width = container.getBoundingClientRect().width || container.offsetWidth; // 容器的宽度 const width = Number.parseInt(getComputedStyle(root.dom).width) // container.style.width || ; _.width = width; // 设置容器里图片层宽度为图片数量 * 容器宽度 wrap.style.width = `${_.slides.length * width}px`; // alert('dvImg width:' + element.style.width + ' swipe: ' // + container.getBoundingClientRect().width + '/' + container.offsetWidth // + '/' + container.style.width + '/' + getComputedStyle(container).width); // alert(slides.length + '/' + element.style.width); // stack elements let pos = _.slides.length; while(pos--){ const slide = _.slides[pos]; slide.style.width = `${width}px`; slide.setAttribute('data-index', pos); slide.style.left = `${pos * -width}px`; // eslint-disable-next-line no-nested-ternary const dist = index > pos ? -width : index < pos ? width : 0; _.move(pos, dist, 0); } // reposition elements before and after index if (opt.loop) { _.move(_.circle(index - 1), -width, 0); _.move(_.circle(index + 1), width, 0); } root.css('visibility', 'visible'); // start auto slideshow if applicable if (opt.auto && _.slides && _.slides.length > 1) _.start(); } /** * 前一幅图片 */ prev() { const _ = this; const { opt, index, slides } = _; if (index) _.slide(index - 1); else if (opt.loop) _.slide(slides.length - 1); } /** * 后一幅图片 */ next() { const _ = this; const { opt, index, slides } = _; if (index < slides.length - 1) _.slide(index + 1); else if (opt.loop) _.slide(0); } /** * 切换图片 * @param {number} to * @param {number} [slideSpeed] * @returns */ slide(to, slideSpeed) { const _ = this; const { opt, index, slides, width } = _; let { speed } = opt; speed = slideSpeed || speed; // do nothing if already on requested slide if (index === to) return; let direction = Math.abs(index - to) / (index - to // 1:right -1:left ); // get the actual position of the slide if (opt.loop) { const orgDir = direction; direction = -_.dists[_.circle(to)] / width; // if going forward but to < index, use to = slides.length + to // if going backward but to > index, use to = -slides.length + to if (direction !== orgDir) to = -direction * slides.length + to; } let diff = Math.abs(index - to) - 1; while(diff--)_.move(_.circle((to > index ? to : index) - diff - 1), width * direction, 0); to = _.circle(to); _.move(index, width * direction, speed); _.move(to, 0, speed); if (opt.loop) _.move(_.circle(to - direction), -(width * direction), 0) // we need to get the next in place ; _.index = to; if (opt.onSlide) nextTick(opt.onSlide(index, slides[index])); } /** * 移动到指定索引 * @param {number} idx - 索引 * @param {number} dist - 距离 * @param {number} speed - 速度 */ move(idx, dist, speed) { const _ = this; _.translate(idx, dist, speed); _.dists[idx] = dist; } /** * 滑动图片 * @param {number} idx - 索引 * @param {number} dist - 距离 * @param {number} speed - 速度 * @returns */ translate(idx, dist, speed) { const _ = this; const { style } = _.slides[idx]; style.transitionDuration = `${speed}ms`; style.transform = `translateX(${dist}px)`; } /** * 启动自动轮播 */ start() { const _ = this; _.timer = setTimeout(_.next.bind(_), _.opt.delay); } /** * 停止自动轮播 */ stop() { // delay = 0; 点击图片后 stop,不再自动播放? if (this.timer) { clearTimeout(this.timer); this.timer = null; } } kill() { const _ = this; const { wrap } = _; // cancel slideshow _.stop(); // reset element wrap.style.width = 'auto'; wrap.style.left = '0'; // reset slides let pos = _.slides.length || 0; while(pos--){ const slide = _.slides[pos]; slide.style.width = '100%'; slide.style.left = 0; _.translate(pos, 0, 0); } // remove current event listeners wrap.removeEventListener('touchstart', _.onTouch.start, false); window.removeEventListener('resize', _.onResize, false); wrap.removeEventListener('transitionend', _.onTransEnd, false); } getIndex() { // return current index position return this.index; } /** * touchstart * @param {*} ev */ touchStart(ev) { const _ = this; const { wrap, opt } = _; const touches = ev.touches[0]; // stop slideshow _.stop(); // measure start values _.touch.start = { // get initial touch coords x: touches.pageX, y: touches.pageY, // store time to determine touch duration time: +new Date() }; // used for testing first move event _.touch.isScrolling = undefined; // reset delta and end measurements _.touch.delta = {}; // attach touchmove and touchend listeners wrap.addEventListener('touchmove', _.onTouch.move, false); wrap.addEventListener('touchend', _.onTouch.end, false); if (opt.stopPropagation) ev.stopPropagation(); } /** * * @param {*} event * @returns */ touchMove(event) { const _ = this; const { opt, index, touch, slides, dists, width } = _; const { start, isScrolling } = touch; // ensure swiping with one touch and not pinching if (event.touches.length > 1 || event.scale && event.scale !== 1) return; if (opt.disableScroll) event.preventDefault(); const touches = event.touches[0]; // measure change in x and y const delta = { x: touches.pageX - start.x, y: touches.pageY - start.y }; _.touch.delta = delta; // determine if scrolling test has run - one time test if (!isScrolling) _.isScrolling = !!(isScrolling || Math.abs(delta.x) < Math.abs(delta.y)); // if user is not trying to scroll vertically if (!isScrolling) { // prevent native scrolling event.preventDefault(); // stop slideshow _.stop(); // increase resistance if first or last slide if (opt.loop) { // we don't add resistance at the end _.translate(_.circle(index - 1), delta.x + _.dists[_.circle(index - 1)], 0); _.translate(index, delta.x + _.dists[index], 0); _.translate(_.circle(index + 1), delta.x + _.dists[_.circle(index + 1)], 0); } else { // increase resistance if first or last slide _.delta.x = delta.x / (!index && delta.x > 0 || // if first slide and sliding left index == slides.length - 1 && // or if last slide and sliding right delta.x < 0 // and if sliding at all ? Math.abs(delta.x) / width + 1 // determine resistance level : 1 // no resistance if false ); // translate 1:1 _.translate(index - 1, delta.x + dists[index - 1], 0); _.translate(index, delta.x + dists[index], 0); _.translate(index + 1, delta.x + dists[index + 1], 0); } } if (opt.stopPropagation) event.stopPropagation(); } /** * * @param {*} event */ touchEnd(event) { const _ = this; const { wrap, opt, touch, slides, dists, width } = _; let { index } = _; const { speed } = opt; const { start, delta, isScrolling } = touch; // measure duration const duration = +new Date() - start.time; // determine if slide attempt triggers next/prev slide const isValidSlide = Number(duration) < 250 && // if slide duration is less than 250ms Math.abs(delta.x) > 20 || // and if slide amt is greater than 20px Math.abs(delta.x) > width / 2 // or if slide amt is greater than half the width ; // determine if slide attempt is past start and end let isPastBounds = !index && delta.x > 0 || // if first slide and slide amt is greater than 0 index == slides.length - 1 && delta.x < 0 // or if last slide and slide amt is less than 0 ; if (opt.loop) isPastBounds = false; // determine direction of swipe (true:right, false:left) const direction = delta.x < 0; // if not scrolling vertically if (!isScrolling) { if (isValidSlide && !isPastBounds) { if (direction) { if (opt.loop) { // we need to get the next in this direction in place _.move(_.circle(index - 1), -width, 0); _.move(_.circle(index + 2), width, 0); } else _.move(index - 1, -width, 0); _.move(index, dists[index] - width, speed); _.move(_.circle(index + 1), dists[_.circle(index + 1)] - width, speed); index = _.circle(index + 1); _.index = index; } else { if (opt.loop) { // we need to get the next in this direction in place _.move(_.circle(index + 1), width, 0); _.move(_.circle(index - 2), -width, 0); } else _.move(index + 1, width, 0); _.move(index, dists[index] + width, speed); _.move(_.circle(index - 1), dists[_.circle(index - 1)] + width, speed); index = _.circle(index - 1); _.index = index; } opt.onSlide && opt.onSlide(index, slides[index]); } else if (opt.loop) { _.move(_.circle(index - 1), -width, speed); _.move(index, 0, speed); _.move(_.circle(index + 1), width, speed); } else { _.move(index - 1, -width, speed); _.move(index, 0, speed); _.move(index + 1, width, speed); } } // kill touchmove and touchend event listeners until touchstart called again wrap.removeEventListener('touchmove', _.onTouch.move, false); wrap.removeEventListener('touchend', _.onTouch.end, false); if (opt.stopPropagation) event.stopPropagation(); } /** * * @param {*} event */ transitionEnd(event) { const _ = this; const { opt, index, slides } = _; if (Number.parseInt(event.target.getAttribute('data-index')) == index) { if (opt.auto) _.start(); opt.onEnd && opt.onEnd(index, slides[index]); } } /** * 头尾图片按环形获取索引,避免超范围 * @param {*} idx * @returns */ circle(idx) { let R; try { const _ = this; const { slides } = _; // a simple positive modulo using slides.length R = (slides.length + idx % slides.length) % slides.length; } catch (e) { console.error(`circle exp:${e.message}`); } return R; } } // utilities function noop() {} // simple no operation function /** * * @param {*} fn */ function nextTick(fn) { setTimeout(fn || noop, 0); }