veui
Version:
Baidu Enterprise UI for Vue.js.
202 lines (181 loc) • 5.43 kB
JavaScript
import { normalize } from 'vue-directive-normalizer'
import { invert, uniqueId, remove, isEqual, difference, omit } from 'lodash'
import { getNodes } from '../utils/context'
import { contains } from '../utils/dom'
const OPTIONS_SCHEMA = {
value: 'handler',
arg: 'refs[]',
modifiers: {
trigger: ['click', 'mousedown', 'mouseup', 'hover', 'focus'],
excludeSelf: false,
delay: 0
}
}
const TRIGGER_EVENT_MAP = {
hover: 'mouseout',
focus: 'focusin'
}
const EVENT_TRIGGER_MAP = invert(TRIGGER_EVENT_MAP)
const TRIGGER_TYPES = ['click', 'mousedown', 'mouseup', 'hover', 'focus']
let handlerBindings = {}
function getBindingKey (type) {
return `__veui_${type}_outside__`
}
function addBinding (type, binding) {
let bindings = handlerBindings[type]
if (!bindings) {
handlerBindings[type] = [binding]
initBindingType(type)
} else {
bindings.push(binding)
}
}
function initBindingType (type) {
let key = getBindingKey(type)
let event = TRIGGER_EVENT_MAP[type] || type
document.addEventListener(
event,
(e) => {
handlerBindings[type].forEach((item) => {
item[key] && item[key].realHandler(e)
})
},
true
)
}
function getElementsByRefs (refs, context) {
const elements = []
refs.forEach((ref) => {
elements.push(...getNodes(ref, context))
})
return elements
}
/**
* 判断 element 在 DOM 树结构上是否被包含在 elements 里面,包括被 Portal 移动过的
*
* @param {Element} element 待判断的元素
* @param {Array.<Element>} elements 元素范围
*/
function isElementIn (element, elements) {
return elements.some((el) => {
return contains(el, element, true)
})
}
function generate (el, { handler, trigger, delay, refs, excludeSelf }, context) {
if (trigger !== 'hover') {
return function (e) {
// 非移动触发的受控模式下,直接判断元素包含情况
let includeTargets = [
...(excludeSelf ? [] : [el]),
...getElementsByRefs(refs, context)
]
if (
(EVENT_TRIGGER_MAP[e.type] || e.type) === trigger &&
!isElementIn(e.target, includeTargets)
) {
handler(e)
}
}
} else {
if (!delay) {
// 如果没有设置 delay 参数,只要检查到鼠标移到 includeTargets 外面去了,就同步触发 outside handler 。
return function handleOutsideSync (e) {
let includeTargets = [
...(excludeSelf ? [] : [el]),
...getElementsByRefs(refs, context)
]
// 从 includeTargets 区域移到外面去了,果断触发 handler
if (
isElementIn(e.target, includeTargets) &&
!isElementIn(e.relatedTarget, includeTargets)
) {
handler(e)
}
}
}
let bindingKey = getBindingKey('hover')
return function handleOutsideAsync (e) {
let includeTargets = [
...(excludeSelf ? [] : [el]),
...getElementsByRefs(refs, context)
]
let isRelatedTargetIn = isElementIn(e.relatedTarget, includeTargets)
if (isRelatedTargetIn) {
clearTimeout(el[bindingKey].timer)
el[bindingKey].timer = null
} else {
clearTimeout(el[bindingKey].timer)
el[bindingKey].delayCb = () => {
// refs 变化太频繁了,必须要实时再算一次
if (el[bindingKey]) {
includeTargets = [
...(excludeSelf ? [] : [el]),
...getElementsByRefs(el[bindingKey].refs, context)
]
if (!isElementIn(e.relatedTarget, includeTargets)) {
handler(e)
}
el[bindingKey].timer = null
el[bindingKey].delayCb = null
} else {
handler(e)
}
}
el[bindingKey].timer = setTimeout(el[bindingKey].delayCb, delay)
}
}
}
}
function clear (el) {
TRIGGER_TYPES.forEach((type) => {
let key = getBindingKey(type)
if (el[key]) {
remove(handlerBindings[type], (item) => item[key].id === el[key].id)
// bug: 导致 Dropdown 同时展开多个 popup
// 这里直接 clearTimeout 可能会导致之前 delay 的 timer 无效,暂时降级下,忽略 delay 直接调用
if (type === 'hover' && el[key].timer != null) {
el[key].delayCb && el[key].delayCb()
clearTimeout(el[key].timer)
}
el[key] = null
}
})
}
const OMIT_OPTIONS = ['refs', 'id', 'realHandler', 'timer', 'delayCb']
function isEqualOption (o1, o2) {
return (
difference(o1.refs, o2.refs).length === 0 &&
isEqual(omit(o1, OMIT_OPTIONS), omit(o2, OMIT_OPTIONS))
)
}
function refresh (el, binding, vnode) {
let options = normalize(binding, OPTIONS_SCHEMA)
let { trigger, refs, excludeSelf, delay } = options
let key = getBindingKey(trigger)
let oldOptions = el[key]
if (oldOptions && isEqualOption(options, oldOptions)) {
return
}
// 让后面的 clear 中的 delayCb 获取到最新的 refs
if (el[key]) {
el[key].refs = refs
}
let timer = el[key] && el[key].timer
clear(el)
el[key] = {
id: uniqueId('veui-outside-'),
handler: options.handler, // to compare with new one
realHandler: generate(el, options, vnode.context),
trigger,
refs,
excludeSelf,
delay,
timer
}
addBinding(trigger, el)
}
export default {
bind: refresh,
componentUpdated: refresh,
unbind: clear
}