stencil-click-outside
Version:
Decorator for StencilJs to run annotated method on click outside of component.
98 lines (97 loc) • 3.32 kB
JavaScript
import { Build as BUILD, getElement } from '@stencil/core';
const ClickOutsideOptionsDefaults = {
triggerEvents: "click",
exclude: ""
};
/**
* Call this function as soon as the click outside of annotated method's host is done.
* @example
```
@ClickOutside()
callback() {
// this will run when click outside of element (host component) is done.
}
```
*/
export function ClickOutside(opt = ClickOutsideOptionsDefaults) {
return (proto, methodName) => {
// this is to resolve the 'compiler optimization issue':
// lifecycle events not being called when not explicitly declared in at least one of components from bundle
BUILD.connectedCallback = true;
BUILD.disconnectedCallback = true;
const { connectedCallback, disconnectedCallback } = proto;
proto.connectedCallback = function () {
const host = getElement(this);
const method = this[methodName];
registerClickOutside(this, host, method, opt);
return connectedCallback && connectedCallback.call(this);
};
proto.disconnectedCallback = function () {
const host = getElement(this);
const method = this[methodName];
removeClickOutside(this, host, method, opt);
return disconnectedCallback && disconnectedCallback.call(this);
};
};
}
/**
* Register callback function for HTMLElement to be executed when user clicks outside of element.
* @example
```
<span
ref={spanEl => registerClickOutside(this, spanEl, () => this.test())}>
Hello, World!
</span>;
```
*/
export function registerClickOutside(component, element, callback, opt = ClickOutsideOptionsDefaults) {
const excludedNodes = getExcludedNodes(opt);
getTriggerEvents(opt).forEach(triggerEvent => {
window.addEventListener(triggerEvent, (e) => {
initClickOutside(e, component, element, callback, excludedNodes);
}, false);
});
}
/**
* Remove click outside callback function for HTMLElement.
*/
export function removeClickOutside(component, element, callback, opt = ClickOutsideOptionsDefaults) {
getTriggerEvents(opt).forEach(triggerEvent => {
window.removeEventListener(triggerEvent, (e) => {
initClickOutside(e, component, element, callback);
}, false);
});
}
function initClickOutside(event, component, element, callback, excludedNodes) {
const target = event.target;
if (!element.contains(target) && !isExcluded(target, excludedNodes)) {
callback.call(component);
}
}
function getTriggerEvents(opt) {
if (opt.triggerEvents) {
return opt.triggerEvents.split(",").map(e => e.trim());
}
return ["click"];
}
function getExcludedNodes(opt) {
if (opt.exclude) {
try {
return Array.from(document.querySelectorAll(opt.exclude));
}
catch (err) {
console.warn(`@ClickOutside: Exclude: '${opt.exclude}' will not be evaluated. Check your exclude selector syntax.`, err);
}
}
return;
}
function isExcluded(target, excudedNodes) {
if (target && excudedNodes) {
for (let excludedNode of excudedNodes) {
if (excludedNode.contains(target)) {
return true;
}
}
}
return false;
}