components/SwipeItem.js

/**
 * Component represent swiping element.
 * This component will work only on childrens, childrens of passed childrens will not have handlers.
 * If you use it with tree of nodes it will not work
 *
 * @module SwipeItem
 */
import React, { Component, cloneElement } from 'react';
/**
 * @typedef {object} props
 * @property {Array|object} children
 * @property {string} className
 * @property {Function} closeModal
 * @property {Function} onClickElement
 * @property {boolean} doNotStyle
 */
export default class SwipeItem extends Component {
  constructor(props) {
    super(props);
    /**
     * @member {object}
     * @property {number} right Number of px from right margin of phone
     * @property {Array|object|null} children All components passed as children
     * with handlers added for swipe
     */
    this.state = {
      right: parseInt(props.rightMargin),
      children: null,
    };
    /**
     * @member {number}
     * @description Right margin on swipe start
     */
    this.originalOffset = 0;
    /**
     * @member {number}
     * @description Number of px added on one move event
     */
    this.velocity = 0;
    /**
     * @member {number}
     * @description Time when move start
     */
    this.timeOfLastDragEvent = 0;
    /**
     * @member {number}
     * @description Position of element on move start event
     */
    this.touchStartX = 0;
    /**
     * @member {number}
     * @description Position of element on move event
     */
    this.prevTouchX = 0;
    /**
     * @member {boolean}
     * @description Indicate move event
     */
    this.beingTouched = false;
    /**
     * @member {number|null}
     */
    this.intervalId = null;
    /**
     * @member {number}
     * @description Width of swipe view in px
     */
    this.elementWidth = 600;
    /**
     * @member {number}
     * @description Contain all click and mouse events
     */
    this.handlers = this.props.viewPlaceBet
      ? {
        onTouchStart: this.handleTouchStart,
        onTouchMove: this.handleTouchMove,
        onTouchEnd: this.handleTouchEnd,
        onMouseDown: this.handleMouseDown,
        onMouseMove: this.handleMouseMove,
        onMouseUp: this.handleMouseUp,
        onMouseLeave: this.handleMouseLeave,
      }
      : {
        onTouchEnd: this.handleTouchEnd,
        onMouseUp: this.handleMouseUp,
      };
  }

  /**
   * Add handlers on childrens
   *
   * @returns {void}
   */
  componentDidMount() {
    let children = this.props.children;
    if (!children.map) {
      children = [children];
    }
    children = children.map((child, i) => cloneElement(child, {
      ...this.handlers,
      key: i,
    }));

    // eslint-disable-next-line react/no-did-mount-set-state
    this.setState({
      children,
    });

    this.selfCloseTimeoutId = setTimeout(this.handleRemoveSelf, 5000);
  }

  /**
   * clear handlers
   *
   * @returns {void}
   */
  componentWillUnmount() {
    this.handlers = null;
    clearTimeout(this.selfCloseTimeoutId);
    clearTimeout(this.tid1);
    window.clearInterval(this.intervalId);
  }

  /**
   * Called in interval to remove slider or to move it to defauld coordinates
   *
   * @function
   * @returns {void}
   */
  animateSlidingToZero = () => {
    let { right } = this.state;
    if (!this.beingTouched && right < -20) {
      this.velocity += 10 * 0.033;
      right += this.velocity;
      if (right <= -(this.elementWidth - 200)) {
        window.clearInterval(this.intervalId);
        this.handleRemoveSelf();
      }
      this.setState({ right });
    } else if (!this.beingTouched) {
      right = 5;
      this.velocity = 0;
      window.clearInterval(this.intervalId);
      this.setState({ right });
      this.originalOffset = 0;
      this.intervalId = null;
    }
  }

  /**
   * Remove slider
   *
   * @function
   * @returns {void}
   */
  handleRemoveSelf = () => {
    this.tid1 = window.setTimeout(() => this.props.closeModal(), 100);
  }

