diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..62c8935
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.idea/
\ No newline at end of file
diff --git a/README.md b/README.md
index cb5292c..8ac2932 100644
--- a/README.md
+++ b/README.md
@@ -63,7 +63,11 @@ the relevant row's HTML element as the execution context ('this'):
// controls which direction is "forgiving" as the user moves their
// cursor from the main menu into the submenu. Can be one of "right",
// "left", "above", or "below". Defaults to "right".
- submenuDirection: "right"
+ submenuDirection: "right",
+
+ // The default number of ms to delay if a user appears to be entering
+ // a submenu.
+ delay: 300
});
menu-aim assumes that you are using a menu with submenus that expand
diff --git a/example/example.html b/example/example.html
index c81ede4..ecd611e 100644
--- a/example/example.html
+++ b/example/example.html
@@ -203,7 +203,8 @@
jQuery-menu-aim example
// Hook up events to be fired on menu row activation.
$menu.menuAim({
activate: activateSubmenu,
- deactivate: deactivateSubmenu
+ deactivate: deactivateSubmenu,
+ trajectory: false
});
// jQuery-menu-aim:
diff --git a/jquery.menu-aim.js b/jquery.menu-aim.js
index 0c32941..043e3b6 100644
--- a/jquery.menu-aim.js
+++ b/jquery.menu-aim.js
@@ -65,12 +65,21 @@
* // Direction the submenu opens relative to the main menu. Can be
* // left, right, above, or below. Defaults to "right".
* submenuDirection: "right"
+ *
+ * // The default number of ms to delay if a user appears to be entering
+ * // a submenu. Defaults to 300 ms.
+ * delay: 300
* });
*
* https://github.com/kamens/jQuery-menu-aim
-*/
+ */
(function($) {
+ // ...
+ var TRAJECTORY_CANVAS = null,
+ $TRAJECTORY_CANVAS = null,
+ TRAJECTORY_CANVAS_CTX = null;
+
$.fn.menuAim = function(opts) {
// Initialize menu-aim for all elements in jQuery collection
this.each(function() {
@@ -91,6 +100,12 @@
submenuSelector: "*",
submenuDirection: "right",
tolerance: 75, // bigger = more forgivey when entering submenu
+ delay: 300, // ms delay when user appears to be entering submenu
+ trajectory: false,
+ trajectoryLineColor: '#222222',
+ trajectoryLineWidth: 2,
+ trajectoryFillColor: '#FFCC00',
+ trajectoryOpacity: '0.5',
enter: $.noop,
exit: $.noop,
activate: $.noop,
@@ -98,38 +113,37 @@
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
+ var MOUSE_LOCS_TRACKED = 3; // number of past mouse locations to track
/**
* Keep track of the last few locations of the mouse.
*/
var mousemoveDocument = function(e) {
- mouseLocs.push({x: e.pageX, y: e.pageY});
+ mouseLocs.push({x: e.pageX, y: e.pageY});
- if (mouseLocs.length > MOUSE_LOCS_TRACKED) {
- mouseLocs.shift();
- }
- };
+ 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);
- }
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
- activeRow = null;
+ // 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.
@@ -151,24 +165,24 @@
* Immediately activate a row if the user clicks on it.
*/
var clickRow = function() {
- activate(this);
- };
+ activate(this);
+ };
/**
* Activate a menu row.
*/
var activate = function(row) {
- if (row == activeRow) {
- return;
- }
+ if (row == activeRow) {
+ return;
+ }
- if (activeRow) {
- options.deactivate(activeRow);
- }
+ if (activeRow) {
+ options.deactivate(activeRow);
+ }
- options.activate(row);
- activeRow = row;
- };
+ options.activate(row);
+ activeRow = row;
+ };
/**
* Possibly activate a menu row. If mouse movement indicates that we
@@ -176,16 +190,16 @@
* a submenu's content, then delay and check again later.
*/
var possiblyActivate = function(row) {
- var delay = activationDelay();
+ var delay = activationDelay();
- if (delay) {
- timeoutId = setTimeout(function() {
- possiblyActivate(row);
- }, delay);
- } else {
- activate(row);
- }
- };
+ 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
@@ -196,125 +210,222 @@
* 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;
- }
+ 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;
+ }
- 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) {
- 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 (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);
+ };
- 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;
- }
+ 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 options.delay;
+ }
+
+ lastDelayLoc = null;
+ 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;
+ /**
+ * Activate a menu row.
+ */
+ var drawTrajectory = function(menu) {
+
+ // Proceed only when activated
+ if (options.trajectory) {
+
+ // Create canvas if not yet created
+ if (!TRAJECTORY_CANVAS) {
+ TRAJECTORY_CANVAS = document.createElement('canvas');
+ TRAJECTORY_CANVAS_CTX = TRAJECTORY_CANVAS.getContext("2d");
+
+ // Initialize canvas
+ TRAJECTORY_CANVAS.id = "jquery-menu-aim-trajectory-canvas";
+ TRAJECTORY_CANVAS.width = $menu.outerWidth();
+ TRAJECTORY_CANVAS.height = $menu.outerHeight();
+ TRAJECTORY_CANVAS.style.position = "absolute";
+ TRAJECTORY_CANVAS.style.opacity = options.trajectoryOpacity;
+
+ // Append canvas to body
+ $('body').append(TRAJECTORY_CANVAS);
+
+ // Quick reference to trajectory canvas
+ $TRAJECTORY_CANVAS = $('#jquery-menu-aim-trajectory-canvas');
+
+ // Initially position of canvas
+ $TRAJECTORY_CANVAS.css({
+ top: $menu.offset().top,
+ left: $menu.offset().left
+ });
+
+ // Move the canvas away if mouseenters it so that the menu still has context
+ $TRAJECTORY_CANVAS.mouseenter(function(e) {
+ $(this).css({
+ 'z-index': -9999
+ });
+ });
+
+ // Account for hiding of the trajectory
+ $menu.mouseleave(function(e) {
+ $TRAJECTORY_CANVAS.css({
+ display: 'none'
+ });
+ });
}
- 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;
+ // Reference new canvas dimensions/positions
+ var menu_offs = $menu.offset();
+ var menu_w = $menu.outerWidth();
+ var menu_h = $menu.outerHeight();
+ var canvas_w = Math.abs((menu_offs.left + menu_w) - menu.pageX);
+ var canvas_h = menu_h;
+ var canvas_dist_from_mouse = 5;
+
+ // Proceed while mouse is within menu
+ if (menu.pageX < (menu_offs.left + menu_w - canvas_dist_from_mouse)) {
+
+ // Set dimensions of canvas with mouse movement
+ TRAJECTORY_CANVAS.style.display = "block";
+ TRAJECTORY_CANVAS.width = canvas_w - canvas_dist_from_mouse;
+ TRAJECTORY_CANVAS.height = canvas_h;
+
+ // Reposition canvas with mouse movement making sure canvas isn't directly
+ // below the cursor.
+ $TRAJECTORY_CANVAS.css({
+ left: menu.pageX + canvas_dist_from_mouse,
+ 'z-index': 9999
+ });
+
+ // Hide the canvas otherwise
+ } else {
+ TRAJECTORY_CANVAS.style.display = "none";
}
- lastDelayLoc = null;
- return 0;
- };
+ // Clear the previous trajectory path
+ TRAJECTORY_CANVAS_CTX.clearRect(0, 0, TRAJECTORY_CANVAS.width, TRAJECTORY_CANVAS.height);
+
+ // Trajectory path
+ TRAJECTORY_CANVAS_CTX.beginPath();
+ TRAJECTORY_CANVAS_CTX.moveTo(options.trajectoryLineWidth, menu.pageY - menu_offs.top);
+ TRAJECTORY_CANVAS_CTX.lineTo(TRAJECTORY_CANVAS.width + options.trajectoryLineWidth, -options.trajectoryLineWidth);
+ TRAJECTORY_CANVAS_CTX.lineTo(TRAJECTORY_CANVAS.width + options.trajectoryLineWidth, TRAJECTORY_CANVAS.height + options.trajectoryLineWidth);
+ TRAJECTORY_CANVAS_CTX.closePath();
+
+ // Draw the path outline
+ TRAJECTORY_CANVAS_CTX.lineWidth = options.trajectoryLineWidth;
+ TRAJECTORY_CANVAS_CTX.strokeStyle = options.trajectoryLineColor;
+ TRAJECTORY_CANVAS_CTX.stroke();
+
+ // Fill the trajectory triangle
+ TRAJECTORY_CANVAS_CTX.fillStyle = options.trajectoryFillColor;
+ TRAJECTORY_CANVAS_CTX.fill();
+ }
+ };
/**
* Hook up initial menu events
*/
$menu
+ .mousemove(drawTrajectory)
.mouseleave(mouseleaveMenu)
.find(options.rowSelector)
- .mouseenter(mouseenterRow)
- .mouseleave(mouseleaveRow)
- .click(clickRow);
+ .mouseenter(mouseenterRow)
+ .mouseleave(mouseleaveRow)
+ .click(clickRow);
$(document).mousemove(mousemoveDocument);
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..38eb3a5
--- /dev/null
+++ b/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "jquery-menu-aim",
+ "version": "1.0.0",
+ "description": "NPM installable version of jquery-menu-aim.",
+ "main": "jquery.menu-aim.js",
+ "directories": {
+ "example": "example"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/Xaxis/jQuery-menu-aim.git"
+ },
+ "author": "",
+ "license": "ISC",
+ "bugs": {
+ "url": "https://github.com/Xaxis/jQuery-menu-aim/issues"
+ },
+ "homepage": "https://github.com/Xaxis/jQuery-menu-aim#readme"
+}