ace.sidebar-scroll-1.js 11.8 KB
/**
 <b>Scrollbars for sidebar</b>. This approach can <span class="text-danger">only</span> be used on <u>fixed</u> sidebar.
 It doesn't use <u>"overflow:hidden"</u> CSS property and therefore can be used with <u>.hover</u> submenus and minimized sidebar.
 Except when in mobile view and menu toggle button is not in the navbar.
*/

ace.sidebar_scrollable = function($ , options) {
	if( !$.fn.ace_scroll ) return;
	

	var old_safari = ace.vars['safari'] && navigator.userAgent.match(/version\/[1-5]/i)
	//NOTE
	//Safari on windows has not been updated for a long time.
	//And it has a problem when sidebar is fixed&scrollable and there is a CSS3 animation inside page content.
	//Very probably windows users of safari have migrated to another browser by now!

	var $sidebar = $('.sidebar'),
		$navbar = $('.navbar'),
		$nav = $sidebar.find('.nav-list'),
		$toggle = $sidebar.find('.sidebar-toggle'),
		$shortcuts = $sidebar.find('.sidebar-shortcuts'),
		$window = $(window),

		sidebar = $sidebar.get(0),
		nav = $nav.get(0);

		if(!sidebar || !nav) return;


	var scroll_div = null,
		scroll_content = null,
		scroll_content_div = null,
		bar = null,
		ace_scroll = null;

	var is_scrolling = false,
		_initiated = false;
		
	
		
	var scroll_to_active = options.scroll_to_active || false,
		include_shortcuts = options.include_shortcuts || false,
		include_toggle = options.include_toggle || false,
		smooth_scroll = options.smooth_scroll || false,
		scrollbars_outside = options.outside || false,
		only_if_fixed = true;
		
		
		
	var is_sidebar_fixed =
	'getComputedStyle' in window ?
	//sidebar.offsetHeight is used to force redraw and recalculate 'sidebar.style.position' esp for webkit!
	function() { sidebar.offsetHeight; return window.getComputedStyle(sidebar).position == 'fixed' }
	:
	function() { sidebar.offsetHeight; return $sidebar.css('position') == 'fixed' }
	//sometimes when navbar is fixed, sidebar automatically becomes fixed without needing ".sidebar-fixed" class
	//currently when mobile_style == 1

	var $avail_height, $content_height;
	var sidebar_fixed = is_sidebar_fixed(),
		horizontal = $sidebar.hasClass('h-sidebar');


	var scrollbars = ace.helper.sidebar_scroll = {
		available_height: function() {
			//available window space
			var offset = $nav.parent().offset();//because `$nav.offset()` considers the "scrolled top" amount as well
			if(sidebar_fixed) offset.top -= ace.helper.scrollTop();

			return $window.innerHeight() - offset.top - ( include_toggle ? 0 : $toggle.outerHeight() );
		},
		content_height: function() {
			return nav.scrollHeight;
		},
		initiate: function(on_page_load) {
			if( _initiated ) return;
			if( !sidebar_fixed ) return;//eligible??
			//return if we want scrollbars only on "fixed" sidebar and sidebar is not "fixed" yet!

			//initiate once
			$nav.wrap('<div style="position: relative;" />');
			$nav.after('<div><div></div></div>');

			$nav.wrap('<div class="nav-wrap" />');
			if(!include_toggle) $toggle.css({'z-index': 1});
			if(!include_shortcuts) $shortcuts.css({'z-index': 99});

			scroll_div = $nav.parent().next()
			.ace_scroll({
				size: scrollbars.available_height(),
				reset: true,
				mouseWheelLock: true,
				hoverReset: false,
				dragEvent: true,
				touchDrag: false//disable touch drag event on scrollbars, we'll add a custom one later
			})
			.closest('.ace-scroll').addClass('nav-scroll');
			
			ace_scroll = scroll_div.data('ace_scroll');

			scroll_content = scroll_div.find('.scroll-content').eq(0);
			scroll_content_div = scroll_content.find(' > div').eq(0);
			bar = scroll_div.find('.scroll-bar').eq(0);

			if(include_shortcuts) {
				$nav.parent().prepend($shortcuts).wrapInner('<div />');
				$nav = $nav.parent();
			}
			if(include_toggle) {
				$nav.append($toggle);
				$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
			}

			$nav.css({position: 'relative'});
			if( scrollbars_outside === true ) scroll_div.addClass('scrollout');
			
			nav = $nav.get(0);
			nav.style.top = 0;
			scroll_content.on('scroll.nav', function() {
				nav.style.top = (-1 * this.scrollTop) + 'px';
			});
			$nav.on('mousewheel.ace_scroll DOMMouseScroll.ace_scroll', function(event){
				//transfer $nav's mousewheel event to scrollbars
				return scroll_div.trigger(event);
			});


			/**$(document.body).on('touchmove.nav', function(event) {
				if( is_scrolling && $.contains(sidebar, event.target) ) {
					event.preventDefault();
					return false;
				}
			});*/

			//you can also use swipe event in a similar way //swipe.nav
			var content = scroll_content.get(0);
			$nav.on('ace_drag.nav', function(event) {
				if( !is_scrolling ) {
					event.retval.cancel = true;
					return;
				}

				if(event.direction == 'up' || event.direction == 'down') {
					
					ace_scroll.move_bar(true);
					
					var distance = event.dy;
					
					distance = parseInt(Math.min($avail_height, distance))
					if(Math.abs(distance) > 2) distance = distance * 2;
					
					if(distance != 0) {
						content.scrollTop = content.scrollTop + distance;
						nav.style.top = (-1 * content.scrollTop) + 'px';
					}
				}
			});
			

			//for drag only
			if(smooth_scroll) {
				$nav
				.on('touchstart.nav MSPointerDown.nav pointerdown.nav', function(event) {
					$nav.css('transition-property', 'none');
					bar.css('transition-property', 'none');
				})
				.on('touchend.nav touchcancel.nav MSPointerUp.nav MSPointerCancel.nav pointerup.nav pointercancel.nav', function(event) {
					$nav.css('transition-property', 'top');
					bar.css('transition-property', 'top');
				});
			}
			
			

			if(old_safari && !include_toggle) {
				var toggle = $toggle.get(0);
				if(toggle) scroll_content.on('scroll.safari', function() {
					ace.helper.redraw(toggle);
				});
			}

			_initiated = true;

			//if the active item is not visible, scroll down so that it becomes visible
			//only the first time, on page load
			if(on_page_load == true) {
				scrollbars.reset();//try resetting at first

				if( scroll_to_active ) {
					scrollbars.scroll_to_active();
				}
				scroll_to_active = false;
			}
			
			
			
			if( typeof smooth_scroll === 'number' && smooth_scroll > 0) {
				$nav.css({'transition-property': 'top', 'transition-duration': (smooth_scroll / 1000).toFixed(2)+'s'})
				bar.css({'transition-property': 'top', 'transition-duration': (smooth_scroll / 1500).toFixed(2)+'s'})
				
				scroll_div
				.on('drag.start', function(e) {
					e.stopPropagation();
					$nav.css('transition-property', 'none')
				})
				.on('drag.end', function(e) {
					e.stopPropagation();
					$nav.css('transition-property', 'top')
				});
			}
			
			if(ace.vars['android']) {
				//force hide address bar, because its changes don't trigger window resize and become kinda ugly
				var val = ace.helper.scrollTop();
				if(val < 2) {
					window.scrollTo( val, 0 );
					setTimeout( function() {
						scrollbars.reset();
					}, 20 );
				}
				
				var last_height = ace.helper.winHeight() , new_height;
				$(window).on('scroll.ace_scroll', function() {
					if(is_scrolling && ace_scroll.is_active()) {
						new_height = ace.helper.winHeight();
						if(new_height != last_height) {
							last_height = new_height;
							scrollbars.reset();
						}
					}
				});
			}

		},
		
		scroll_to_active: function() {
			if( !ace_scroll || !ace_scroll.is_active() ) return;
			try {
				//sometimes there's no active item or not 'offsetTop' property
				var $active;

				var nav_list = $sidebar.find('.nav-list')
				if(ace.vars['minimized'] && !ace.vars['collapsible']) {
					$active = nav_list.find('> .active')
				}
				else {
					$active = $nav.find('> .active.hover')
					if($active.length == 0)	$active = $nav.find('.active:not(.open)')
				}

			
				var top = $active.outerHeight();
				nav_list = nav_list.get(0);
				var active = $active.get(0);
				while(active != nav_list) {
					top += active.offsetTop;
					active = active.parentNode;
				}

				var scroll_amount = top - scroll_div.height();
				if(scroll_amount > 0) {
					nav.style.top = -scroll_amount + 'px';
					scroll_content.scrollTop(scroll_amount);
				}
			}catch(e){}
		},
		
		reset: function() {
			if( !sidebar_fixed ) {
				scrollbars.disable();
				return;//eligible??
			}
			//return if we want scrollbars only on "fixed" sidebar and sidebar is not "fixed" yet!

			if( !_initiated ) scrollbars.initiate();
			//initiate scrollbars if not yet
			

			

			//enable if:
			//menu is not collapsible mode (responsive navbar-collapse mode which has default browser scrollbar)
			//menu is not horizontal or horizontal but mobile view (which is not navbar-collapse)
			//and available height is less than nav's height
			
			var enable_scroll = !ace.vars['collapsible']
								&& (!horizontal || (horizontal && ace.vars['mobile_view']))
								&& ($avail_height = scrollbars.available_height()) < ($content_height = nav.scrollHeight);

			is_scrolling = true;
			if( enable_scroll ) {
				scroll_content_div.css({height: $content_height, width: 8});
				scroll_div.prev().css({'max-height' : $avail_height})
				ace_scroll.update({size: $avail_height}).enable().reset();
			}
			if( !enable_scroll || !ace_scroll.is_active() ) {
				if(is_scrolling) scrollbars.disable();
			}
			else {
				$sidebar.addClass('sidebar-scroll');
			}

			//return is_scrolling;
		},
		disable : function() {
			is_scrolling = false;
			if(scroll_div) {
				scroll_div.css({'height' : '', 'max-height' : ''});
				scroll_content_div.css({height: '', width: ''});//otherwise it will have height and takes up some space even when invisible
				scroll_div.prev().css({'max-height' : ''})
				ace_scroll.disable();
			}

			if(parseInt(nav.style.top) < 0 && smooth_scroll && ace.vars['transition']) {
				$nav.one('transitionend.trans webkitTransitionEnd.trans mozTransitionEnd.trans oTransitionEnd.trans', function() {
					$sidebar.removeClass('sidebar-scroll');
					$nav.off('.trans');
				});
			} else {
				$sidebar.removeClass('sidebar-scroll');
			}

			nav.style.top = 0;
		},
		prehide: function(height_change) {
			if(!is_scrolling || ace.vars['minimized']) return;

			if(scrollbars.content_height() + height_change < scrollbars.available_height()) {
				scrollbars.disable();
			}
			else if(height_change < 0) {
				//if content height is decreasing
				//let's move nav down while a submenu is being hidden
				var scroll_top = scroll_content.scrollTop() + height_change
				if(scroll_top < 0) return;

				nav.style.top = (-1 * scroll_top) + 'px';
			}
		},
		_reset: function() {
			if(ace.vars['webkit']) 
				setTimeout(function() { scrollbars.reset() } , 0);
			else scrollbars.reset();
		}
	}
	scrollbars.initiate(true);//true = on_page_load

	//reset on document and window changes
	$(document).on('settings.ace.scroll', function(ev, event_name, event_val){
		if( event_name == 'sidebar_collapsed' && sidebar_fixed ) {
			scrollbars.reset();
		}
		else if( event_name === 'sidebar_fixed' || event_name === 'navbar_fixed' ) {
			//sidebar_fixed = event_val;
			sidebar_fixed = is_sidebar_fixed()
			
			if(sidebar_fixed && !is_scrolling) {
				scrollbars.reset();
			}
			else if( !sidebar_fixed ) {
				scrollbars.disable();
			}
		}
	});
	$window.on('resize.ace.scroll', function(){
		sidebar_fixed = is_sidebar_fixed()
		scrollbars.reset();
	})
	

	//change scrollbar size after a submenu is hidden/shown
	//but don't change if sidebar is minimized
	$sidebar.on('hidden.ace.submenu shown.ace.submenu', '.submenu', function(e) {
		e.stopPropagation();

		if(!ace.vars['minimized']) {
			//webkit has a little bit of a glitch!!!
			scrollbars._reset();
		}
	});

}