UNPKG

vue-show-slide

Version:

Vue.js directive for animating element to and from height: auto in a sliding motion

297 lines (255 loc) 8.89 kB
const VShowSlide = { easingOptions: { builtIn: ['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out'], custom: {}, }, targets: [], /** * Called when plugin is initialized */ install(Vue, options) { this.validateOptions(options) Vue.directive('show-slide', { created: this.bind.bind(this), mounted: this.inserted.bind(this), updated: this.componentUpdated.bind(this), }) }, /** * Bind directive hook. Called only once, when the directive is first bound to the element. */ bind(el, binding) { this.parseArgs(el, binding) }, /** * Inserted directive hook. Called when the bound element has been inserted into its parent node */ inserted(el, binding) { this.initializeTarget(el, binding.value) }, /** * Update directive hook. Called after the containing component’s VNode and the VNodes of its children have updated */ componentUpdated(el, binding) { this.toggleSlide(el, binding) }, /** * Get target by element */ getTargetByEl(el) { // Use `filter` instead of `find` for IE 11 compatibility const target = this.targets.filter(target => target.el.isSameNode(el))[0] if (target === undefined) { throw 'Element not found!' } return target }, setTargetByEl(el, target) { const targetIndex = this.targets.findIndex(target => target.el.isSameNode(el)) this.targets[targetIndex] = target }, /** * Set target property by element */ setTargetPropertyByEl(el, property, value) { const target = this.getTargetByEl(el) const filteredTargets = this.targets.filter( target => !target.el.isSameNode(el) ) this.targets = [ ...filteredTargets, { ...target, [property]: value, }, ] }, /** * Validate options passed to plugin */ validateOptions(options) { if ( typeof options !== 'undefined' && Object.prototype.hasOwnProperty.call(options, 'customEasing') ) { this.easingOptions.custom = options.customEasing } }, /** * Convert a string from kebab-case to camelCase */ kebabToCamel(string) { return string.replace(/-([a-z])/g, function(g) { return g[1].toUpperCase() }) }, /** * Fire a custom event */ fireEvent(el, eventName) { // CustomEvent is supported if (typeof window.CustomEvent === 'function') { el.dispatchEvent(new CustomEvent(eventName)) } else { // CustomEvent is not supported, fire the event the old fashioned way const event = document.createEvent('CustomEvent') event.initCustomEvent(eventName, false, false, null) el.dispatchEvent(event) } }, /** * Parse directive arguments */ parseArgs(el, binding) { if ( Object.prototype.hasOwnProperty.call(binding, 'arg') && typeof binding.arg === 'string' ) { const argsArray = binding.arg.split(':') const easing = this.validateEasing(argsArray) const duration = this.validateDuration(argsArray) this.targets.push({ el, duration, durationInSeconds: `${duration / 1000}s`, easing, isAnimating: false }) } else { this.targets.push({ el, duration: 300, durationInSeconds: '0.3s', easing: 'ease', isAnimating: false }) } }, /** * Validate easing option */ validateEasing(argsArray) { if (Object.prototype.hasOwnProperty.call(argsArray, 1)) { if (this.easingOptions.builtIn.indexOf(argsArray[1]) > -1) { return argsArray[1] } else if ( Object.prototype.hasOwnProperty.call( this.easingOptions.custom, this.kebabToCamel(argsArray[1]) ) ) { return this.easingOptions.custom[this.kebabToCamel(argsArray[1])] } else { return 'ease' } } else { return 'ease' } }, /** * Validate duration */ validateDuration(argsArray) { return Object.prototype.hasOwnProperty.call(argsArray, 0) ? parseInt(argsArray[0]) : 300 }, /** * Initialize styles on target element */ initializeTarget(el, open) { // console.log('initializeTarget') const computedData = window.getComputedStyle(el) const paddingTop = parseInt(computedData.paddingTop) || 0 const paddingBottom = parseInt(computedData.paddingBottom) || 0 const target = this.getTargetByEl(el) target.paddingTop = paddingTop target.paddingBottom = paddingBottom this.setTargetByEl(el, target) if (!open) { el.style.height = '0px' el.style.visibility = 'hidden' el.style.paddingTop = '0' el.style.paddingBottom = '0' el.style.boxSizing = 'border-box' } const { easing, durationInSeconds } = target el.style.overflow = 'hidden' el.style.transition = `height ${durationInSeconds} ${easing}, padding ${durationInSeconds} ${easing}` }, /** * Toggle the element */ toggleSlide(el, binding) { if (binding.value !== binding.oldValue) { if (binding.value) { this.slideOpen(el) } else { this.slideClosed(el) } } }, /** * Slide element open */ async slideOpen(el) { this.fireEvent(el, 'slide-open-start') const { isAnimating, timeout, duration, paddingTop, paddingBottom } = this.getTargetByEl(el) // Check if element is animating if (isAnimating) { clearTimeout(timeout) } // Set animating to true this.setTargetPropertyByEl(el, 'isAnimating', true) // Make element visible again el.style.visibility = 'visible' el.style.paddingTop = '' el.style.paddingBottom = '' // Set element height to scroll height + calculated border height const scrollHeight = el.scrollHeight const computedStyle = window.getComputedStyle(el) const borderBottom = parseFloat(computedStyle.getPropertyValue('border-bottom-width')) const borderTop = parseFloat(computedStyle.getPropertyValue('border-top-width')) // el.style.height = `${scrollHeight + borderBottom + borderTop}px` el.style.height = `${scrollHeight + paddingTop + paddingBottom + borderBottom + borderTop}px` // Reset element height to auto after animating const newTimeout = setTimeout(() => { el.style.height = 'auto' el.style.overflow = '' el.style.boxSizing = '' this.setTargetPropertyByEl(el, 'isAnimating', false) this.fireEvent(el, 'slide-open-end') }, duration) this.setTargetPropertyByEl(el, 'timeout', newTimeout) }, /** * Slide element closed */ slideClosed(el) { this.fireEvent(el, 'slide-close-start') const { isAnimating, timeout, duration } = this.getTargetByEl(el) // Check if element is animating if (isAnimating) { clearTimeout(timeout) } // Set animating to true this.setTargetPropertyByEl(el, 'isAnimating', true) const scrollHeight = el.scrollHeight el.style.height = `${scrollHeight}px` const forceRedraw = el.offsetLeft el.style.height = '0px' el.style.overflow = 'hidden' el.style.paddingTop = '0' el.style.paddingBottom = '0' el.style.boxSizing = 'border-box' // Update isAnimating after animation is done // And set visibility to `hidden` const newTimeout = setTimeout(() => { this.setTargetPropertyByEl(el, 'isAnimating', false) el.style.visibility = 'hidden' this.fireEvent(el, 'slide-close-end') }, duration) this.setTargetPropertyByEl(el, 'timeout', newTimeout) }, } export default VShowSlide