react-onclickoutside
Version:
An onClickOutside wrapper for React components
356 lines (283 loc) • 11.6 kB
JavaScript
(function(g,f){typeof exports==='object'&&typeof module!=='undefined'?f(exports,require('react'),require('react-dom')):typeof define==='function'&&define.amd?define(['exports','react','react-dom'],f):(g=typeof globalThis!=='undefined'?globalThis:g||self,f(g.onClickOutside={},g.React,g.ReactDOM));}(this,(function(exports, react, reactDom){'use strict';function _inheritsLoose(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype);
subClass.prototype.constructor = subClass;
_setPrototypeOf(subClass, superClass);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
function _objectWithoutPropertiesLoose(source, excluded) {
if (source == null) return {};
var target = {};
var sourceKeys = Object.keys(source);
var key, i;
for (i = 0; i < sourceKeys.length; i++) {
key = sourceKeys[i];
if (excluded.indexOf(key) >= 0) continue;
target[key] = source[key];
}
return target;
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return self;
}/**
* Check whether some DOM node is our Component's node.
*/
function isNodeFound(current, componentNode, ignoreClass) {
if (current === componentNode) {
return true;
} // SVG <use/> elements do not technically reside in the rendered DOM, so
// they do not have classList directly, but they offer a link to their
// corresponding element, which can have classList. This extra check is for
// that case.
// See: http://www.w3.org/TR/SVG11/struct.html#InterfaceSVGUseElement
// Discussion: https://github.com/Pomax/react-onclickoutside/pull/17
if (current.correspondingElement) {
return current.correspondingElement.classList.contains(ignoreClass);
}
return current.classList.contains(ignoreClass);
}
/**
* Try to find our node in a hierarchy of nodes, returning the document
* node as highest node if our node is not found in the path up.
*/
function findHighest(current, componentNode, ignoreClass) {
if (current === componentNode) {
return true;
} // If source=local then this event came from 'somewhere'
// inside and should be ignored. We could handle this with
// a layered approach, too, but that requires going back to
// thinking in terms of Dom node nesting, running counter
// to React's 'you shouldn't care about the DOM' philosophy.
// Also cover shadowRoot node by checking current.host
while (current.parentNode || current.host) {
// Only check normal node without shadowRoot
if (current.parentNode && isNodeFound(current, componentNode, ignoreClass)) {
return true;
}
current = current.parentNode || current.host;
}
return current;
}
/**
* Check if the browser scrollbar was clicked
*/
function clickedScrollbar(evt) {
return document.documentElement.clientWidth <= evt.clientX || document.documentElement.clientHeight <= evt.clientY;
}// ideally will get replaced with external dep
// when rafrex/detect-passive-events#4 and rafrex/detect-passive-events#5 get merged in
var testPassiveEventSupport = function testPassiveEventSupport() {
if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') {
return;
}
var passive = false;
var options = Object.defineProperty({}, 'passive', {
get: function get() {
passive = true;
}
});
var noop = function noop() {};
window.addEventListener('testPassiveEventSupport', noop, options);
window.removeEventListener('testPassiveEventSupport', noop, options);
return passive;
};function autoInc(seed) {
if (seed === void 0) {
seed = 0;
}
return function () {
return ++seed;
};
}
var uid = autoInc();var passiveEventSupport;
var handlersMap = {};
var enabledInstances = {};
var touchEvents = ['touchstart', 'touchmove'];
var IGNORE_CLASS_NAME = 'ignore-react-onclickoutside';
/**
* Options for addEventHandler and removeEventHandler
*/
function getEventHandlerOptions(instance, eventName) {
var handlerOptions = {};
var isTouchEvent = touchEvents.indexOf(eventName) !== -1;
if (isTouchEvent && passiveEventSupport) {
handlerOptions.passive = !instance.props.preventDefault;
}
return handlerOptions;
}
/**
* This function generates the HOC function that you'll use
* in order to impart onOutsideClick listening to an
* arbitrary component. It gets called at the end of the
* bootstrapping code to yield an instance of the
* onClickOutsideHOC function defined inside setupHOC().
*/
function onClickOutsideHOC(WrappedComponent, config) {
var _class, _temp;
var componentName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
return _temp = _class = /*#__PURE__*/function (_Component) {
_inheritsLoose(onClickOutside, _Component);
function onClickOutside(props) {
var _this;
_this = _Component.call(this, props) || this;
_this.__outsideClickHandler = function (event) {
if (typeof _this.__clickOutsideHandlerProp === 'function') {
_this.__clickOutsideHandlerProp(event);
return;
}
var instance = _this.getInstance();
if (typeof instance.props.handleClickOutside === 'function') {
instance.props.handleClickOutside(event);
return;
}
if (typeof instance.handleClickOutside === 'function') {
instance.handleClickOutside(event);
return;
}
throw new Error("WrappedComponent: " + componentName + " lacks a handleClickOutside(event) function for processing outside click events.");
};
_this.__getComponentNode = function () {
var instance = _this.getInstance();
if (config && typeof config.setClickOutsideRef === 'function') {
return config.setClickOutsideRef()(instance);
}
if (typeof instance.setClickOutsideRef === 'function') {
return instance.setClickOutsideRef();
}
return reactDom.findDOMNode(instance);
};
_this.enableOnClickOutside = function () {
if (typeof document === 'undefined' || enabledInstances[_this._uid]) {
return;
}
if (typeof passiveEventSupport === 'undefined') {
passiveEventSupport = testPassiveEventSupport();
}
enabledInstances[_this._uid] = true;
var events = _this.props.eventTypes;
if (!events.forEach) {
events = [events];
}
handlersMap[_this._uid] = function (event) {
if (_this.componentNode === null) return;
if (_this.initTimeStamp > event.timeStamp) return;
if (_this.props.preventDefault) {
event.preventDefault();
}
if (_this.props.stopPropagation) {
event.stopPropagation();
}
if (_this.props.excludeScrollbar && clickedScrollbar(event)) return;
var current = event.composed && event.composedPath && event.composedPath().shift() || event.target;
if (findHighest(current, _this.componentNode, _this.props.outsideClickIgnoreClass) !== document) {
return;
}
_this.__outsideClickHandler(event);
};
events.forEach(function (eventName) {
document.addEventListener(eventName, handlersMap[_this._uid], getEventHandlerOptions(_assertThisInitialized(_this), eventName));
});
};
_this.disableOnClickOutside = function () {
delete enabledInstances[_this._uid];
var fn = handlersMap[_this._uid];
if (fn && typeof document !== 'undefined') {
var events = _this.props.eventTypes;
if (!events.forEach) {
events = [events];
}
events.forEach(function (eventName) {
return document.removeEventListener(eventName, fn, getEventHandlerOptions(_assertThisInitialized(_this), eventName));
});
delete handlersMap[_this._uid];
}
};
_this.getRef = function (ref) {
return _this.instanceRef = ref;
};
_this._uid = uid();
_this.initTimeStamp = performance.now();
return _this;
}
/**
* Access the WrappedComponent's instance.
*/
var _proto = onClickOutside.prototype;
_proto.getInstance = function getInstance() {
if (WrappedComponent.prototype && !WrappedComponent.prototype.isReactComponent) {
return this;
}
var ref = this.instanceRef;
return ref.getInstance ? ref.getInstance() : ref;
};
/**
* Add click listeners to the current document,
* linked to this component's state.
*/
_proto.componentDidMount = function componentDidMount() {
// If we are in an environment without a DOM such
// as shallow rendering or snapshots then we exit
// early to prevent any unhandled errors being thrown.
if (typeof document === 'undefined' || !document.createElement) {
return;
}
var instance = this.getInstance();
if (config && typeof config.handleClickOutside === 'function') {
this.__clickOutsideHandlerProp = config.handleClickOutside(instance);
if (typeof this.__clickOutsideHandlerProp !== 'function') {
throw new Error("WrappedComponent: " + componentName + " lacks a function for processing outside click events specified by the handleClickOutside config option.");
}
}
this.componentNode = this.__getComponentNode(); // return early so we dont initiate onClickOutside
if (this.props.disableOnClickOutside) return;
this.enableOnClickOutside();
};
_proto.componentDidUpdate = function componentDidUpdate() {
this.componentNode = this.__getComponentNode();
}
/**
* Remove all document's event listeners for this component
*/
;
_proto.componentWillUnmount = function componentWillUnmount() {
this.disableOnClickOutside();
}
/**
* Can be called to explicitly enable event listening
* for clicks and touches outside of this element.
*/
;
/**
* Pass-through render
*/
_proto.render = function render() {
// eslint-disable-next-line no-unused-vars
var _this$props = this.props;
_this$props.excludeScrollbar;
var props = _objectWithoutPropertiesLoose(_this$props, ["excludeScrollbar"]);
if (WrappedComponent.prototype && WrappedComponent.prototype.isReactComponent) {
props.ref = this.getRef;
} else {
props.wrappedRef = this.getRef;
}
props.disableOnClickOutside = this.disableOnClickOutside;
props.enableOnClickOutside = this.enableOnClickOutside;
return react.createElement(WrappedComponent, props);
};
return onClickOutside;
}(react.Component), _class.displayName = "OnClickOutside(" + componentName + ")", _class.defaultProps = {
eventTypes: ['mousedown', 'touchstart'],
excludeScrollbar: config && config.excludeScrollbar || false,
outsideClickIgnoreClass: IGNORE_CLASS_NAME,
preventDefault: false,
stopPropagation: false
}, _class.getClass = function () {
return WrappedComponent.getClass ? WrappedComponent.getClass() : WrappedComponent;
}, _temp;
}exports.IGNORE_CLASS_NAME=IGNORE_CLASS_NAME;exports.default=onClickOutsideHOC;Object.defineProperty(exports,'__esModule',{value:true});})));