gabrielbull/react-aim

View on GitHub
src/source.js

Summary

Maintainability
F
3 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(target, spec = null) {
  if (spec === null && typeof target === 'object') {
    spec = target;
    target = null;
  }

  return function(WrappedComponent) {
    return class extends Component {
      _isOver = false;
      _isMounted = false;
      childrenTargets = [];

      static childContextTypes = {
        source: PropTypes.object
      };

      static contextTypes = {
        target: PropTypes.object
      };

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

      constructor() {
        super();
        this._target = target;
        this.spec = spec;
      }

      isOver() {
        return this._isOver;
      }

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

      hasChildrenAimed() {
        const target = this.target;
        if (target && (target.isAimed() || target.hasChildrenAimed())) return true;
        for (let i = 0, len = this.childrenTargets.length; i < len; ++i) {
          if (this.childrenTargets[i].isAimed() || this.childrenTargets[i].hasChildrenAimed()) return true;
        }
        return false;
      }

      addChildrenTarget(target) {
        this.childrenTargets.push(target);
      }

      removeChildrenTarget(target) {
        this.childrenTargets = this.childrenTargets.filter(item => item !== target);
      }

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

      buffer = (e, cb, timeout = 0) => {
        setTimeout(() => cb(e), timeout);
      };

      bufferHandleMouseMove = e => this.buffer(e, this.handleMouseMove);
      bufferHandleMouseOut = e => this.buffer(e, this.handleMouseOut);

      componentDidMount() {
        if (this.context.target) {
          this.context.target.addChildrenSource(this);
        }

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

      componentWillUnmount() {
        if (this.context.target) {
          this.context.target.removeChildrenSource(this);
        }

        this.unbindEvents();
        this._isMounted = false;
      }

      unbindEvents() {
        if (this._isMounted) {
          const element = ReactDOM.findDOMNode(this);
          element.removeEventListener('mousemove', this.bufferHandleMouseMove);
        }
        document.removeEventListener('mousemove', this.bufferHandleMouseMove);
        document.removeEventListener('mouseout', this.bufferHandleMouseOut);
      }

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

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

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

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

      handleMouseEnter = () => {
        if (!this._isOver) {
          monitor
            .requestMouseEnter(this)
            .then(() => {
              this.forceMouseEnter();
            })
            .catch(() => null);
        }
      };

      forceMouseEnter = () => {
        this._isOver = true;
        this.triggerMouseEnter();
        this.trackMouseLeave();
      };

      handleMouseLeave = () => {
        if (this._isOver) {
          monitor
            .requestMouseLeave(this)
            .then(() => this.forceMouseLeave())
            .catch(() => null);
        }
      };

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

      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} />;
      }
    };
  };
}