jquery.jdMenu.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. /*
  2. * jdMenu 1.3.beta2 (2007-03-06)
  3. *
  4. * Copyright (c) 2006,2007 Jonathan Sharp (http://jdsharp.us)
  5. * Dual licensed under the MIT (MIT-LICENSE.txt)
  6. * and GPL (GPL-LICENSE.txt) licenses.
  7. *
  8. * http://jdsharp.us/
  9. *
  10. * Built upon jQuery 1.1.1 (http://jquery.com)
  11. * This also requires the jQuery dimensions plugin
  12. */
  13. (function($){
  14. // This will store an element list of all our menu objects
  15. var jdMenu = [];
  16. // Public methods
  17. $.fn.jdMenu = function(inSettings) {
  18. var settings = $.extend({}, arguments.callee.defaults, inSettings);
  19. return this.each(function() {
  20. jdMenu.push(this);
  21. $(this).addClass('jd_menu_flag_root');
  22. this.$settings = $.extend({}, settings, {isVerticalMenu: $(this).is('.jd_menu_vertical')});
  23. addEvents(this);
  24. });
  25. };
  26. $.fn.jdMenuShow = function() {
  27. return this.each(function() {
  28. showMenuLI.apply(this);
  29. });
  30. };
  31. $.fn.jdMenuHide = function() {
  32. return this.each(function() {
  33. hideMenuUL.apply(this);
  34. });
  35. };
  36. // Private methods and logic
  37. $(window)
  38. // Bind a click event to hide all visible menus when the document is clicked
  39. .bind('click', function(){
  40. $(jdMenu).find('ul:visible').jdMenuHide();
  41. })
  42. // Cleanup after ourself by nulling the $settings object
  43. .bind('unload', function() {
  44. $(jdMenu).each(function() {
  45. this.$settings = null;
  46. });
  47. });
  48. // These are our default settings for this plugin
  49. $.fn.jdMenu.defaults = {
  50. activateDelay: 750,
  51. showDelay: 150,
  52. hideDelay: 550,
  53. onShow: null,
  54. onHideCheck: null,
  55. onHide: null,
  56. onAnimate: null,
  57. onClick: null,
  58. offsetX: 4,
  59. offsetY: 2,
  60. iframe: $.browser.msie
  61. };
  62. // Our special parentsUntil method to get all parents up to and including the matched element
  63. $.fn.parentsUntil = function(match) {
  64. var a = [];
  65. $(this[0]).parents().each(function() {
  66. a.push(this);
  67. return !$(this).is(match);
  68. });
  69. return this.pushStack(a, arguments);
  70. };
  71. // Returns our settings object for this menu
  72. function getSettings(el) {
  73. return $(el).parents('ul.jd_menu_flag_root')[0].$settings;
  74. }
  75. // Unbind any events and then rebind them
  76. function addEvents(ul) {
  77. removeEvents(ul);
  78. $('> li', ul)
  79. .hover(hoverOverLI, hoverOutLI)
  80. .bind('click', itemClick)
  81. .find('> a.accessible')
  82. .bind('click', accessibleClick);
  83. };
  84. // Remove all events for this menu
  85. function removeEvents(ul) {
  86. $('> li', ul)
  87. .unbind('mouseover').unbind('mouseout')
  88. .unbind('click')
  89. .find('> a.accessible')
  90. .unbind('click');
  91. };
  92. function hoverOverLI() {
  93. var cls = 'jd_menu_hover' + ($(this).parent().is('.jd_menu_flag_root') ? '_menubar' : '');
  94. $(this).addClass(cls).find('> a').addClass(cls);
  95. if (this.$timer) {
  96. clearTimeout(this.$timer);
  97. }
  98. // Do we have a sub menu?
  99. if ($('> ul', this).size() > 0) {
  100. var settings = getSettings(this);
  101. // Which delay to use, the longer activate one or the shorter show delay if a menu is already visible
  102. var delay = ($(this).parents('ul.jd_menu_flag_root').find('ul:visible').size() == 0)
  103. ? settings.activateDelay : settings.showDelay;
  104. var t = this;
  105. this.$timer = setTimeout(function() {
  106. showMenuLI.apply(t);
  107. }, delay);
  108. }
  109. };
  110. function hoverOutLI() {
  111. // Remove both classes so we do not have to test which one we are
  112. $(this) .removeClass('jd_menu_hover').removeClass('jd_menu_hover_menubar')
  113. .find('> a')
  114. .removeClass('jd_menu_hover').removeClass('jd_menu_hover_menubar');
  115. if (this.$timer) {
  116. clearTimeout(this.$timer);
  117. }
  118. // TODO: Possible bug with our test for visibility in that parent menus are hidden child menus are not
  119. // If we have a visible menu, hide it
  120. if ($(this).is(':visible') && $('> ul', this).size() > 0) {
  121. var settings = getSettings(this);
  122. var ul = $('> ul', this)[0];
  123. this.$timer = setTimeout(function() {
  124. hideMenuUL.apply(ul);
  125. }, settings.hideDelay);
  126. }
  127. };
  128. // "this" is a reference to the LI element that contains
  129. // the UL that will be shown
  130. function showMenuLI() {
  131. var ul = $('> ul', this).get(0);
  132. // We are already visible, just return
  133. if ($(ul).is(':visible')) {
  134. return false;
  135. }
  136. // Clear our timer if it exists
  137. if (this.$timer) {
  138. clearTimeout(this.$timer);
  139. }
  140. // Get our settings object
  141. var settings = getSettings(this);
  142. // Call our callback
  143. if (settings.onShow != null && settings.onShow.apply(this) == false) {
  144. return false;
  145. }
  146. // Add hover classes, needed for accessible functionality
  147. var isRoot = $(this).parent().is('.jd_menu_flag_root');
  148. var c = 'jd_menu_active' + (isRoot ? '_menubar' : '');
  149. $(this).addClass(c).find('> a').addClass(c);
  150. if (!isRoot) {
  151. // Add the active class to the parent list item which maybe our menubar
  152. var c = 'jd_menu_active' + ($(this).parent().parent().parent().is('.jd_menu_flag_root') ? '_menubar' : '');
  153. $(this).parent().parent().addClass(c).find('> a').addClass(c);
  154. }
  155. // Hide any existing menues at the same level
  156. $(this).parent().find('> li > ul:visible').not(ul).each(function() {
  157. hideMenuUL.apply(this);
  158. });
  159. addEvents(ul);
  160. // Our range object is used in calculating menu positions
  161. var Range = function(x1, x2, y1, y2) {
  162. this.x1 = x1;
  163. this.x2 = x2;
  164. this.y1 = y1;
  165. this.y2 = y2;
  166. }
  167. Range.prototype.contains = function(range) {
  168. return (this.x1 <= range.x1 && range.x2 <= this.x2)
  169. &&
  170. (this.y1 <= range.y1 && range.y2 <= this.y2);
  171. }
  172. Range.prototype.transform = function(x, y) {
  173. return new Range(this.x1 + x, this.x2 + x, this.y1 + y, this.y2 + y);
  174. }
  175. Range.prototype.nudgeX = function(range) {
  176. if (this.x1 < range.x1) {
  177. return new Range(range.x1, range.x1 + (this.x2 - this.x1), this.y1, this.y2);
  178. } else if (this.x2 > range.x2) {
  179. return new Range(range.x2 - (this.x2 - this.x1), range.x2, this.y1, this.y2);
  180. }
  181. return this;
  182. }
  183. Range.prototype.nudgeY = function(range) {
  184. if (this.y1 < range.y1) {
  185. return new Range(this.x1, this.x2, range.y1, range.y1 + (this.y2 - this.y1));
  186. } else if (this.y2 > range.y2) {
  187. return new Range(this.x1, this.x2, range.y2 - (this.y2 - this.y1), range.y2);
  188. }
  189. return this;
  190. }
  191. // window width & scroll offset
  192. var sx = $(window).scrollLeft()
  193. var sy = $(window).scrollTop();
  194. var ww = $(window).innerWidth();
  195. var wh = $(window).innerHeight();
  196. var viewport = new Range( sx, sx + ww,
  197. sy, sy + wh);
  198. // "Show" our menu so we can calculate its width, set left and top so that it does not accidentally
  199. // go offscreen and trigger browser scroll bars
  200. $(ul).css({visibility: 'hidden', left: 0, top: 0}).show();
  201. var menuWidth = $(ul).outerWidth();
  202. var menuHeight = $(ul).outerHeight();
  203. // Get the LI parent UL outerwidth in case borders are applied to it
  204. var tp = $(this).parent();
  205. var thisWidth = tp.outerWidth();
  206. var thisBorderWidth = parseInt(tp.css('borderLeftWidth')) + parseInt(tp.css('borderRightWidth'));
  207. //var thisBorderTop = parseInt(tp.css('borderTopWidth'));
  208. var thisHeight = $(this).outerHeight();
  209. var thisOffset = $(this).offset({border: false});
  210. $(ul).hide().css({visibility: ''});
  211. // We define a list of valid positions for our menu and then test against them to find one that works best
  212. var position = [];
  213. // Bottom Horizontal
  214. // Menu is directly below and left edges aligned to parent item
  215. position[0] = new Range(thisOffset.left, thisOffset.left + menuWidth,
  216. thisOffset.top + thisHeight, thisOffset.top + thisHeight + menuHeight);
  217. // Menu is directly below and right edges aligned to parent item
  218. position[1] = new Range((thisOffset.left + thisWidth) - menuWidth, thisOffset.left + thisWidth,
  219. position[0].y1, position[0].y2);
  220. // Menu is "nudged" horizontally below parent item
  221. position[2] = position[0].nudgeX(viewport);
  222. // Right vertical
  223. // Menu is directly right and top edge aligned to parent item
  224. position[3] = new Range(thisOffset.left + thisWidth - thisBorderWidth, thisOffset.left + thisWidth - thisBorderWidth + menuWidth,
  225. thisOffset.top, thisOffset.top + menuHeight);
  226. // Menu is directly right and bottom edges aligned with parent item
  227. position[4] = new Range(position[3].x1, position[3].x2,
  228. position[0].y1 - menuHeight, position[0].y1);
  229. // Menu is "nudged" vertically to right of parent item
  230. position[5] = position[3].nudgeY(viewport);
  231. // Top Horizontal
  232. // Menu is directly top and left edges aligned to parent item
  233. position[6] = new Range(thisOffset.left, thisOffset.left + menuWidth,
  234. thisOffset.top - menuHeight, thisOffset.top);
  235. // Menu is directly top and right edges aligned to parent item
  236. position[7] = new Range((thisOffset.left + thisWidth) - menuWidth, thisOffset.left + thisWidth,
  237. position[6].y1, position[6].y2);
  238. // Menu is "nudged" horizontally to the top of parent item
  239. position[8] = position[6].nudgeX(viewport);
  240. // Left vertical
  241. // Menu is directly left and top edges aligned to parent item
  242. position[9] = new Range(thisOffset.left - menuWidth, thisOffset.left,
  243. position[3].y1, position[3].y2);
  244. // Menu is directly left and bottom edges aligned to parent item
  245. position[10]= new Range(position[9].x1, position[9].x2,
  246. position[4].y1 + thisHeight - menuHeight, position[4].y1 + thisHeight);
  247. // Menu is "nudged" vertically to left of parent item
  248. position[11]= position[10].nudgeY(viewport);
  249. // This defines the order in which we test our positions
  250. var order = [];
  251. if ($(this).parent().is('.jd_menu_flag_root') && !settings.isVerticalMenu) {
  252. order = [0, 1, 2, 6, 7, 8, 5, 11];
  253. } else {
  254. order = [3, 4, 5, 9, 10, 11, 0, 1, 2, 6, 7, 8];
  255. }
  256. // Set our default position (first position) if no others can be found
  257. var pos = order[0];
  258. for (var i = 0, j = order.length; i < j; i++) {
  259. // If this position for our menu is within the viewport of the browser, use this position
  260. if (viewport.contains(position[order[i]])) {
  261. pos = order[i];
  262. break;
  263. }
  264. }
  265. var menuPosition = position[pos];
  266. // Find if we are absolutely positioned or have an absolutely positioned parent
  267. $(this).add($(this).parents()).each(function() {
  268. if ($(this).css('position') == 'absolute') {
  269. var abs = $(this).offset();
  270. // Transform our coordinates to be relative to the absolute parent
  271. menuPosition = menuPosition.transform(-abs.left, -abs.top);
  272. return false;
  273. }
  274. });
  275. switch (pos) {
  276. case 3:
  277. menuPosition.y1 += settings.offsetY;
  278. case 4:
  279. menuPosition.x1 -= settings.offsetX;
  280. break;
  281. case 9:
  282. menuPosition.y1 += settings.offsetY;
  283. case 10:
  284. menuPosition.x1 += settings.offsetX;
  285. break;
  286. }
  287. if (settings.iframe) {
  288. $(ul).bgiframe();
  289. }
  290. if (settings.onAnimate) {
  291. $(ul).css({left: menuPosition.x1, top: menuPosition.y1});
  292. // The onAnimate method is expected to "show" the element it is passed
  293. settings.onAnimate.apply(ul, [true]);
  294. } else {
  295. $(ul).css({left: menuPosition.x1, top: menuPosition.y1}).show();
  296. }
  297. return true;
  298. }
  299. // "this" is a reference to a UL menu to be hidden
  300. function hideMenuUL(recurse) {
  301. if (!$(this).is(':visible')) {
  302. return false;
  303. }
  304. var settings = getSettings(this);
  305. // Test if this menu should get hidden
  306. if (settings.onHideCheck != null && settings.onHideCheck.apply(this) == false) {
  307. return false;
  308. }
  309. // Hide all of our child menus first
  310. $('> li ul:visible', this).each(function() {
  311. hideMenuUL.apply(this, [false]);
  312. });
  313. // If we are the root, do not hide ourself
  314. if ($(this).is('.jd_menu_flag_root')) {
  315. alert('We are root');
  316. return false;
  317. }
  318. var elms = $('> li', this).add($(this).parent());
  319. elms.removeClass('jd_menu_hover').removeClass('jd_menu_hover_menubar')
  320. .removeClass('jd_menu_active').removeClass('jd_menu_active_menubar')
  321. .find('> a')
  322. .removeClass('jd_menu_hover').removeClass('jd_menu_hover_menubar')
  323. .removeClass('jd_menu_active').removeClass('jd_menu_active_menubar');
  324. removeEvents(this);
  325. $(this).each(function() {
  326. if (settings.onAnimate != null) {
  327. settings.onAnimate.apply(this, [false]);
  328. } else {
  329. $(this).hide();
  330. }
  331. }).find('> .bgiframe').remove();
  332. // Our callback for after our menu is hidden
  333. if (settings.onHide != null) {
  334. settings.onHide.apply(this);
  335. }
  336. // Recursively hide our parent menus
  337. if (recurse == true) {
  338. $(this).parentsUntil('ul.jd_menu_flag_root')
  339. .removeClass('jd_menu_hover').removeClass('jd_menu_hover_menubar')
  340. .not('.jd_menu_flag_root').filter('ul')
  341. .each(function() {
  342. hideMenuUL.apply(this, [false]);
  343. });
  344. }
  345. return true;
  346. }
  347. // Prevent the default (usually following a link)
  348. function accessibleClick(e) {
  349. if ($(this).is('.accessible')) {
  350. // Stop the browser from the default link action allowing the
  351. // click event to propagate to propagate to our LI (itemClick function)
  352. e.preventDefault();
  353. }
  354. }
  355. // Trigger a menu click
  356. function itemClick(e) {
  357. e.stopPropagation();
  358. var settings = getSettings(this);
  359. if (settings.onClick != null && settings.onClick.apply(this) == false) {
  360. return false;
  361. }
  362. if ($('> ul', this).size() > 0) {
  363. showMenuLI.apply(this);
  364. } else {
  365. if (e.target == this) {
  366. var link = $('> a', e.target).not('.accessible');
  367. if (link.size() > 0) {
  368. var a = link.get(0);
  369. if (!a.onclick) {
  370. window.open(a.href, a.target || '_self');
  371. } else {
  372. $(a).click();
  373. }
  374. }
  375. }
  376. hideMenuUL.apply($(this).parent(), [true]);
  377. }
  378. }
  379. })(jQuery);