ace.submenu-hover.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. /**
  2. <b>Submenu hover adjustment</b>. Automatically move up a submenu to fit into screen when some part of it goes beneath window.
  3. Pass a "true" value as an argument and submenu will have native browser scrollbars when necessary.
  4. */
  5. (function($ , undefined) {
  6. if( ace.vars['very_old_ie'] ) return;
  7. //ignore IE7 & below
  8. var hasTouch = ace.vars['touch'];
  9. var nativeScroll = ace.vars['old_ie'] || hasTouch;
  10. var is_element_pos =
  11. 'getComputedStyle' in window ?
  12. //el.offsetHeight is used to force redraw and recalculate 'el.style.position' esp. for webkit!
  13. function(el, pos) { el.offsetHeight; return window.getComputedStyle(el).position == pos }
  14. :
  15. function(el, pos) { el.offsetHeight; return $(el).css('position') == pos }
  16. $(window).on('resize.sidebar.ace_hover', function() {
  17. $('.sidebar[data-sidebar-hover=true]').ace_sidebar_hover('update_vars').ace_sidebar_hover('reset');
  18. })
  19. $(document).on('settings.ace.ace_hover', function(e, event_name, event_val) {
  20. if(event_name == 'sidebar_collapsed') $('.sidebar[data-sidebar-hover=true]').ace_sidebar_hover('reset');
  21. else if(event_name == 'navbar_fixed') $('.sidebar[data-sidebar-hover=true]').ace_sidebar_hover('update_vars');
  22. })
  23. var sidebars = [];
  24. function Sidebar_Hover(sidebar , settings) {
  25. var self = this;
  26. var $sidebar = $(sidebar), nav_list = $sidebar.find('.nav-list').get(0);
  27. $sidebar.attr('data-sidebar-hover', 'true');
  28. sidebars.push($sidebar);
  29. var sidebar_vars = {};
  30. var old_ie = ace.vars['old_ie'];
  31. var hover_delay = settings.sub_hover_delay || ace.helper.intAttr(sidebar, 'data-sub-hover-delay') || 750;
  32. var scroll_style = settings.sub_scroll_style || $sidebar.attr('data-sub-scroll-style') || 'no-track scroll-thin';
  33. var scroll_right = false;
  34. //scroll style class
  35. if(hasTouch) hover_delay = parseInt(Math.max(hover_delay, 2500));//for touch device, delay is at least 2.5sec
  36. var $window = $(window);
  37. //navbar used for adding extra offset from top when adjusting submenu
  38. var $navbar = $('.navbar').eq(0);
  39. var navbar_fixed = $navbar.css('position') == 'fixed';
  40. this.update_vars = function() {
  41. navbar_fixed = $navbar.css('position') == 'fixed';
  42. }
  43. self.dirty = false;
  44. //on window resize or sidebar expand/collapse a previously "pulled up" submenu should be reset back to its default position
  45. //for example if "pulled up" in "responsive-min" mode, in "fullmode" should not remain "pulled up"
  46. this.reset = function() {
  47. if( self.dirty == false ) return;
  48. self.dirty = false;//so don't reset is not called multiple times in a row!
  49. $sidebar.find('.submenu').each(function() {
  50. var $sub = $(this), li = $sub.parent();
  51. $sub.css({'top': '', 'bottom': '', 'max-height': ''});
  52. if($sub.hasClass('ace-scroll')) {
  53. $sub.ace_scroll('disable');
  54. }
  55. else {
  56. $sub.removeClass('sub-scroll');
  57. }
  58. if( is_element_pos(this, 'absolute') ) $sub.addClass('can-scroll');
  59. else $sub.removeClass('can-scroll');
  60. li.removeClass('pull_up').find('.menu-text:first').css('margin-top', '');
  61. })
  62. $sidebar.find('.hover-show').removeClass('hover-show hover-shown hover-flip');
  63. }
  64. this.updateStyle = function(newStyle) {
  65. scroll_style = newStyle;
  66. $sidebar.find('.submenu.ace-scroll').ace_scroll('update', {styleClass: newStyle});
  67. }
  68. this.changeDir = function(dir) {
  69. scroll_right = (dir === 'right');
  70. }
  71. //update submenu scrollbars on submenu hide & show
  72. var lastScrollHeight = -1;
  73. //hide scrollbars if it's going to be not needed anymore!
  74. if(!nativeScroll)
  75. $sidebar.on('hide.ace.submenu.sidebar_hover', '.submenu', function(e) {
  76. if(lastScrollHeight < 1) return;
  77. e.stopPropagation();
  78. var $sub = $(this).closest('.ace-scroll.can-scroll');
  79. if($sub.length == 0 || !is_element_pos($sub[0], 'absolute')) return;
  80. if($sub[0].scrollHeight - this.scrollHeight < lastScrollHeight) {
  81. $sub.ace_scroll('disable');
  82. }
  83. });
  84. //reset scrollbars
  85. if(!nativeScroll)
  86. $sidebar.on('shown.ace.submenu.sidebar_hover hidden.ace.submenu.sidebar_hover', '.submenu', function(e) {
  87. if(lastScrollHeight < 1) return;
  88. var $sub = $(this).closest('.ace-scroll.can-scroll');
  89. if($sub.length == 0 || !is_element_pos($sub[0], 'absolute') ) return;
  90. var sub_h = $sub[0].scrollHeight;
  91. if(lastScrollHeight > 14 && sub_h - lastScrollHeight > 4) {
  92. $sub.ace_scroll('enable').ace_scroll('reset');//don't update track position
  93. }
  94. else {
  95. $sub.ace_scroll('disable');
  96. }
  97. });
  98. ///////////////////////
  99. var currentScroll = -1;
  100. //some mobile browsers don't have mouseenter
  101. var event_1 = !hasTouch ? 'mouseenter.sub_hover' : 'touchstart.sub_hover';// pointerdown.sub_hover';
  102. var event_2 = !hasTouch ? 'mouseleave.sub_hover' : 'touchend.sub_hover touchcancel.sub_hover';// pointerup.sub_hover pointercancel.sub_hover';
  103. $sidebar.on(event_1, '.nav-list li, .sidebar-shortcuts', function (e) {
  104. sidebar_vars = $sidebar.ace_sidebar('vars');
  105. //ignore if collapsible mode (mobile view .navbar-collapse) so it doesn't trigger submenu movements
  106. //or return if horizontal but not mobile_view (style 1&3)
  107. if( sidebar_vars['collapsible'] /**|| sidebar_vars['horizontal']*/ ) return;
  108. var $this = $(this);
  109. var shortcuts = false;
  110. var has_hover = $this.hasClass('hover');
  111. var sub = $this.find('> .submenu').get(0);
  112. if( !(sub || ((this.parentNode == nav_list || has_hover || (shortcuts = $this.hasClass('sidebar-shortcuts'))) /**&& sidebar_vars['minimized']*/)) ) {
  113. if(sub) $(sub).removeClass('can-scroll');
  114. return;//include .compact and .hover state as well?
  115. }
  116. var target_element = sub, is_abs = false;
  117. if( !target_element && this.parentNode == nav_list ) target_element = $this.find('> a > .menu-text').get(0);
  118. if( !target_element && shortcuts ) target_element = $this.find('.sidebar-shortcuts-large').get(0);
  119. if( (!target_element || !(is_abs = is_element_pos(target_element, 'absolute'))) && !has_hover ) {
  120. if(sub) $(sub).removeClass('can-scroll');
  121. return;
  122. }
  123. var sub_hide = getSubHide(this);
  124. //var show_sub = false;
  125. if(sub) {
  126. if(is_abs) {
  127. self.dirty = true;
  128. var newScroll = ace.helper.scrollTop();
  129. //if submenu is becoming visible for first time or document has been scrolled, then adjust menu
  130. if( !sub_hide.is_visible() || (!hasTouch && newScroll != currentScroll) || old_ie ) {
  131. //try to move/adjust submenu if the parent is a li.hover or if submenu is minimized
  132. //if( is_element_pos(sub, 'absolute') ) {//for example in small device .hover > .submenu may not be absolute anymore!
  133. $(sub).addClass('can-scroll');
  134. //show_sub = true;
  135. if(!old_ie && !hasTouch) adjust_submenu.call(this, sub);
  136. else {
  137. //because ie8 needs some time for submenu to be displayed and real value of sub.scrollHeight be kicked in
  138. var that = this;
  139. setTimeout(function() { adjust_submenu.call(that, sub) }, 0)
  140. }
  141. //}
  142. //else $(sub).removeClass('can-scroll');
  143. }
  144. currentScroll = newScroll;
  145. }
  146. else {
  147. $(sub).removeClass('can-scroll');
  148. }
  149. }
  150. //if(show_sub)
  151. sub_hide.show();
  152. }).on(event_2, '.nav-list li, .sidebar-shortcuts', function (e) {
  153. sidebar_vars = $sidebar.ace_sidebar('vars');
  154. if( sidebar_vars['collapsible'] /**|| sidebar_vars['horizontal']*/ ) return;
  155. if( !$(this).hasClass('hover-show') ) return;
  156. getSubHide(this).hideDelay();
  157. });
  158. function subHide(li_sub) {
  159. var self = li_sub, $self = $(self);
  160. var timer = null;
  161. var visible = false;
  162. this.show = function() {
  163. if(timer != null) clearTimeout(timer);
  164. timer = null;
  165. $self.addClass('hover-show hover-shown');
  166. visible = true;
  167. //let's hide .hover-show elements that are not .hover-shown anymore (i.e. marked for hiding in hideDelay)
  168. for(var i = 0; i < sidebars.length ; i++)
  169. {
  170. sidebars[i].find('.hover-show').not('.hover-shown').each(function() {
  171. getSubHide(this).hide();
  172. })
  173. }
  174. }
  175. this.hide = function() {
  176. visible = false;
  177. $self.removeClass('hover-show hover-shown hover-flip');
  178. if(timer != null) clearTimeout(timer);
  179. timer = null;
  180. var sub = $self.find('> .submenu').get(0);
  181. if(sub) getSubScroll(sub, 'hide');
  182. }
  183. this.hideDelay = function(callback) {
  184. if(timer != null) clearTimeout(timer);
  185. $self.removeClass('hover-shown');//somehow marked for hiding
  186. timer = setTimeout(function() {
  187. visible = false;
  188. $self.removeClass('hover-show hover-flip');
  189. timer = null;
  190. var sub = $self.find('> .submenu').get(0);
  191. if(sub) getSubScroll(sub, 'hide');
  192. if(typeof callback === 'function') callback.call(this);
  193. }, hover_delay);
  194. }
  195. this.is_visible = function() {
  196. return visible;
  197. }
  198. }
  199. function getSubHide(el) {
  200. var sub_hide = $(el).data('subHide');
  201. if(!sub_hide) $(el).data('subHide', (sub_hide = new subHide(el)));
  202. return sub_hide;
  203. }
  204. function getSubScroll(el, func) {
  205. var sub_scroll = $(el).data('ace_scroll');
  206. if(!sub_scroll) return false;
  207. if(typeof func === 'string') {
  208. sub_scroll[func]();
  209. return true;
  210. }
  211. return sub_scroll;
  212. }
  213. function adjust_submenu(sub) {
  214. var $li = $(this);
  215. var $sub = $(sub);
  216. sub.style.top = '';
  217. sub.style.bottom = '';
  218. var menu_text = null
  219. if( sidebar_vars['minimized'] && (menu_text = $li.find('.menu-text').get(0)) ) {
  220. //2nd level items don't have .menu-text
  221. menu_text.style.marginTop = '';
  222. }
  223. var scroll = ace.helper.scrollTop();
  224. var navbar_height = 0;
  225. var $scroll = scroll;
  226. if( navbar_fixed ) {
  227. navbar_height = sidebar.offsetTop;//$navbar.height();
  228. $scroll += navbar_height + 1;
  229. //let's avoid our submenu from going below navbar
  230. //because of chrome z-index stacking issue and firefox's normal .submenu over fixed .navbar flicker issue
  231. }
  232. var off = $li.offset();
  233. off.top = parseInt(off.top);
  234. var extra = 0, parent_height;
  235. sub.style.maxHeight = '';//otherwise scrollHeight won't be consistent in consecutive calls!?
  236. var sub_h = sub.scrollHeight;
  237. var parent_height = $li.height();
  238. if(menu_text) {
  239. extra = parent_height;
  240. off.top += extra;
  241. }
  242. var sub_bottom = parseInt(off.top + sub_h)
  243. var move_up = 0;
  244. var winh = $window.height();
  245. //if the bottom of menu is going to go below visible window
  246. var top_space = parseInt(off.top - $scroll - extra);//available space on top
  247. var win_space = winh;//available window space
  248. var horizontal = sidebar_vars['horizontal'], horizontal_sub = false;
  249. if(horizontal && this.parentNode == nav_list) {
  250. move_up = 0;//don't move up first level submenu in horizontal mode
  251. off.top += $li.height();
  252. horizontal_sub = true;//first level submenu
  253. }
  254. if(!horizontal_sub && (move_up = (sub_bottom - (winh + scroll))) >= 0 ) {
  255. //don't move up more than available space
  256. move_up = move_up < top_space ? move_up : top_space;
  257. //move it up a bit more if there's empty space
  258. if(move_up == 0) move_up = 20;
  259. if(top_space - move_up > 10) {
  260. move_up += parseInt(Math.min(25, top_space - move_up));
  261. }
  262. //move it down if submenu's bottom is going above parent LI
  263. if(off.top + (parent_height - extra) > (sub_bottom - move_up)) {
  264. move_up -= (off.top + (parent_height - extra) - (sub_bottom - move_up));
  265. }
  266. if(move_up > 0) {
  267. sub.style.top = -(move_up) + 'px';
  268. if( menu_text ) {
  269. menu_text.style.marginTop = -(move_up) + 'px';
  270. }
  271. }
  272. }
  273. if(move_up < 0) move_up = 0;//when it goes below
  274. var pull_up = move_up > 0 && move_up > parent_height - 20;
  275. if(pull_up) {
  276. $li.addClass('pull_up');
  277. }
  278. else $li.removeClass('pull_up');
  279. //flip submenu if out of window width
  280. if(horizontal) {
  281. if($li.parent().parent().hasClass('hover-flip')) $li.addClass('hover-flip');//if a parent is already flipped, flip it then!
  282. else {
  283. var sub_off = $sub.offset();
  284. var sub_w = $sub.width();
  285. var win_w = $window.width();
  286. if(sub_off.left + sub_w > win_w) {
  287. $li.addClass('hover-flip');
  288. }
  289. }
  290. }
  291. //don't add scrollbars if it contains .hover menus
  292. var has_hover = $li.hasClass('hover') && !sidebar_vars['mobile_view'];
  293. if(has_hover && $sub.find('> li > .submenu').length > 0) return;
  294. //if( ) {
  295. var scroll_height = (win_space - (off.top - scroll)) + (move_up);
  296. //if after scroll, the submenu is above parent LI, then move it down
  297. var tmp = move_up - scroll_height;
  298. if(tmp > 0 && tmp < parent_height) scroll_height += parseInt(Math.max(parent_height, parent_height - tmp));
  299. scroll_height -= 5;
  300. if(scroll_height < 90) {
  301. return;
  302. }
  303. var ace_scroll = false;
  304. if(!nativeScroll) {
  305. ace_scroll = getSubScroll(sub);
  306. if(ace_scroll == false) {
  307. $sub.ace_scroll({
  308. //hideOnIdle: true,
  309. observeContent: true,
  310. detached: true,
  311. updatePos: false,
  312. reset: true,
  313. mouseWheelLock: true,
  314. styleClass: scroll_style
  315. });
  316. ace_scroll = getSubScroll(sub);
  317. var track = ace_scroll.get_track();
  318. if(track) {
  319. //detach it from body and insert it after submenu for better and cosistent positioning
  320. $sub.after(track);
  321. }
  322. }
  323. ace_scroll.update({size: scroll_height});
  324. }
  325. else {
  326. $sub
  327. .addClass('sub-scroll')
  328. .css('max-height', (scroll_height)+'px')
  329. }
  330. lastScrollHeight = scroll_height;
  331. if(!nativeScroll && ace_scroll) {
  332. if(scroll_height > 14 && sub_h - scroll_height > 4) {
  333. ace_scroll.enable()
  334. ace_scroll.reset();
  335. }
  336. else {
  337. ace_scroll.disable();
  338. }
  339. //////////////////////////////////
  340. var track = ace_scroll.get_track();
  341. if(track) {
  342. track.style.top = -(move_up - extra - 1) + 'px';
  343. var off = $sub.position();
  344. var left = off.left
  345. if( !scroll_right ) {
  346. left += ($sub.outerWidth() - ace_scroll.track_size());
  347. }
  348. else {
  349. left += 2;
  350. }
  351. track.style.left = parseInt(left) + 'px';
  352. if(horizontal_sub) {//first level submenu
  353. track.style.left = parseInt(left - 2) + 'px';
  354. track.style.top = parseInt(off.top) + (menu_text ? extra - 2 : 0) + 'px';
  355. }
  356. }
  357. }
  358. //}
  359. //again force redraw for safari!
  360. if( ace.vars['safari'] ) {
  361. ace.helper.redraw(sub)
  362. }
  363. }
  364. }
  365. /////////////////////////////////////////////
  366. $.fn.ace_sidebar_hover = function (option, value) {
  367. var method_call;
  368. var $set = this.each(function () {
  369. var $this = $(this);
  370. var data = $this.data('ace_sidebar_hover');
  371. var options = typeof option === 'object' && option;
  372. if (!data) $this.data('ace_sidebar_hover', (data = new Sidebar_Hover(this, options)));
  373. if (typeof option === 'string' && typeof data[option] === 'function') {
  374. method_call = data[option](value);
  375. }
  376. });
  377. return (method_call === undefined) ? $set : method_call;
  378. };
  379. })(window.jQuery);