vuetify
Version:
Vue Material Component Framework
152 lines (127 loc) • 3.66 kB
text/typescript
// Mixins
import Bootable from '../bootable'
// Utilities
import { getObjectValueByPath } from '../../util/helpers'
import mixins, { ExtractVue } from '../../util/mixins'
import { consoleWarn } from '../../util/console'
// Types
import Vue, { PropOptions } from 'vue'
import { VNode } from 'vue/types'
interface options extends Vue {
$el: HTMLElement
$refs: {
content: HTMLElement
}
}
function validateAttachTarget (val: any) {
const type = typeof val
if (type === 'boolean' || type === 'string') return true
return val.nodeType === Node.ELEMENT_NODE
}
/* @vue/component */
export default mixins<options &
/* eslint-disable indent */
ExtractVue<typeof Bootable>
/* eslint-enable indent */
>(Bootable).extend({
name: 'detachable',
props: {
attach: {
default: false,
validator: validateAttachTarget,
} as PropOptions<boolean | string | Element>,
contentClass: {
type: String,
default: '',
},
},
data: () => ({
activatorNode: null as null | VNode | VNode[],
hasDetached: false,
}),
watch: {
attach () {
this.hasDetached = false
this.initDetach()
},
hasContent () {
this.$nextTick(this.initDetach)
},
},
beforeMount () {
this.$nextTick(() => {
if (this.activatorNode) {
const activator = Array.isArray(this.activatorNode) ? this.activatorNode : [this.activatorNode]
activator.forEach(node => {
if (!node.elm) return
if (!this.$el.parentNode) return
const target = this.$el === this.$el.parentNode.firstChild
? this.$el
: this.$el.nextSibling
this.$el.parentNode.insertBefore(node.elm, target)
})
}
})
},
mounted () {
this.hasContent && this.initDetach()
},
deactivated () {
this.isActive = false
},
beforeDestroy () {
// IE11 Fix
try {
if (
this.$refs.content &&
this.$refs.content.parentNode
) {
this.$refs.content.parentNode.removeChild(this.$refs.content)
}
if (this.activatorNode) {
const activator = Array.isArray(this.activatorNode) ? this.activatorNode : [this.activatorNode]
activator.forEach(node => {
node.elm &&
node.elm.parentNode &&
node.elm.parentNode.removeChild(node.elm)
})
}
} catch (e) { console.log(e) } /* eslint-disable-line no-console */
},
methods: {
getScopeIdAttrs () {
const scopeId = getObjectValueByPath(this.$vnode, 'context.$options._scopeId')
return scopeId && {
[scopeId]: '',
}
},
initDetach () {
if (this._isDestroyed ||
!this.$refs.content ||
this.hasDetached ||
// Leave menu in place if attached
// and dev has not changed target
this.attach === '' || // If used as a boolean prop (<v-menu attach>)
this.attach === true || // If bound to a boolean (<v-menu :attach="true">)
this.attach === 'attach' // If bound as boolean prop in pug (v-menu(attach))
) return
let target
if (this.attach === false) {
// Default, detach to app
target = document.querySelector('[data-app]')
} else if (typeof this.attach === 'string') {
// CSS selector
target = document.querySelector(this.attach)
} else {
// DOM Element
target = this.attach
}
if (!target) {
consoleWarn(`Unable to locate target ${this.attach || '[data-app]'}`, this)
return
}
target.appendChild(this.$refs.content)
this.hasDetached = true
},
},
})