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