quasar-framework
Version:
Build responsive SPA, SSR, PWA, Hybrid Mobile Apps and Electron apps, all simultaneously using the same codebase
487 lines (465 loc) • 12.9 kB
JavaScript
import QBtn from '../btn/QBtn.js'
import TouchPan from '../../directives/touch-pan.js'
import { isNumber } from '../../utils/is.js'
import { between, normalizeToInterval } from '../../utils/format.js'
import { start, stop } from '../../utils/animate.js'
import { decelerate, standard } from '../../utils/easing.js'
import { getEventKey } from '../../utils/event.js'
import FullscreenMixin from '../../mixins/fullscreen.js'
export default {
name: 'QCarousel',
mixins: [FullscreenMixin],
directives: {
TouchPan
},
props: {
value: Number,
color: {
type: String,
default: 'primary'
},
height: String,
arrows: Boolean,
infinite: Boolean,
animation: {
type: [Number, Boolean],
default: true
},
easing: Function,
swipeEasing: Function,
noSwipe: Boolean,
autoplay: [Number, Boolean],
handleArrowKeys: Boolean,
quickNav: Boolean,
quickNavPosition: {
type: String,
default: 'bottom',
validator: v => ['top', 'bottom'].includes(v)
},
quickNavIcon: String,
thumbnails: {
type: Array,
default: () => ([])
},
thumbnailsIcon: String,
thumbnailsHorizontal: Boolean
},
provide () {
return {
'carousel': this
}
},
data () {
return {
position: 0,
slide: 0,
positionSlide: 0,
slidesNumber: 0,
animUid: false,
viewThumbnails: false
}
},
watch: {
value (v) {
if (v !== this.slide) {
this.goToSlide(v)
}
},
autoplay () {
this.__planAutoPlay()
},
infinite () {
this.__planAutoPlay()
},
handleArrowKeys (v) {
this.__setArrowKeys(v)
}
},
computed: {
rtlDir () {
return this.$q.i18n.rtl ? -1 : 1
},
arrowIcon () {
const ico = [ this.$q.icon.carousel.left, this.$q.icon.carousel.right ]
return this.$q.i18n.rtl
? ico.reverse()
: ico
},
trackPosition () {
return { transform: `translateX(${this.rtlDir * this.position}%)` }
},
infiniteLeft () {
return this.infinite && this.slidesNumber > 1 && this.positionSlide < 0
},
infiniteRight () {
return this.infinite && this.slidesNumber > 1 && this.positionSlide >= this.slidesNumber
},
canGoToPrevious () {
return this.infinite ? this.slidesNumber > 1 : this.slide > 0
},
canGoToNext () {
return this.infinite ? this.slidesNumber > 1 : this.slide < this.slidesNumber - 1
},
computedQuickNavIcon () {
return this.quickNavIcon || this.$q.icon.carousel.quickNav
},
computedStyle () {
if (!this.inFullscreen && this.height) {
return `height: ${this.height}`
}
},
slotScope () {
return {
slide: this.slide,
slidesNumber: this.slidesNumber,
percentage: this.slidesNumber < 2
? 100
: 100 * this.slide / (this.slidesNumber - 1),
goToSlide: this.goToSlide,
previous: this.previous,
next: this.next,
color: this.color,
inFullscreen: this.inFullscreen,
toggleFullscreen: this.toggleFullscreen,
canGoToNext: this.canGoToNext,
canGoToPrevious: this.canGoToPrevious
}
},
computedThumbnailIcon () {
return this.thumbnailsIcon || this.$q.icon.carousel.thumbnails
}
},
methods: {
previous () {
return this.canGoToPrevious
? this.goToSlide(this.slide - 1)
: Promise.resolve()
},
next () {
return this.canGoToNext
? this.goToSlide(this.slide + 1)
: Promise.resolve()
},
goToSlide (slide, fromSwipe = false) {
return new Promise(resolve => {
let
direction = '',
curSlide = this.slide,
pos
this.__cleanup()
const finish = () => {
this.$emit('input', this.slide)
this.$emit('slide', this.slide, direction)
this.$emit('slide-direction', direction)
this.__planAutoPlay()
resolve()
}
if (this.slidesNumber < 2) {
this.slide = 0
this.positionSlide = 0
pos = 0
}
else {
if (!this.hasOwnProperty('initialPosition')) {
this.position = -this.slide * 100
}
direction = slide > this.slide ? 'next' : 'previous'
if (this.infinite) {
this.slide = normalizeToInterval(slide, 0, this.slidesNumber - 1)
pos = normalizeToInterval(slide, -1, this.slidesNumber)
if (!fromSwipe) {
this.positionSlide = pos
}
}
else {
this.slide = between(slide, 0, this.slidesNumber - 1)
this.positionSlide = this.slide
pos = this.slide
}
}
this.$emit('slide-trigger', curSlide, this.slide, direction)
pos = pos * -100
if (!this.animation) {
this.position = pos
finish()
return
}
this.animationInProgress = true
this.animUid = start({
from: this.position,
to: pos,
duration: isNumber(this.animation) ? this.animation : 300,
easing: fromSwipe
? this.swipeEasing || decelerate
: this.easing || standard,
apply: pos => {
this.position = pos
},
done: () => {
if (this.infinite) {
this.position = -this.slide * 100
this.positionSlide = this.slide
}
this.animationInProgress = false
finish()
}
})
})
},
stopAnimation () {
stop(this.animUid)
this.animationInProgress = false
},
__pan (event) {
if (this.infinite && this.animationInProgress) {
return
}
if (event.isFirst) {
this.initialPosition = this.position
this.__cleanup()
}
let delta = this.rtlDir * (event.direction === 'left' ? -1 : 1) * event.distance.x
if (
(this.infinite && this.slidesNumber < 2) ||
(
!this.infinite &&
(
(this.slide === 0 && delta > 0) ||
(this.slide === this.slidesNumber - 1 && delta < 0)
)
)
) {
delta = 0
}
const
pos = this.initialPosition + delta / this.$refs.track.offsetWidth * 100,
slidePos = this.slide + this.rtlDir * (event.direction === 'left' ? 1 : -1)
if (this.position !== pos) {
this.position = pos
}
if (this.positionSlide !== slidePos) {
this.positionSlide = slidePos
}
if (event.isFinal) {
this.goToSlide(
event.distance.x < 40
? this.slide
: this.positionSlide,
true
).then(() => {
delete this.initialPosition
})
}
},
__planAutoPlay () {
this.$nextTick(() => {
if (this.autoplay) {
clearTimeout(this.timer)
this.timer = setTimeout(
this.next,
isNumber(this.autoplay) ? this.autoplay : 5000
)
}
})
},
__cleanup () {
this.stopAnimation()
clearTimeout(this.timer)
},
__handleArrowKey (e) {
const key = getEventKey(e)
if (key === 37) { // left arrow key
this.previous()
}
else if (key === 39) { // right arrow key
this.next()
}
},
__setArrowKeys (/* boolean */ state) {
const op = `${state === true ? 'add' : 'remove'}EventListener`
document[op]('keydown', this.__handleArrowKey)
},
__registerSlide () {
this.slidesNumber++
},
__unregisterSlide () {
this.slidesNumber--
},
__getScopedSlots (h) {
if (this.slidesNumber === 0) {
return
}
let slots = this.$scopedSlots
if (slots) {
return Object.keys(slots)
.filter(key => key.startsWith('control-'))
.map(key => slots[key](this.slotScope))
}
},
__getQuickNav (h) {
if (this.slidesNumber === 0 || !this.quickNav) {
return
}
const
slot = this.$scopedSlots['quick-nav'],
items = []
if (slot) {
for (let i = 0; i < this.slidesNumber; i++) {
items.push(slot({
slide: i,
before: i < this.slide,
current: i === this.slide,
after: i > this.slide,
color: this.color,
goToSlide: slide => { this.goToSlide(slide || i) }
}))
}
}
else {
for (let i = 0; i < this.slidesNumber; i++) {
items.push(h(QBtn, {
key: i,
'class': { inactive: i !== this.slide },
props: {
icon: this.computedQuickNavIcon,
round: true,
flat: true,
dense: true,
color: this.color
},
on: {
click: () => { this.goToSlide(i) }
}
}))
}
}
return h('div', {
staticClass: 'q-carousel-quick-nav scroll text-center',
'class': [`text-${this.color}`, `absolute-${this.quickNavPosition}`]
}, items)
},
__getThumbnails (h) {
const slides = this.thumbnails.map((img, index) => {
if (!img) {
return
}
return h('div', {
on: {
click: () => { this.goToSlide(index) }
}
}, [
h('img', {
attrs: { src: img },
'class': { active: this.slide === index }
})
])
})
const nodes = [
h(QBtn, {
staticClass: 'q-carousel-thumbnail-btn absolute',
props: {
icon: this.computedThumbnailIcon,
fabMini: true,
flat: true,
color: this.color
},
on: {
click: () => {
this.viewThumbnails = !this.viewThumbnails
}
}
}),
h('div', {
staticClass: 'q-carousel-thumbnails scroll absolute-bottom',
'class': { active: this.viewThumbnails }
}, [h('div', {
staticClass: 'row gutter-xs',
'class': this.thumbnailsHorizontal ? 'no-wrap' : 'justify-center'
}, slides)])
]
if (this.viewThumbnails) {
nodes.unshift(
h('div', {
staticClass: 'absolute-full',
on: {
click: () => { this.viewThumbnails = false }
}
})
)
}
return nodes
}
},
render (h) {
return h('div', {
staticClass: 'q-carousel',
style: this.computedStyle,
'class': { fullscreen: this.inFullscreen }
}, [
h('div', {
staticClass: 'q-carousel-inner',
directives: this.noSwipe
? null
: [{
name: 'touch-pan',
modifiers: {
horizontal: true,
prevent: true,
stop: true
},
value: this.__pan
}]
}, [
h('div', {
ref: 'track',
staticClass: 'q-carousel-track',
style: this.trackPosition,
'class': {
'infinite-left': this.infiniteLeft,
'infinite-right': this.infiniteRight
}
}, [
this.infiniteRight ? h('div', { staticClass: 'q-carousel-slide', style: `flex: 0 0 ${100}%` }) : null,
this.$slots.default,
this.infiniteLeft ? h('div', { staticClass: 'q-carousel-slide', style: `flex: 0 0 ${100}%` }) : null
])
]),
this.arrows ? h(QBtn, {
staticClass: 'q-carousel-left-arrow absolute',
props: { color: this.color, icon: this.arrowIcon[0], fabMini: true, flat: true },
directives: [{ name: 'show', value: this.canGoToPrevious }],
on: { click: this.previous }
}) : null,
this.arrows ? h(QBtn, {
staticClass: 'q-carousel-right-arrow absolute',
props: { color: this.color, icon: this.arrowIcon[1], fabMini: true, flat: true },
directives: [{ name: 'show', value: this.canGoToNext }],
on: { click: this.next }
}) : null,
this.__getQuickNav(h),
this.__getScopedSlots(h),
this.thumbnails.length
? this.__getThumbnails(h)
: null,
this.$slots.control
])
},
mounted () {
this.__planAutoPlay()
if (this.handleArrowKeys) {
this.__setArrowKeys(true)
}
this.__stopSlideNumberNotifier = this.$watch('slidesNumber', val => {
if (this.value >= val) {
this.$emit('input', val - 1)
}
}, { immediate: true })
},
beforeDestroy () {
this.__cleanup()
this.__stopSlideNumberNotifier()
if (this.handleArrowKeys) {
this.__setArrowKeys(false)
}
}
}