Vue.js 指令 inserted/bind 覆盖 eventListener 点击事件
Vue.js Directive inserted/bind overwrites eventListener click event
假设我在页面上有多个下拉菜单或元素,它们都使用了我用过的称为 closable 的指令。如果单击的元素在使用指令的元素之外,这将调用传入的表达式。
然而,预期的行为是,如果我单击页面上的一个元素,即另一个带有指令的下拉菜单,它应该获得该单击事件路径,将它们与现有的进行比较,如果它们不匹配或不包含在它应该关闭它的元素。
实际发生的是点击事件从未被注册,它只是初始化了另一个指令并且由于某种原因点击事件丢失了。
只有当我点击没有指令的东西时才会注册点击事件。
Vue.directive ( 'closable', {
inserted: ( el, binding, vnode ) => {
// assign event to the element
el.clickOutsideEvent = function ( event ) {
console.log ( {el, event} );
// here we check if the click event is outside the element and it's children
if ( !( el == event.path[0] || el.contains ( event.path[0] ) ) ) {
// if clicked outside, call the provided method
vnode.context[binding.expression] ( event );
}
};
// register click and touch events
document.body.addEventListener ( 'click', el.clickOutsideEvent );
document.body.addEventListener ( 'touchstart', el.clickOutsideEvent );
},
unbind: function ( el ) {
// unregister click and touch events before the element is unmounted
document.body.removeEventListener ( 'click', el.clickOutsideEvent );
document.body.removeEventListener ( 'touchstart', el.clickOutsideEvent );
},
stopProp ( event ) {
event.stopPropagation ();
},
} );
更新
这是 v-click-outside
指令的另一种变体 - 在本地,就在您的组件内:
directives:
{
clickOutside:
{
bind(elem, binding, vnode)
{
elem.clickOutsideEvent = function(evt)
{
if (elem !== evt.target && !elem.contains(evt.target)) vnode.context[binding.expression](evt);
};
document.body.addEventListener('click', elem.clickOutsideEvent);
},
unbind(elem)
{
document.body.removeEventListener('click', elem.clickOutsideEvent);
}
}
},
您可以试试这个实现:
import Vue from 'vue'
const HAS_WINDOWS = typeof window !== 'undefined';
const HAS_NAVIGATOR = typeof navigator !== 'undefined';
const IS_TOUCH = HAS_WINDOWS && ('ontouchstart' in window || (HAS_NAVIGATOR && navigator.msMaxTouchPoints > 0));
const EVENTS = IS_TOUCH ? ['touchstart'] : ['click'];
const IDENTITY = (item) => item;
const directive = {
instances: [],
};
function processDirectiveArguments (bindingValue)
{
const isFunction = typeof bindingValue === 'function';
if (!isFunction && typeof bindingValue !== 'object')
{
throw new Error('v-click-outside: Binding value must be a function or an object')
}
return {
handler: isFunction ? bindingValue : bindingValue.handler,
middleware: bindingValue.middleware || IDENTITY,
events: bindingValue.events || EVENTS,
isActive: !(bindingValue.isActive === false),
}
}
function onEvent ({ el, event, handler, middleware })
{
const isClickOutside = event.target !== el && !el.contains(event.target);
if (!isClickOutside)
{
return
}
if (middleware(event, el))
{
handler(event, el)
}
}
function createInstance ({ el, events, handler, middleware })
{
return {
el,
eventHandlers: events.map((eventName) => ({
event: eventName,
handler: (event) => onEvent({
event,
el,
handler,
middleware
}),
})),
}
}
function removeInstance (el)
{
const instanceIndex = directive.instances.findIndex((instance) => instance.el === el);
if (instanceIndex === -1)
{
// Note: This can happen when active status changes from false to false
return
}
const instance = directive.instances[instanceIndex];
instance.eventHandlers.forEach(({ event, handler }) =>
document.removeEventListener(event, handler)
);
directive.instances.splice(instanceIndex, 1)
}
function bind (el, { value })
{
const { events, handler, middleware, isActive } = processDirectiveArguments(value);
if (!isActive)
{
return
}
const instance = createInstance({
el,
events,
handler,
middleware
});
instance.eventHandlers.forEach(({ event, handler }) =>
setTimeout(() => document.addEventListener(event, handler), 0)
);
directive.instances.push(instance)
}
function update (el, { value, oldValue })
{
if (JSON.stringify(value) === JSON.stringify(oldValue))
{
return
}
const { events, handler, middleware, isActive } = processDirectiveArguments(value);
if (!isActive)
{
removeInstance(el);
return
}
let instance = directive.instances.find((instance) => instance.el === el);
if (instance)
{
instance.eventHandlers.forEach(({ event, handler }) =>
document.removeEventListener(event, handler)
);
instance.eventHandlers = events.map((eventName) => ({
event: eventName,
handler: (event) => onEvent({
event,
el,
handler,
middleware
}),
}))
}
else
{
instance = createInstance({
el,
events,
handler,
middleware
});
directive.instances.push(instance)
}
instance.eventHandlers.forEach(({ event, handler }) =>
setTimeout(() => document.addEventListener(event, handler), 0)
)
}
directive.bind = bind;
directive.update = update;
directive.unbind = removeInstance;
Vue.directive('click-outside', directive);
所以在尝试让事件注册之后,我决定用不同的方式来解决这个问题。
每次插入可关闭指令时,它都会调用之前打开的任何先前表达式,然后将新表达式处理程序添加到名为 prevNodes 的变量中,因此下次插入可关闭指令时,它会调用该表达式
let prevNodes = [];
Vue.directive ( 'closable', {
inserted: ( el, binding, vnode ) => {
console.log ( {prevNodes} );
prevNodes.forEach ( item => {
//console.log ( item );
const {vnode, binding} = item;
vnode.context[binding.expression] ();
} );
// assign event to the element
el.clickOutsideEvent = function ( event ) {
// here we check if the click event is outside the element and it's children
if ( !( el == event.path[0] || el.contains ( event.path[0] ) ) ) {
// if clicked outside, call the provided method
vnode.context[binding.expression] ( event );
}
};
prevNodes.push ( {vnode, binding} );
// register click and touch events
document.body.addEventListener ( 'click', el.clickOutsideEvent );
document.body.addEventListener ( 'touchstart', el.clickOutsideEvent );
},
unbind: function ( el, binding, vnode ) {
const removeIndex = prevNodes.findIndex ( item => item.vnode.elm === vnode.elm );
prevNodes.splice ( removeIndex, 1 );
// unregister click and touch events before the element is unmounted
document.body.removeEventListener ( 'click', el.clickOutsideEvent );
document.body.removeEventListener ( 'touchstart', el.clickOutsideEvent );
},
stopProp ( event ) {
event.stopPropagation ();
},
} );
假设我在页面上有多个下拉菜单或元素,它们都使用了我用过的称为 closable 的指令。如果单击的元素在使用指令的元素之外,这将调用传入的表达式。
然而,预期的行为是,如果我单击页面上的一个元素,即另一个带有指令的下拉菜单,它应该获得该单击事件路径,将它们与现有的进行比较,如果它们不匹配或不包含在它应该关闭它的元素。
实际发生的是点击事件从未被注册,它只是初始化了另一个指令并且由于某种原因点击事件丢失了。
只有当我点击没有指令的东西时才会注册点击事件。
Vue.directive ( 'closable', {
inserted: ( el, binding, vnode ) => {
// assign event to the element
el.clickOutsideEvent = function ( event ) {
console.log ( {el, event} );
// here we check if the click event is outside the element and it's children
if ( !( el == event.path[0] || el.contains ( event.path[0] ) ) ) {
// if clicked outside, call the provided method
vnode.context[binding.expression] ( event );
}
};
// register click and touch events
document.body.addEventListener ( 'click', el.clickOutsideEvent );
document.body.addEventListener ( 'touchstart', el.clickOutsideEvent );
},
unbind: function ( el ) {
// unregister click and touch events before the element is unmounted
document.body.removeEventListener ( 'click', el.clickOutsideEvent );
document.body.removeEventListener ( 'touchstart', el.clickOutsideEvent );
},
stopProp ( event ) {
event.stopPropagation ();
},
} );
更新
这是 v-click-outside
指令的另一种变体 - 在本地,就在您的组件内:
directives:
{
clickOutside:
{
bind(elem, binding, vnode)
{
elem.clickOutsideEvent = function(evt)
{
if (elem !== evt.target && !elem.contains(evt.target)) vnode.context[binding.expression](evt);
};
document.body.addEventListener('click', elem.clickOutsideEvent);
},
unbind(elem)
{
document.body.removeEventListener('click', elem.clickOutsideEvent);
}
}
},
您可以试试这个实现:
import Vue from 'vue'
const HAS_WINDOWS = typeof window !== 'undefined';
const HAS_NAVIGATOR = typeof navigator !== 'undefined';
const IS_TOUCH = HAS_WINDOWS && ('ontouchstart' in window || (HAS_NAVIGATOR && navigator.msMaxTouchPoints > 0));
const EVENTS = IS_TOUCH ? ['touchstart'] : ['click'];
const IDENTITY = (item) => item;
const directive = {
instances: [],
};
function processDirectiveArguments (bindingValue)
{
const isFunction = typeof bindingValue === 'function';
if (!isFunction && typeof bindingValue !== 'object')
{
throw new Error('v-click-outside: Binding value must be a function or an object')
}
return {
handler: isFunction ? bindingValue : bindingValue.handler,
middleware: bindingValue.middleware || IDENTITY,
events: bindingValue.events || EVENTS,
isActive: !(bindingValue.isActive === false),
}
}
function onEvent ({ el, event, handler, middleware })
{
const isClickOutside = event.target !== el && !el.contains(event.target);
if (!isClickOutside)
{
return
}
if (middleware(event, el))
{
handler(event, el)
}
}
function createInstance ({ el, events, handler, middleware })
{
return {
el,
eventHandlers: events.map((eventName) => ({
event: eventName,
handler: (event) => onEvent({
event,
el,
handler,
middleware
}),
})),
}
}
function removeInstance (el)
{
const instanceIndex = directive.instances.findIndex((instance) => instance.el === el);
if (instanceIndex === -1)
{
// Note: This can happen when active status changes from false to false
return
}
const instance = directive.instances[instanceIndex];
instance.eventHandlers.forEach(({ event, handler }) =>
document.removeEventListener(event, handler)
);
directive.instances.splice(instanceIndex, 1)
}
function bind (el, { value })
{
const { events, handler, middleware, isActive } = processDirectiveArguments(value);
if (!isActive)
{
return
}
const instance = createInstance({
el,
events,
handler,
middleware
});
instance.eventHandlers.forEach(({ event, handler }) =>
setTimeout(() => document.addEventListener(event, handler), 0)
);
directive.instances.push(instance)
}
function update (el, { value, oldValue })
{
if (JSON.stringify(value) === JSON.stringify(oldValue))
{
return
}
const { events, handler, middleware, isActive } = processDirectiveArguments(value);
if (!isActive)
{
removeInstance(el);
return
}
let instance = directive.instances.find((instance) => instance.el === el);
if (instance)
{
instance.eventHandlers.forEach(({ event, handler }) =>
document.removeEventListener(event, handler)
);
instance.eventHandlers = events.map((eventName) => ({
event: eventName,
handler: (event) => onEvent({
event,
el,
handler,
middleware
}),
}))
}
else
{
instance = createInstance({
el,
events,
handler,
middleware
});
directive.instances.push(instance)
}
instance.eventHandlers.forEach(({ event, handler }) =>
setTimeout(() => document.addEventListener(event, handler), 0)
)
}
directive.bind = bind;
directive.update = update;
directive.unbind = removeInstance;
Vue.directive('click-outside', directive);
所以在尝试让事件注册之后,我决定用不同的方式来解决这个问题。
每次插入可关闭指令时,它都会调用之前打开的任何先前表达式,然后将新表达式处理程序添加到名为 prevNodes 的变量中,因此下次插入可关闭指令时,它会调用该表达式
let prevNodes = [];
Vue.directive ( 'closable', {
inserted: ( el, binding, vnode ) => {
console.log ( {prevNodes} );
prevNodes.forEach ( item => {
//console.log ( item );
const {vnode, binding} = item;
vnode.context[binding.expression] ();
} );
// assign event to the element
el.clickOutsideEvent = function ( event ) {
// here we check if the click event is outside the element and it's children
if ( !( el == event.path[0] || el.contains ( event.path[0] ) ) ) {
// if clicked outside, call the provided method
vnode.context[binding.expression] ( event );
}
};
prevNodes.push ( {vnode, binding} );
// register click and touch events
document.body.addEventListener ( 'click', el.clickOutsideEvent );
document.body.addEventListener ( 'touchstart', el.clickOutsideEvent );
},
unbind: function ( el, binding, vnode ) {
const removeIndex = prevNodes.findIndex ( item => item.vnode.elm === vnode.elm );
prevNodes.splice ( removeIndex, 1 );
// unregister click and touch events before the element is unmounted
document.body.removeEventListener ( 'click', el.clickOutsideEvent );
document.body.removeEventListener ( 'touchstart', el.clickOutsideEvent );
},
stopProp ( event ) {
event.stopPropagation ();
},
} );