UNPKG

rabbit-simple-ui

Version:

A simple UI component library based on JavaScript

394 lines (313 loc) 13.9 kB
/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { $el, bind, createElem, getBooleanTypeAttr, getNumTypeAttr, getStrTypeAttr, removeAttrs, setCss, setHtml, siblings } from '../../dom-utils'; import { moreThanOneNode } from '../../mixins'; import { type, validComps } from '../../utils'; import PREFIX from '../prefix'; interface Config { config( el: string ): { events({ onClick, onChange }: CarouselEvents): void; }; } interface CarouselEvents { // 点击幻灯片时触发,返回索引值 onClick?: (index: number) => void; // 幻灯片切换时触发,目前激活的幻灯片的索引,原幻灯片的索引 onChange?: ([oldValue, value]: [number, number]) => void; } const AUTOPLAYSPEED = 2000; const DURATION = 520; class Carousel implements Config { readonly VERSION: string; readonly COMPONENTS: NodeListOf<Element>; constructor() { this.VERSION = 'v1.0'; this.COMPONENTS = $el('r-carousel', { all: true }); this._create(this.COMPONENTS); } public config( el: string ): { events({ onClick, onChange }: CarouselEvents): void; } { const target = $el(el) as HTMLElement; validComps(target, 'carousel'); const { _attrs } = Carousel.prototype; const { autoplay, autoplaySpeed, hoverPause } = _attrs(target); return { events({ onClick, onChange }: CarouselEvents) { const elems = target.querySelectorAll(`.${PREFIX.carousel}-item`); const LeftArrow = target.querySelector(`.${PREFIX.carousel}-arrow.left`); const RightArrow = target.querySelector(`.${PREFIX.carousel}-arrow.right`); const lastIndex = elems.length - 1; const handleChange = ( siblingType: 'nextElementSibling' | 'previousElementSibling', newSetElem: Element ) => { const oldActiveElem = target.querySelector( `.${PREFIX.carousel}-item.active` )! as HTMLElement; const activeElem = oldActiveElem[siblingType] || newSetElem; const oldValue = Number(oldActiveElem.dataset['index']); // @ts-ignore const value = Number(activeElem.dataset['index']); onChange && type.isFn(onChange, [oldValue, value]); }; const autoPlayUseChangeEvent = () => { if (!autoplay) return; let eventTimer: any = null; const startEvent = () => { eventTimer = window.setInterval(() => { handleChange('nextElementSibling', elems[0]); }, autoplaySpeed); }; const pauseEvent = () => eventTimer ? window.clearInterval(eventTimer) : false; startEvent(); if (hoverPause === 'false') return; bind(target, 'mouseenter', () => pauseEvent()); bind(target, 'mouseleave', () => startEvent()); }; const handleClick = () => { elems.forEach((elem, index) => { bind(elem, 'click', () => onClick && type.isFn(onClick, index)); }); }; bind(LeftArrow, 'click', () => handleChange('previousElementSibling', elems[lastIndex]) ); bind(RightArrow, 'click', () => handleChange('nextElementSibling', elems[0])); handleClick(); autoPlayUseChangeEvent(); } }; } private _create(components: NodeListOf<Element>): void { components.forEach((node) => { if (moreThanOneNode(node)) return; const placeholderNode = node.firstElementChild; if (!placeholderNode) return; const carouselItemCount = placeholderNode.childElementCount; const { dots, arrow, effect, easing, radiusDot, trigger, autoplay, hoverPause, autoplaySpeed } = this._attrs(node); this._setMainTemplate(node, dots, arrow); this._setFadeCls(node, effect); this._setItem(node, placeholderNode, carouselItemCount, easing); this._setIndicators(node, carouselItemCount, radiusDot, trigger); this._autoPlay(autoplay, node, hoverPause, autoplaySpeed); this._handleArrowClick(node, arrow); removeAttrs(node, [ 'dots', 'arrow', 'effect', 'easing', 'trigger', 'radius-dot', 'autoplay', 'hover-pause', 'autoplay-speed' ]); }); } private _setMainTemplate(node: Element, dots: string, arrow: string): void { const template = ` <button type="button" class="left ${PREFIX.carousel}-arrow ${PREFIX.carousel}-arrow-${arrow}"> <i class="${PREFIX.icon} ${PREFIX.icon}-ios-arrow-back"></i> </button> <div class="${PREFIX.carousel}-list"></div> <button type="button" class="right ${PREFIX.carousel}-arrow ${PREFIX.carousel}-arrow-${arrow}"> <i class="${PREFIX.icon} ${PREFIX.icon}-ios-arrow-forward"></i> </button> <ul class="${PREFIX.carousel}-dots ${PREFIX.carousel}-dots-${dots}"></ul> `; setHtml(node, template); } private _setFadeCls(node: Element, effect: string): void { effect === 'fade' ? node.classList.add(`${PREFIX.carousel}-${effect}`) : ''; } private _setItem( node: Element, placeholderNode: Element, carouselItemCount: number, esaing: string ): void { const CarouselList = node.querySelector(`.${PREFIX.carousel}-list`); const Fragment = document.createDocumentFragment(); const children = Array.from(placeholderNode.children); let i = 0; for (; i < carouselItemCount; i++) { const CarouselItem = createElem('div'); CarouselItem.dataset['index'] = `${i}`; CarouselItem.className = `${PREFIX.carousel}-item`; CarouselItem.appendChild(children[i]); this._setEasing(CarouselItem, esaing); Fragment.appendChild(CarouselItem); Fragment.firstElementChild?.classList.add('active'); } CarouselList?.appendChild(Fragment); } private _setEasing(item: HTMLElement, easing: string): void { if (!easing) return; setCss(item, 'transitionTimingFunction', easing); } private _setIndicators( node: Element, carouselItemCount: number, radiusDot: boolean, trigger: string ): void { const CarouselDots = node.querySelector(`.${PREFIX.carousel}-dots`); const Fragment = document.createDocumentFragment(); let i = 0; for (; i < carouselItemCount; i++) { const CarouselDot = createElem('li'); CarouselDot.dataset['slideTo'] = `${i}`; setHtml( CarouselDot, `<button type="button" class=${radiusDot ? 'radius' : ''}></button>` ); this._handleDotChange(trigger, node, CarouselDot); Fragment.appendChild(CarouselDot); Fragment.firstElementChild?.classList.add(`${PREFIX.carousel}-active`); } CarouselDots?.appendChild(Fragment); } private _autoPlay( autoplay: boolean, node: Element, hoverPause: string, autoplaySpeed: number ): void { if (!autoplay) return; let autoPlayTimer: any = null; const play = () => { let speed = autoplaySpeed; // 当轮播图自动播放的切换速度低于 650ms 会出现问题, // 因此低于这个数值的都会被重置为 650ms。 if (speed < 650) { speed = 650; console.warn( `[Rabbit warn] Please do not set the sliding speed of carousel to less than 650ms. There are known problems with doing so, so it has been reset to 650ms. --> ${autoplaySpeed}ms` ); } autoPlayTimer = window.setInterval(() => this._slide('next', node), speed); }; play(); if (hoverPause === 'false') return; const pause = () => (autoPlayTimer ? window.clearInterval(autoPlayTimer) : false); bind(node, 'mouseenter', () => pause()); bind(node, 'mouseleave', () => play()); } private _handleArrowClick(node: Element, arrow: string): void { if (arrow === 'none') return; const LeftArrow = node.querySelector(`.${PREFIX.carousel}-arrow.left`); const RightArrow = node.querySelector(`.${PREFIX.carousel}-arrow.right`); bind(LeftArrow, 'click', () => this._slide('prev', node)); bind(RightArrow, 'click', () => this._slide('next', node)); } private _handleDotChange(trigger: string, node: Element, dot: HTMLElement): void { let activeIndex: number, activeElem: Element; const _C = () => { activeIndex = Number(dot.dataset['slideTo']); activeElem = node.querySelector( `.${PREFIX.carousel}-item[data-index="${activeIndex}"]` )!; if (activeElem.classList.contains('active')) return; this._dotChange(node, activeIndex); this._showActiveItem(activeElem); siblings(activeElem).forEach((otherElem: HTMLElement) => otherElem.classList.contains('active') ? this._hideOldActiveItem(otherElem) : '' ); }; if (trigger === 'hover') { bind(dot, 'mouseenter', () => _C()); } else { bind(dot, 'click', () => _C()); } } private _slide(type: 'prev' | 'next', node: Element): void { const direction = type === 'prev' ? 'right' : 'left'; const CarouselList = node.querySelector(`.${PREFIX.carousel}-list`)!; const firstIndex = 0; const lastIndex = CarouselList.childElementCount - 1; const ActiveItem = node.querySelector(`.${PREFIX.carousel}-item.active`)! as HTMLElement; const PrevItem = ActiveItem.previousElementSibling || CarouselList.children[lastIndex]; const NextItem = ActiveItem.nextElementSibling || CarouselList.children[firstIndex]; const __change = (elem: Element) => this._change(type, direction, node, ActiveItem, elem); type === 'prev' ? __change(PrevItem) : __change(NextItem); } private _change( type: string, direction: string, node: Element, oldActiveItem: Element, curActiveItem: Element ): void { // @ts-ignore const activeIndex = Number(curActiveItem.dataset['index']); this._dotChange(node, activeIndex); this._showActiveItem(curActiveItem, type, direction); this._hideOldActiveItem(oldActiveItem, direction); } private _dotChange(node: Element, activeIndex: number): void { const CarouselDots = node.querySelector(`.${PREFIX.carousel}-dots`)!; const ActiveDot = CarouselDots.children[activeIndex]; ActiveDot.classList.add(`${PREFIX.carousel}-active`); siblings(ActiveDot).forEach((dot: HTMLElement) => dot.classList.contains(`${PREFIX.carousel}-active`) ? dot.classList.remove(`${PREFIX.carousel}-active`) : '' ); } private _showActiveItem(activeElem: Element, type = 'next', direction = 'left'): void { activeElem.classList.add(`${PREFIX.carousel}-item-${type}`); setTimeout(() => activeElem.classList.add(`${PREFIX.carousel}-item-${direction}`), 20); setTimeout(() => { activeElem.classList.add('active'); activeElem.classList.remove(`${PREFIX.carousel}-item-${type}`); activeElem.classList.remove(`${PREFIX.carousel}-item-${direction}`); }, DURATION); } private _hideOldActiveItem(oldElem: Element, direction = 'left'): void { setTimeout(() => oldElem.classList.add(`${PREFIX.carousel}-item-${direction}`), 20); setTimeout(() => { oldElem.classList.remove('active'); oldElem.classList.remove(`${PREFIX.carousel}-item-${direction}`); }, DURATION); } private _attrs(node: Element) { return { dots: getStrTypeAttr(node, 'dots', 'inside'), arrow: getStrTypeAttr(node, 'arrow', 'hover'), effect: getStrTypeAttr(node, 'effect', ''), easing: getStrTypeAttr(node, 'easing', ''), trigger: getStrTypeAttr(node, 'trigger', 'click'), hoverPause: getStrTypeAttr(node, 'hover-pause', 'true'), radiusDot: getBooleanTypeAttr(node, 'radius-dot'), autoplay: getBooleanTypeAttr(node, 'autoplay'), autoplaySpeed: getNumTypeAttr(node, 'autoplay-speed', AUTOPLAYSPEED) }; } } export default Carousel;