diff --git a/src/directives/v-click-outside-x.js b/src/directives/v-click-outside-x.js new file mode 100644 index 00000000..819dd866 --- /dev/null +++ b/src/directives/v-click-outside-x.js @@ -0,0 +1,210 @@ +const CLICK = 'click'; +const captureInstances = Object.create(null); +const nonCaptureInstances = Object.create(null); +const instancesList = [captureInstances, nonCaptureInstances]; + +/** + * The common event handler for bot capture and non-capture events. + * + * @param {!Object} context - The event context. + * @param {!Object} instances - The capture or non-capture registered instances. + * @param {Event} event - The event object. + * @returns {undefined} Default. + */ +const commonHandler = function _onCommonEvent(context, instances, event) { + const {target} = event; + + const itemIteratee = function _itemIteratee(item) { + const {el} = item; + + if (el !== target && !el.contains(target)) { + const {binding} = item; + + if (binding.modifiers.stop) { + event.stopPropagation(); + } + + if (binding.modifiers.prevent) { + event.preventDefault(); + } + + binding.value.call(context, event); + } + }; + + const keysIteratee = function _keysIteratee(eventName) { + return instances[eventName].forEach(itemIteratee); + }; + + Object.keys(instances).forEach(keysIteratee); +}; + +/** + * Event handler for capture events. + * + * @param {Event} event - The event object. + */ +const captureEventHandler = function onCaptureEvent(event) { + /* eslint-disable-next-line babel/no-invalid-this */ + commonHandler(this, captureInstances, event); +}; + +/** + * Event handler for non-capture events. + * + * @param {Event} event - The event object. + */ +const nonCaptureEventHandler = function onNonCaptureEvent(event) { + /* eslint-disable-next-line babel/no-invalid-this */ + commonHandler(this, nonCaptureInstances, event); +}; + +/** + * Get the correct event handler: Capture or non-capture. + * + * @param {boolean} useCapture - Indicate which handler to use; 'true' to use + * capture handler or 'false' for non-capture. + * @returns {Function} - The event handler. + */ +const getEventHandler = function _getEventHandler(useCapture) { + return useCapture ? captureEventHandler : nonCaptureEventHandler; +}; + +/** + * The directive definition. + * {@link https://vuejs.org/v2/guide/custom-directive.html|Custom directive} + * + * @namespace + * @property {!Object} $_captureInstances - Registered capture instances. + * @property {!Object} $_nonCaptureInstances - Registered non-capture instances. + * @property {Function} $_onCaptureEvent - Event handler for capture events. + * @property {Function} $_onNonCaptureEvent - Event handler for non-capture events. + * @property {Function} bind - Called only once, when the directive is first + * bound to the element. + * @property {Function} unbind - Called only once, when the directive is unbound + * from the element. + * @property {string} version - The version number of this release. + */ +export const directive = Object.defineProperties( + {}, + { + $_captureInstances: { + value: captureInstances, + }, + + $_nonCaptureInstances: { + value: nonCaptureInstances, + }, + + $_onCaptureEvent: { + value: captureEventHandler, + }, + + $_onNonCaptureEvent: { + value: nonCaptureEventHandler, + }, + + bind: { + value: function bind(el, binding) { + if (typeof binding.value !== 'function') { + throw new TypeError('Binding value must be a function.'); + } + + const arg = binding.arg || CLICK; + const normalisedBinding = { + ...binding, + ...{ + arg, + modifiers: { + ...{ + capture: false, + prevent: false, + stop: false, + }, + ...binding.modifiers, + }, + }, + }; + + const useCapture = normalisedBinding.modifiers.capture; + const instances = useCapture ? captureInstances : nonCaptureInstances; + + if (!Array.isArray(instances[arg])) { + instances[arg] = []; + } + + if (instances[arg].push({el, binding: normalisedBinding}) === 1) { + if (typeof document === 'object' && document) { + document.addEventListener( + arg, + getEventHandler(useCapture), + useCapture, + ); + } + } + }, + }, + + unbind: { + value: function unbind(el) { + const compareElements = function _compareElements(item) { + return item.el !== el; + }; + + const instancesIteratee = function _instancesIteratee(instances) { + const instanceKeys = Object.keys(instances); + + if (instanceKeys.length) { + const useCapture = instances === captureInstances; + + const keysIteratee = function _keysIteratee(eventName) { + const newInstance = instances[eventName].filter(compareElements); + + if (newInstance.length) { + instances[eventName] = newInstance; + } else { + if (typeof document === 'object' && document) { + document.removeEventListener( + eventName, + getEventHandler(useCapture), + useCapture, + ); + } + + delete instances[eventName]; + } + }; + + instanceKeys.forEach(keysIteratee); + } + }; + + instancesList.forEach(instancesIteratee); + }, + }, + + /* Note: This needs to be manually updated to match package.json. */ + version: { + enumerable: true, + value: '3.7.1', + }, + }, +); + +/** + * @typedef {Function} Vue - The constructor. + * @property {Function} directive - You can register a global custom directive + * with the Vue.directive() method, passing in a directiveID followed by a + * definition object. + */ + +/** + * A Vue.js plugin should expose an install method. The method will be called + * with the Vue constructor as the first argument, along with possible options. + * {@link https://vuejs.org/v2/guide/plugins.html#Writing-a-Plugin|Writing a plugin}. + * + * @param {Vue} Vue - The Vue function. + */ +export function install(Vue) { + Vue.directive('click-outside', directive); +}