﻿function Scroller( element ) 
{
	this.id = Scroller.Instances.length;
	Scroller.Instances[ this.id ] = this;


	// ----------------------------------------------------------------------------------------------------
	// ПЕРЕМЕННЫЕ
	// ----------------------------------------------------------------------------------------------------

	// элементы системы скроллирования
	Scroller.prototype.element;					// контейнер, содержащий всю систему
	Scroller.prototype.contentContainer;		// контейнер, ограничивающий размеры контента
	Scroller.prototype.contentBlock;			// контейнер, содержащий контент
	Scroller.prototype.sliderPathVertical;		// область перемещения вертикального слайнер
	Scroller.prototype.sliderVertical;			// вертикальный слайдер
	Scroller.prototype.scrollTopControl;		// верхний контрол прокрутки
	Scroller.prototype.scrollBottomControl;		// нижний контрол прокрутки

	// настройка параметров скроллирования
	Scroller.prototype.scrollStep = 2;

	// габариты и координаты элементов системы
	Scroller.prototype.sliderStartY;
	Scroller.prototype.sliderStartTop;
	Scroller.prototype.sliderHeight;
	Scroller.prototype.sliderPathHeight;
	Scroller.prototype.scrolledSliderPath;
	Scroller.prototype.scrolledContentHeight;
	Scroller.prototype.contentMarginTop;
	Scroller.prototype.contentMarginBottom;
	Scroller.prototype.mouseWheelTimeout;

	// [construct] > конструктор системы скроллирования
	// инициализация элементов системы
	// формирование необходимой DOM струкуры 
	// формирование системы обработки событий
	// ----------------------------------------------------------------------------------------------------
	Scroller.prototype.construct = function() {
		this.element 				= element;
		this.contentContainer		= document.createElement( 'div' );
		this.contentBlock 			= document.createElement( 'div' );
		this.sliderPathVertical		= document.createElement( 'div' );
		this.sliderVertical			= document.createElement( 'span' );
		this.scrollTopControl 		= document.createElement( 'div' );
		this.scrollBottomControl 	= document.createElement( 'div' );

		// формирование струкуры системы
		this.sliderPathVertical.style.display = 'none';
		this.scrollTopControl.style.display = 'none';
		this.scrollBottomControl.style.display = 'none';
		
		while( this.element.childNodes.length )
			this.contentBlock.insertBefore( this.element.childNodes[0], null );

		this.element.appendChild( this.contentContainer );
		this.contentContainer.appendChild( this.contentBlock );
		this.element.appendChild( this.sliderPathVertical ); 
		this.sliderPathVertical.appendChild( this.sliderVertical );
		this.element.appendChild( this.scrollTopControl );
		this.element.appendChild( this.scrollBottomControl );

		// простановка необходимых классов
		$( this.element ).removeClass( 'scrolledBlock' );
		$( this.element ).addClass( 'scrollContainer' );
		$( this.contentContainer ).addClass( 'scrollContentContainer' );
		$( this.contentBlock ).addClass( 'scrollContent' );
		$( this.sliderPathVertical ).addClass( 'scrollSliderPathVertical' );
		$( this.sliderVertical ).addClass( 'scrollSliderVertical' );
		$( this.scrollTopControl ).addClass( 'scrollTopControl' );
		$( this.scrollBottomControl ).addClass( 'scrollBottomControl' );

		// простановка необходимых обработчиков событий
		$( this.sliderVertical ).bind( 'mousedown', this.startSliderMoving );

		$( this.scrollTopControl ).bind( 'click', this.scrollUp );

		$( this.scrollBottomControl	).bind( 'click', this.scrollDown );

		this.updateScrollScope();

		$( window ).bind( 'resize', Scroller.recalculateDimension );
	}
	
	// [updateScrollScope] > расчет границ скроллирования
	// ----------------------------------------------------------------------------------------------------
	Scroller.prototype.updateScrollScope = function() {
		
		// предрасчёт размеров
		this.scrolledContentHeight = this.contentBlock.clientHeight - this.contentContainer.clientHeight;
		// элементы управления скроллером отображаются только, если контент не помещается в контейнер
		if ( this.scrolledContentHeight > 0 )
		{
			// если ранее элементы были скрыты и нужен скролинг, то теперь их нужно отобразить
			if ( this.sliderPathVertical.style.display == 'none' )
			{
				this.sliderPathVertical.style.display = 'block';
				this.scrollTopControl.style.display = 'block';
				this.scrollBottomControl.style.display = 'block';
			}
			
			this.scrolledSliderHeight = this.sliderPathVertical.clientHeight - this.sliderVertical.clientHeight;
			
			this.setSliderPosition( ( ( this.contentBlock.offsetTop ) * (-1) ) / ( this.scrolledContentHeight ) );
			$( this.element ).bind( 'mouseover', this.initMouseWheel );
		}
		else 
		{
			this.scrollContentUp( this.contentContainer.clientHeight );
			this.sliderPathVertical.style.display = 'none';
			this.scrollTopControl.style.display = 'none';
			this.scrollBottomControl.style.display = 'none';
			$( this.element ).unbind( 'mouseover', this.initMouseWheel );
		}
	}
	
	
	// ----------------------------------------------------------------------------------------------------
	// ОБРАБОТКА СОБЫТИЙ ПРЕМЕЩЕНИЯ СЛАЙДЕРА
	// ----------------------------------------------------------------------------------------------------
	
	// [startSliderMoving] > обработка события [onmousedown] начала перетаскивания слайдера
	// добавление обработчиков перемещения слайдеров
	// удаления обработчиков управления контролами
	// ----------------------------------------------------------------------------------------------------
	Scroller.prototype.startSliderMoving = function( e ) {
		var instance = Scroller.getCurrentInstance( e, 'sliderVertical' );
		
		if ( instance.contentContainer.clientHeight < instance.contentBlock.clientHeight )
		{
			Scroller.ActiveSlideredInstance = instance;
			instance.sliderIsMoving = true;
			instance.sliderStartY = e.clientY;
			instance.sliderStartTop = parseInt( instance.sliderVertical.style.top + 0 );
   
			// перемещение слайдера обрабатывается на фоне всего документа
			$( document ).bind( 'mousemove', 	instance.sliderMoving );
			$( document ).bind( 'mouseup',		instance.stopSliderMoving );
		}
	}
	
	// [sliderMoving] > обработка обытия [onmousemove] перемещения слайдера
	// ----------------------------------------------------------------------------------------------------
	Scroller.prototype.sliderMoving = function( e ) {
		var instance = Scroller.ActiveSlideredInstance;
		var newSliderTop = e.clientY - instance.sliderStartY + instance.sliderStartTop;
		
		if ( newSliderTop < 0 )
		{
			instance.sliderVertical.style.top = 0 + 'px';
			instance.contentBlock.style.top = 0 + 'px';
		}
		else if ( newSliderTop > instance.scrolledSliderHeight )
		{
			instance.sliderVertical.style.top = instance.scrolledSliderHeight + 'px';
			instance.contentBlock.style.top = (-1)*( instance.scrolledContentHeight ) + 'px';
		}
		else
		{			
			instance.sliderVertical.style.top = newSliderTop + "px";
			instance.contentBlock.style.top = (-1)*( newSliderTop )*( instance.scrolledContentHeight )/( instance.scrolledSliderHeight ) + 'px';
		}
		/* Отключение стандартной обработки события, чтобы не происходило выделения текста */
		if ( e.preventDefault )
			e.preventDefault();
		e.returnValue = false;
	}
	
	// [stopSliderMoving] > обработка события [onmouseup] прекращения перемещения слайдера
	// удаление обработчиков событий перемещения слайдера
	// возвращение обработчиков событий управления контролами
	// ----------------------------------------------------------------------------------------------------
	Scroller.prototype.stopSliderMoving = function() {
		var instance = Scroller.ActiveSlideredInstance;
		$( document ).unbind( 'mousemove', 	instance.sliderMoving );
		$( document ).unbind( 'mouseup',	instance.stopSliderMoving );
		Scroller.ActiveSlideredInstance = undefined;
	}
	
	// ----------------------------------------------------------------------------------------------------
	// ОБРАБОТКА СОБЫТИЙ УПРАВЛЕНИЯ КОНТРОЛАМИ
	// ----------------------------------------------------------------------------------------------------
	
	// [setSliderPosition] > изменение позиции слайдера в зависимости
	// от позиции передвинутого контролами контента
	// ----------------------------------------------------------------------------------------------------
	Scroller.prototype.setSliderPosition = function( factor ) {
		// срабатывание resize окна случилось раньше, чем создался скроллер
		if( !this.scrolledSliderHeight )
			return;
		//не даем вылезти за границы скроллера
		if( factor > 1 )
			factor = 1;
		var sliderTop = this.scrolledSliderHeight * factor;
		if ( sliderTop <= 0 )
			this.sliderVertical.style.top = 0 + 'px';
		else if ( sliderTop > this.sliderPathHeight - this.sliderHeight )
			this.sliderVertical.style.top = this.sliderPathHeight - this.sliderHeight + 'px';
		else
			this.sliderVertical.style.top = sliderTop + 'px';
	}
	
	// [scrollContentUp] > скроллироваие содержимого вверх на [step] пикселей
	// ----------------------------------------------------------------------------------------------------
	Scroller.prototype.scrollContentUp = function( step ) {
		
		if ( step == undefined ) step = this.scrollStep;
		if( step != 0 )
		{
			if ( this.contentBlock.offsetTop < 0 ) {
				if ( this.contentBlock.offsetTop + step < 0 )
					this.contentBlock.style.top = ( parseInt( this.contentBlock.style.top + 0 ) + step ) + 'px';
				else
					this.contentBlock.style.top = 0 + 'px';
				this.setSliderPosition( ( ( this.contentBlock.offsetTop ) * (-1) ) / ( this.scrolledContentHeight ) );
			}
		}
	}
	
	// [scrollContentDown] > скроллироваие содержимого вниз на [step] пикселей
	// ----------------------------------------------------------------------------------------------------
	Scroller.prototype.scrollContentDown = function( step ) {
		if ( step == undefined ) step = this.scrollStep;
		if( step != 0 )
		{
			if ( this.scrolledContentHeight + this.contentBlock.offsetTop > 0 ) {
					if ( this.scrolledContentHeight + this.contentBlock.offsetTop - step < 0 )
						this.contentBlock.style.top = ( -this.scrolledContentHeight ) + 'px';
					else
						this.contentBlock.style.top = ( parseInt( this.contentBlock.style.top + 0 ) - step ) + 'px';
				this.setSliderPosition( ( this.contentBlock.offsetTop * (-1) ) / ( this.scrolledContentHeight ) );
			}
		}
	}
	
	// [scrollUp] > обработка события [onсlick] для контрола перемещения вверх
	// установка интервала и изменение классов
	// ----------------------------------------------------------------------------------------------------
	Scroller.prototype.scrollUp = function( e ) {
		var instance = Scroller.getCurrentInstance( e, 'scrollTopControl' );
		Scroller.Instances[ instance.id ].scrollContentUp( 30 );
	}
	
	// [scrollDown] > обработка события [onсlick] для контрола перемещения вниз
	// установка интервала и изменение классов
	// ----------------------------------------------------------------------------------------------------
	Scroller.prototype.scrollDown = function( e ) {
		var instance = Scroller.getCurrentInstance( e, 'scrollBottomControl' );
		Scroller.Instances[ instance.id ].scrollContentDown( 30 );
	}	
	
	// [initMouseWheel] > обработка события [onmouseover] для системы скроллинга
	// добавление обработчика вращения колеса мышки
	// ----------------------------------------------------------------------------------------------------
	Scroller.prototype.initMouseWheel = function( e ) {
		var curInstance = Scroller.getCurrentInstanceRecursive( e, 'element' );
		
		if ( undefined != Scroller.ActiveWheeledInstance && Scroller.ActiveWheeledInstance == curInstance )
		{
			clearTimeout( Scroller.ActiveWheeledInstance.mouseWheelTimeout );
		}
		else if ( curInstance )
		{
			if ( window.addEventListener )
				/* DOMMouseScroll для mozilla. */
				window.addEventListener( ( $.browser.mozilla ? 'DOMMouseScroll' : 'mousewheel' ), curInstance.processMouseWheel, false );
			else
				/* IE/Opera. */
				window.onmousewheel = document.onmousewheel = curInstance.processMouseWheel;
   
   			Scroller.ActiveWheeledInstance = curInstance;
			$(  curInstance.element ).bind( 'mouseout', 	curInstance.checkMouseWheelHandler );
		}
	}
	
	// [checkMouseWheelHandler] > проверка действительно ли мышка больше не в области прокрутка
	// или она просто заскочила на один из дочерних элементов
	// ----------------------------------------------------------------------------------------------------
	Scroller.prototype.checkMouseWheelHandler = function( e ) {
		var instance = Scroller.ActiveWheeledInstance;
		instance.mouseWheelTimeout = setTimeout( 'Scroller.Instances[' + instance.id + '].removeMouseWheelHandler()', 0 );
	}
	
	// [removeMouseWheelHandler] > обрадотчик прокрутки колеса снимается,
	// когда мышка покидает область скроллинга
	// ----------------------------------------------------------------------------------------------------
	Scroller.prototype.removeMouseWheelHandler = function( e ) {
		var instance = Scroller.ActiveWheeledInstance;
		if( instance != undefined )
		{
			if ( window.addEventListener )
				/* DOMMouseScroll is for mozilla. */
				window.removeEventListener( 'DOMMouseScroll', instance.processMouseWheel, false );
			else
				/* IE/Opera. */
				window.onmousewheel = document.onmousewheel = null;
			
			$( instance.element ).unbind( 'mouseout', 	instance.checkMouseWheelHandler );
			Scroller.ActiveWheeledInstance = undefined;
		}
	}
	
	// [processMouseWheel] > прокрутка контента в зависимости от вращение колеса мышки
	// ----------------------------------------------------------------------------------------------------
	Scroller.prototype.processMouseWheel = function( e ) {
		
		var instance = Scroller.ActiveWheeledInstance;
		var delta = 0;
		
		/*IE*/
		if (!e) e = window.event;
		/* IE/Opera */
		if ( e.wheelDelta ) {
				delta = e.wheelDelta/120;
		}
		/* Mozilla */
		else if (e.detail) {
				/* В Mozilla, знак delta в 3 раза больше отличается от delta IE */
				delta = -e.detail/3;
		}
		/* Обработка ненулевой delta
		 * Теперь, если delta положительна колесо было прокручено вверх, отрицательна - вниз */
		if ( delta > 0 )
			instance.scrollContentUp( instance.scrollStep*delta*10 );
		else if ( delta < 0 )
			instance.scrollContentDown( -instance.scrollStep*delta*10 );

		/* Отключение обработки колеса по умолчанию, где это возможно */
		if ( e.preventDefault )
			e.preventDefault();
		e.returnValue = false;
	}
}

