Guest User

Untitled

a guest
Feb 18th, 2019
103
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 13.29 KB | None | 0 0
  1. import React, { Children, cloneElement } from 'react';
  2. import PropTypes from 'prop-types';
  3. import objectAssign from 'object-assign';
  4. import omit from 'object.omit';
  5. import Core from './lib/ElementRelativeCursorPosition';
  6. import addEventListener from './utils/addEventListener';
  7. import {
  8. INTERACTIONS,
  9. MOUSE_EMULATION_GUARD_TIMER_NAME
  10. } from './constants';
  11. import noop from './utils/noop';
  12. import PressActivation from './lib/PressActivation';
  13. import TouchActivation from './lib/TouchActivation';
  14. import TapActivation from './lib/TapActivation';
  15. import HoverActivation from './lib/HoverActivation';
  16. import ClickActivation from './lib/ClickActivation';
  17.  
  18. export { INTERACTIONS };
  19.  
  20. export default class extends React.Component {
  21. constructor(props) {
  22. super(props);
  23.  
  24. this.state = {
  25. detectedEnvironment: {
  26. isMouseDetected: false,
  27. isTouchDetected: false
  28. },
  29. elementDimensions: {
  30. width: 0,
  31. height: 0
  32. },
  33. isActive: false,
  34. isPositionOutside: true,
  35. position: {
  36. x: 0,
  37. y: 0
  38. }
  39. };
  40.  
  41. this.shouldGuardAgainstMouseEmulationByDevices = false;
  42. this.eventListeners = [];
  43. this.timers = [];
  44. this.elementOffset = {
  45. x: 0,
  46. y: 0
  47. };
  48.  
  49. this.onTouchStart = this.onTouchStart.bind(this);
  50. this.onTouchMove = this.onTouchMove.bind(this);
  51. this.onTouchEnd = this.onTouchEnd.bind(this);
  52. this.onTouchCancel = this.onTouchCancel.bind(this);
  53. this.onMouseEnter = this.onMouseEnter.bind(this);
  54. this.onMouseMove = this.onMouseMove.bind(this);
  55. this.onMouseLeave = this.onMouseLeave.bind(this);
  56. this.onClick = this.onClick.bind(this);
  57. this.onIsActiveChanged = this.onIsActiveChanged.bind(this);
  58.  
  59. this.setTouchActivationStrategy(props.activationInteractionTouch);
  60. this.setMouseActivationStrategy(props.activationInteractionMouse);
  61. }
  62.  
  63. static displayName = 'ReactCursorPosition';
  64.  
  65. static propTypes = {
  66. activationInteractionMouse: PropTypes.oneOf([
  67. INTERACTIONS.CLICK,
  68. INTERACTIONS.HOVER
  69. ]),
  70. activationInteractionTouch: PropTypes.oneOf([
  71. INTERACTIONS.PRESS,
  72. INTERACTIONS.TAP,
  73. INTERACTIONS.TOUCH
  74. ]),
  75. children: PropTypes.any,
  76. className: PropTypes.string,
  77. hoverDelayInMs: PropTypes.number,
  78. hoverOffDelayInMs: PropTypes.number,
  79. isEnabled: PropTypes.bool,
  80. mapChildProps: PropTypes.func,
  81. onActivationChanged: PropTypes.func,
  82. onDetectedEnvironmentChanged: PropTypes.func,
  83. onPositionChanged: PropTypes.func,
  84. pressDurationInMs: PropTypes.number,
  85. pressMoveThreshold: PropTypes.number,
  86. shouldDecorateChildren: PropTypes.bool,
  87. shouldStopTouchMovePropagation: PropTypes.bool,
  88. style: PropTypes.object,
  89. tapDurationInMs: PropTypes.number,
  90. tapMoveThreshold: PropTypes.number,
  91. };
  92.  
  93. static defaultProps = {
  94. activationInteractionMouse: INTERACTIONS.HOVER,
  95. activationInteractionTouch: INTERACTIONS.PRESS,
  96. hoverDelayInMs: 0,
  97. hoverOffDelayInMs: 0,
  98. isEnabled: true,
  99. mapChildProps: props => props,
  100. onActivationChanged: noop,
  101. onDetectedEnvironmentChanged: noop,
  102. onPositionChanged: noop,
  103. pressDurationInMs: 500,
  104. pressMoveThreshold: 5,
  105. shouldDecorateChildren: true,
  106. shouldStopTouchMovePropagation: false,
  107. tapDurationInMs: 180,
  108. tapMoveThreshold: 5,
  109. };
  110.  
  111. onIsActiveChanged({ isActive }) {
  112. if (isActive) {
  113. this.activate();
  114. } else {
  115. this.deactivate();
  116. }
  117. }
  118.  
  119. onTouchStart(e) {
  120. this.init();
  121. this.onTouchDetected();
  122. this.setShouldGuardAgainstMouseEmulationByDevices();
  123.  
  124. const position = this.core.getCursorPosition(this.getTouchEvent(e));
  125. this.setPositionState(position);
  126.  
  127. this.touchActivation.touchStarted({ e, position });
  128. }
  129.  
  130. onTouchMove(e) {
  131. if (!this.isCoreReady) {
  132. return;
  133. }
  134.  
  135. const position = this.core.getCursorPosition(this.getTouchEvent(e));
  136. this.touchActivation.touchMoved({ e, position });
  137.  
  138. if (!this.state.isActive) {
  139. return;
  140. }
  141.  
  142. this.setPositionState(position);
  143. e.preventDefault();
  144.  
  145. if (this.props.shouldStopTouchMovePropagation) {
  146. e.stopPropagation();
  147. }
  148. }
  149.  
  150. onTouchEnd() {
  151. this.touchActivation.touchEnded();
  152. this.unsetShouldGuardAgainstMouseEmulationByDevices();
  153. }
  154.  
  155. onTouchCancel() {
  156. this.touchActivation.touchCanceled();
  157.  
  158. this.unsetShouldGuardAgainstMouseEmulationByDevices();
  159. }
  160.  
  161. onMouseEnter(e) {
  162. if (this.shouldGuardAgainstMouseEmulationByDevices) {
  163. return;
  164. }
  165.  
  166. this.init();
  167. this.onMouseDetected();
  168. this.setPositionState(this.core.getCursorPosition(e));
  169. this.mouseActivation.mouseEntered();
  170. }
  171.  
  172. onMouseMove(e) {
  173. if (!this.isCoreReady) {
  174. return;
  175. }
  176.  
  177. const position = this.core.getCursorPosition(e);
  178. this.setPositionState(position);
  179. this.mouseActivation.mouseMoved(position);
  180. }
  181.  
  182. onMouseLeave() {
  183. this.mouseActivation.mouseLeft();
  184. this.setState({ isPositionOutside: true });
  185. }
  186.  
  187. onClick(e) {
  188. this.setPositionState(this.core.getCursorPosition(e));
  189. this.mouseActivation.mouseClicked();
  190. this.onMouseDetected();
  191. }
  192.  
  193. onTouchDetected() {
  194. const environment = {
  195. isTouchDetected: true,
  196. isMouseDetected: false
  197. };
  198.  
  199. this.setState({ detectedEnvironment: environment });
  200. this.props.onDetectedEnvironmentChanged(environment);
  201. }
  202.  
  203. onMouseDetected() {
  204. const environment = {
  205. isTouchDetected: false,
  206. isMouseDetected: true
  207. };
  208.  
  209. this.setState({ detectedEnvironment: environment });
  210. this.props.onDetectedEnvironmentChanged(environment);
  211. }
  212.  
  213. onPositionChanged = () => {
  214. const { onPositionChanged } = this.props;
  215. onPositionChanged(this.state);
  216. }
  217.  
  218. componentDidMount() {
  219. if (this.props.isEnabled) {
  220. this.enable();
  221. }
  222. }
  223.  
  224. componentWillReceiveProps({ isEnabled: willBeEnabled }) {
  225. const { isEnabled } = this.props;
  226. const isEnabledWillChange = isEnabled !== willBeEnabled;
  227.  
  228. if (!isEnabledWillChange) {
  229. return;
  230. }
  231.  
  232. if (willBeEnabled) {
  233. this.enable();
  234. } else {
  235. this.disable();
  236. }
  237. }
  238.  
  239. componentWillUnmount() {
  240. this.disable();
  241. }
  242.  
  243. get isCoreReady() {
  244. return !!this.core;
  245. }
  246.  
  247. enable() {
  248. this.addEventListeners();
  249. }
  250.  
  251. disable() {
  252. this.removeEventListeners();
  253. }
  254.  
  255. init() {
  256. this.core = new Core(this.el);
  257.  
  258. this.setElementDimensionsState(
  259. this.getElementDimensions(this.el)
  260. );
  261. }
  262.  
  263. setTouchActivationStrategy(interaction) {
  264. const {
  265. pressDurationInMs,
  266. pressMoveThreshold,
  267. tapDurationInMs,
  268. tapMoveThreshold
  269. }= this.props;
  270.  
  271. const {
  272. TOUCH,
  273. TAP,
  274. PRESS
  275. } = INTERACTIONS;
  276.  
  277. switch (interaction) {
  278. case PRESS :
  279. this.touchActivation = new PressActivation({
  280. onIsActiveChanged: this.onIsActiveChanged,
  281. pressDurationInMs,
  282. pressMoveThreshold
  283. });
  284. break;
  285. case TAP :
  286. this.touchActivation = new TapActivation({
  287. onIsActiveChanged: this.onIsActiveChanged,
  288. tapDurationInMs,
  289. tapMoveThreshold
  290. });
  291. break;
  292. case TOUCH :
  293. this.touchActivation = new TouchActivation({
  294. onIsActiveChanged: this.onIsActiveChanged
  295. });
  296. break;
  297. default :
  298. throw new Error('Must implement a touch activation strategy');
  299. }
  300. }
  301.  
  302. setMouseActivationStrategy(interaction) {
  303. const {
  304. hoverDelayInMs,
  305. hoverOffDelayInMs
  306. }= this.props;
  307.  
  308. const {
  309. HOVER,
  310. CLICK
  311. } = INTERACTIONS;
  312.  
  313. switch (interaction) {
  314. case HOVER :
  315. this.mouseActivation = new HoverActivation({
  316. onIsActiveChanged: this.onIsActiveChanged,
  317. hoverDelayInMs,
  318. hoverOffDelayInMs
  319. });
  320. break;
  321. case CLICK :
  322. this.mouseActivation = new ClickActivation({
  323. onIsActiveChanged: this.onIsActiveChanged
  324. });
  325. break;
  326. default :
  327. throw new Error('Must implement a mouse activation strategy');
  328. }
  329. }
  330.  
  331. reset() {
  332. const {
  333. core: {
  334. lastEvent: lastMouseEvent
  335. } = {}
  336. } = this;
  337.  
  338. this.init();
  339.  
  340. if (!lastMouseEvent) {
  341. return;
  342. }
  343.  
  344. this.setPositionState(
  345. this.core.getCursorPosition(lastMouseEvent)
  346. );
  347. }
  348.  
  349. activate() {
  350. this.setState({ isActive: true });
  351. this.props.onActivationChanged({ isActive: true });
  352. }
  353.  
  354. deactivate() {
  355. this.setState({ isActive: false }, () => {
  356. const { isPositionOutside, position } = this.state;
  357.  
  358. this.props.onPositionChanged({
  359. isPositionOutside,
  360. position
  361. });
  362.  
  363. this.props.onActivationChanged({ isActive: false });
  364. });
  365. }
  366.  
  367. setPositionState(position) {
  368. const isPositionOutside = this.getIsPositionOutside(position);
  369.  
  370. this.setState(
  371. {
  372. isPositionOutside,
  373. position
  374. },
  375. this.onPositionChanged
  376. );
  377. }
  378.  
  379. setElementDimensionsState(dimensions) {
  380. this.setState({
  381. elementDimensions: dimensions
  382. })
  383. }
  384.  
  385. setShouldGuardAgainstMouseEmulationByDevices() {
  386. this.shouldGuardAgainstMouseEmulationByDevices = true;
  387. }
  388.  
  389. unsetShouldGuardAgainstMouseEmulationByDevices() {
  390. this.timers.push({
  391. name: MOUSE_EMULATION_GUARD_TIMER_NAME,
  392. id: setTimeout(() => {
  393. this.shouldGuardAgainstMouseEmulationByDevices = false;
  394. }, 0)
  395. });
  396. }
  397.  
  398. getElementDimensions(el) {
  399. const {
  400. width,
  401. height
  402. } = el.getBoundingClientRect();
  403.  
  404. return {
  405. width,
  406. height
  407. }
  408. }
  409.  
  410. getIsPositionOutside(position) {
  411. const { x, y } = position;
  412. const {
  413. elementDimensions: {
  414. width,
  415. height
  416. }
  417. } = this.state
  418.  
  419. const isPositionOutside = (
  420. x < 0 ||
  421. y < 0 ||
  422. x > width ||
  423. y > height
  424. );
  425.  
  426. return isPositionOutside;
  427. }
  428.  
  429. getTouchEvent(e) {
  430. return e.touches[0];
  431. }
  432.  
  433. getIsReactComponent(reactElement) {
  434. return typeof reactElement.type === 'function';
  435. }
  436.  
  437. shouldDecorateChild(child) {
  438. return (
  439. !!child &&
  440. this.getIsReactComponent(child) &&
  441. this.props.shouldDecorateChildren
  442. );
  443. }
  444.  
  445. decorateChild(child, props) {
  446. return cloneElement(child, props);
  447. }
  448.  
  449. decorateChildren(children, props) {
  450. return Children.map(children, (child) => {
  451. return this.shouldDecorateChild(child) ? this.decorateChild(child, props) : child;
  452. });
  453. }
  454.  
  455. addEventListeners() {
  456. this.eventListeners.push(
  457. addEventListener(this.el, 'touchstart', this.onTouchStart, { passive: false }),
  458. addEventListener(this.el, 'touchmove', this.onTouchMove, { passive: false }),
  459. addEventListener(this.el, 'touchend', this.onTouchEnd),
  460. addEventListener(this.el, 'touchcancel', this.onTouchCancel),
  461. addEventListener(this.el, 'mouseenter', this.onMouseEnter),
  462. addEventListener(this.el, 'mousemove', this.onMouseMove),
  463. addEventListener(this.el, 'mouseleave', this.onMouseLeave),
  464. addEventListener(this.el, 'click', this.onClick)
  465. );
  466. }
  467.  
  468. removeEventListeners() {
  469. while (this.eventListeners.length) {
  470. this.eventListeners.pop().removeEventListener();
  471. }
  472. }
  473.  
  474. getPassThroughProps() {
  475. const ownPropNames = Object.keys(this.constructor.propTypes);
  476. return omit(this.props, ownPropNames);
  477. }
  478.  
  479. render() {
  480. const { children, className, mapChildProps, style } = this.props;
  481. const props = objectAssign(
  482. {},
  483. mapChildProps(this.state),
  484. this.getPassThroughProps()
  485. );
  486.  
  487. return (
  488. <div { ...{
  489. className,
  490. ref: (el) => this.el = el,
  491. style: objectAssign({}, style, {
  492. WebkitUserSelect: 'none'
  493. })
  494. }}>
  495. {this.decorateChildren(children, props)}
  496. </div>
  497. );
  498. }
  499. }
Add Comment
Please, Sign In to add comment