Say I have multiple dropdowns or elements on the page that all use this directive I've used called closable. This calls an expression passed in if the element clicked is outside of the element using the directive.
However the expected behaviour is that if I click an element on page i.e. another dropdown with a directive it should get that click event path compare them to the existing one and if they don't match or aren't contained in the elemement it should close it.
What actually happens is the click event is never registered, it just initalizes another directive and for some reason that click event is lost.
The only time the click event is registerd is if I click on something that doesn't have the directive.
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 ();
},
} );
CodePudding user response:
UPDATE
Here is another variant for a v-click-outside
directive - locally, right inside your component:
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);
}
}
},
You can try this implementation:
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);
CodePudding user response:
So aftering trying to get the events to register I just decided to go about this a diffrent way.
Everytime a closable directive is inserted it calls any previous expressions that where open before,and then adds the new expression handler to a variable called prevNodes so next time a closable directive is inserted it calls that expression
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 ();
},
} );