Open KareemDa opened 4 years ago
Here's a very messy working verticalized extension of the existing component that might help some people out:
<script>
import { VSlideGroup, VIcon } from 'vuetify/lib'
import { composedPath } from 'vuetify/lib/util/helpers'
// Resources:
// https://github.com/vuejs/vue/issues/2977
// https://littlelines.com/blog/2020/03/13/extending-vue-components
// https://github.com/vuetifyjs/vuetify/blob/master/packages/vuetify/src/components/VSlideGroup/VSlideGroup.ts
function bias (val) {
const c = 0.501
const x = Math.abs(val)
return Math.sign(val) * (x / ((1 / c - 2) * (1 - x) + 1))
}
export function calculateUpdatedOffset (
selectedElement,
heights,
rtl,
currentScrollOffset
) {
const clientHeight = selectedElement.clientHeight
const offsetTop = selectedElement.offsetTop
// const offsetLeft = rtl
// ? (widths.content - selectedElement.offsetLeft - clientHeight)
// : selectedElement.offsetLeft
// if (rtl) {
// currentScrollOffset = -currentScrollOffset
// }
const totalHeight = heights.wrapper + currentScrollOffset
const itemOffset = clientHeight + offsetTop
const additionalOffset = clientHeight * 0.4
console.log('calculateUpdatedOffset', offsetTop, currentScrollOffset, totalHeight)
if (offsetTop <= currentScrollOffset) {
currentScrollOffset = Math.max(offsetTop - additionalOffset, 0)
} else if (totalHeight <= itemOffset) {
currentScrollOffset = Math.min(currentScrollOffset - (totalHeight - itemOffset - additionalOffset), heights.content - heights.wrapper)
}
// return rtl ? -currentScrollOffset : currentScrollOffset
return currentScrollOffset
}
export function calculateCenteredOffset (
selectedElement,
heights,
rtl
) {
const { offsetTop, clientHeight } = selectedElement
// if (rtl) {
// const offsetCentered = heights.content - offsetLeft - clientWidth / 2 - heights.wrapper / 2
// return -Math.min(heights.content - heights.wrapper, Math.max(0, offsetCentered))
// } else {
// const offsetCentered = offsetLeft + clientWidth / 2 - heights.wrapper / 2
// return Math.min(heights.content - heights.wrapper, Math.max(0, offsetCentered))
// }
const offsetCentered = offsetTop + clientHeight / 2 - heights.wrapper / 2
return Math.min(heights.content - heights.wrapper, Math.max(0, offsetCentered))
}
export default {
extends: VSlideGroup,
data () {
return {
vertical: true,
// isOverflowing: false,
// resizeTimeout: 0,
// startX: 0,
startY: 0,
// isSwipingHorizontal: false,
isSwipingVertical: false,
// isSwiping: false,
// scrollOffset: 0,
scrollYOffset: 0, // ?
// widths: {
// content: 0,
// wrapper: 0,
// },
heights: {
content: 0,
wrapper: 0
}
}
},
watch: {
// internalValue: 'setHeights',
// When overflow changes, the arrows alter
// the widths of the content and wrapper
// and need to be recalculated
// isOverflowing: 'setHeights',
scrollOffset (val) {
// if (this.$vuetify.rtl) val = -val
const scroll =
val <= 0
? bias(-val)
: val > this.heights.content - this.heights.wrapper
? -(this.heights.content - this.heights.wrapper) + bias(this.heights.content - this.heights.wrapper - val)
: -val
// if (this.$vuetify.rtl) scroll = -scroll
// this.$refs.content.style.transform = `translateX(${scroll}px)`
this.$refs.content.style.transform = `translateY(${scroll}px)`
}
},
computed: {
// canTouch () {} ok
// __cachedNext () {} ok
// __cachedPrev () {} ok
classes () {
return {
...VSlideGroup.options.computed.classes.call(this),
'v-slide-group--vertical': this.vertical
}
},
// hasAffixes () {} ok
hasNext () {
if (!this.hasAffixes) return false
const { content, wrapper } = this.heights
// Check one scroll ahead to know the width of right-most item
return content > Math.abs(this.scrollOffset) + wrapper
}
// hasPrev () {} ok
},
methods: {
onFocusin (e) {
if (!this.isOverflowing) return
// Focused element is likely to be the root of an item, so a
// breadth-first search will probably find it in the first iteration
for (const el of composedPath(e)) {
for (const vm of this.items) {
if (vm.$el === el) {
this.scrollOffset = calculateUpdatedOffset(
vm.$el,
this.heights,
this.$vuetify.rtl,
this.scrollOffset
)
return
}
}
}
},
// genNext () {} ok
// genContent () {} ok
// genData () {} ok
genIcon (location) {
let icon = location
if (location === 'prev') {
icon = 'mdi-chevron-up'
} else if (location === 'next') {
icon = 'mdi-chevron-down'
}
const upperLocation = `${location[0].toUpperCase()}${location.slice(1)}`
const hasAffix = this[`has${upperLocation}`]
if (
!this.showArrows &&
!hasAffix
) return null
return this.$createElement(VIcon, {
props: {
disabled: !hasAffix
}
}, [icon])
},
// genPrev () {} ok
// genTransition () {} ok
// genWrapper () {} ok
calculateNewOffset (direction, heights, rtl, currentScrollOffset) {
// const sign = rtl ? -1 : 1
const sign = 1
const newAbosluteOffset = sign * currentScrollOffset +
(direction === 'prev' ? -1 : 1) * heights.wrapper
return sign * Math.max(Math.min(newAbosluteOffset, heights.content - heights.wrapper), 0)
},
// onAffixClick () {} ok
// onResize () {} ok
onTouchStart (e) {
const { content } = this.$refs
this.startY = this.scrollOffset + e.touchstartY
content.style.setProperty('transition', 'none')
content.style.setProperty('willChange', 'transform')
},
onTouchMove (e) {
if (!this.canTouch) return
if (!this.isSwiping) {
// only calculate disableSwipeHorizontal during the first onTouchMove invoke
// in order to ensure disableSwipeHorizontal value is consistent between onTouchStart and onTouchEnd
const diffX = e.touchmoveX - e.touchstartX
const diffY = e.touchmoveY - e.touchstartY
// this.isSwipingHorizontal = Math.abs(diffX) > Math.abs(diffY)
this.isSwipingVertical = Math.abs(diffY) > Math.abs(diffX)
this.isSwiping = true
}
// if (this.isSwipingHorizontal) {
// // sliding horizontally
// this.scrollOffset = this.startX - e.touchmoveX
// // temporarily disable window vertical scrolling
// document.documentElement.style.overflowY = 'hidden'
// }
if (this.isSwipingVertical) {
// sliding horizontally
this.scrollOffset = this.startY - e.touchmoveY
// temporarily disable window vertical scrolling
document.documentElement.style.overflowY = 'hidden'
}
},
onTouchEnd () {
if (!this.canTouch) return
const { content, wrapper } = this.$refs
const maxScrollOffset = content.clientHeight - wrapper.clientHeight
content.style.setProperty('transition', null)
content.style.setProperty('willChange', null)
// if (this.$vuetify.rtl) {
// /* istanbul ignore else */
// if (this.scrollOffset > 0 || !this.isOverflowing) {
// this.scrollOffset = 0
// } else if (this.scrollOffset <= -maxScrollOffset) {
// this.scrollOffset = -maxScrollOffset
// }
// } else {
if (this.scrollOffset < 0 || !this.isOverflowing) {
this.scrollOffset = 0
} else if (this.scrollOffset >= maxScrollOffset) {
this.scrollOffset = maxScrollOffset
}
// }
this.isSwiping = false
// rollback whole page scrolling to default
document.documentElement.style.removeProperty('overflow-y')
},
// overflowCheck() {} ok
scrollIntoView () {
if (!this.selectedItem && this.items.length) {
const lastItemPosition = this.items[this.items.length - 1].$el.getBoundingClientRect()
const wrapperPosition = this.$refs.wrapper.getBoundingClientRect()
// if (
// (this.$vuetify.rtl && wrapperPosition.right < lastItemPosition.right) ||
// (!this.$vuetify.rtl && wrapperPosition.left > lastItemPosition.left)
// ) {
// this.scrollTo('prev')
// }
// }
if (
(!this.$vuetify.rtl && wrapperPosition.top > lastItemPosition.top)
) {
this.scrollTo('prev')
}
}
if (!this.selectedItem) {
return
}
if (
this.selectedIndex === 0 ||
(!this.centerActive && !this.isOverflowing)
) {
this.scrollOffset = 0
} else if (this.centerActive) {
this.scrollOffset = calculateCenteredOffset(
this.selectedItem.$el,
this.heights,
this.$vuetify.rtl
)
} else if (this.isOverflowing) {
this.scrollOffset = calculateUpdatedOffset(
this.selectedItem.$el,
this.heights,
this.$vuetify.rtl,
this.scrollOffset
)
}
},
scrollTo (location) {
this.scrollOffset = this.calculateNewOffset(location, {
// Force reflow
content: this.$refs.content ? this.$refs.content.clientHeight : 0,
wrapper: this.$refs.wrapper ? this.$refs.wrapper.clientHeight : 0
}, this.$vuetify.rtl, this.scrollOffset)
},
setWidths () {
this.setHeights()
},
setHeights () {
window.requestAnimationFrame(() => {
if (this._isDestroyed) return
const { content, wrapper } = this.$refs
this.heights = {
content: content ? content.clientHeight : 0,
wrapper: wrapper ? wrapper.clientHeight : 0
}
// https://github.com/vuetifyjs/vuetify/issues/13212
// We add +1 to the wrappers width to prevent an issue where the `clientWidth`
// gets calculated wrongly by the browser if using a different zoom-level.
this.isOverflowing = this.heights.wrapper + 1 < this.heights.content
console.log(this.isOverflowing)
this.scrollIntoView()
})
}
}
}
</script>
<style scoped lang="scss">
.v-slide-group.v-slide-group--vertical {
max-height: 100%;
flex-direction: column;
.v-slide-group__wrapper {
flex-direction: column;
}
.v-slide-group__content {
display: block;
white-space: normal;
}
.v-slide-group__next, .v-slide-group__prev {
flex: 0 1 52px;
justify-content: center;
min-width: auto;
min-height: 52px;
}
}
</style>
Problem to solve
to achieve slider like this example
Proposed solution
add prop to change view to vertical