gabrielbull/react-aim

View on GitHub
src/target.js

Summary

Maintainability
D
2 days
Test Coverage
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import monitor from './monitor';

export default function(source, spec = null) {
  if (spec === null && typeof source === 'object') {
    spec = source;
    source = null;
  }

  return function(WrappedComponent) {
    return class extends Component {
      _source;
      aiming = false;
      skippedStops = 0;
      stopTimeout;
      _isOver = false;
      maxDistance;
      prevDistance;
      childrenSources = [];
      _isMounted = false;

      static childContextTypes = {
        target: PropTypes.object
      };

      static contextTypes = {
        source: PropTypes.object
      };

      getChildContext() {
        return {
          target: this
        };
      }

      constructor() {
        super();
        this._source = source;
        this.spec = spec;
      }

      addChildrenSource(source) {
        this.childrenSources.push(source);
      }

      removeChildrenSource(source) {
        this.childrenSources = this.childrenSources.filter(item => item !== source);
      }

      get source() {
        if (typeof this._source === 'function' && this.wrappedComponent) return this._source(this.wrappedComponent.props, this.wrappedComponent);
        return null;
      }

      isOver() {
        return this._isOver;
      }

      hasChildrenOver() {
        for (let i = 0, len = this.childrenSources.length; i < len; ++i) {
          if (this.childrenSources[i].isOver() || this.childrenSources[i].hasChildrenOver()) return true;
        }
        return false;
      }

      isAimed() {
        return this.aiming;
      }

      hasChildrenAimed() {
        for (let i = 0, len = this.childrenSources.length; i < len; ++i) {
          if (this.childrenSources[i].hasChildrenAimed()) return true;
        }
        return false;
      }

      hasChildrenSource(source) {
        if (this.childrenSources.includes(source)) return true;

        let result = false;
        this.childrenSources.forEach(item => {
          item.childrenTargets.forEach(target => {
            if (target.hasChildrenSource(source)) result = true;
          });
        });

        return result;
      }

      componentDidMount() {
        if (this.context.source) {
          this.context.source.addChildrenTarget(this);
        }

        this._isMounted = true;
        monitor.addTarget(this);
        const element = ReactDOM.findDOMNode(this);
        element.addEventListener('mousemove', this.handleMouseMove);
      }

      componentWillUnmount() {
        if (this.context.source) {
          this.context.source.removeChildrenTarget(this);
        }

        this._isMounted = false;
        monitor.removeTarget(this);
        const element = ReactDOM.findDOMNode(this);
        element.removeEventListener('mousemove', this.handleMouseMove);
        document.removeEventListener('mousemove', this.handleMouseMove);
        document.removeEventListener('mouseout', this.handleMouseOut);
        clearTimeout(this.stopTimeout);
      }

      trackMouseLeave() {
        const element = ReactDOM.findDOMNode(this);
        document.addEventListener('mousemove', this.handleMouseMove);
        document.addEventListener('mouseout', this.handleMouseOut);
        element.removeEventListener('mousemove', this.handleMouseMove);
      }

      untrackMouseLeave() {
        const element = ReactDOM.findDOMNode(this);
        document.removeEventListener('mousemove', this.handleMouseMove);
        document.removeEventListener('mouseout', this.handleMouseOut);
        element.addEventListener('mousemove', this.handleMouseMove);
      }

      handleMouseOut = e => {
        if (e.toElement == null && e.relatedTarget == null) {
          this.handleMouseLeave(e);
        } else {
          this.handleMouseMove(e);
        }
      };

      handleMouseMove = e => {
        if (monitor.mouseOver(e, this)) this.handleMouseEnter(e);
        else this.handleMouseLeave(e);
      };

      handleMouseEnter = () => {
        if (!this._isOver) {
          this._isOver = true;
          this.trackMouseLeave();
          this.triggerMouseEnter();
        }
      };

      handleMouseLeave = () => {
        if (this._isOver) {
          this._isOver = false;
          this.untrackMouseLeave();
          this.triggerMouseLeave();
        }
      };

      triggerAimMove(distance) {
        if (!this.maxDistance) this.maxDistance = distance;
        distance = Math.round((1 - (1 / this.maxDistance) * distance) * 1000) / 1000;
        if (this.prevDistance === null || this.prevDistance < distance || this.aiming) {
          if (!this.aiming) {
            this.aiming = true;
            if (typeof this.spec === 'object' && this.spec && typeof this.spec.aimStart === 'function') {
              this.spec.aimStart(this.wrappedComponent.props, this.wrappedComponent, distance);
            }
          }

          this.skippedStops = 0;
          if (this.stopTimeout) clearTimeout(this.stopTimeout);

          this.stopTimeout = setTimeout(() => {
            this.triggerAimStop(true);
            if (!this._isOver) monitor.aimStopped();
          }, 100);

          if (typeof this.spec === 'object' && this.spec && typeof this.spec.aimMove === 'function') {
            this.spec.aimMove(this.wrappedComponent.props, this.wrappedComponent, distance);
          }
        }

        this.prevDistance = distance;
      }

      triggerAimStop(force = false) {
        if (this.stopTimeout) clearTimeout(this.stopTimeout);
        if (this.aiming) {
          const doStop = () => {
            this.prevDistance = null;
            this.skippedStops = 0;
            this.maxDistance = null;
            this.aiming = false;
            if (typeof this.spec === 'object' && this.spec && typeof this.spec.aimStop === 'function') {
              if (this.wrappedComponent) {
                this.spec.aimStop(this.wrappedComponent.props, this.wrappedComponent);
              }
            }
          };

          if (!force && this.skippedStops < 10) {
            this.skippedStops++;
            this.stopTimeout = setTimeout(() => {
              doStop();
              if (!this._isOver) monitor.aimStopped();
            }, 100);
          } else {
            doStop();
          }
        }
      }

      triggerMouseEnter() {
        if (typeof this.spec === 'object' && this.spec && typeof this.spec.mouseEnter === 'function') {
          this.spec.mouseEnter(this.wrappedComponent.props, this.wrappedComponent);
        }
      }

      triggerMouseLeave() {
        if (typeof this.spec === 'object' && this.spec && typeof this.spec.mouseLeave === 'function') {
          this.spec.mouseLeave(this.wrappedComponent.props, this.wrappedComponent);
        }
      }

      render() {
        return <WrappedComponent ref={ref => (this.wrappedComponent = ref)} {...this.props} />;
      }
    };
  };
}