floating-vue
Version:
Easy Vue tooltips, dropdowns, menus & popovers using floating-ui
1,180 lines (1,003 loc) • 31.8 kB
text/typescript
import { defineComponent } from 'vue'
import {
autoPlacement,
computePosition,
offset,
shift,
flip,
arrow,
getScrollParents,
size,
} from '@floating-ui/dom'
import { supportsPassive, isIOS } from '../util/env'
import { placements, Placement } from '../util/popper'
import { SHOW_EVENT_MAP, HIDE_EVENT_MAP } from '../util/events'
import { removeFromArray } from '../util/lang'
import { nextFrame } from '../util/frame'
import { getDefaultConfig, getAllParentThemes } from '../config'
export type ComputePositionConfig = Parameters<typeof computePosition>[2]
const shownPoppers = []
let hidingPopper = null
const shownPoppersByTheme: Record<string, any[]> = {}
function getShownPoppersByTheme (theme: string) {
let list = shownPoppersByTheme[theme]
if (!list) {
list = shownPoppersByTheme[theme] = []
}
return list
}
let Element: any = function () {}
if (typeof window !== 'undefined') {
Element = window.Element
}
function defaultPropFactory (prop: string) {
return function (props) {
return getDefaultConfig(props.theme, prop)
}
}
const PROVIDE_KEY = '__floating-vue__popper'
export default () => defineComponent({
name: 'VPopper',
provide () {
return {
[PROVIDE_KEY]: {
parentPopper: this,
},
}
},
inject: {
[PROVIDE_KEY]: { default: null },
},
props: {
theme: {
type: String,
required: true,
},
targetNodes: {
type: Function,
required: true,
},
referenceNode: {
type: Function,
default: null,
},
popperNode: {
type: Function,
required: true,
},
shown: {
type: Boolean,
default: false,
},
showGroup: {
type: String,
default: null,
},
// eslint-disable-next-line vue/require-prop-types
ariaId: {
default: null,
},
disabled: {
type: Boolean,
default: defaultPropFactory('disabled'),
},
positioningDisabled: {
type: Boolean,
default: defaultPropFactory('positioningDisabled'),
},
placement: {
type: String,
default: defaultPropFactory('placement'),
validator: (value: Placement) => placements.includes(value),
},
delay: {
type: [String, Number, Object],
default: defaultPropFactory('delay'),
},
distance: {
type: [Number, String],
default: defaultPropFactory('distance'),
},
skidding: {
type: [Number, String],
default: defaultPropFactory('skidding'),
},
triggers: {
type: Array,
default: defaultPropFactory('triggers'),
},
showTriggers: {
type: [Array, Function],
default: defaultPropFactory('showTriggers'),
},
hideTriggers: {
type: [Array, Function],
default: defaultPropFactory('hideTriggers'),
},
popperTriggers: {
type: Array,
default: defaultPropFactory('popperTriggers'),
},
popperShowTriggers: {
type: [Array, Function],
default: defaultPropFactory('popperShowTriggers'),
},
popperHideTriggers: {
type: [Array, Function],
default: defaultPropFactory('popperHideTriggers'),
},
container: {
type: [String, Object, Element, Boolean],
default: defaultPropFactory('container'),
},
boundary: {
type: [String, Element],
default: defaultPropFactory('boundary'),
},
strategy: {
type: String,
validator: (value: string) => ['absolute', 'fixed'].includes(value),
default: defaultPropFactory('strategy'),
},
autoHide: {
type: [Boolean, Function],
default: defaultPropFactory('autoHide'),
},
handleResize: {
type: Boolean,
default: defaultPropFactory('handleResize'),
},
instantMove: {
type: Boolean,
default: defaultPropFactory('instantMove'),
},
eagerMount: {
type: Boolean,
default: defaultPropFactory('eagerMount'),
},
popperClass: {
type: [String, Array, Object],
default: defaultPropFactory('popperClass'),
},
computeTransformOrigin: {
type: Boolean,
default: defaultPropFactory('computeTransformOrigin'),
},
/**
* @deprecated
*/
autoMinSize: {
type: Boolean,
default: defaultPropFactory('autoMinSize'),
},
autoSize: {
type: [Boolean, String],
default: defaultPropFactory('autoSize'),
},
/**
* @deprecated
*/
autoMaxSize: {
type: Boolean,
default: defaultPropFactory('autoMaxSize'),
},
autoBoundaryMaxSize: {
type: Boolean,
default: defaultPropFactory('autoBoundaryMaxSize'),
},
preventOverflow: {
type: Boolean,
default: defaultPropFactory('preventOverflow'),
},
overflowPadding: {
type: [Number, String],
default: defaultPropFactory('overflowPadding'),
},
arrowPadding: {
type: [Number, String],
default: defaultPropFactory('arrowPadding'),
},
arrowOverflow: {
type: Boolean,
default: defaultPropFactory('arrowOverflow'),
},
flip: {
type: Boolean,
default: defaultPropFactory('flip'),
},
shift: {
type: Boolean,
default: defaultPropFactory('shift'),
},
shiftCrossAxis: {
type: Boolean,
default: defaultPropFactory('shiftCrossAxis'),
},
noAutoFocus: {
type: Boolean,
default: defaultPropFactory('noAutoFocus'),
},
},
emits: [
'show',
'hide',
'update:shown',
'apply-show',
'apply-hide',
'close-group',
'close-directive',
'auto-hide',
'resize',
'dispose',
],
data () {
return {
isShown: false,
isMounted: false,
skipTransition: false,
classes: {
showFrom: false,
showTo: false,
hideFrom: false,
hideTo: true,
},
result: {
x: 0,
y: 0,
placement: '',
strategy: this.strategy,
arrow: {
x: 0,
y: 0,
centerOffset: 0,
},
transformOrigin: null,
},
shownChildren: new Set(),
lastAutoHide: true,
}
},
computed: {
popperId () {
return this.ariaId != null ? this.ariaId : this.randomId
},
shouldMountContent () {
return this.eagerMount || this.isMounted
},
slotData () {
return {
popperId: this.popperId,
isShown: this.isShown,
shouldMountContent: this.shouldMountContent,
skipTransition: this.skipTransition,
autoHide: typeof this.autoHide === 'function' ? this.lastAutoHide : this.autoHide,
show: this.show,
hide: this.hide,
handleResize: this.handleResize,
onResize: this.onResize,
classes: {
...this.classes,
popperClass: this.popperClass,
},
result: this.positioningDisabled ? null : this.result,
attrs: this.$attrs,
}
},
parentPopper () {
return this[PROVIDE_KEY]?.parentPopper
},
hasPopperShowTriggerHover () {
return this.popperTriggers?.includes('hover') || this.popperShowTriggers?.includes('hover')
},
},
watch: {
shown: '$_autoShowHide',
disabled (value) {
if (value) {
this.dispose()
} else {
this.init()
}
},
async container () {
if (this.isShown) {
this.$_ensureTeleport()
await this.$_computePosition()
}
},
...[
'triggers',
'positioningDisabled',
].reduce((acc, prop) => {
acc[prop] = '$_refreshListeners'
return acc
}, {}),
...[
'placement',
'distance',
'skidding',
'boundary',
'strategy',
'overflowPadding',
'arrowPadding',
'preventOverflow',
'shift',
'shiftCrossAxis',
'flip',
].reduce((acc, prop) => {
acc[prop] = '$_computePosition'
return acc
}, {}),
},
created () {
this.$_isDisposed = true
this.randomId = `popper_${[Math.random(), Date.now()].map(n => n.toString(36).substring(2, 10)).join('_')}`
if (this.autoMinSize) {
console.warn('[floating-vue] `autoMinSize` option is deprecated. Use `autoSize="min"` instead.')
}
if (this.autoMaxSize) {
console.warn('[floating-vue] `autoMaxSize` option is deprecated. Use `autoBoundaryMaxSize` instead.')
}
},
mounted () {
this.init()
this.$_detachPopperNode()
},
activated () {
this.$_autoShowHide()
},
deactivated () {
this.hide()
},
beforeUnmount () {
this.dispose()
},
methods: {
show ({ event = null, skipDelay = false, force = false } = {}) {
if (this.parentPopper?.lockedChild && this.parentPopper.lockedChild !== this) return
this.$_pendingHide = false
if (force || !this.disabled) {
if (this.parentPopper?.lockedChild === this) {
this.parentPopper.lockedChild = null
}
this.$_scheduleShow(event, skipDelay)
this.$emit('show')
// Prevent hiding with global handler
this.$_showFrameLocked = true
requestAnimationFrame(() => {
this.$_showFrameLocked = false
})
}
this.$emit('update:shown', true)
},
hide ({ event = null, skipDelay = false } = {}) {
if (this.$_hideInProgress) return
// Abort if child is shown
if (this.shownChildren.size > 0) {
this.$_pendingHide = true
return
}
// Abort if aiming for the popper
if (this.hasPopperShowTriggerHover && this.$_isAimingPopper()) {
if (this.parentPopper) {
this.parentPopper.lockedChild = this
clearTimeout(this.parentPopper.lockedChildTimer)
this.parentPopper.lockedChildTimer = setTimeout(() => {
if (this.parentPopper.lockedChild === this) {
this.parentPopper.lockedChild.hide({ skipDelay })
this.parentPopper.lockedChild = null
}
}, 1000)
}
return
}
if (this.parentPopper?.lockedChild === this) {
this.parentPopper.lockedChild = null
}
this.$_pendingHide = false
this.$_scheduleHide(event, skipDelay)
this.$emit('hide')
this.$emit('update:shown', false)
},
init () {
if (!this.$_isDisposed) return
this.$_isDisposed = false
this.isMounted = false
this.$_events = []
this.$_preventShow = false
// Nodes
this.$_referenceNode = this.referenceNode?.() ?? this.$el
this.$_targetNodes = this.targetNodes().filter(e => e.nodeType === e.ELEMENT_NODE)
this.$_popperNode = this.popperNode()
this.$_innerNode = this.$_popperNode.querySelector('.v-popper__inner')
this.$_arrowNode = this.$_popperNode.querySelector('.v-popper__arrow-container')
this.$_swapTargetAttrs('title', 'data-original-title')
this.$_detachPopperNode()
if (this.triggers.length) {
this.$_addEventListeners()
}
if (this.shown) {
this.show()
}
},
dispose () {
if (this.$_isDisposed) return
this.$_isDisposed = true
this.$_removeEventListeners()
this.hide({ skipDelay: true })
this.$_detachPopperNode()
this.isMounted = false
this.isShown = false
this.$_updateParentShownChildren(false)
this.$_swapTargetAttrs('data-original-title', 'title')
this.$emit('dispose')
},
async onResize () {
if (this.isShown) {
await this.$_computePosition()
this.$emit('resize')
}
},
async $_computePosition () {
if (this.$_isDisposed || this.positioningDisabled) return
const options: ComputePositionConfig = {
strategy: this.strategy,
middleware: [],
}
// Offset
if (this.distance || this.skidding) {
options.middleware.push(offset({
mainAxis: this.distance,
crossAxis: this.skidding,
}))
}
// Placement
const isPlacementAuto = this.placement.startsWith('auto')
if (isPlacementAuto) {
options.middleware.push(autoPlacement({
alignment: this.placement.split('-')[1] ?? '',
}))
} else {
options.placement = this.placement
}
if (this.preventOverflow) {
// Shift
if (this.shift) {
options.middleware.push(shift({
padding: this.overflowPadding,
boundary: this.boundary,
crossAxis: this.shiftCrossAxis,
}))
}
// Flip
if (!isPlacementAuto && this.flip) {
options.middleware.push(flip({
padding: this.overflowPadding,
boundary: this.boundary,
}))
}
}
// Arrow
options.middleware.push(arrow({
element: this.$_arrowNode,
padding: this.arrowPadding,
}))
// Arrow overflow
if (this.arrowOverflow) {
options.middleware.push({
name: 'arrowOverflow',
fn: ({ placement, rects, middlewareData }) => {
let overflow: boolean
const { centerOffset } = middlewareData.arrow
if (placement.startsWith('top') || placement.startsWith('bottom')) {
overflow = Math.abs(centerOffset) > rects.reference.width / 2
} else {
overflow = Math.abs(centerOffset) > rects.reference.height / 2
}
return {
data: {
overflow,
},
}
},
})
}
// Auto min size for the popper inner
if (this.autoMinSize || this.autoSize) {
const autoSize = this.autoSize ? this.autoSize : this.autoMinSize ? 'min' : null
options.middleware.push({
name: 'autoSize',
fn: ({ rects, placement, middlewareData }) => {
if (middlewareData.autoSize?.skip) {
return {}
}
let width: number
let height: number
if (placement.startsWith('top') || placement.startsWith('bottom')) {
width = rects.reference.width
} else {
height = rects.reference.height
}
// Apply and re-compute
this.$_innerNode.style[autoSize === 'min' ? 'minWidth' : autoSize === 'max' ? 'maxWidth' : 'width'] = width != null ? `${width}px` : null
this.$_innerNode.style[autoSize === 'min' ? 'minHeight' : autoSize === 'max' ? 'maxHeight' : 'height'] = height != null ? `${height}px` : null
return {
data: {
skip: true,
},
reset: {
rects: true,
},
}
},
})
}
// Auto max size for the popper inner
if (this.autoMaxSize || this.autoBoundaryMaxSize) {
// Reset size to bestFit strategy can apply
this.$_innerNode.style.maxWidth = null
this.$_innerNode.style.maxHeight = null
options.middleware.push(size({
boundary: this.boundary,
padding: this.overflowPadding,
apply: ({ width, height }) => {
// Apply and re-compute
this.$_innerNode.style.maxWidth = width != null ? `${width}px` : null
this.$_innerNode.style.maxHeight = height != null ? `${height}px` : null
},
}))
}
const data = await computePosition(this.$_referenceNode, this.$_popperNode, options)
Object.assign(this.result, {
x: data.x,
y: data.y,
placement: data.placement,
strategy: data.strategy,
arrow: {
...data.middlewareData.arrow,
...data.middlewareData.arrowOverflow,
},
})
},
$_scheduleShow (event = null, skipDelay = false) {
this.$_updateParentShownChildren(true)
this.$_hideInProgress = false
clearTimeout(this.$_scheduleTimer)
if (hidingPopper && this.instantMove && hidingPopper.instantMove && hidingPopper !== this.parentPopper) {
hidingPopper.$_applyHide(true)
this.$_applyShow(true)
return
}
if (skipDelay) {
this.$_applyShow()
} else {
this.$_scheduleTimer = setTimeout(this.$_applyShow.bind(this), this.$_computeDelay('show'))
}
},
$_scheduleHide (event = null, skipDelay = false) {
if (this.shownChildren.size > 0) {
this.$_pendingHide = true
return
}
this.$_updateParentShownChildren(false)
this.$_hideInProgress = true
clearTimeout(this.$_scheduleTimer)
if (this.isShown) {
hidingPopper = this
}
if (skipDelay) {
this.$_applyHide()
} else {
this.$_scheduleTimer = setTimeout(this.$_applyHide.bind(this), this.$_computeDelay('hide'))
}
},
$_computeDelay (type) {
const delay = this.delay
return parseInt((delay && delay[type]) || delay || 0)
},
async $_applyShow (skipTransition = false) {
clearTimeout(this.$_disposeTimer)
clearTimeout(this.$_scheduleTimer)
this.skipTransition = skipTransition
// Already shown
if (this.isShown) {
return
}
this.$_ensureTeleport()
await nextFrame()
await this.$_computePosition()
await this.$_applyShowEffect()
// Scroll
if (!this.positioningDisabled) {
this.$_registerEventListeners([
...getScrollParents(this.$_referenceNode),
...getScrollParents(this.$_popperNode),
], 'scroll', () => {
this.$_computePosition()
})
}
},
async $_applyShowEffect () {
if (this.$_hideInProgress) return
// Advanced animations
if (this.computeTransformOrigin) {
const bounds = this.$_referenceNode.getBoundingClientRect()
const popperWrapper = this.$_popperNode.querySelector('.v-popper__wrapper')
const parentBounds = popperWrapper.parentNode.getBoundingClientRect()
const x = (bounds.x + bounds.width / 2) - (parentBounds.left + popperWrapper.offsetLeft)
const y = (bounds.y + bounds.height / 2) - (parentBounds.top + popperWrapper.offsetTop)
this.result.transformOrigin = `${x}px ${y}px`
}
this.isShown = true
this.$_applyAttrsToTarget({
'aria-describedby': this.popperId,
'data-popper-shown': '',
})
const showGroup = this.showGroup
if (showGroup) {
let popover
for (let i = 0; i < shownPoppers.length; i++) {
popover = shownPoppers[i]
if (popover.showGroup !== showGroup) {
popover.hide()
popover.$emit('close-group')
}
}
}
shownPoppers.push(this)
document.body.classList.add('v-popper--some-open')
for (const theme of getAllParentThemes(this.theme)) {
getShownPoppersByTheme(theme).push(this)
document.body.classList.add(`v-popper--some-open--${theme}`)
}
this.$emit('apply-show')
// Advanced classes
this.classes.showFrom = true
this.classes.showTo = false
this.classes.hideFrom = false
this.classes.hideTo = false
await nextFrame()
this.classes.showFrom = false
this.classes.showTo = true
if (!this.noAutoFocus) this.$_popperNode.focus()
},
async $_applyHide (skipTransition = false) {
if (this.shownChildren.size > 0) {
this.$_pendingHide = true
this.$_hideInProgress = false
return
}
clearTimeout(this.$_scheduleTimer)
// Already hidden
if (!this.isShown) {
return
}
this.skipTransition = skipTransition
removeFromArray(shownPoppers, this)
if (shownPoppers.length === 0) {
document.body.classList.remove('v-popper--some-open')
}
for (const theme of getAllParentThemes(this.theme)) {
const list = getShownPoppersByTheme(theme)
removeFromArray(list, this)
if (list.length === 0) {
document.body.classList.remove(`v-popper--some-open--${theme}`)
}
}
if (hidingPopper === this) {
hidingPopper = null
}
this.isShown = false
this.$_applyAttrsToTarget({
'aria-describedby': undefined,
'data-popper-shown': undefined,
})
clearTimeout(this.$_disposeTimer)
const disposeTime = getDefaultConfig(this.theme, 'disposeTimeout')
if (disposeTime !== null) {
this.$_disposeTimer = setTimeout(() => {
if (this.$_popperNode) {
// Don't remove popper instance, just the HTML element
this.$_detachPopperNode()
this.isMounted = false
}
}, disposeTime)
}
this.$_removeEventListeners('scroll')
this.$emit('apply-hide')
// Advanced classes
this.classes.showFrom = false
this.classes.showTo = false
this.classes.hideFrom = true
this.classes.hideTo = false
await nextFrame()
this.classes.hideFrom = false
this.classes.hideTo = true
},
$_autoShowHide () {
if (this.shown) {
this.show()
} else {
this.hide()
}
},
$_ensureTeleport () {
if (this.$_isDisposed) return
let container = this.container
// if container is a query, get the relative element
if (typeof container === 'string') {
container = window.document.querySelector(container)
} else if (container === false) {
// if container is `false`, set it to reference parent
container = this.$_targetNodes[0].parentNode
}
if (!container) {
throw new Error('No container for popover: ' + this.container)
}
container.appendChild(this.$_popperNode)
this.isMounted = true
},
$_addEventListeners () {
// Add trigger show events
const handleShow = event => {
if (this.isShown && !this.$_hideInProgress) {
return
}
event.usedByTooltip = true
// Prevent open on mobile touch in global close
!this.$_preventShow && this.show({ event })
}
this.$_registerTriggerListeners(this.$_targetNodes, SHOW_EVENT_MAP, this.triggers, this.showTriggers, handleShow)
this.$_registerTriggerListeners([this.$_popperNode], SHOW_EVENT_MAP, this.popperTriggers, this.popperShowTriggers, handleShow)
// Add trigger hide events
const handleHide = event => {
if (event.usedByTooltip) {
return
}
this.hide({ event })
}
this.$_registerTriggerListeners(this.$_targetNodes, HIDE_EVENT_MAP, this.triggers, this.hideTriggers, handleHide)
this.$_registerTriggerListeners([this.$_popperNode], HIDE_EVENT_MAP, this.popperTriggers, this.popperHideTriggers, handleHide)
},
$_registerEventListeners (targetNodes: any[], eventType: string, handler: (event: Event) => void) {
this.$_events.push({ targetNodes, eventType, handler })
targetNodes.forEach(node => node.addEventListener(eventType, handler, supportsPassive
? {
passive: true,
}
: undefined))
},
$_registerTriggerListeners (targetNodes: any[], eventMap: Record<string, string>, commonTriggers, customTrigger, handler: (event: Event) => void) {
let triggers = commonTriggers
if (customTrigger != null) {
triggers = typeof customTrigger === 'function' ? customTrigger(triggers) : customTrigger
}
triggers.forEach(trigger => {
const eventType = eventMap[trigger]
if (eventType) {
this.$_registerEventListeners(targetNodes, eventType, handler)
}
})
},
$_removeEventListeners (filterEventType?: string) {
const newList = []
this.$_events.forEach(listener => {
const { targetNodes, eventType, handler } = listener
if (!filterEventType || filterEventType === eventType) {
targetNodes.forEach(node => node.removeEventListener(eventType, handler))
} else {
newList.push(listener)
}
})
this.$_events = newList
},
$_refreshListeners () {
if (!this.$_isDisposed) {
this.$_removeEventListeners()
this.$_addEventListeners()
}
},
$_handleGlobalClose (event, touch = false) {
if (this.$_showFrameLocked) return
this.hide({ event: event })
if (event.closePopover) {
this.$emit('close-directive')
} else {
this.$emit('auto-hide')
}
if (touch) {
this.$_preventShow = true
setTimeout(() => {
this.$_preventShow = false
}, 300)
}
},
$_detachPopperNode () {
this.$_popperNode.parentNode && this.$_popperNode.parentNode.removeChild(this.$_popperNode)
},
$_swapTargetAttrs (attrFrom, attrTo) {
for (const el of this.$_targetNodes) {
const value = el.getAttribute(attrFrom)
if (value) {
el.removeAttribute(attrFrom)
el.setAttribute(attrTo, value)
}
}
},
$_applyAttrsToTarget (attrs) {
for (const el of this.$_targetNodes) {
for (const n in attrs) {
const value = attrs[n]
if (value == null) {
el.removeAttribute(n)
} else {
el.setAttribute(n, value)
}
}
}
},
$_updateParentShownChildren (value) {
let parent = this.parentPopper
while (parent) {
if (value) {
parent.shownChildren.add(this.randomId)
} else {
parent.shownChildren.delete(this.randomId)
if (parent.$_pendingHide) {
parent.hide()
}
}
parent = parent.parentPopper
}
},
$_isAimingPopper () {
const referenceBounds: DOMRect = this.$_referenceNode.getBoundingClientRect()
if (mouseX >= referenceBounds.left && mouseX <= referenceBounds.right && mouseY >= referenceBounds.top && mouseY <= referenceBounds.bottom) {
const popperBounds: DOMRect = this.$_popperNode.getBoundingClientRect()
const vectorX = mouseX - mousePreviousX
const vectorY = mouseY - mousePreviousY
const distance = (popperBounds.left + popperBounds.width / 2) - mousePreviousX + (popperBounds.top + popperBounds.height / 2) - mousePreviousY
// Make the vector long enough to be sure that it can intersect with the popper
const newVectorLength = distance + popperBounds.width + popperBounds.height
const edgeX = mousePreviousX + vectorX * newVectorLength
const edgeY = mousePreviousY + vectorY * newVectorLength
// Check for collision between the vector and the popper bounds
return (
lineIntersectsLine(mousePreviousX, mousePreviousY, edgeX, edgeY, popperBounds.left, popperBounds.top, popperBounds.left, popperBounds.bottom) || // Left edge
lineIntersectsLine(mousePreviousX, mousePreviousY, edgeX, edgeY, popperBounds.left, popperBounds.top, popperBounds.right, popperBounds.top) || // Top edge
lineIntersectsLine(mousePreviousX, mousePreviousY, edgeX, edgeY, popperBounds.right, popperBounds.top, popperBounds.right, popperBounds.bottom) || // Right edge
lineIntersectsLine(mousePreviousX, mousePreviousY, edgeX, edgeY, popperBounds.left, popperBounds.bottom, popperBounds.right, popperBounds.bottom) // Bottom edge
)
}
return false
},
},
render () {
return this.$slots.default(this.slotData)
},
})
if (typeof document !== 'undefined' && typeof window !== 'undefined') {
if (isIOS) {
document.addEventListener('touchstart', handleGlobalMousedown, supportsPassive
? {
passive: true,
capture: true,
}
: true)
document.addEventListener('touchend', handleGlobalTouchend, supportsPassive
? {
passive: true,
capture: true,
}
: true)
} else {
window.addEventListener('mousedown', handleGlobalMousedown, true)
window.addEventListener('click', handleGlobalClick, true)
}
window.addEventListener('resize', computePositionAllShownPoppers)
}
function handleGlobalMousedown (event) {
for (let i = 0; i < shownPoppers.length; i++) {
const popper = shownPoppers[i]
try {
const popperContent = popper.popperNode()
popper.$_mouseDownContains = popperContent.contains(event.target)
} catch (e) {
// noop
}
}
}
function handleGlobalClick (event) {
handleGlobalClose(event)
}
function handleGlobalTouchend (event) {
handleGlobalClose(event, true)
}
function handleGlobalClose (event, touch = false) {
const preventClose: Record<string, true> = {}
for (let i = shownPoppers.length - 1; i >= 0; i--) {
const popper = shownPoppers[i]
try {
const contains = popper.$_containsGlobalTarget = isContainingEventTarget(popper, event)
popper.$_pendingHide = false
// Delay so that close directive has time to set values (closeAllPopover, closePopover)
requestAnimationFrame(() => {
popper.$_pendingHide = false
if (preventClose[popper.randomId]) return
if (shouldAutoHide(popper, contains, event)) {
popper.$_handleGlobalClose(event, touch)
// Only close child popper
if (!event.closeAllPopover && event.closePopover && contains) {
let parent = popper.parentPopper
while (parent) {
preventClose[parent.randomId] = true
parent = parent.parentPopper
}
return
}
// Auto hide parents
let parent = popper.parentPopper
while (parent) {
if (shouldAutoHide(parent, parent.$_containsGlobalTarget, event)) {
parent.$_handleGlobalClose(event, touch)
} else {
break
}
parent = parent.parentPopper
}
}
})
} catch (e) {
// noop
}
}
}
function isContainingEventTarget (popper, event): boolean {
const popperContent = popper.popperNode()
return popper.$_mouseDownContains || popperContent.contains(event.target)
}
function shouldAutoHide (popper, contains, event): boolean {
return event.closeAllPopover || (event.closePopover && contains) || (getAutoHideResult(popper, event) && !contains)
}
function getAutoHideResult (popper, event) {
if (typeof popper.autoHide === 'function') {
const result = popper.autoHide(event)
popper.lastAutoHide = result
return result
}
return popper.autoHide
}
function computePositionAllShownPoppers (event) {
for (let i = 0; i < shownPoppers.length; i++) {
const popper = shownPoppers[i]
popper.$_computePosition(event)
}
}
export function hideAllPoppers () {
for (let i = 0; i < shownPoppers.length; i++) {
const popper = shownPoppers[i]
popper.hide()
}
}
// Track mouse movement to detect aiming at the popper
let mousePreviousX = 0
let mousePreviousY = 0
let mouseX = 0
let mouseY = 0
if (typeof window !== 'undefined') {
window.addEventListener('mousemove', event => {
mousePreviousX = mouseX
mousePreviousY = mouseY
mouseX = event.clientX
mouseY = event.clientY
}, supportsPassive
? {
passive: true,
}
: undefined)
}
function lineIntersectsLine (x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number) {
const uA = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1))
const uB = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1))
return (uA >= 0 && uA <= 1 && uB >= 0 && uB <= 1)
}