import debounce from 'lodash-es/debounce'; import ResizeObserver from 'resize-observer-polyfill'; import classnames from '../_util/classNames'; import BaseMixin from '../_util/BaseMixin'; import defaultProps from './default-props'; import initialState from './initial-state'; import { getOnDemandLazySlides, extractObject, initializedState, getHeight, canGoNext, slideHandler, changeSlide, keyHandler, swipeStart, swipeMove, swipeEnd, getPreClones, getPostClones, getTrackLeft, getTrackCSS, } from './utils/innerSliderUtils'; import Track from './track'; import Dots from './dots'; import { PrevArrow, NextArrow } from './arrows'; import supportsPassive from '../_util/supportsPassive'; function noop() {} export default { name: 'InnerSlider', mixins: [BaseMixin], inheritAttrs: false, props: { ...defaultProps, }, data() { this.preProps = { ...this.$props }; this.list = null; this.track = null; this.callbackTimers = []; this.clickable = true; this.debouncedResize = null; const ssrState = this.ssrInit(); return { ...initialState, currentSlide: this.initialSlide, slideCount: this.children.length, ...ssrState, }; }, watch: { __propsSymbol__() { const nextProps = this.$props; const spec = { listRef: this.list, trackRef: this.track, ...nextProps, ...this.$data, }; let setTrackStyle = false; for (const key of Object.keys(this.preProps)) { if (!nextProps.hasOwnProperty(key)) { setTrackStyle = true; break; } if ( typeof nextProps[key] === 'object' || typeof nextProps[key] === 'function' || typeof nextProps[key] === 'symbol' ) { continue; } if (nextProps[key] !== this.preProps[key]) { setTrackStyle = true; break; } } this.updateState(spec, setTrackStyle, () => { if (this.currentSlide >= nextProps.children.length) { this.changeSlide({ message: 'index', index: nextProps.children.length - nextProps.slidesToShow, currentSlide: this.currentSlide, }); } if (!this.preProps.autoplay && nextProps.autoplay) { this.handleAutoPlay('playing'); } else if (nextProps.autoplay) { this.handleAutoPlay('update'); } else { this.pause('paused'); } }); this.preProps = { ...nextProps }; }, }, mounted() { this.__emit('init'); if (this.lazyLoad) { const slidesToLoad = getOnDemandLazySlides({ ...this.$props, ...this.$data, }); if (slidesToLoad.length > 0) { this.setState(prevState => ({ lazyLoadedList: prevState.lazyLoadedList.concat(slidesToLoad), })); this.__emit('lazyLoad', slidesToLoad); } } this.$nextTick(() => { const spec = { listRef: this.list, trackRef: this.track, children: this.children, ...this.$props, }; this.updateState(spec, true, () => { this.adaptHeight(); this.autoplay && this.handleAutoPlay('playing'); }); if (this.lazyLoad === 'progressive') { this.lazyLoadTimer = setInterval(this.progressiveLazyLoad, 1000); } this.ro = new ResizeObserver(() => { if (this.animating) { this.onWindowResized(false); // don't set trackStyle hence don't break animation this.callbackTimers.push(setTimeout(() => this.onWindowResized(), this.speed)); } else { this.onWindowResized(); } }); this.ro.observe(this.list); document.querySelectorAll && Array.prototype.forEach.call(document.querySelectorAll('.slick-slide'), slide => { slide.onfocus = this.$props.pauseOnFocus ? this.onSlideFocus : null; slide.onblur = this.$props.pauseOnFocus ? this.onSlideBlur : null; }); if (window.addEventListener) { window.addEventListener('resize', this.onWindowResized); } else { window.attachEvent('onresize', this.onWindowResized); } }); }, beforeUnmount() { if (this.animationEndCallback) { clearTimeout(this.animationEndCallback); } if (this.lazyLoadTimer) { clearInterval(this.lazyLoadTimer); } if (this.callbackTimers.length) { this.callbackTimers.forEach(timer => clearTimeout(timer)); this.callbackTimers = []; } if (window.addEventListener) { window.removeEventListener('resize', this.onWindowResized); } else { window.detachEvent('onresize', this.onWindowResized); } if (this.autoplayTimer) { clearInterval(this.autoplayTimer); } this.ro?.disconnect(); }, updated() { this.checkImagesLoad(); this.__emit('reInit'); if (this.lazyLoad) { const slidesToLoad = getOnDemandLazySlides({ ...this.$props, ...this.$data, }); if (slidesToLoad.length > 0) { this.setState(prevState => ({ lazyLoadedList: prevState.lazyLoadedList.concat(slidesToLoad), })); this.__emit('lazyLoad'); } } // if (this.props.onLazyLoad) { // this.props.onLazyLoad([leftMostSlide]) // } this.adaptHeight(); }, methods: { listRefHandler(ref) { this.list = ref; }, trackRefHandler(ref) { this.track = ref; }, adaptHeight() { if (this.adaptiveHeight && this.list) { const elem = this.list.querySelector(`[data-index="${this.currentSlide}"]`); this.list.style.height = getHeight(elem) + 'px'; } }, onWindowResized(setTrackStyle) { if (this.debouncedResize) this.debouncedResize.cancel(); this.debouncedResize = debounce(() => this.resizeWindow(setTrackStyle), 50); this.debouncedResize(); }, resizeWindow(setTrackStyle = true) { const isTrackMounted = Boolean(this.track); if (!isTrackMounted) return; const spec = { listRef: this.list, trackRef: this.track, children: this.children, ...this.$props, ...this.$data, }; this.updateState(spec, setTrackStyle, () => { if (this.autoplay) { this.handleAutoPlay('update'); } else { this.pause('paused'); } }); // animating state should be cleared while resizing, otherwise autoplay stops working this.setState({ animating: false, }); clearTimeout(this.animationEndCallback); delete this.animationEndCallback; }, updateState(spec, setTrackStyle, callback) { const updatedState = initializedState(spec); spec = { ...spec, ...updatedState, slideIndex: updatedState.currentSlide }; const targetLeft = getTrackLeft(spec); spec = { ...spec, left: targetLeft }; const trackStyle = getTrackCSS(spec); if (setTrackStyle || this.children.length !== spec.children.length) { updatedState['trackStyle'] = trackStyle; } this.setState(updatedState, callback); }, ssrInit() { const children = this.children; if (this.variableWidth) { let trackWidth = 0; let trackLeft = 0; const childrenWidths = []; const preClones = getPreClones({ ...this.$props, ...this.$data, slideCount: children.length, }); const postClones = getPostClones({ ...this.$props, ...this.$data, slideCount: children.length, }); children.forEach(child => { const childWidth = child.props.style?.width?.split('px')[0] || 0; childrenWidths.push(childWidth); trackWidth += childWidth; }); for (let i = 0; i < preClones; i++) { trackLeft += childrenWidths[childrenWidths.length - 1 - i]; trackWidth += childrenWidths[childrenWidths.length - 1 - i]; } for (let i = 0; i < postClones; i++) { trackWidth += childrenWidths[i]; } for (let i = 0; i < this.currentSlide; i++) { trackLeft += childrenWidths[i]; } const trackStyle = { width: trackWidth + 'px', left: -trackLeft + 'px', }; if (this.centerMode) { const currentWidth = `${childrenWidths[this.currentSlide]}px`; trackStyle.left = `calc(${trackStyle.left} + (100% - ${currentWidth}) / 2 ) `; } return { trackStyle, }; } const childrenCount = children.length; const spec = { ...this.$props, ...this.$data, slideCount: childrenCount }; const slideCount = getPreClones(spec) + getPostClones(spec) + childrenCount; const trackWidth = (100 / this.slidesToShow) * slideCount; const slideWidth = 100 / slideCount; let trackLeft = (-slideWidth * (getPreClones(spec) + this.currentSlide) * trackWidth) / 100; if (this.centerMode) { trackLeft += (100 - (slideWidth * trackWidth) / 100) / 2; } const trackStyle = { width: trackWidth + '%', left: trackLeft + '%', }; return { slideWidth: slideWidth + '%', trackStyle, }; }, checkImagesLoad() { let images = (this.list && this.list.querySelectorAll && this.list.querySelectorAll('.slick-slide img')) || []; const imagesCount = images.length; let loadedCount = 0; Array.prototype.forEach.call(images, image => { const handler = () => ++loadedCount && loadedCount >= imagesCount && this.onWindowResized(); if (!image.onclick) { image.onclick = () => image.parentNode.focus(); } else { const prevClickHandler = image.onclick; image.onclick = () => { prevClickHandler(); image.parentNode.focus(); }; } if (!image.onload) { if (this.$props.lazyLoad) { image.onload = () => { this.adaptHeight(); this.callbackTimers.push(setTimeout(this.onWindowResized, this.speed)); }; } else { image.onload = handler; image.onerror = () => { handler(); this.__emit('lazyLoadError'); }; } } }); }, progressiveLazyLoad() { const slidesToLoad = []; const spec = { ...this.$props, ...this.$data }; for (let index = this.currentSlide; index < this.slideCount + getPostClones(spec); index++) { if (this.lazyLoadedList.indexOf(index) < 0) { slidesToLoad.push(index); break; } } for (let index = this.currentSlide - 1; index >= -getPreClones(spec); index--) { if (this.lazyLoadedList.indexOf(index) < 0) { slidesToLoad.push(index); break; } } if (slidesToLoad.length > 0) { this.setState(state => ({ lazyLoadedList: state.lazyLoadedList.concat(slidesToLoad), })); this.__emit('lazyLoad', slidesToLoad); } else { if (this.lazyLoadTimer) { clearInterval(this.lazyLoadTimer); delete this.lazyLoadTimer; } } }, slideHandler(index, dontAnimate = false) { const { asNavFor, currentSlide, beforeChange, speed, afterChange } = this.$props; const { state, nextState } = slideHandler({ index, ...this.$props, ...this.$data, trackRef: this.track, useCSS: this.useCSS && !dontAnimate, }); if (!state) return; beforeChange && beforeChange(currentSlide, state.currentSlide); const slidesToLoad = state.lazyLoadedList.filter( value => this.lazyLoadedList.indexOf(value) < 0, ); if (this.$attrs.onLazyLoad && slidesToLoad.length > 0) { this.__emit('lazyLoad', slidesToLoad); } if (!this.$props.waitForAnimate && this.animationEndCallback) { clearTimeout(this.animationEndCallback); afterChange && afterChange(currentSlide); delete this.animationEndCallback; } this.setState(state, () => { if (asNavFor && this.asNavForIndex !== index) { this.asNavForIndex = index; asNavFor.innerSlider.slideHandler(index); } if (!nextState) return; this.animationEndCallback = setTimeout(() => { const { animating, ...firstBatch } = nextState; this.setState(firstBatch, () => { this.callbackTimers.push(setTimeout(() => this.setState({ animating }), 10)); afterChange && afterChange(state.currentSlide); delete this.animationEndCallback; }); }, speed); }); }, changeSlide(options, dontAnimate = false) { const spec = { ...this.$props, ...this.$data }; const targetSlide = changeSlide(spec, options); if (targetSlide !== 0 && !targetSlide) return; if (dontAnimate === true) { this.slideHandler(targetSlide, dontAnimate); } else { this.slideHandler(targetSlide); } this.$props.autoplay && this.handleAutoPlay('update'); if (this.$props.focusOnSelect) { const nodes = this.list.querySelectorAll('.slick-current'); nodes[0] && nodes[0].focus(); } }, clickHandler(e) { if (this.clickable === false) { e.stopPropagation(); e.preventDefault(); } this.clickable = true; }, keyHandler(e) { const dir = keyHandler(e, this.accessibility, this.rtl); dir !== '' && this.changeSlide({ message: dir }); }, selectHandler(options) { this.changeSlide(options); }, disableBodyScroll() { const preventDefault = e => { e = e || window.event; if (e.preventDefault) e.preventDefault(); e.returnValue = false; }; window.ontouchmove = preventDefault; }, enableBodyScroll() { window.ontouchmove = null; }, swipeStart(e) { if (this.verticalSwiping) { this.disableBodyScroll(); } const state = swipeStart(e, this.swipe, this.draggable); state !== '' && this.setState(state); }, swipeMove(e) { const state = swipeMove(e, { ...this.$props, ...this.$data, trackRef: this.track, listRef: this.list, slideIndex: this.currentSlide, }); if (!state) return; if (state['swiping']) { this.clickable = false; } this.setState(state); }, swipeEnd(e) { const state = swipeEnd(e, { ...this.$props, ...this.$data, trackRef: this.track, listRef: this.list, slideIndex: this.currentSlide, }); if (!state) return; const triggerSlideHandler = state['triggerSlideHandler']; delete state['triggerSlideHandler']; this.setState(state); if (triggerSlideHandler === undefined) return; this.slideHandler(triggerSlideHandler); if (this.$props.verticalSwiping) { this.enableBodyScroll(); } }, touchEnd(e) { this.swipeEnd(e); this.clickable = true; }, slickPrev() { // this and fellow methods are wrapped in setTimeout // to make sure initialize setState has happened before // any of such methods are called this.callbackTimers.push(setTimeout(() => this.changeSlide({ message: 'previous' }), 0)); }, slickNext() { this.callbackTimers.push(setTimeout(() => this.changeSlide({ message: 'next' }), 0)); }, slickGoTo(slide, dontAnimate = false) { slide = Number(slide); if (isNaN(slide)) return ''; this.callbackTimers.push( setTimeout( () => this.changeSlide( { message: 'index', index: slide, currentSlide: this.currentSlide, }, dontAnimate, ), 0, ), ); }, play() { let nextIndex; if (this.rtl) { nextIndex = this.currentSlide - this.slidesToScroll; } else { if (canGoNext({ ...this.$props, ...this.$data })) { nextIndex = this.currentSlide + this.slidesToScroll; } else { return false; } } this.slideHandler(nextIndex); }, handleAutoPlay(playType) { if (this.autoplayTimer) { clearInterval(this.autoplayTimer); } const autoplaying = this.autoplaying; if (playType === 'update') { if (autoplaying === 'hovered' || autoplaying === 'focused' || autoplaying === 'paused') { return; } } else if (playType === 'leave') { if (autoplaying === 'paused' || autoplaying === 'focused') { return; } } else if (playType === 'blur') { if (autoplaying === 'paused' || autoplaying === 'hovered') { return; } } this.autoplayTimer = setInterval(this.play, this.autoplaySpeed + 50); this.setState({ autoplaying: 'playing' }); }, pause(pauseType) { if (this.autoplayTimer) { clearInterval(this.autoplayTimer); this.autoplayTimer = null; } const autoplaying = this.autoplaying; if (pauseType === 'paused') { this.setState({ autoplaying: 'paused' }); } else if (pauseType === 'focused') { if (autoplaying === 'hovered' || autoplaying === 'playing') { this.setState({ autoplaying: 'focused' }); } } else { // pauseType is 'hovered' if (autoplaying === 'playing') { this.setState({ autoplaying: 'hovered' }); } } }, onDotsOver() { this.autoplay && this.pause('hovered'); }, onDotsLeave() { this.autoplay && this.autoplaying === 'hovered' && this.handleAutoPlay('leave'); }, onTrackOver() { this.autoplay && this.pause('hovered'); }, onTrackLeave() { this.autoplay && this.autoplaying === 'hovered' && this.handleAutoPlay('leave'); }, onSlideFocus() { this.autoplay && this.pause('focused'); }, onSlideBlur() { this.autoplay && this.autoplaying === 'focused' && this.handleAutoPlay('blur'); }, customPaging({ i }) { return ; }, appendDots({ dots }) { return