// массив с указателями на все экземпляры класса
Scroller.Instances = new Array();
// экземпляр класса, актиивный в данный момент
Scroller.ActiveSlideredInstance = undefined;
Scroller.ActiveWheeledInstance = undefined;

// [Init] > Инициализация всех скроллируемых блоков на странице
// также удаление обработчиков прокрутки колёсика мышки для существующих блоков
// обнуление структуры, содержащей информацию о существующих блоках
// ----------------------------------------------------------------------------------------------------
Scroller.Init = function() {
	
	// удаление глобальных обработчиков для всех экземляров скроллера
	for ( var i = 0; i < Scroller.Instances.length; i++ ) {
		var instance = Scroller.Instances[ i ];
		$( instance.element ).unbind( 'mouseover', instance.initMouseWheel );
		if ( Scroller.ActiveWheeledInstance == instance )
			instance.removeMouseWheelHandler();	
		instance = null;
	}
	Scroller.Instances = null;
	Scroller.Instances = new Array();
	
	Scroller.ActiveSlideredInstance = undefined;
	Scroller.ActiveWheeledInstance = undefined;
	
	$( 'div.scrolledBlock' ).each( function(){ ( new Scroller( this ) ).construct(); } );
}

// [getCurrentInstance] > выяснение событие какого экземляра класса было спровоцировано
// ( разрещение экземпляра класса по [e.target] )
// ----------------------------------------------------------------------------------------------------
Scroller.getCurrentInstance = function( e, elementName ) {
	for ( var i = 0; i < Scroller.Instances.length; i++ )
	{
		if ( getEventSrcElement(e) == Scroller.Instances[i][elementName] )
			return Scroller.Instances[i];
	}
}

