Administrator
2023-04-21 195945efc5db921a4c9eb8cf9421c172273293f5
提交 | 用户 | 时间
58d006 1 /**
A 2  <b>Scrollbars for sidebar</b>. This approach can <span class="text-danger">only</span> be used on <u>fixed</u> sidebar.
3  It doesn't use <u>"overflow:hidden"</u> CSS property and therefore can be used with <u>.hover</u> submenus and minimized sidebar.
4  Except when in mobile view and menu toggle button is not in the navbar.
5 */
6
7 (function($ , undefined) {
8     //if( !$.fn.ace_scroll ) return;
9
10     var old_safari = ace.vars['safari'] && navigator.userAgent.match(/version\/[1-5]/i)
11     //NOTE
12     //Safari on windows has not been updated for a long time.
13     //And it has a problem when sidebar is fixed & scrollable and there is a CSS3 animation inside page content.
14     //Very probably windows users of safari have migrated to another browser by now!
15
16     var is_element_pos =
17     'getComputedStyle' in window ?
18     //el.offsetHeight is used to force redraw and recalculate 'el.style.position' esp. for webkit!
19     function(el, pos) { el.offsetHeight; return window.getComputedStyle(el).position == pos }
20     :
21     function(el, pos) { el.offsetHeight; return $(el).css('position') == pos }
22     
23         
24     function Sidebar_Scroll(sidebar , settings) {
25         var self = this;
26
27         var $window = $(window);
28         var $sidebar = $(sidebar),
29             $nav = $sidebar.find('.nav-list'),
30             $toggle = $sidebar.find('.sidebar-toggle').eq(0),
31             $shortcuts = $sidebar.find('.sidebar-shortcuts').eq(0);
32             
33         var nav = $nav.get(0);
34         if(!nav) return;
35         
36         
37         var attrib_values = ace.helper.getAttrSettings(sidebar, $.fn.ace_sidebar_scroll.defaults);
38         this.settings = $.extend({}, $.fn.ace_sidebar_scroll.defaults, settings, attrib_values);
39         var scroll_to_active = self.settings.scroll_to_active;
40     
41     
42         var ace_sidebar = $sidebar.ace_sidebar('ref');
43         $sidebar.attr('data-sidebar-scroll', 'true');
44             
45         
46         var scroll_div = null,
47             scroll_content = null,
48             scroll_content_div = null,
49             bar = null,
50             track = null,
51             ace_scroll = null;
52
53
54         this.is_scrolling = false;
55         var _initiated = false;
56         this.sidebar_fixed = is_element_pos(sidebar, 'fixed');
57         
58         var $avail_height, $content_height;
59
60         
61         var available_height = function() {
62             //available window space
63             var offset = $nav.parent().offset();//because `$nav.offset()` considers the "scrolled top" amount as well
64             if(self.sidebar_fixed) offset.top -= ace.helper.scrollTop();
65
66             return $window.innerHeight() - offset.top - ( self.settings.include_toggle ? 0 : $toggle.outerHeight() ) + 1;
67         }
68         var content_height = function() {
69             return nav.clientHeight;//we don't use nav.scrollHeight here, because hover submenus are considered in calculating scrollHeight despite position=absolute!
70         }
71
72         
73         
74         var initiate = function(on_page_load) {
75             if( _initiated ) return;
76             if( !self.sidebar_fixed ) return;//eligible??
77             //return if we want scrollbars only on "fixed" sidebar and sidebar is not "fixed" yet!
78
79             //initiate once
80             $nav.wrap('<div class="nav-wrap-up pos-rel" />');
81             $nav.after('<div><div></div></div>');
82
83             $nav.wrap('<div class="nav-wrap" />');
84             if(!self.settings.include_toggle) $toggle.css({'z-index': 1});
85             if(!self.settings.include_shortcuts) $shortcuts.css({'z-index': 99});
86
87             scroll_div = $nav.parent().next()
88             .ace_scroll({
89                 size: available_height(),
90                 //reset: true,
91                 mouseWheelLock: true,
92                 hoverReset: false,
93                 dragEvent: true,
94                 styleClass: self.settings.scroll_style,
95                 touchDrag: false//disable touch drag event on scrollbars, we'll add a custom one later
96             })
97             .closest('.ace-scroll').addClass('nav-scroll');
98             
99             ace_scroll = scroll_div.data('ace_scroll');
100
101             scroll_content = scroll_div.find('.scroll-content').eq(0);
102             scroll_content_div = scroll_content.find(' > div').eq(0);
103             
104             track = $(ace_scroll.get_track());
105             bar = track.find('.scroll-bar').eq(0);
106
107             if(self.settings.include_shortcuts && $shortcuts.length != 0) {
108                 $nav.parent().prepend($shortcuts).wrapInner('<div />');
109                 $nav = $nav.parent();
110             }
111             if(self.settings.include_toggle && $toggle.length != 0) {
112                 $nav.append($toggle);
113                 $nav.closest('.nav-wrap').addClass('nav-wrap-t');//it just helps to remove toggle button's top border and restore li:last-child's bottom border
114             }
115
116             $nav.css({position: 'relative'});
117             if( self.settings.scroll_outside == true ) scroll_div.addClass('scrollout');
118             
119             nav = $nav.get(0);
120             nav.style.top = 0;
121             scroll_content.on('scroll.nav', function() {
122                 nav.style.top = (-1 * this.scrollTop) + 'px';
123             });
124             
125             //mousewheel library available?
126             $nav.on(!!$.event.special.mousewheel ? 'mousewheel.ace_scroll' : 'mousewheel.ace_scroll DOMMouseScroll.ace_scroll', function(event){
127                 if( !self.is_scrolling || !ace_scroll.is_active() ) {
128                     return !self.settings.lock_anyway;
129                 }
130                 //transfer $nav's mousewheel event to scrollbars
131                 return scroll_div.trigger(event);
132             });
133             
134             $nav.on('mouseenter.ace_scroll', function() {
135                 track.addClass('scroll-hover');
136             }).on('mouseleave.ace_scroll', function() {
137                 track.removeClass('scroll-hover');
138             });
139
140
141             /**
142             $(document.body).on('touchmove.nav', function(event) {
143                 if( self.is_scrolling && $.contains(sidebar, event.target) ) {
144                     event.preventDefault();
145                     return false;
146                 }
147             })
148             */
149
150             //you can also use swipe event in a similar way //swipe.nav
151             var content = scroll_content.get(0);
152             $nav.on('ace_drag.nav', function(event) {
153                 if( !self.is_scrolling || !ace_scroll.is_active() ) {
154                     event.retval.cancel = true;
155                     return;
156                 }
157                 
158                 //if submenu hover is being scrolled let's cancel sidebar scroll!
159                 if( $(event.target).closest('.can-scroll').length != 0 ) {
160                     event.retval.cancel = true;
161                     return;
162                 }
163
164                 if(event.direction == 'up' || event.direction == 'down') {
165                     
166                     ace_scroll.move_bar(true);
167                     
168                     var distance = event.dy;
169                     
170                     distance = parseInt(Math.min($avail_height, distance))
171                     if(Math.abs(distance) > 2) distance = distance * 2;
172                     
173                     if(distance != 0) {
174                         content.scrollTop = content.scrollTop + distance;
175                         nav.style.top = (-1 * content.scrollTop) + 'px';
176                     }
177                 }
178             });
179             
180
181             //for drag only
182             if(self.settings.smooth_scroll) {
183                 $nav
184                 .on('touchstart.nav MSPointerDown.nav pointerdown.nav', function(event) {
185                     $nav.css('transition-property', 'none');
186                     bar.css('transition-property', 'none');
187                 })
188                 .on('touchend.nav touchcancel.nav MSPointerUp.nav MSPointerCancel.nav pointerup.nav pointercancel.nav', function(event) {
189                     $nav.css('transition-property', 'top');
190                     bar.css('transition-property', 'top');
191                 });
192             }
193             
194             
195
196             if(old_safari && !self.settings.include_toggle) {
197                 var toggle = $toggle.get(0);
198                 if(toggle) scroll_content.on('scroll.safari', function() {
199                     ace.helper.redraw(toggle);
200                 });
201             }
202
203             _initiated = true;
204
205             //if the active item is not visible, scroll down so that it becomes visible
206             //only the first time, on page load
207             if(on_page_load == true) {
208                 self.reset();//try resetting at first
209
210                 if( scroll_to_active ) {
211                     self.scroll_to_active();
212                 }
213                 scroll_to_active = false;
214             }
215             
216             
217             
218             if( typeof self.settings.smooth_scroll === 'number' && self.settings.smooth_scroll > 0) {
219                 $nav.css({'transition-property': 'top', 'transition-duration': (self.settings.smooth_scroll / 1000).toFixed(2)+'s'})
220                 bar.css({'transition-property': 'top', 'transition-duration': (self.settings.smooth_scroll / 1500).toFixed(2)+'s'})
221                 
222                 scroll_div
223                 .on('drag.start', function(e) {
224                     e.stopPropagation();
225                     $nav.css('transition-property', 'none')
226                 })
227                 .on('drag.end', function(e) {
228                     e.stopPropagation();
229                     $nav.css('transition-property', 'top')
230                 });
231             }
232             
233             if(ace.vars['android']) {
234                 //force hide address bar, because its changes don't trigger window resize and become kinda ugly
235                 var val = ace.helper.scrollTop();
236                 if(val < 2) {
237                     window.scrollTo( val, 0 );
238                     setTimeout( function() {
239                         self.reset();
240                     }, 20 );
241                 }
242                 
243                 var last_height = ace.helper.winHeight() , new_height;
244                 $(window).on('scroll.ace_scroll', function() {
245                     if(self.is_scrolling && ace_scroll.is_active()) {
246                         new_height = ace.helper.winHeight();
247                         if(new_height != last_height) {
248                             last_height = new_height;
249                             self.reset();
250                         }
251                     }
252                 });
253             }
254         }
255         
256         
257         
258         
259         this.scroll_to_active = function() {
260             if( !ace_scroll || !ace_scroll.is_active() ) return;
261             try {
262                 //sometimes there's no active item or not 'offsetTop' property
263                 var $active;
264                 
265                 var vars = ace_sidebar['vars']()
266
267                 var nav_list = $sidebar.find('.nav-list')
268                 if(vars['minimized'] && !vars['collapsible']) {
269                     $active = nav_list.find('> .active')
270                 }
271                 else {
272                     $active = $nav.find('> .active.hover')
273                     if($active.length == 0)    $active = $nav.find('.active:not(.open)')
274                 }
275
276             
277                 var top = $active.outerHeight();
278                 nav_list = nav_list.get(0);
279                 var active = $active.get(0);
280                 while(active != nav_list) {
281                     top += active.offsetTop;
282                     active = active.parentNode;
283                 }
284
285                 var scroll_amount = top - scroll_div.height();
286                 if(scroll_amount > 0) {
287                     nav.style.top = -scroll_amount + 'px';
288                     scroll_content.scrollTop(scroll_amount);
289                 }
290             }catch(e){}
291         }
292         
293         
294         
295         this.reset = function(recalc) {
296             if(recalc === true) {
297                 this.sidebar_fixed = is_element_pos(sidebar, 'fixed');
298             }
299             
300             if( !this.sidebar_fixed ) {
301                 this.disable();
302                 return;//eligible??
303             }
304
305             //return if we want scrollbars only on "fixed" sidebar and sidebar is not "fixed" yet!
306
307             if( !_initiated ) initiate();
308             //initiate scrollbars if not yet
309             
310             var vars = ace_sidebar['vars']();
311             
312
313             //enable if:
314             //menu is not collapsible mode (responsive navbar-collapse mode which has default browser scrollbar)
315             //menu is not horizontal or horizontal but mobile view (which is not navbar-collapse)
316             //and available height is less than nav's height
317             
318
319             var enable_scroll = !vars['collapsible'] && !vars['horizontal']
320                                 && ($avail_height = available_height()) < ($content_height = nav.clientHeight);
321                                 //we don't use nav.scrollHeight here, because hover submenus are considered in calculating scrollHeight despite position=absolute!
322
323                                 
324             this.is_scrolling = true;
325             if( enable_scroll ) {
326                 scroll_content_div.css({height: $content_height, width: 8});
327                 scroll_div.prev().css({'max-height' : $avail_height})
328                 ace_scroll.update({size: $avail_height})
329                 ace_scroll.enable();
330                 ace_scroll.reset();
331             }
332             if( !enable_scroll || !ace_scroll.is_active() ) {
333                 if(this.is_scrolling) this.disable();
334             }
335             else {
336                 $sidebar.addClass('sidebar-scroll');
337             }
338             
339             //return this.is_scrolling;
340         }
341         
342         
343         
344         this.disable = function() {
345             this.is_scrolling = false;
346             if(scroll_div) {
347                 scroll_div.css({'height' : '', 'max-height' : ''});
348                 scroll_content_div.css({height: '', width: ''});//otherwise it will have height and takes up some space even when invisible
349                 scroll_div.prev().css({'max-height' : ''})
350                 ace_scroll.disable();
351             }
352
353             if(parseInt(nav.style.top) < 0 && self.settings.smooth_scroll && $.support.transition.end) {
354                 $nav.one($.support.transition.end, function() {
355                     $sidebar.removeClass('sidebar-scroll');
356                     $nav.off('.trans');
357                 });
358             } else {
359                 $sidebar.removeClass('sidebar-scroll');
360             }
361
362             nav.style.top = 0;
363         }
364         
365         this.prehide = function(height_change) {
366             if(!this.is_scrolling || ace_sidebar.get('minimized')) return;//when minimized submenu's toggle should have no effect
367             
368             if(content_height() + height_change < available_height()) {
369                 this.disable();
370             }
371             else if(height_change < 0) {
372                 //if content height is decreasing
373                 //let's move nav down while a submenu is being hidden
374                 var scroll_top = scroll_content.scrollTop() + height_change
375                 if(scroll_top < 0) return;
376
377                 nav.style.top = (-1 * scroll_top) + 'px';
378             }
379         }
380         
381         
382         this._reset = function(recalc) {
383             if(recalc === true) {
384                 this.sidebar_fixed = is_element_pos(sidebar, 'fixed');
385             }
386             
387             if(ace.vars['webkit']) 
388                 setTimeout(function() { self.reset() } , 0);
389             else this.reset();
390         }
391         
392         
393         this.set_hover = function() {
394             if(track) track.addClass('scroll-hover');
395         }
396         
397         this.get = function(name) {
398             if(this.hasOwnProperty(name)) return this[name];
399         }
400         this.set = function(name, value) {
401             if(this.hasOwnProperty(name)) this[name] = value;
402         }
403         this.ref = function() {
404             //return a reference to self
405             return this;
406         }
407         
408         this.updateStyle = function(styleClass) {
409             if(ace_scroll == null) return;
410             ace_scroll.update({styleClass: styleClass});
411         }
412
413         
414         //change scrollbar size after a submenu is hidden/shown
415         //but don't change if sidebar is minimized
416         $sidebar.on('hidden.ace.submenu.sidebar_scroll shown.ace.submenu.sidebar_scroll', '.submenu', function(e) {
417             e.stopPropagation();
418
419             if( !ace_sidebar.get('minimized') ) {
420                 //webkit has a little bit of a glitch!!!
421                 self._reset();
422                 if( e.type == 'shown' ) self.set_hover();
423             }
424         });
425
426         
427         initiate(true);//true = on_page_load
428     }
429     
430
431     
432     //reset on document and window changes
433     $(document).on('settings.ace.sidebar_scroll', function(ev, event_name, event_val){
434         $('.sidebar[data-sidebar-scroll=true]').each(function() {
435             var $this = $(this);
436             var sidebar_scroll = $this.ace_sidebar_scroll('ref');
437
438             if( event_name == 'sidebar_collapsed' && is_element_pos(this, 'fixed') ) {
439                 if( $this.attr('data-sidebar-hover') == 'true' ) $this.ace_sidebar_hover('reset');
440                 sidebar_scroll._reset();
441             }
442             else if( event_name === 'sidebar_fixed' || event_name === 'navbar_fixed' ) {
443                 var is_scrolling = sidebar_scroll.get('is_scrolling');
444                 var sidebar_fixed = is_element_pos(this, 'fixed')
445                 sidebar_scroll.set('sidebar_fixed', sidebar_fixed);
446
447                 if(sidebar_fixed && !is_scrolling) {
448                     sidebar_scroll._reset();
449                 }
450                 else if( !sidebar_fixed ) {
451                     sidebar_scroll.disable();
452                 }
453             }
454         
455         });
456     });
457     
458     $(window).on('resize.ace.sidebar_scroll', function(){
459         $('.sidebar[data-sidebar-scroll=true]').each(function() {
460             var $this = $(this);
461             if( $this.attr('data-sidebar-hover') == 'true' ) $this.ace_sidebar_hover('reset');
462             /////////////
463             var sidebar_scroll = $(this).ace_sidebar_scroll('ref');
464             
465             var sidebar_fixed = is_element_pos(this, 'fixed')
466             sidebar_scroll.set('sidebar_fixed', sidebar_fixed);
467             sidebar_scroll._reset();
468         });
469     })
470     
471
472     
473     
474      /////////////////////////////////////////////
475      if(!$.fn.ace_sidebar_scroll) {
476       $.fn.ace_sidebar_scroll = function (option, value) {
477         var method_call;
478
479         var $set = this.each(function () {
480             var $this = $(this);
481             var data = $this.data('ace_sidebar_scroll');
482             var options = typeof option === 'object' && option;
483
484             if (!data) $this.data('ace_sidebar_scroll', (data = new Sidebar_Scroll(this, options)));
485             if (typeof option === 'string' && typeof data[option] === 'function') {
486                 method_call = data[option](value);
487             }
488         });
489
490         return (method_call === undefined) ? $set : method_call;
491      }
492      
493      
494      $.fn.ace_sidebar_scroll.defaults = {
495         'scroll_to_active': true,
496         'include_shortcuts': true,
497         'include_toggle': false,
498         'smooth_scroll': 150,
499         'scroll_outside': false,
500         'scroll_style': '',
501         'lock_anyway': false
502      }
503      
504     }
505
506 })(window.jQuery);