  /**
   * Record start position
   *
   * @function
   * @param {number} clientX
   * @returns {void}
   */
  handleStart = (clientX) => {
    if (this.intervalId !== null) {
      window.clearInterval(this.intervalId);
    }

    this.originalOffset = this.state.right;
    this.velocity = 0;
    this.timeOfLastDragEvent = Date.now();
    this.touchStartX = clientX;
    this.beingTouched = true;
    this.intervalId = null;
  }

  /**
   * Calculate moving of slider
   *
   * @function
   * @param {number} clientX
   * @returns {void}
   */
  handleMove = (clientX) => {
    if (this.beingTouched) {
      const touchX = clientX;
      const currTime = Date.now();
      const elapsed = currTime - this.timeOfLastDragEvent;
      const velocity = (20 * (touchX - this.prevTouchX)) / elapsed;
      let deltaX = this.touchStartX - (touchX + this.originalOffset);
      if (deltaX === -this.elementWidth + 10) {
        this.handleRemoveSelf();
      } else if (deltaX > 5) {
        deltaX = 5;
      }
      this.setState({
        right: deltaX,
      });

      this.velocity = velocity;
      this.timeOfLastDragEvent = currTime;
      this.prevTouchX = touchX;
    }
  }

  /**
   * Record end position and check is it click. Move slider to beggining position or remove it.
   *
   * @function
   * @param {number} clientX
   * @returns {void}
   */
  handleEnd = (clientX) => {
    if (this.touchStartX === clientX) {
      this.props.onClickElement();
    } else {
      this.touchStartX = 0;
      this.beingTouched = false;
      this.intervalId = window.setInterval(this.animateSlidingToZero.bind(this), 33);
    }
  }

  /**
   * @function
   * @param {Event} touchStartEvent
   * @returns {void}
   */
  handleTouchStart = (touchStartEvent) => {
    touchStartEvent.stopPropagation();
    this.handleStart(touchStartEvent.targetTouches[0].clientX);
  }

  /**
   * @function
   * @param {Event} touchMoveEvent
   * @returns {void}
   */
  handleTouchMove = (touchMoveEvent) => {
    touchMoveEvent.stopPropagation();
    this.handleMove(touchMoveEvent.targetTouches[0].clientX);
  }

  /**
   * @function
   * @param {Event} touchEndEvent
   * @returns {void}
   */
  handleTouchEnd = (touchEndEvent) => {
    touchEndEvent.stopPropagation();
    if (this.props.viewPlaceBet) {
      this.handleEnd(touchEndEvent.changedTouches[0] && touchEndEvent.changedTouches[0].clientX);
    } else {
      this.handleRemoveSelf();
    }
  }

  /**
   * @function
   * @param {Event} mouseDownEvent
   * @returns {void}
   */
  handleMouseDown = (mouseDownEvent) => {
    mouseDownEvent.stopPropagation();
    this.handleStart(mouseDownEvent.clientX);
  }

  /**
   * @function
   * @param {Event} mouseMoveEvent
   * @returns {void}
   */
  handleMouseMove = (mouseMoveEvent) => {
    mouseMoveEvent.stopPropagation();
    this.handleMove(mouseMoveEvent.clientX);
  }

  /**
   * @function
   * @param {Event} mouseUpEvent
   * @returns {void}
   */
  handleMouseUp = (mouseUpEvent) => {
    mouseUpEvent.stopPropagation();
    if (this.props.viewPlaceBet) {
      this.handleEnd(mouseUpEvent.clientX);
    } else {
      this.handleRemoveSelf();
    }
  }

  /**
   * @function
   * @param {Event} mouseUpEvent
   * @returns {void}
   */
  handleMouseLeave = (mouseUpEvent) => {
    mouseUpEvent.stopPropagation();
    this.handleMouseUp(mouseUpEvent);
  }

  /**
   * Render
   *
   * @returns {view}
   */
  render() {
    return (
      <div
        className={this.props.className}
        ref={(reff) => {
          if (reff) {
            this.elementWidth = reff.getBoundingClientRect().width;
            if (this.elementWidth > 600) {
              this.elementWidth = 600;
            }
          }
        }}
        style={{
          right: `${this.state.right}px`,
        }}
        {...this.handlers}
      >
        {this.state.children}
      </div>
    );
  }
}