Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 

364 lignes
10 KiB

  1. import { Component, createElement } from 'react';
  2. import { findDOMNode } from 'react-dom';
  3. function _inheritsLoose(subClass, superClass) {
  4. subClass.prototype = Object.create(superClass.prototype);
  5. subClass.prototype.constructor = subClass;
  6. subClass.__proto__ = superClass;
  7. }
  8. function _objectWithoutProperties(source, excluded) {
  9. if (source == null) return {};
  10. var target = {};
  11. var sourceKeys = Object.keys(source);
  12. var key, i;
  13. for (i = 0; i < sourceKeys.length; i++) {
  14. key = sourceKeys[i];
  15. if (excluded.indexOf(key) >= 0) continue;
  16. target[key] = source[key];
  17. }
  18. if (Object.getOwnPropertySymbols) {
  19. var sourceSymbolKeys = Object.getOwnPropertySymbols(source);
  20. for (i = 0; i < sourceSymbolKeys.length; i++) {
  21. key = sourceSymbolKeys[i];
  22. if (excluded.indexOf(key) >= 0) continue;
  23. if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
  24. target[key] = source[key];
  25. }
  26. }
  27. return target;
  28. }
  29. /**
  30. * Check whether some DOM node is our Component's node.
  31. */
  32. function isNodeFound(current, componentNode, ignoreClass) {
  33. if (current === componentNode) {
  34. return true;
  35. } // SVG <use/> elements do not technically reside in the rendered DOM, so
  36. // they do not have classList directly, but they offer a link to their
  37. // corresponding element, which can have classList. This extra check is for
  38. // that case.
  39. // See: http://www.w3.org/TR/SVG11/struct.html#InterfaceSVGUseElement
  40. // Discussion: https://github.com/Pomax/react-onclickoutside/pull/17
  41. if (current.correspondingElement) {
  42. return current.correspondingElement.classList.contains(ignoreClass);
  43. }
  44. return current.classList.contains(ignoreClass);
  45. }
  46. /**
  47. * Try to find our node in a hierarchy of nodes, returning the document
  48. * node as highest node if our node is not found in the path up.
  49. */
  50. function findHighest(current, componentNode, ignoreClass) {
  51. if (current === componentNode) {
  52. return true;
  53. } // If source=local then this event came from 'somewhere'
  54. // inside and should be ignored. We could handle this with
  55. // a layered approach, too, but that requires going back to
  56. // thinking in terms of Dom node nesting, running counter
  57. // to React's 'you shouldn't care about the DOM' philosophy.
  58. while (current.parentNode) {
  59. if (isNodeFound(current, componentNode, ignoreClass)) {
  60. return true;
  61. }
  62. current = current.parentNode;
  63. }
  64. return current;
  65. }
  66. /**
  67. * Check if the browser scrollbar was clicked
  68. */
  69. function clickedScrollbar(evt) {
  70. return document.documentElement.clientWidth <= evt.clientX || document.documentElement.clientHeight <= evt.clientY;
  71. }
  72. // ideally will get replaced with external dep
  73. // when rafrex/detect-passive-events#4 and rafrex/detect-passive-events#5 get merged in
  74. var testPassiveEventSupport = function testPassiveEventSupport() {
  75. if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') {
  76. return;
  77. }
  78. var passive = false;
  79. var options = Object.defineProperty({}, 'passive', {
  80. get: function get() {
  81. passive = true;
  82. }
  83. });
  84. var noop = function noop() {};
  85. window.addEventListener('testPassiveEventSupport', noop, options);
  86. window.removeEventListener('testPassiveEventSupport', noop, options);
  87. return passive;
  88. };
  89. function autoInc(seed) {
  90. if (seed === void 0) {
  91. seed = 0;
  92. }
  93. return function () {
  94. return ++seed;
  95. };
  96. }
  97. var uid = autoInc();
  98. var passiveEventSupport;
  99. var handlersMap = {};
  100. var enabledInstances = {};
  101. var touchEvents = ['touchstart', 'touchmove'];
  102. var IGNORE_CLASS_NAME = 'ignore-react-onclickoutside';
  103. /**
  104. * Options for addEventHandler and removeEventHandler
  105. */
  106. function getEventHandlerOptions(instance, eventName) {
  107. var handlerOptions = null;
  108. var isTouchEvent = touchEvents.indexOf(eventName) !== -1;
  109. if (isTouchEvent && passiveEventSupport) {
  110. handlerOptions = {
  111. passive: !instance.props.preventDefault
  112. };
  113. }
  114. return handlerOptions;
  115. }
  116. /**
  117. * This function generates the HOC function that you'll use
  118. * in order to impart onOutsideClick listening to an
  119. * arbitrary component. It gets called at the end of the
  120. * bootstrapping code to yield an instance of the
  121. * onClickOutsideHOC function defined inside setupHOC().
  122. */
  123. function onClickOutsideHOC(WrappedComponent, config) {
  124. var _class, _temp;
  125. var componentName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
  126. return _temp = _class =
  127. /*#__PURE__*/
  128. function (_Component) {
  129. _inheritsLoose(onClickOutside, _Component);
  130. function onClickOutside(props) {
  131. var _this;
  132. _this = _Component.call(this, props) || this;
  133. _this.__outsideClickHandler = function (event) {
  134. if (typeof _this.__clickOutsideHandlerProp === 'function') {
  135. _this.__clickOutsideHandlerProp(event);
  136. return;
  137. }
  138. var instance = _this.getInstance();
  139. if (typeof instance.props.handleClickOutside === 'function') {
  140. instance.props.handleClickOutside(event);
  141. return;
  142. }
  143. if (typeof instance.handleClickOutside === 'function') {
  144. instance.handleClickOutside(event);
  145. return;
  146. }
  147. throw new Error("WrappedComponent: " + componentName + " lacks a handleClickOutside(event) function for processing outside click events.");
  148. };
  149. _this.__getComponentNode = function () {
  150. var instance = _this.getInstance();
  151. if (config && typeof config.setClickOutsideRef === 'function') {
  152. return config.setClickOutsideRef()(instance);
  153. }
  154. if (typeof instance.setClickOutsideRef === 'function') {
  155. return instance.setClickOutsideRef();
  156. }
  157. return findDOMNode(instance);
  158. };
  159. _this.enableOnClickOutside = function () {
  160. if (typeof document === 'undefined' || enabledInstances[_this._uid]) {
  161. return;
  162. }
  163. if (typeof passiveEventSupport === 'undefined') {
  164. passiveEventSupport = testPassiveEventSupport();
  165. }
  166. enabledInstances[_this._uid] = true;
  167. var events = _this.props.eventTypes;
  168. if (!events.forEach) {
  169. events = [events];
  170. }
  171. handlersMap[_this._uid] = function (event) {
  172. if (_this.componentNode === null) return;
  173. if (_this.props.preventDefault) {
  174. event.preventDefault();
  175. }
  176. if (_this.props.stopPropagation) {
  177. event.stopPropagation();
  178. }
  179. if (_this.props.excludeScrollbar && clickedScrollbar(event)) return;
  180. var current = event.target;
  181. if (findHighest(current, _this.componentNode, _this.props.outsideClickIgnoreClass) !== document) {
  182. return;
  183. }
  184. _this.__outsideClickHandler(event);
  185. };
  186. events.forEach(function (eventName) {
  187. document.addEventListener(eventName, handlersMap[_this._uid], getEventHandlerOptions(_this, eventName));
  188. });
  189. };
  190. _this.disableOnClickOutside = function () {
  191. delete enabledInstances[_this._uid];
  192. var fn = handlersMap[_this._uid];
  193. if (fn && typeof document !== 'undefined') {
  194. var events = _this.props.eventTypes;
  195. if (!events.forEach) {
  196. events = [events];
  197. }
  198. events.forEach(function (eventName) {
  199. return document.removeEventListener(eventName, fn, getEventHandlerOptions(_this, eventName));
  200. });
  201. delete handlersMap[_this._uid];
  202. }
  203. };
  204. _this.getRef = function (ref) {
  205. return _this.instanceRef = ref;
  206. };
  207. _this._uid = uid();
  208. return _this;
  209. }
  210. /**
  211. * Access the WrappedComponent's instance.
  212. */
  213. var _proto = onClickOutside.prototype;
  214. _proto.getInstance = function getInstance() {
  215. if (!WrappedComponent.prototype.isReactComponent) {
  216. return this;
  217. }
  218. var ref = this.instanceRef;
  219. return ref.getInstance ? ref.getInstance() : ref;
  220. };
  221. /**
  222. * Add click listeners to the current document,
  223. * linked to this component's state.
  224. */
  225. _proto.componentDidMount = function componentDidMount() {
  226. // If we are in an environment without a DOM such
  227. // as shallow rendering or snapshots then we exit
  228. // early to prevent any unhandled errors being thrown.
  229. if (typeof document === 'undefined' || !document.createElement) {
  230. return;
  231. }
  232. var instance = this.getInstance();
  233. if (config && typeof config.handleClickOutside === 'function') {
  234. this.__clickOutsideHandlerProp = config.handleClickOutside(instance);
  235. if (typeof this.__clickOutsideHandlerProp !== 'function') {
  236. throw new Error("WrappedComponent: " + componentName + " lacks a function for processing outside click events specified by the handleClickOutside config option.");
  237. }
  238. }
  239. this.componentNode = this.__getComponentNode(); // return early so we dont initiate onClickOutside
  240. if (this.props.disableOnClickOutside) return;
  241. this.enableOnClickOutside();
  242. };
  243. _proto.componentDidUpdate = function componentDidUpdate() {
  244. this.componentNode = this.__getComponentNode();
  245. };
  246. /**
  247. * Remove all document's event listeners for this component
  248. */
  249. _proto.componentWillUnmount = function componentWillUnmount() {
  250. this.disableOnClickOutside();
  251. };
  252. /**
  253. * Can be called to explicitly enable event listening
  254. * for clicks and touches outside of this element.
  255. */
  256. /**
  257. * Pass-through render
  258. */
  259. _proto.render = function render() {
  260. // eslint-disable-next-line no-unused-vars
  261. var _props = this.props,
  262. excludeScrollbar = _props.excludeScrollbar,
  263. props = _objectWithoutProperties(_props, ["excludeScrollbar"]);
  264. if (WrappedComponent.prototype.isReactComponent) {
  265. props.ref = this.getRef;
  266. } else {
  267. props.wrappedRef = this.getRef;
  268. }
  269. props.disableOnClickOutside = this.disableOnClickOutside;
  270. props.enableOnClickOutside = this.enableOnClickOutside;
  271. return createElement(WrappedComponent, props);
  272. };
  273. return onClickOutside;
  274. }(Component), _class.displayName = "OnClickOutside(" + componentName + ")", _class.defaultProps = {
  275. eventTypes: ['mousedown', 'touchstart'],
  276. excludeScrollbar: config && config.excludeScrollbar || false,
  277. outsideClickIgnoreClass: IGNORE_CLASS_NAME,
  278. preventDefault: false,
  279. stopPropagation: false
  280. }, _class.getClass = function () {
  281. return WrappedComponent.getClass ? WrappedComponent.getClass() : WrappedComponent;
  282. }, _temp;
  283. }
  284. export { IGNORE_CLASS_NAME };
  285. export default onClickOutsideHOC;