You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
314 lines
7.6 KiB
Vue
314 lines
7.6 KiB
Vue
<script>
|
|
export default {
|
|
name: 'CollapseTransition',
|
|
|
|
props: {
|
|
name: {
|
|
type: String,
|
|
required: false,
|
|
default: 'collapse',
|
|
},
|
|
dimension: {
|
|
type: String,
|
|
required: false,
|
|
default: 'height',
|
|
validator: (value) => {
|
|
return ['height', 'width'].includes(value)
|
|
},
|
|
},
|
|
duration: {
|
|
type: Number,
|
|
required: false,
|
|
default: 300,
|
|
},
|
|
easing: {
|
|
type: String,
|
|
required: false,
|
|
default: 'ease-in-out',
|
|
},
|
|
},
|
|
|
|
emits: ['before-appear', 'appear', 'after-appear', 'appear-cancelled', 'before-enter', 'enter', 'after-enter', 'enter-cancelled', 'before-leave', 'leave', 'after-leave', 'leave-cancelled'],
|
|
|
|
data() {
|
|
return {
|
|
cachedStyles: null,
|
|
}
|
|
},
|
|
|
|
computed: {
|
|
transition() {
|
|
const transitions = []
|
|
|
|
Object.keys(this.cachedStyles).forEach((key) => {
|
|
transitions.push(
|
|
`${this.convertToCssProperty(key)} ${this.duration}ms ${this.easing}`,
|
|
)
|
|
})
|
|
|
|
return transitions.join(', ')
|
|
},
|
|
},
|
|
|
|
watch: {
|
|
dimension() {
|
|
this.clearCachedDimensions()
|
|
},
|
|
},
|
|
|
|
methods: {
|
|
beforeAppear(el) {
|
|
// Emit the event to the parent
|
|
this.$emit('before-appear', el)
|
|
},
|
|
|
|
appear(el) {
|
|
// Emit the event to the parent
|
|
this.$emit('appear', el)
|
|
},
|
|
|
|
afterAppear(el) {
|
|
// Emit the event to the parent
|
|
this.$emit('after-appear', el)
|
|
},
|
|
|
|
appearCancelled(el) {
|
|
// Emit the event to the parent
|
|
this.$emit('appear-cancelled', el)
|
|
},
|
|
|
|
beforeEnter(el) {
|
|
// Emit the event to the parent
|
|
this.$emit('before-enter', el)
|
|
},
|
|
|
|
enter(el, done) {
|
|
// Because width and height may be 'auto',
|
|
// first detect and cache the dimensions
|
|
this.detectAndCacheDimensions(el)
|
|
|
|
// The order of applying styles is important:
|
|
// - 1. Set styles for state before transition
|
|
// - 2. Force repaint
|
|
// - 3. Add transition style
|
|
// - 4. Set styles for state after transition
|
|
// If the order is not right and you open any 2nd level submenu
|
|
// for the first time, the transition will not work.
|
|
this.setClosedDimensions(el)
|
|
this.hideOverflow(el)
|
|
this.forceRepaint(el)
|
|
this.setTransition(el)
|
|
this.setOpenedDimensions(el)
|
|
|
|
// Emit the event to the parent
|
|
this.$emit('enter', el, done)
|
|
|
|
// Call done() when the transition ends
|
|
// to trigger the @after-enter event.
|
|
setTimeout(done, this.duration)
|
|
},
|
|
|
|
afterEnter(el) {
|
|
// Clean up inline styles
|
|
this.unsetOverflow(el)
|
|
this.unsetTransition(el)
|
|
this.unsetDimensions(el)
|
|
this.clearCachedDimensions()
|
|
|
|
// Emit the event to the parent
|
|
this.$emit('after-enter', el)
|
|
},
|
|
|
|
enterCancelled(el) {
|
|
// Emit the event to the parent
|
|
this.$emit('enter-cancelled', el)
|
|
},
|
|
|
|
beforeLeave(el) {
|
|
// Emit the event to the parent
|
|
this.$emit('before-leave', el)
|
|
},
|
|
|
|
leave(el, done) {
|
|
// For some reason, @leave triggered when starting
|
|
// from open state on page load. So for safety,
|
|
// check if the dimensions have been cached.
|
|
this.detectAndCacheDimensions(el)
|
|
|
|
// The order of applying styles is less important
|
|
// than in the enter phase, as long as we repaint
|
|
// before setting the closed dimensions.
|
|
// But it is probably best to use the same
|
|
// order as the enter phase.
|
|
this.setOpenedDimensions(el)
|
|
this.hideOverflow(el)
|
|
this.forceRepaint(el)
|
|
this.setTransition(el)
|
|
this.setClosedDimensions(el)
|
|
|
|
// Emit the event to the parent
|
|
this.$emit('leave', el, done)
|
|
|
|
// Call done() when the transition ends
|
|
// to trigger the @after-leave event.
|
|
// This will also cause v-show
|
|
// to reapply 'display: none'.
|
|
setTimeout(done, this.duration)
|
|
},
|
|
|
|
afterLeave(el) {
|
|
// Clean up inline styles
|
|
this.unsetOverflow(el)
|
|
this.unsetTransition(el)
|
|
this.unsetDimensions(el)
|
|
this.clearCachedDimensions()
|
|
|
|
// Emit the event to the parent
|
|
this.$emit('after-leave', el)
|
|
},
|
|
|
|
leaveCancelled(el) {
|
|
// Emit the event to the parent
|
|
this.$emit('leave-cancelled', el)
|
|
},
|
|
|
|
detectAndCacheDimensions(el) {
|
|
// Cache actual dimensions
|
|
// only once to void invalid values when
|
|
// triggering during a transition
|
|
if (this.cachedStyles)
|
|
return
|
|
|
|
const visibility = el.style.visibility
|
|
const display = el.style.display
|
|
|
|
// Trick to get the width and
|
|
// height of a hidden element
|
|
el.style.visibility = 'hidden'
|
|
el.style.display = ''
|
|
|
|
this.cachedStyles = this.detectRelevantDimensions(el)
|
|
|
|
// Restore any original styling
|
|
el.style.visibility = visibility
|
|
el.style.display = display
|
|
},
|
|
|
|
clearCachedDimensions() {
|
|
this.cachedStyles = null
|
|
},
|
|
|
|
detectRelevantDimensions(el) {
|
|
// These properties will be transitioned
|
|
if (this.dimension === 'height') {
|
|
return {
|
|
height: `${el.offsetHeight}px`,
|
|
paddingTop:
|
|
el.style.paddingTop || this.getCssValue(el, 'padding-top'),
|
|
paddingBottom:
|
|
el.style.paddingBottom || this.getCssValue(el, 'padding-bottom'),
|
|
}
|
|
}
|
|
|
|
if (this.dimension === 'width') {
|
|
return {
|
|
width: `${el.offsetWidth}px`,
|
|
paddingLeft:
|
|
el.style.paddingLeft || this.getCssValue(el, 'padding-left'),
|
|
paddingRight:
|
|
el.style.paddingRight || this.getCssValue(el, 'padding-right'),
|
|
}
|
|
}
|
|
|
|
return {}
|
|
},
|
|
|
|
setTransition(el) {
|
|
el.style.transition = this.transition
|
|
},
|
|
|
|
unsetTransition(el) {
|
|
el.style.transition = ''
|
|
},
|
|
|
|
hideOverflow(el) {
|
|
el.style.overflow = 'hidden'
|
|
},
|
|
|
|
unsetOverflow(el) {
|
|
el.style.overflow = ''
|
|
},
|
|
|
|
setClosedDimensions(el) {
|
|
Object.keys(this.cachedStyles).forEach((key) => {
|
|
el.style[key] = '0'
|
|
})
|
|
},
|
|
|
|
setOpenedDimensions(el) {
|
|
Object.keys(this.cachedStyles).forEach((key) => {
|
|
el.style[key] = this.cachedStyles[key]
|
|
})
|
|
},
|
|
|
|
unsetDimensions(el) {
|
|
Object.keys(this.cachedStyles).forEach((key) => {
|
|
el.style[key] = ''
|
|
})
|
|
},
|
|
|
|
forceRepaint(el) {
|
|
// Force repaint to make sure the animation is triggered correctly.
|
|
// Thanks: https://markus.oberlehner.net/blog/transition-to-height-auto-with-vue/
|
|
// eslint-disable-next-line no-unused-expressions
|
|
getComputedStyle(el)[this.dimension]
|
|
},
|
|
|
|
getCssValue(el, style) {
|
|
return getComputedStyle(el, null).getPropertyValue(style)
|
|
},
|
|
|
|
convertToCssProperty(style) {
|
|
// Example: convert 'paddingTop' to 'padding-top'
|
|
// Thanks: https://gist.github.com/tan-yuki/3450323
|
|
const upperChars = style.match(/([A-Z])/g)
|
|
|
|
if (!upperChars)
|
|
return style
|
|
|
|
for (let i = 0, n = upperChars.length; i < n; i++) {
|
|
style = style.replace(
|
|
new RegExp(upperChars[i]),
|
|
`-${upperChars[i].toLowerCase()}`,
|
|
)
|
|
}
|
|
|
|
if (style.slice(0, 1) === '-')
|
|
style = style.slice(1)
|
|
|
|
return style
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<transition
|
|
:name="name"
|
|
@before-appear="beforeAppear"
|
|
@appear="appear"
|
|
@after-appear="afterAppear"
|
|
@appear-cancelled="appearCancelled"
|
|
@before-enter="beforeEnter"
|
|
@enter="enter"
|
|
@after-enter="afterEnter"
|
|
@enter-cancelled="enterCancelled"
|
|
@before-leave="beforeLeave"
|
|
@leave="leave"
|
|
@after-leave="afterLeave"
|
|
@leave-cancelled="leaveCancelled"
|
|
>
|
|
<slot />
|
|
</transition>
|
|
</template>
|