|
|
@@ -1,324 +0,0 @@
|
|
|
-/**
|
|
|
- * menu-aim is a jQuery plugin for dropdown menus that can differentiate
|
|
|
- * between a user trying hover over a dropdown item vs trying to navigate into
|
|
|
- * a submenu's contents.
|
|
|
- *
|
|
|
- * menu-aim assumes that you have are using a menu with submenus that expand
|
|
|
- * to the menu's right. It will fire events when the user's mouse enters a new
|
|
|
- * dropdown item *and* when that item is being intentionally hovered over.
|
|
|
- *
|
|
|
- * __________________________
|
|
|
- * | Monkeys >| Gorilla |
|
|
|
- * | Gorillas >| Content |
|
|
|
- * | Chimps >| Here |
|
|
|
- * |___________|____________|
|
|
|
- *
|
|
|
- * In the above example, "Gorillas" is selected and its submenu content is
|
|
|
- * being shown on the right. Imagine that the user's cursor is hovering over
|
|
|
- * "Gorillas." When they move their mouse into the "Gorilla Content" area, they
|
|
|
- * may briefly hover over "Chimps." This shouldn't close the "Gorilla Content"
|
|
|
- * area.
|
|
|
- *
|
|
|
- * This problem is normally solved using timeouts and delays. menu-aim tries to
|
|
|
- * solve this by detecting the direction of the user's mouse movement. This can
|
|
|
- * make for quicker transitions when navigating up and down the menu. The
|
|
|
- * experience is hopefully similar to amazon.com/'s "Shop by Department"
|
|
|
- * dropdown.
|
|
|
- *
|
|
|
- * Use like so:
|
|
|
- *
|
|
|
- * $("#menu").menuAim({
|
|
|
- * activate: $.noop, // fired on row activation
|
|
|
- * deactivate: $.noop // fired on row deactivation
|
|
|
- * });
|
|
|
- *
|
|
|
- * ...to receive events when a menu's row has been purposefully (de)activated.
|
|
|
- *
|
|
|
- * The following options can be passed to menuAim. All functions execute with
|
|
|
- * the relevant row's HTML element as the execution context ('this'):
|
|
|
- *
|
|
|
- * .menuAim({
|
|
|
- * // Function to call when a row is purposefully activated. Use this
|
|
|
- * // to show a submenu's content for the activated row.
|
|
|
- * activate: function() {},
|
|
|
- *
|
|
|
- * // Function to call when a row is deactivated.
|
|
|
- * deactivate: function() {},
|
|
|
- *
|
|
|
- * // Function to call when mouse enters a menu row. Entering a row
|
|
|
- * // does not mean the row has been activated, as the user may be
|
|
|
- * // mousing over to a submenu.
|
|
|
- * enter: function() {},
|
|
|
- *
|
|
|
- * // Function to call when mouse exits a menu row.
|
|
|
- * exit: function() {},
|
|
|
- *
|
|
|
- * // Selector for identifying which elements in the menu are rows
|
|
|
- * // that can trigger the above events. Defaults to "> li".
|
|
|
- * rowSelector: "> li",
|
|
|
- *
|
|
|
- * // You may have some menu rows that aren't submenus and therefore
|
|
|
- * // shouldn't ever need to "activate." If so, filter submenu rows w/
|
|
|
- * // this selector. Defaults to "*" (all elements).
|
|
|
- * submenuSelector: "*",
|
|
|
- *
|
|
|
- * // Direction the submenu opens relative to the main menu. Can be
|
|
|
- * // left, right, above, or below. Defaults to "right".
|
|
|
- * submenuDirection: "right"
|
|
|
- * });
|
|
|
- *
|
|
|
- * https://github.com/kamens/jQuery-menu-aim
|
|
|
- * MIT License
|
|
|
-*/
|
|
|
-(function($) {
|
|
|
-
|
|
|
- $.fn.menuAim = function(opts) {
|
|
|
- // Initialize menu-aim for all elements in jQuery collection
|
|
|
- this.each(function() {
|
|
|
- init.call(this, opts);
|
|
|
- });
|
|
|
-
|
|
|
- return this;
|
|
|
- };
|
|
|
-
|
|
|
- function init(opts) {
|
|
|
- var $menu = $(this),
|
|
|
- activeRow = null,
|
|
|
- mouseLocs = [],
|
|
|
- lastDelayLoc = null,
|
|
|
- timeoutId = null,
|
|
|
- options = $.extend({
|
|
|
- rowSelector: "> li",
|
|
|
- submenuSelector: "*",
|
|
|
- submenuDirection: "right",
|
|
|
- tolerance: 75, // bigger = more forgivey when entering submenu
|
|
|
- enter: $.noop,
|
|
|
- exit: $.noop,
|
|
|
- activate: $.noop,
|
|
|
- deactivate: $.noop,
|
|
|
- exitMenu: $.noop
|
|
|
- }, opts);
|
|
|
-
|
|
|
- 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 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 || !$(activeRow).is(options.submenuSelector)) {
|
|
|
- // If there is no other submenu row already active, then
|
|
|
- // go ahead and activate immediately.
|
|
|
- return 0;
|
|
|
- }
|
|
|
-
|
|
|
- var offset = $menu.offset(),
|
|
|
- upperLeft = {
|
|
|
- x: offset.left,
|
|
|
- y: offset.top - options.tolerance
|
|
|
- },
|
|
|
- upperRight = {
|
|
|
- x: offset.left + $menu.outerWidth(),
|
|
|
- y: upperLeft.y
|
|
|
- },
|
|
|
- lowerLeft = {
|
|
|
- x: offset.left,
|
|
|
- y: offset.top + $menu.outerHeight() + options.tolerance
|
|
|
- },
|
|
|
- lowerRight = {
|
|
|
- x: offset.left + $menu.outerWidth(),
|
|
|
- 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;
|
|
|
- };
|
|
|
-
|
|
|
- /**
|
|
|
- * Hook up initial menu events
|
|
|
- */
|
|
|
- $menu
|
|
|
- .mouseleave(mouseleaveMenu)
|
|
|
- .find(options.rowSelector)
|
|
|
- .mouseenter(mouseenterRow)
|
|
|
- .mouseleave(mouseleaveRow)
|
|
|
- .click(clickRow);
|
|
|
-
|
|
|
- $(document).mousemove(mousemoveDocument);
|
|
|
-
|
|
|
- };
|
|
|
-})(jQuery);
|
|
|
-
|