'use strict';

class HoverIntent {

  constructor({
    sensitivity = 0.1, // speed less than 0.1px/ms means "hovering over an element"
    interval = 100,    // measure mouse speed once per 100ms
    elem,
    over,
    out
  }) {
    this.sensitivity = sensitivity;
    this.interval = interval;
    this.elem = elem;
    this.over = over;
    this.out = out;

    // make sure "this" is the object in event handlers.
    this.onMouseMove = this.onMouseMove.bind(this);
    this.onMouseOver = this.onMouseOver.bind(this);
    this.onMouseOut = this.onMouseOut.bind(this);

    // and in time-measuring function (called from setInterval)
    this.trackSpeed = this.trackSpeed.bind(this);
    console.log('this in constructor')
    console.log(this);
    elem.addEventListener("mouseover", this.onMouseOver);
    // console.log(this.onMouseOut)
    elem.addEventListener("mouseout", this.onMouseOut);

  }

  onMouseOver(event) {
    console.log('this in onMouseOver');
    console.log(this);
    if (this.isOverElement) {
      // if we're over the element, then ignore the event
      // we are already measuring the speed
      return;
    }

    this.isOverElement = true;

    // after every mousemove we'll be check the distance
    // between the previous and the current mouse coordinates
    // if it's less than sensivity, then the speed is slow

    this.prevX = event.pageX;
    this.prevY = event.pageY;
    this.prevTime = Date.now();

    elem.addEventListener('mousemove', this.onMouseMove);
    this.checkSpeedInterval = setInterval(this.trackSpeed, this.interval);
  }

  onMouseOut(event) {
    // if left the element
    if (!event.relatedTarget || !elem.contains(event.relatedTarget)) {
      this.isOverElement = false;
      // console.log('this.onMouseMove');
      // console.log(this.onMouseMove);
      console.log('this in onMouseOut');
      console.log(this);
      // console.log('this.elem');
      // console.log(this.elem);
      this.elem.removeEventListener('mousemove', this.onMouseMove);
      clearInterval(this.checkSpeedInterval);
      if (this.isHover) {
        // if there was a stop over the element
        this.out.call(this.elem, event);
        this.isHover = false;
      }
    }
  }

  onMouseMove(event) {
    this.lastX = event.pageX;
    this.lastY = event.pageY;
    this.lastTime = Date.now();
  }

  trackSpeed() {

    let speed;

    if (!this.lastTime || this.lastTime == this.prevTime) {
      // cursor didn't move
      speed = 0;
    } else {
      speed = Math.sqrt(
        Math.pow(this.prevX - this.lastX, 2) +
        Math.pow(this.prevY - this.lastY, 2)
      ) / (this.lastTime - this.prevTime);
    }

    if (speed < this.sensitivity) {
      clearInterval(this.checkSpeedInterval);
      this.isHover = true;
      this.over.call(this.elem);
    } else {
      // speed fast, remember new coordinates as the previous ones
      this.prevX = this.lastX;
      this.prevY = this.lastY;
      this.prevTime = this.lastTime;
    }
  }

  destroy() {
    elem.removeEventListener('mousemove', this.onMouseMove);
    elem.removeEventListener('mouseover', this.onMouseOver);
    elem.removeEventListener('mouseout', this.onMouseOut);
  }

}
'use strict';

describe("hoverIntent", function() {

  function mouse(eventType, x, y, options) {
    let eventOptions = Object.assign({
      bubbles: true,
      clientX: x,
      clientY: y,
      pageX: x,
      pageY: y,
      target: elem
    }, options || {});

    elem.dispatchEvent(new MouseEvent(eventType, eventOptions));
  }


  let isOver;
  let hoverIntent;


  before(function() {
    this.clock = sinon.useFakeTimers();
  });

  after(function() {
    this.clock.restore();
  });


  beforeEach(function() {
    isOver = false;

    hoverIntent = new HoverIntent({
      elem: elem,
      over: function() {
        isOver = true;
      },
      out: function() {
        isOver = false;
      }
    });
  })

  afterEach(function() {
    if (hoverIntent) {
      hoverIntent.destroy();
    }
  })

  it("mouseover -> when the pointer just arrived, no tooltip", function() {
    mouse('mouseover', 10, 10);
    assert.isFalse(isOver);
  });

  it("mouseover -> after a delay, the tooltip shows up", function() {
    mouse('mouseover', 10, 10);
    this.clock.tick(100);
    assert.isTrue(isOver);
  });

  it("mouseover -> followed by fast mouseout leads doesn't show tooltip", function() {
    mouse('mouseover', 10, 10);
    setTimeout(
      () => mouse('mouseout', 300, 300, { relatedTarget: document.body}),
      30
    );
    this.clock.tick(100);
    assert.isFalse(isOver);
  });


  it("mouseover -> slow move -> tooltips", function() {
    mouse('mouseover', 10, 10);
    for(let i=10; i<200; i+= 10) {
      setTimeout(
        () => mouse('mousemove', i/5, 10),
        i
      );
    }
    this.clock.tick(200);
    assert.isTrue(isOver);
  });

  it("mouseover -> fast move -> no tooltip", function() {
    mouse('mouseover', 10, 10);
    for(let i=10; i<200; i+= 10) {
      setTimeout(
        () => mouse('mousemove', i, 10),
        i
      );
    }
    this.clock.tick(200);
    assert.isFalse(isOver);
  });

});
.hours {
  color: red;
}

body {
  margin: 0;
}

.minutes {
  color: green;
}

.seconds {
  color: blue;
}

.clock {
  border: 1px dashed black;
  padding: 5px;
  display: inline-block;
  background: yellow;
  position: absolute;
  left: 0;
  top: 0;
}

#tooltip {
  position: absolute;
  padding: 10px 20px;
  border: 1px solid #b3c9ce;
  border-radius: 4px;
  text-align: center;
  font: italic 14px/1.3 sans-serif;
  color: #333;
  background: #fff;
  z-index: 100000;
  box-shadow: 3px 3px 3px rgba(0, 0, 0, .3);
}
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="style.css">
  <script src="hoverIntent.js"></script>
    <script src="https://en.js.cx/test/libs.js"></script>	
  <script src="test.js"></script>
</head>

<body>

  <div id="elem" class="clock">
    <span class="hours">12</span> :
    <span class="minutes">30</span> :
    <span class="seconds">00</span>
  </div>

  <div id="tooltip" hidden>Tooltip</div>

  <script>
   let test = new HoverIntent({
      elem,
      over() {
        tooltip.style.left = elem.getBoundingClientRect().left + 5 + 'px';
        tooltip.style.top = elem.getBoundingClientRect().bottom + 5 + 'px';
        tooltip.hidden = false;
      },
      out() {
        tooltip.hidden = true;
      }
    });
  </script>

</body>
</html>