UNPKG

vuetify

Version:

Vue.js 2 Semantic Component Framework

359 lines (317 loc) 8.76 kB
import Vue from 'vue' import Positionable from './positionable' import Stackable from './stackable' /* eslint-disable object-property-newline */ const dimensions = { activator: { top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0, offsetTop: 0, scrollHeight: 0 }, content: { top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0, offsetTop: 0, scrollHeight: 0 }, hasWindow: false } /* eslint-enable object-property-newline */ /** * Menuable * * @mixin * * Used for fixed or absolutely positioning * elements within the DOM * Can calculate X and Y axis overflows * As well as be manually positioned */ /* @vue/component */ export default Vue.extend({ name: 'menuable', mixins: [ Positionable, Stackable ], props: { activator: { default: null, validator: val => { return ['string', 'object'].includes(typeof val) } }, allowOverflow: Boolean, inputActivator: Boolean, light: Boolean, dark: Boolean, maxWidth: { type: [Number, String], default: 'auto' }, minWidth: [Number, String], nudgeBottom: { type: [Number, String], default: 0 }, nudgeLeft: { type: [Number, String], default: 0 }, nudgeRight: { type: [Number, String], default: 0 }, nudgeTop: { type: [Number, String], default: 0 }, nudgeWidth: { type: [Number, String], default: 0 }, offsetOverflow: Boolean, positionX: { type: Number, default: null }, positionY: { type: Number, default: null }, zIndex: { type: [Number, String], default: null } }, data: () => ({ absoluteX: 0, absoluteY: 0, dimensions: Object.assign({}, dimensions), isContentActive: false, pageYOffset: 0, stackClass: 'v-menu__content--active', stackMinZIndex: 6 }), computed: { computedLeft () { const a = this.dimensions.activator const c = this.dimensions.content const minWidth = a.width < c.width ? c.width : a.width let left = 0 left += this.left ? a.left - (minWidth - a.width) : a.left if (this.offsetX) left += this.left ? -a.width : a.width if (this.nudgeLeft) left -= parseInt(this.nudgeLeft) if (this.nudgeRight) left += parseInt(this.nudgeRight) return left }, computedTop () { const a = this.dimensions.activator const c = this.dimensions.content let top = this.top ? a.bottom - c.height : a.top if (!this.isAttached) top += this.pageYOffset if (this.offsetY) top += this.top ? -a.height : a.height if (this.nudgeTop) top -= parseInt(this.nudgeTop) if (this.nudgeBottom) top += parseInt(this.nudgeBottom) return top }, hasActivator () { return !!this.$slots.activator || this.activator || this.inputActivator }, isAttached () { return this.attach !== false } }, watch: { disabled (val) { val && this.callDeactivate() }, isActive (val) { if (this.disabled) return val ? this.callActivate() : this.callDeactivate() } }, beforeMount () { this.checkForWindow() }, methods: { absolutePosition () { return { offsetTop: 0, scrollHeight: 0, top: this.positionY || this.absoluteY, bottom: this.positionY || this.absoluteY, left: this.positionX || this.absoluteX, right: this.positionX || this.absoluteX, height: 0, width: 0 } }, activate () {}, calcLeft () { return `${this.isAttached ? this.computedLeft : this.calcXOverflow(this.computedLeft) }px` }, calcTop () { return `${this.isAttached ? this.computedTop : this.calcYOverflow(this.computedTop) }px` }, calcXOverflow (left) { const parsedMaxWidth = isNaN(parseInt(this.maxWidth)) ? 0 : parseInt(this.maxWidth) const innerWidth = this.getInnerWidth() const maxWidth = Math.max( this.dimensions.content.width, parsedMaxWidth ) const totalWidth = left + maxWidth const availableWidth = totalWidth - innerWidth if ((!this.left || this.right) && availableWidth > 0) { left = ( innerWidth - maxWidth - (innerWidth > 600 ? 30 : 12) // Account for scrollbar ) } if (left < 0) left = 12 return left + this.getOffsetLeft() }, calcYOverflow (top) { const documentHeight = this.getInnerHeight() const toTop = this.pageYOffset + documentHeight const activator = this.dimensions.activator const contentHeight = this.dimensions.content.height const totalHeight = top + contentHeight const isOverflowing = toTop < totalHeight // If overflowing bottom and offset // TODO: set 'bottom' position instead of 'top' if (isOverflowing && this.offsetOverflow && // If we don't have enough room to offset // the overflow, don't offset activator.top > contentHeight ) { top = this.pageYOffset + (activator.top - contentHeight) // If overflowing bottom } else if (isOverflowing && !this.allowOverflow) { top = toTop - contentHeight - 12 // If overflowing top } else if (top < this.pageYOffset && !this.allowOverflow) { top = this.pageYOffset + 12 } return top < 12 ? 12 : top }, callActivate () { if (!this.hasWindow) return this.activate() }, callDeactivate () { this.isContentActive = false this.deactivate() }, checkForWindow () { if (!this.hasWindow) { this.hasWindow = typeof window !== 'undefined' } }, checkForPageYOffset () { if (this.hasWindow) { this.pageYOffset = this.getOffsetTop() } }, deactivate () {}, getActivator () { if (this.inputActivator) { return this.$el.querySelector('.v-input__slot') } if (this.activator) { return typeof this.activator === 'string' ? document.querySelector(this.activator) : this.activator } return this.$refs.activator.children.length > 0 ? this.$refs.activator.children[0] : this.$refs.activator }, getInnerHeight () { if (!this.hasWindow) return 0 return window.innerHeight || document.documentElement.clientHeight }, getInnerWidth () { if (!this.hasWindow) return 0 return window.innerWidth }, getOffsetLeft () { if (!this.hasWindow) return 0 return window.pageXOffset || document.documentElement.scrollLeft }, getOffsetTop () { if (!this.hasWindow) return 0 return window.pageYOffset || document.documentElement.scrollTop }, getRoundedBoundedClientRect (el) { const rect = el.getBoundingClientRect() return { top: Math.round(rect.top), left: Math.round(rect.left), bottom: Math.round(rect.bottom), right: Math.round(rect.right), width: Math.round(rect.width), height: Math.round(rect.height) } }, measure (el, selector) { el = selector ? el.querySelector(selector) : el if (!el || !this.hasWindow) return null const rect = this.getRoundedBoundedClientRect(el) // Account for activator margin if (this.isAttached) { const style = window.getComputedStyle(el) rect.left = parseInt(style.marginLeft) rect.top = parseInt(style.marginTop) } return rect }, sneakPeek (cb) { requestAnimationFrame(() => { const el = this.$refs.content if (!el || this.isShown(el)) return cb() el.style.display = 'inline-block' cb() el.style.display = 'none' }) }, startTransition () { return new Promise(resolve => requestAnimationFrame(() => { this.isContentActive = this.hasJustFocused = this.isActive resolve() })) }, isShown (el) { return el.style.display !== 'none' }, updateDimensions () { this.checkForWindow() this.checkForPageYOffset() const dimensions = {} // Activator should already be shown dimensions.activator = !this.hasActivator || this.absolute ? this.absolutePosition() : this.measure(this.getActivator()) // Display and hide to get dimensions this.sneakPeek(() => { dimensions.content = this.measure(this.$refs.content) this.dimensions = dimensions }) } } })