/milestone3/js/jquery.menu-aim.js

https://gitlab.com/maesace/CMSC142Project · JavaScript · 323 lines · 149 code · 33 blank · 141 comment · 30 complexity · 5f48c0c21a7d9754183f78d8fe83a7b5 MD5 · raw file

  1. /**
  2. * menu-aim is a jQuery plugin for dropdown menus that can differentiate
  3. * between a user trying hover over a dropdown item vs trying to navigate into
  4. * a submenu's contents.
  5. *
  6. * menu-aim assumes that you have are using a menu with submenus that expand
  7. * to the menu's right. It will fire events when the user's mouse enters a new
  8. * dropdown item *and* when that item is being intentionally hovered over.
  9. *
  10. * __________________________
  11. * | Monkeys >| Gorilla |
  12. * | Gorillas >| Content |
  13. * | Chimps >| Here |
  14. * |___________|____________|
  15. *
  16. * In the above example, "Gorillas" is selected and its submenu content is
  17. * being shown on the right. Imagine that the user's cursor is hovering over
  18. * "Gorillas." When they move their mouse into the "Gorilla Content" area, they
  19. * may briefly hover over "Chimps." This shouldn't close the "Gorilla Content"
  20. * area.
  21. *
  22. * This problem is normally solved using timeouts and delays. menu-aim tries to
  23. * solve this by detecting the direction of the user's mouse movement. This can
  24. * make for quicker transitions when navigating up and down the menu. The
  25. * experience is hopefully similar to amazon.com/'s "Shop by Department"
  26. * dropdown.
  27. *
  28. * Use like so:
  29. *
  30. * $("#menu").menuAim({
  31. * activate: $.noop, // fired on row activation
  32. * deactivate: $.noop // fired on row deactivation
  33. * });
  34. *
  35. * ...to receive events when a menu's row has been purposefully (de)activated.
  36. *
  37. * The following options can be passed to menuAim. All functions execute with
  38. * the relevant row's HTML element as the execution context ('this'):
  39. *
  40. * .menuAim({
  41. * // Function to call when a row is purposefully activated. Use this
  42. * // to show a submenu's content for the activated row.
  43. * activate: function() {},
  44. *
  45. * // Function to call when a row is deactivated.
  46. * deactivate: function() {},
  47. *
  48. * // Function to call when mouse enters a menu row. Entering a row
  49. * // does not mean the row has been activated, as the user may be
  50. * // mousing over to a submenu.
  51. * enter: function() {},
  52. *
  53. * // Function to call when mouse exits a menu row.
  54. * exit: function() {},
  55. *
  56. * // Selector for identifying which elements in the menu are rows
  57. * // that can trigger the above events. Defaults to "> li".
  58. * rowSelector: "> li",
  59. *
  60. * // You may have some menu rows that aren't submenus and therefore
  61. * // shouldn't ever need to "activate." If so, filter submenu rows w/
  62. * // this selector. Defaults to "*" (all elements).
  63. * submenuSelector: "*",
  64. *
  65. * // Direction the submenu opens relative to the main menu. Can be
  66. * // left, right, above, or below. Defaults to "right".
  67. * submenuDirection: "right"
  68. * });
  69. *
  70. * https://github.com/kamens/jQuery-menu-aim
  71. */
  72. (function($) {
  73. $.fn.menuAim = function(opts) {
  74. // Initialize menu-aim for all elements in jQuery collection
  75. this.each(function() {
  76. init.call(this, opts);
  77. });
  78. return this;
  79. };
  80. function init(opts) {
  81. var $menu = $(this),
  82. activeRow = null,
  83. mouseLocs = [],
  84. lastDelayLoc = null,
  85. timeoutId = null,
  86. options = $.extend({
  87. rowSelector: "> li",
  88. submenuSelector: "*",
  89. submenuDirection: "right",
  90. tolerance: 75, // bigger = more forgivey when entering submenu
  91. enter: $.noop,
  92. exit: $.noop,
  93. activate: $.noop,
  94. deactivate: $.noop,
  95. exitMenu: $.noop
  96. }, opts);
  97. var MOUSE_LOCS_TRACKED = 3, // number of past mouse locations to track
  98. DELAY = 300; // ms delay when user appears to be entering submenu
  99. /**
  100. * Keep track of the last few locations of the mouse.
  101. */
  102. var mousemoveDocument = function(e) {
  103. mouseLocs.push({x: e.pageX, y: e.pageY});
  104. if (mouseLocs.length > MOUSE_LOCS_TRACKED) {
  105. mouseLocs.shift();
  106. }
  107. };
  108. /**
  109. * Cancel possible row activations when leaving the menu entirely
  110. */
  111. var mouseleaveMenu = function() {
  112. if (timeoutId) {
  113. clearTimeout(timeoutId);
  114. }
  115. // If exitMenu is supplied and returns true, deactivate the
  116. // currently active row on menu exit.
  117. if (options.exitMenu(this)) {
  118. if (activeRow) {
  119. options.deactivate(activeRow);
  120. }
  121. activeRow = null;
  122. }
  123. };
  124. /**
  125. * Trigger a possible row activation whenever entering a new row.
  126. */
  127. var mouseenterRow = function() {
  128. if (timeoutId) {
  129. // Cancel any previous activation delays
  130. clearTimeout(timeoutId);
  131. }
  132. options.enter(this);
  133. possiblyActivate(this);
  134. },
  135. mouseleaveRow = function() {
  136. options.exit(this);
  137. };
  138. /*
  139. * Immediately activate a row if the user clicks on it.
  140. */
  141. var clickRow = function() {
  142. activate(this);
  143. };
  144. /**
  145. * Activate a menu row.
  146. */
  147. var activate = function(row) {
  148. if (row == activeRow) {
  149. return;
  150. }
  151. if (activeRow) {
  152. options.deactivate(activeRow);
  153. }
  154. options.activate(row);
  155. activeRow = row;
  156. };
  157. /**
  158. * Possibly activate a menu row. If mouse movement indicates that we
  159. * shouldn't activate yet because user may be trying to enter
  160. * a submenu's content, then delay and check again later.
  161. */
  162. var possiblyActivate = function(row) {
  163. var delay = activationDelay();
  164. if (delay) {
  165. timeoutId = setTimeout(function() {
  166. possiblyActivate(row);
  167. }, delay);
  168. } else {
  169. activate(row);
  170. }
  171. };
  172. /**
  173. * Return the amount of time that should be used as a delay before the
  174. * currently hovered row is activated.
  175. *
  176. * Returns 0 if the activation should happen immediately. Otherwise,
  177. * returns the number of milliseconds that should be delayed before
  178. * checking again to see if the row should be activated.
  179. */
  180. var activationDelay = function() {
  181. if (!activeRow || !$(activeRow).is(options.submenuSelector)) {
  182. // If there is no other submenu row already active, then
  183. // go ahead and activate immediately.
  184. return 0;
  185. }
  186. var offset = $menu.offset(),
  187. upperLeft = {
  188. x: offset.left,
  189. y: offset.top - options.tolerance
  190. },
  191. upperRight = {
  192. x: offset.left + $menu.outerWidth(),
  193. y: upperLeft.y
  194. },
  195. lowerLeft = {
  196. x: offset.left,
  197. y: offset.top + $menu.outerHeight() + options.tolerance
  198. },
  199. lowerRight = {
  200. x: offset.left + $menu.outerWidth(),
  201. y: lowerLeft.y
  202. },
  203. loc = mouseLocs[mouseLocs.length - 1],
  204. prevLoc = mouseLocs[0];
  205. if (!loc) {
  206. return 0;
  207. }
  208. if (!prevLoc) {
  209. prevLoc = loc;
  210. }
  211. if (prevLoc.x < offset.left || prevLoc.x > lowerRight.x ||
  212. prevLoc.y < offset.top || prevLoc.y > lowerRight.y) {
  213. // If the previous mouse location was outside of the entire
  214. // menu's bounds, immediately activate.
  215. return 0;
  216. }
  217. if (lastDelayLoc &&
  218. loc.x == lastDelayLoc.x && loc.y == lastDelayLoc.y) {
  219. // If the mouse hasn't moved since the last time we checked
  220. // for activation status, immediately activate.
  221. return 0;
  222. }
  223. // Detect if the user is moving towards the currently activated
  224. // submenu.
  225. //
  226. // If the mouse is heading relatively clearly towards
  227. // the submenu's content, we should wait and give the user more
  228. // time before activating a new row. If the mouse is heading
  229. // elsewhere, we can immediately activate a new row.
  230. //
  231. // We detect this by calculating the slope formed between the
  232. // current mouse location and the upper/lower right points of
  233. // the menu. We do the same for the previous mouse location.
  234. // If the current mouse location's slopes are
  235. // increasing/decreasing appropriately compared to the
  236. // previous's, we know the user is moving toward the submenu.
  237. //
  238. // Note that since the y-axis increases as the cursor moves
  239. // down the screen, we are looking for the slope between the
  240. // cursor and the upper right corner to decrease over time, not
  241. // increase (somewhat counterintuitively).
  242. function slope(a, b) {
  243. return (b.y - a.y) / (b.x - a.x);
  244. };
  245. var decreasingCorner = upperRight,
  246. increasingCorner = lowerRight;
  247. // Our expectations for decreasing or increasing slope values
  248. // depends on which direction the submenu opens relative to the
  249. // main menu. By default, if the menu opens on the right, we
  250. // expect the slope between the cursor and the upper right
  251. // corner to decrease over time, as explained above. If the
  252. // submenu opens in a different direction, we change our slope
  253. // expectations.
  254. if (options.submenuDirection == "left") {
  255. decreasingCorner = lowerLeft;
  256. increasingCorner = upperLeft;
  257. } else if (options.submenuDirection == "below") {
  258. decreasingCorner = lowerRight;
  259. increasingCorner = lowerLeft;
  260. } else if (options.submenuDirection == "above") {
  261. decreasingCorner = upperLeft;
  262. increasingCorner = upperRight;
  263. }
  264. var decreasingSlope = slope(loc, decreasingCorner),
  265. increasingSlope = slope(loc, increasingCorner),
  266. prevDecreasingSlope = slope(prevLoc, decreasingCorner),
  267. prevIncreasingSlope = slope(prevLoc, increasingCorner);
  268. if (decreasingSlope < prevDecreasingSlope &&
  269. increasingSlope > prevIncreasingSlope) {
  270. // Mouse is moving from previous location towards the
  271. // currently activated submenu. Delay before activating a
  272. // new menu row, because user may be moving into submenu.
  273. lastDelayLoc = loc;
  274. return DELAY;
  275. }
  276. lastDelayLoc = null;
  277. return 0;
  278. };
  279. /**
  280. * Hook up initial menu events
  281. */
  282. $menu
  283. .mouseleave(mouseleaveMenu)
  284. .find(options.rowSelector)
  285. .mouseenter(mouseenterRow)
  286. .mouseleave(mouseleaveRow)
  287. .click(clickRow);
  288. $(document).mousemove(mousemoveDocument);
  289. };
  290. })(jQuery);