// File#: _1_diagonal-movement // Usage: codyhouse.co/license /* Modified version of the jQuery-menu-aim plugin https://github.com/kamens/jQuery-menu-aim - Replaced jQuery with Vanilla JS - Minor changes */ (function() { var menuAim = function(opts) { init(opts); }; window.menuAim = menuAim; function init(opts) { var activeRow = null, mouseLocs = [], lastDelayLoc = null, timeoutId = null, options = Util.extend({ menu: '', rows: false, //if false, get direct children - otherwise pass nodes list submenuSelector: "*", submenuDirection: "right", tolerance: 75, // bigger = more forgivey when entering submenu enter: function(){}, exit: function(){}, activate: function(){}, deactivate: function(){}, exitMenu: function(){} }, opts), menu = options.menu; var MOUSE_LOCS_TRACKED = 3, // number of past mouse locations to track DELAY = 300; // ms delay when user appears to be entering submenu /** * Keep track of the last few locations of the mouse. */ var mouseMoveFallback = function(event) { (!window.requestAnimationFrame) ? mousemoveDocument(event) : window.requestAnimationFrame(function(){mousemoveDocument(event);}); }; var mousemoveDocument = function(e) { mouseLocs.push({x: e.pageX, y: e.pageY}); if (mouseLocs.length > MOUSE_LOCS_TRACKED) { mouseLocs.shift(); } }; /** * Cancel possible row activations when leaving the menu entirely */ var mouseleaveMenu = function() { if (timeoutId) { clearTimeout(timeoutId); } // If exitMenu is supplied and returns true, deactivate the // currently active row on menu exit. if (options.exitMenu(this)) { if (activeRow) { options.deactivate(activeRow); } activeRow = null; } }; /** * Trigger a possible row activation whenever entering a new row. */ var mouseenterRow = function() { if (timeoutId) { // Cancel any previous activation delays clearTimeout(timeoutId); } options.enter(this); possiblyActivate(this); }, mouseleaveRow = function() { options.exit(this); }; /* * Immediately activate a row if the user clicks on it. */ var clickRow = function() { activate(this); }; /** * Activate a menu row. */ var activate = function(row) { if (row == activeRow) { return; } if (activeRow) { options.deactivate(activeRow); } options.activate(row); activeRow = row; }; /** * Possibly activate a menu row. If mouse movement indicates that we * shouldn't activate yet because user may be trying to enter * a submenu's content, then delay and check again later. */ var possiblyActivate = function(row) { var delay = activationDelay(); if (delay) { timeoutId = setTimeout(function() { possiblyActivate(row); }, delay); } else { activate(row); } }; /** * Return the amount of time that should be used as a delay before the * currently hovered row is activated. * * Returns 0 if the activation should happen immediately. Otherwise, * returns the number of milliseconds that should be delayed before * checking again to see if the row should be activated. */ var activationDelay = function() { if (!activeRow || !Util.is(activeRow, options.submenuSelector)) { // If there is no other submenu row already active, then // go ahead and activate immediately. return 0; } function getOffset(element) { var rect = element.getBoundingClientRect(); return { top: rect.top + window.pageYOffset, left: rect.left + window.pageXOffset }; }; var offset = getOffset(menu), upperLeft = { x: offset.left, y: offset.top - options.tolerance }, upperRight = { x: offset.left + menu.offsetWidth, y: upperLeft.y }, lowerLeft = { x: offset.left, y: offset.top + menu.offsetHeight + options.tolerance }, lowerRight = { x: offset.left + menu.offsetWidth, y: lowerLeft.y }, loc = mouseLocs[mouseLocs.length - 1], prevLoc = mouseLocs[0]; if (!loc) { return 0; } if (!prevLoc) { prevLoc = loc; } if (prevLoc.x < offset.left || prevLoc.x > lowerRight.x || prevLoc.y < offset.top || prevLoc.y > lowerRight.y) { // If the previous mouse location was outside of the entire // menu's bounds, immediately activate. return 0; } if (lastDelayLoc && loc.x == lastDelayLoc.x && loc.y == lastDelayLoc.y) { // If the mouse hasn't moved since the last time we checked // for activation status, immediately activate. return 0; } // Detect if the user is moving towards the currently activated // submenu. // // If the mouse is heading relatively clearly towards // the submenu's content, we should wait and give the user more // time before activating a new row. If the mouse is heading // elsewhere, we can immediately activate a new row. // // We detect this by calculating the slope formed between the // current mouse location and the upper/lower right points of // the menu. We do the same for the previous mouse location. // If the current mouse location's slopes are // increasing/decreasing appropriately compared to the // previous's, we know the user is moving toward the submenu. // // Note that since the y-axis increases as the cursor moves // down the screen, we are looking for the slope between the // cursor and the upper right corner to decrease over time, not // increase (somewhat counterintuitively). function slope(a, b) { return (b.y - a.y) / (b.x - a.x); }; var decreasingCorner = upperRight, increasingCorner = lowerRight; // Our expectations for decreasing or increasing slope values // depends on which direction the submenu opens relative to the // main menu. By default, if the menu opens on the right, we // expect the slope between the cursor and the upper right // corner to decrease over time, as explained above. If the // submenu opens in a different direction, we change our slope // expectations. if (options.submenuDirection == "left") { decreasingCorner = lowerLeft; increasingCorner = upperLeft; } else if (options.submenuDirection == "below") { decreasingCorner = lowerRight; increasingCorner = lowerLeft; } else if (options.submenuDirection == "above") { decreasingCorner = upperLeft; increasingCorner = upperRight; } var decreasingSlope = slope(loc, decreasingCorner), increasingSlope = slope(loc, increasingCorner), prevDecreasingSlope = slope(prevLoc, decreasingCorner), prevIncreasingSlope = slope(prevLoc, increasingCorner); if (decreasingSlope < prevDecreasingSlope && increasingSlope > prevIncreasingSlope) { // Mouse is moving from previous location towards the // currently activated submenu. Delay before activating a // new menu row, because user may be moving into submenu. lastDelayLoc = loc; return DELAY; } lastDelayLoc = null; return 0; }; var reset = function(triggerDeactivate) { if (timeoutId) { clearTimeout(timeoutId); } if (activeRow && triggerDeactivate) { options.deactivate(activeRow); } activeRow = null; }; var destroyInstance = function() { menu.removeEventListener('mouseleave', mouseleaveMenu); document.removeEventListener('mousemove', mouseMoveFallback); if(rows.length > 0) { for(var i = 0; i < rows.length; i++) { rows[i].removeEventListener('mouseenter', mouseenterRow); rows[i].removeEventListener('mouseleave', mouseleaveRow); rows[i].removeEventListener('click', clickRow); } } }; /** * Hook up initial menu events */ menu.addEventListener('mouseleave', mouseleaveMenu); var rows = (options.rows) ? options.rows : menu.children; if(rows.length > 0) { for(var i = 0; i < rows.length; i++) {(function(i){ rows[i].addEventListener('mouseenter', mouseenterRow); rows[i].addEventListener('mouseleave', mouseleaveRow); rows[i].addEventListener('click', clickRow); })(i);} } document.addEventListener('mousemove', mouseMoveFallback); /* Reset/destroy menu */ menu.addEventListener('reset', function(event){ reset(event.detail); }); menu.addEventListener('destroy', destroyInstance); }; }());