@olasearch/react-onclickoutside
Version:
An onClickOutside wrapper for React components
363 lines (285 loc) • 10.7 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var react = require('react');
var reactDom = require('react-dom');
function _inheritsLoose(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype);
subClass.prototype.constructor = subClass;
subClass.__proto__ = superClass;
}
function _objectWithoutProperties(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];
}
if (Object.getOwnPropertySymbols) {
var sourceSymbolKeys = Object.getOwnPropertySymbols(source);
for (i = 0; i < sourceSymbolKeys.length; i++) {
key = sourceSymbolKeys[i];
if (excluded.indexOf(key) >= 0) continue;
if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
target[key] = source[key];
}
}
return target;
}
/**
* 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.
while (current.parentNode) {
if (isNodeFound(current, componentNode, ignoreClass)) {
return true;
}
current = current.parentNode;
}
return current;
}
/**
* Check if the browser scrollbar was clicked
*/
function clickedScrollbar(evt, document) {
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 = null;
var isTouchEvent = touchEvents.indexOf(eventName) !== -1;
if (isTouchEvent && passiveEventSupport) {
handlerOptions = {
passive: !instance.props.preventDefault
};
}
return handlerOptions;
}
/**
* Check if browser
*/
var isBrowser = typeof document !== 'undefined';
/**
* Default document
*/
var defaultDocument = isBrowser ? document : undefined;
/**
* 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;
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 lacks a handleClickOutside(event) function for processing outside click events.');
};
_this.enableOnClickOutside = function () {
if (typeof _this.props.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.props.disableOnClickOutside) return;
if (_this.componentNode === null) return;
if (_this.props.preventDefault) {
event.preventDefault();
}
if (_this.props.stopPropagation) {
event.stopPropagation();
}
if (_this.props.excludeScrollbar && clickedScrollbar(event, _this.props.document)) return;
var current = event.target;
if (findHighest(current, _this.componentNode, _this.props.outsideClickIgnoreClass) !== _this.props.document) {
return;
}
_this.__outsideClickHandler(event);
};
events.forEach(function (eventName) {
_this.props.document.addEventListener(eventName, handlersMap[_this._uid], getEventHandlerOptions(_this, eventName));
});
};
_this.disableOnClickOutside = function () {
delete enabledInstances[_this._uid];
var fn = handlersMap[_this._uid];
if (fn && typeof _this.props.document !== 'undefined') {
var events = _this.props.eventTypes;
if (!events.forEach) {
events = [events];
}
events.forEach(function (eventName) {
return _this.props.document.removeEventListener(eventName, fn, getEventHandlerOptions(_this, eventName));
});
delete handlersMap[_this._uid];
}
};
_this.getRef = function (ref) {
return _this.instanceRef = ref;
};
_this._uid = uid();
return _this;
}
/**
* Access the WrappedComponent's instance.
*/
var _proto = onClickOutside.prototype;
_proto.getInstance = function getInstance() {
if (!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 this.props.document === 'undefined' || !this.props.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 lacks a function for processing outside click events specified by the handleClickOutside config option.');
}
}
this.componentNode = reactDom.findDOMNode(this.getInstance());
this.enableOnClickOutside();
};
_proto.componentDidUpdate = function componentDidUpdate() {
this.componentNode = reactDom.findDOMNode(this.getInstance());
};
/**
* 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 _props = this.props,
excludeScrollbar = _props.excludeScrollbar,
props = _objectWithoutProperties(_props, ["excludeScrollbar"]);
if (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(" + (WrappedComponent.displayName || WrappedComponent.name || 'Component') + ")", _class.defaultProps = {
eventTypes: ['mousedown', 'touchstart'],
excludeScrollbar: config && config.excludeScrollbar || false,
outsideClickIgnoreClass: IGNORE_CLASS_NAME,
preventDefault: false,
stopPropagation: false,
document: defaultDocument
}, _class.getClass = function () {
return WrappedComponent.getClass ? WrappedComponent.getClass() : WrappedComponent;
}, _temp;
}
exports.IGNORE_CLASS_NAME = IGNORE_CLASS_NAME;
exports['default'] = onClickOutsideHOC;