UNPKG

vuetify

Version:

Vue Material Component Framework

236 lines (195 loc) 6.29 kB
// Components import VOverlay from '../../components/VOverlay' // Utilities import { keyCodes, addOnceEventListener, addPassiveEventListener, getZIndex, } from '../../util/helpers' // Types import Vue from 'vue' interface Toggleable extends Vue { isActive?: boolean } interface Stackable extends Vue { activeZIndex: number } interface options { absolute?: boolean $refs: { dialog?: HTMLElement content?: HTMLElement } } /* @vue/component */ export default Vue.extend<Vue & Toggleable & Stackable & options>().extend({ name: 'overlayable', props: { hideOverlay: Boolean, overlayColor: String, overlayOpacity: [Number, String], }, data () { return { animationFrame: 0, overlay: null as InstanceType<typeof VOverlay> | null, } }, watch: { hideOverlay (value) { if (!this.isActive) return if (value) this.removeOverlay() else this.genOverlay() }, }, beforeDestroy () { this.removeOverlay() }, methods: { createOverlay () { const overlay = new VOverlay({ propsData: { absolute: this.absolute, value: false, color: this.overlayColor, opacity: this.overlayOpacity, }, }) overlay.$mount() const parent = this.absolute ? this.$el.parentNode : document.querySelector('[data-app]') parent && parent.insertBefore(overlay.$el, parent.firstChild) this.overlay = overlay }, genOverlay () { this.hideScroll() if (this.hideOverlay) return if (!this.overlay) this.createOverlay() this.animationFrame = requestAnimationFrame(() => { if (!this.overlay) return if (this.activeZIndex !== undefined) { this.overlay.zIndex = String(this.activeZIndex - 1) } else if (this.$el) { this.overlay.zIndex = getZIndex(this.$el) } this.overlay.value = true }) return true }, /** removeOverlay(false) will not restore the scollbar afterwards */ removeOverlay (showScroll = true) { if (this.overlay) { addOnceEventListener(this.overlay.$el, 'transitionend', () => { if ( !this.overlay || !this.overlay.$el || !this.overlay.$el.parentNode || this.overlay.value ) return this.overlay.$el.parentNode.removeChild(this.overlay.$el) this.overlay.$destroy() this.overlay = null }) // Cancel animation frame in case // overlay is removed before it // has finished its animation cancelAnimationFrame(this.animationFrame) this.overlay.value = false } showScroll && this.showScroll() }, scrollListener (e: WheelEvent & KeyboardEvent) { if (e.type === 'keydown') { if ( ['INPUT', 'TEXTAREA', 'SELECT'].includes((e.target as Element).tagName) || // https://github.com/vuetifyjs/vuetify/issues/4715 (e.target as HTMLElement).isContentEditable ) return const up = [keyCodes.up, keyCodes.pageup] const down = [keyCodes.down, keyCodes.pagedown] if (up.includes(e.keyCode)) { (e as any).deltaY = -1 } else if (down.includes(e.keyCode)) { (e as any).deltaY = 1 } else { return } } if (e.target === this.overlay || (e.type !== 'keydown' && e.target === document.body) || this.checkPath(e)) e.preventDefault() }, hasScrollbar (el?: Element) { if (!el || el.nodeType !== Node.ELEMENT_NODE) return false const style = window.getComputedStyle(el) return ['auto', 'scroll'].includes(style.overflowY!) && el.scrollHeight > el.clientHeight }, shouldScroll (el: Element, delta: number) { if (el.scrollTop === 0 && delta < 0) return true return el.scrollTop + el.clientHeight === el.scrollHeight && delta > 0 }, isInside (el: Element, parent: Element): boolean { if (el === parent) { return true } else if (el === null || el === document.body) { return false } else { return this.isInside(el.parentNode as Element, parent) } }, checkPath (e: WheelEvent) { const path = e.path || this.composedPath(e) const delta = e.deltaY if (e.type === 'keydown' && path[0] === document.body) { const dialog = this.$refs.dialog // getSelection returns null in firefox in some edge cases, can be ignored const selected = window.getSelection()!.anchorNode as Element if (dialog && this.hasScrollbar(dialog) && this.isInside(selected, dialog)) { return this.shouldScroll(dialog, delta) } return true } for (let index = 0; index < path.length; index++) { const el = path[index] if (el === document) return true if (el === document.documentElement) return true if (el === this.$refs.content) return true if (this.hasScrollbar(el as Element)) return this.shouldScroll(el as Element, delta) } return true }, /** * Polyfill for Event.prototype.composedPath */ composedPath (e: WheelEvent): EventTarget[] { if (e.composedPath) return e.composedPath() const path = [] let el = e.target as Element while (el) { path.push(el) if (el.tagName === 'HTML') { path.push(document) path.push(window) return path } el = el.parentElement! } return path }, hideScroll () { if (this.$vuetify.breakpoint.smAndDown) { document.documentElement!.classList.add('overflow-y-hidden') } else { addPassiveEventListener(window, 'wheel', this.scrollListener as EventHandlerNonNull, { passive: false }) window.addEventListener('keydown', this.scrollListener as EventHandlerNonNull) } }, showScroll () { document.documentElement!.classList.remove('overflow-y-hidden') window.removeEventListener('wheel', this.scrollListener as EventHandlerNonNull) window.removeEventListener('keydown', this.scrollListener as EventHandlerNonNull) }, }, })