// [getCurrentInstanceRecursive] > выяснение событие какого экземляра класса было спровоцировано
// ( разрещение экземпляра класса по [e.target], а также предкам [e.target] до [elementName] )
// ----------------------------------------------------------------------------------------------------
Scroller.getCurrentInstanceRecursive = function( e, elementName ) {
	
	for ( var i = 0; i < Scroller.Instances.length; i++ )
	{
		var target = getEventSrcElement(e);
		do {
			if ( target == Scroller.Instances[i][elementName] )
				return Scroller.Instances[i];
		} while ( !$( target ).hasClass( 'scrollContainer' ) && (target = target.parentNode) );
	}
}

// [getInstanceByContainer] > Поиск экземпляра скроллера по контейнеру
// ----------------------------------------------------------------------------------------------------
Scroller.getInstanceByContainer = function( el ) {
	
	for ( var i = 0; i < Scroller.Instances.length; i++ )
	{
		if ( el == Scroller.Instances[i].element )
			return Scroller.Instances[i];
	}
}

// [recalculateDimension] > пересчёт размеров контента при изменении размеров окна
// ----------------------------------------------------------------------------------------------------
Scroller.recalculateDimension = function() {
	var instance;
	for ( var i = 0; i < Scroller.Instances.length; i++ ){
		instance = Scroller.Instances[i];
		var oldScrolledHeight = instance.scrolledContentHeight;
		instance.scrolledContentHeight = (instance.contentBlock.clientHeight - instance.contentContainer.clientHeight);
		//Если поменялись размеры
		if( oldScrolledHeight != instance.scrolledContentHeight )
		{
			// если нужен скролинг
			if ( instance.scrolledContentHeight > 0 )
			{
				// если ранее элементы были скрыты и нужен скролинг, то теперь их нужно отобразить
				if ( instance.sliderPathVertical.style.display == 'none' )
				{
					instance.sliderPathVertical.style.display = 'block';
					instance.scrollTopControl.style.display = 'block';
					instance.scrollBottomControl.style.display = 'block';
				}
				
				if ( instance.contentBlock.offsetTop <= 0 )
					instance.contentBlock.style.top = 0 + 'px';
				else if ( instance.scrolledContentHeight + instance.contentBlock.offsetTop < 0 )
					instance.contentBlock.style.top = ( -instance.scrolledContentHeight ) + 'px';

				instance.setSliderPosition( ( instance.contentBlock.offsetTop * (-1) ) / ( instance.scrolledContentHeight ) );
			}
			// если скролинг не нужен - элементы скролинга скрываются, контент перемещается вверх
			else
			{
				instance.contentBlock.style.top = 0 + 'px';
				instance.sliderPathVertical.style.display = 'none';
				instance.scrollTopControl.style.display = 'none';
				instance.scrollBottomControl.style.display = 'none';
			}
		}
	}
}
$( function(){ Scroller.Init() });