Source: mtr-datepicker.js


/**
 * The main class of the MtrDatepicker
 * Here inside is covered everything that you need to know
 *
 * @class MtrDatepicker
 * @param {Object} inputConfig used for user configurations
 */
function MtrDatepicker(inputConfig) {

	/**
	 * The real implementation of the library starts here
	 */

	var self = this;

	// The main configuration properties
	// All of them can be overided by the init method
	var config = {
		targetElement: null,
		defaultValues: {
			hours: 				[],
			minutes: 			[],
			dates: 				[],
			datesNames: 	[],
			months: 			[],
			years: 				[],
		},
		hours: {
			min: 1,
			max: 12,
			step: 1,
			maxlength: 2
		},
		minutes: {
			min: 0,
			max: 50,
			step: 10,
			maxlength: 2
		},
		months: {
			min: 0,
			max: 11,
			step: 1,
			maxlength: 2
		},
		years: {
			min: 2000,
			max: 2030,
			step: 1,
			maxlength: 4
		},
		animations: true,				// Responsible for the transition of the sliders - animated or static
		smartHours: false,			// Make auto swicth between AM/PM when moving from 11AM to 12PM or backwards
		future: false,					// Validate the date to be only in the future
		disableAmPm: false,     // Disable the 12 hours time format and go to a full 24 hours experience
		validateAfter: true,		// perform the future validation after the date change
		utcTimezone: 0,					// change the local timezone to a specific one

		transitionDelay: 100,
		transitionValidationDelay: 500,
		references: { // Used to store references to the main elements
			hours: null
		},

		monthsNames: {
			0: "Jan",
			1: "Feb",
			2: "Mar",
			3: "Apr",
			4: "May",
			5: "Jun",
			6: "Jul",
			7: "Aug",
			8: "Sep",
			9: "Oct",
			10: "Nov",
			11: "Dec",
		},

		daysNames: {
			0: "Sun",
			1: "Mon",
			2: "Tue",
			3: "Wed",
			4: "Thu",
			5: "Fri",
			6: "Sat",
		},

		timezones: null
	};

	// The main element which holds the datepicker
	var targetElement;

	var values = {
		date: null,
		timestamp: null,
		ampm: true,
	};

	var browser = null;

	// Here are the attached user events
	var defaultChangeEventsCategories = {
		'all': [],
		'time': [],
		'date': [],

		'hour': [],
		'minute': [],
		'ampm': [],
		'day': [],
		'month': [],
		'year': [],
	};

	var events = {
		'onChange': clone(defaultChangeEventsCategories),
		'beforeChange': clone(defaultChangeEventsCategories),
		'afterChange': clone(defaultChangeEventsCategories)
	};

	var plugins = {

	};

	// Keep the wheel scroll in a timeout
	var wheelTimeout = null;

	// Keep the arrow click in a timeout
	var arrowTimeout = {};

	/**
	 * The main init function which prepares the datepicker for use
	 *
	 * @param  {Object} inputConfig used to setup datepicker specific features
	 */
	var init = function(inputConfig) {

		browser = detectBrowser();

		if (!validateInputConfig(inputConfig)) {
			console.error('Initialization of the datepicker is blocked because of erros in the config.');
			return;
		}

		setConfig(inputConfig);

		targetElement = byId(config.targetElement);

		setDatesRange();

		createMarkup();

		attachEvents();
	};


	/**
	 * Attaching the user input config settings to ovverride the default one
	 *
	 * @param {Object} input user input settings
	 */
	var setConfig = function(input) {
		config.targetElement = input.target;

		config.animations = input.animations !== undefined ? input.animations : config.animations;
		config.future = input.future !== undefined ? input.future : config.future;
		config.validateAfter = input.validateAfter !== undefined ? input.validateAfter : config.validateAfter;
		config.smartHours = input.smartHours !== undefined ? input.smartHours : config.smartHours;
		config.disableAmPm = input.disableAmPm !== undefined ? input.disableAmPm : config.disableAmPm;

		// Change the defauls if the AM/PM is disabled
		if (config.disableAmPm) {
			config.hours.min = 0;
			config.hours.max = 23;
		}

		values.date = input.timestamp ? new Date(input.timestamp) : new Date();
		values.date.setSeconds(0);

		if (input.utcTimezone !== undefined) {
			// We are sure that the timezones plugin is loaded because we've made a check in the input validation
			plugins.timezones = new MtrDatepickerTimezones();
			config.utcTimezone = plugins.timezones.getTimezone(input.utcTimezone);
		}
		else {
			config.utcTimezone = {
				offset: input.utcTimezone !== undefined ? input.utcTimezone : (values.date.getTimezoneOffset() / 60 * -1)
			};
		}

		var localTimezoneOffsetTimestamp = values.date.getTime() + (values.date.getTimezoneOffset() * 60 * 1000);
		var timezoneOffsetTimestamp = localTimezoneOffsetTimestamp + (config.utcTimezone.offset * 60 * 60 * 1000);
		values.date = new Date(timezoneOffsetTimestamp);
		values.timestamp = values.date.getTime();

		// Override minutes
		config.minutes.min = (input.minutes !== undefined && input.minutes.min !== undefined) ? parseInt(input.minutes.min) : config.minutes.min;
		config.minutes.max = (input.minutes !== undefined && input.minutes.max !== undefined) ? parseInt(input.minutes.max) : config.minutes.max;
		config.minutes.step = (input.minutes !== undefined && input.minutes.step !== undefined) ? parseInt(input.minutes.step) : config.minutes.step;

		// Override months
		config.months.min = (input.months !== undefined && input.months.min !== undefined) ? parseInt(input.months.min) : config.months.min;
		config.months.max = (input.months !== undefined && input.months.max !== undefined) ? parseInt(input.months.max) : config.months.max;
		config.months.step = (input.months !== undefined && input.months.step !== undefined) ? parseInt(input.months.step) : config.months.step;

		// Override years
		config.years.min = (input.years !== undefined && input.years.min !== undefined) ? parseInt(input.years.min) : config.years.min;
		config.years.max = (input.years !== undefined && input.years.max !== undefined) ? parseInt(input.years.max) : config.years.max;
		config.years.step = (input.years !== undefined && input.years.step !== undefined) ? parseInt(input.years.step) : config.years.step;

		// Init hours
		config.defaultValues.hours = createRange(config.hours);
		config.defaultValues.minutes = createRange(config.minutes);
		config.defaultValues.months = createRange(config.months);
		config.defaultValues.years = createRange(config.years);
	};

	var validateInputConfig = function(input) {
		var result = true;

		// Validate minutes
		if (input.minutes) {
			// Validate data type
			if (input.minutes.min !== undefined && !isNumber(input.minutes.min)) {
				console.error('Invalid argument: minutes.min should be a number.');
				result = false;
			}
			if (input.minutes.max !== undefined && !isNumber(input.minutes.max)) {
				console.error('Invalid argument: minutes.max should be a number.');
				result = false;
			}
			if (input.minutes.step !== undefined && !isNumber(input.minutes.step)) {
				console.error('Invalid argument: minutes.step should be a number.');
				result = false;
			}

			// Validate the range
			if (input.minutes.min !== undefined && input.minutes.max !== undefined && input.minutes.max < input.minutes.min) {
				console.error('Invalid argument: minutes.max should be larger than minutes.min.');
				result = false;
			}

			if (input.minutes.min !== undefined &&
				input.minutes.max !== undefined &&
				input.minutes.step !== undefined &&
				(input.minutes.step > (input.minutes.max - input.minutes.min))) {
				console.error('Invalid argument: minutes.step should be less than minutes.max-minutes.min.');
				result = false;
			}
		}

		if (input.hours) {
			// Validate data type
			if (input.hours.min !== undefined && !isNumber(input.hours.min)) {
				console.error('Invalid argument: hours.min should be a number.');
				result = false;
			}
			if (input.hours.max !== undefined && !isNumber(input.hours.max)) {
				console.error('Invalid argument: hours.max should be a number.');
				result = false;
			}
			if (input.hours.step !== undefined && !isNumber(input.hours.step)) {
				console.error('Invalid argument: hours.step should be a number.');
				result = false;
			}

			// Validate the range
			if (input.hours.min !== undefined && input.hours.max !== undefined && input.hours.max < input.hours.min) {
				console.error('Invalid argument: hours.max should be larger than hours.min.');
				result = false;
			}

			if (input.hours.min !== undefined &&
				input.hours.max !== undefined &&
				input.hours.step !== undefined &&
				(input.hours.step > (input.hours.max - input.hours.min))) {
				console.error('Invalid argument: hours.step should be less than hours.max-hours.min.');
				result = false;
			}
		}

		if (input.dates) {
			// Validate data type
			if (input.dates.min !== undefined && !isNumber(input.dates.min)) {
				console.error('Invalid argument: dates.min should be a number.');
				result = false;
			}
			if (input.dates.max !== undefined && !isNumber(input.dates.max)) {
				console.error('Invalid argument: dates.max should be a number.');
				result = false;
			}
			if (input.dates.step !== undefined && !isNumber(input.dates.step)) {
				console.error('Invalid argument: dates.step should be a number.');
				result = false;
			}

			// Validate the range
			if (input.dates.min !== undefined && input.dates.max !== undefined && input.dates.max < input.dates.min) {
				console.error('Invalid argument: dates.max should be larger than dates.min.');
				result = false;
			}

			if (input.dates.min !== undefined &&
				input.dates.max !== undefined &&
				input.dates.step !== undefined &&
				(input.dates.step > (input.dates.max - input.dates.min))) {
				console.error('Invalid argument: dates.step should be less than dates.max-dates.min.');
				result = false;
			}
		}

		if (input.months) {
			// Validate data type
			if (input.months.min !== undefined && !isNumber(input.months.min)) {
				console.error('Invalid argument: months.min should be a number.');
				result = false;
			}
			if (input.months.max !== undefined && !isNumber(input.months.max)) {
				console.error('Invalid argument: months.max should be a number.');
				result = false;
			}
			if (input.months.step !== undefined && !isNumber(input.months.step)) {
				console.error('Invalid argument: months.step should be a number.');
				result = false;
			}

			// Validate the range
			if (input.months.min !== undefined && input.months.max !== undefined && input.months.max < input.months.min) {
				console.error('Invalid argument: months.max should be larger than months.min.');
				result = false;
			}

			if (input.months.min !== undefined &&
				input.months.max !== undefined &&
				input.months.step !== undefined &&
				(input.months.step > (input.months.max - input.months.min))) {
				console.error('Invalid argument: months.step should be less than months.max-months.min.');
				result = false;
			}
		}

		if (input.years) {
			// Validate data type
			if (input.years.min !== undefined && !isNumber(input.years.min)) {
				console.error('Invalid argument: years.min should be a number.');
				result = false;
			}
			if (input.years.max !== undefined && !isNumber(input.years.max)) {
				console.error('Invalid argument: years.max should be a number.');
				result = false;
			}
			if (input.years.step !== undefined && !isNumber(input.years.step)) {
				console.error('Invalid argument: years.step should be a number.');
				result = false;
			}

			// Validate the range
			if (input.years.min !== undefined && input.years.max !== undefined && input.years.max < input.years.min) {
				console.error('Invalid argument: years.max should be larger than years.min.');
				result = false;
			}

			if (input.years.min !== undefined &&
				input.years.max !== undefined &&
				input.years.step !== undefined && (input.years.step > (input.years.max - input.years.min))) {
				console.error('Invalid argument: years.step should be less than years.max-years.min.');
				result = false;
			}
		}

		// Validate input timestamp
		if (input.timestamp) {

			// If the future dates is enabed, it will be a good idea to check the input timestamp, maybe it is in the past?
			if (input.future) {
				var timestampDate = new Date(input.timestamp);
				var todayDate = new Date();

				if (timestampDate.getTime() < todayDate.getTime()) {
					console.error('Invalid argument: timestamp should be in the future if the future check is enabled.');
					result = false;
				}
			}
		}

		if (input.utcTimezone !== undefined && typeof MtrDatepickerTimezones !== 'function') {
			console.error('In order to use the timezones feature you should load the mtr-datepicker-timezones.min.js first.');
			result = false;
		}

		// If there are any erros return a new target element with notice for the users
		if (!result) {
			targetElement = byId(input.target);

			while (targetElement.firstChild) {
				targetElement.removeChild(targetElement.firstChild);
			}

			var errorElement = document.createElement('div');
			addClass(errorElement, 'mtr-error-message');
			errorElement.appendChild(document.createTextNode('An error has occured during the initialization of the datepicker.'));

			targetElement.appendChild(errorElement);
		}

		return result;
	};

	var attachEvents = function() {

	};

	var setDatesRange = function(month, year) {
		month = month !== undefined ? month : getMonth();
		year = year !== undefined ? year : getYear();

		var datesRange = createRangeForDate(month, year);
		config.dates = {
			min: datesRange.min,
			max: datesRange.max,
			step: datesRange.step,
			maxlength: 2,
		};
		config.defaultValues.dates = datesRange.values;
		config.defaultValues.datesNames = datesRange.names;
	};

	/**
	 * Generate the main markup used from the datepicker
	 * This means that here we are generating input sliders for hours, minutes, months, dates and years
	 * and a radio input for swithing the time AM/PM
	 */
	var createMarkup = function() {

		// Clear all of the content of the target element
		removeClass(targetElement, 'mtr-datepicker');
		addClass(targetElement, 'mtr-datepicker');
		while (targetElement.firstChild) {
			targetElement.removeChild(targetElement.firstChild);
		}

		// Create time elements
		var hoursElement = createSliderInput({
			name: 'hours',
			values: config.defaultValues.hours,
			value: getHours()
		});

		var minutesElement = createSliderInput({
			name: 'minutes',
			values: config.defaultValues.minutes,
			value: getMinutes()
		});

		var amPmElement;
		if (!config.disableAmPm) {
			amPmElement = createRadioInput({
				name: 'ampm',
			});
		}

		var rowTime = document.createElement('div');
		rowTime.className = 'mtr-row';

		var rowClearfixTime = document.createElement('div');
		rowClearfixTime.className = 'mtr-clearfix';

		rowTime.appendChild(hoursElement);
		rowTime.appendChild(minutesElement);

		if (!config.disableAmPm) {
			rowTime.appendChild(amPmElement);
		}

		targetElement.appendChild(rowTime);
		targetElement.appendChild(rowClearfixTime);

		// Create date elements
		var monthElement = createSliderInput({
			name: 'months',
			values: config.defaultValues.months,
			valuesNames: config.monthsNames,
			value: getMonth()
		});

		var dateElement = createSliderInput({
			name: 'dates',
			values: config.defaultValues.dates,
			valuesNames: config.defaultValues.datesNames,
			value: getDate()
		});

		var yearElement = createSliderInput({
			name: 'years',
			values: config.defaultValues.years,
			value: getYear()
		});

		var rowDate = document.createElement('div');
		rowDate.className = 'mtr-row';

		var rowClearfixDate = document.createElement('div');
		rowClearfixDate.className = 'mtr-clearfix';

		rowDate.appendChild(monthElement);
		rowDate.appendChild(dateElement);
		rowDate.appendChild(yearElement);

		targetElement.appendChild(rowDate);
		targetElement.appendChild(rowClearfixDate);

		setTimestamp(values.timestamp);
	};

	/**
	 * This function is creating a slider input
	 *
	 * It is generating the required markup and attaching the needed event listeners
	 * The returned element is fully functional input field with arrows for navigating
	 * through the values
	 *
	 * @param  {object} elementConfig
	 * @return {HtmlElement}
	 */

	var createSliderInput = function(elementConfig) {
		var element = document.createElement('div');
		element.className = 'mtr-input-slider';
		config.references[elementConfig.name] = config.targetElement + '-input-' + elementConfig.name;
		element.id = config.references[elementConfig.name];

		// First, let's init the main elements
		var divArrowUp = createUpArrow();
		var divArrowDown = createDownArrow();

		// Content of the input, holding the input and the available values
		var divContent = document.createElement('div');
		divContent.className = "mtr-content";

		var inputValue = createInputValue();
		var divValues = createValues(inputValue);

		// The, let's append them to the element in the correct order
		element.appendChild(divArrowUp);

		// Append holder of the input and values to the main element
		divContent.appendChild(inputValue);
		divContent.appendChild(divValues);

		element.appendChild(divContent);

		element.appendChild(divArrowDown);

		// Here are the definitios of the functions which are used to generate the markup
		// and to attach the needed event listeners

		function createUpArrow() {
			var divArrowUp = document.createElement('div');
			divArrowUp.className = 'mtr-arrow up';
			divArrowUp.appendChild(document.createElement('span'));

			// Attach event listener
			divArrowUp.addEventListener('click', function() {
				// Prevent blur event
				var input = qSelect(inputValue, '.mtr-input');
				addClass(inputValue, 'arrow-click');
				addClass(divContent, 'mtr-active');

				if (arrowTimeout[elementConfig.name]) {
					window.clearTimeout(arrowTimeout[elementConfig.name]);
				}

				arrowTimeout[elementConfig.name] = setTimeout(function() {
					removeClass(inputValue, 'arrow-click');
					removeClass(divContent, 'mtr-active');
				}, 1000);

				// Change the value with the next one
				var name = elementConfig.name;
				var currentValue;

				switch(name) {
					case 'hours': currentValue = getHours(); break;
					case 'minutes': currentValue = getMinutes(); break;
					case 'dates': currentValue = getDate(); break;
					case 'months': currentValue = getMonth(); break;
					case 'years': currentValue = getYear(); break;
				}

				var indexInArray = config.defaultValues[name].indexOf(currentValue);
				indexInArray++;

				if (indexInArray >= config.defaultValues[name].length) {
					indexInArray = 0;
				}

				switch(name) {
					//case 'hours': setHours(config.defaultValues[name][indexInArray]); break;
					case 'hours':
						// Check is we have to make a transform of the hour
						var newHour = config.defaultValues[name][indexInArray];
						if (!config.disableAmPm && (getIsPm() && newHour !== 12)) {
							newHour += 12;
						}
						setHours(newHour);
						break;
					case 'minutes': setMinutes(config.defaultValues[name][indexInArray]); break;
					case 'dates': setDate(config.defaultValues[name][indexInArray]); break;
					case 'months': setMonth(config.defaultValues[name][indexInArray]); break;
					case 'years': setYear(config.defaultValues[name][indexInArray]); break;
				}
			}, false);

			return divArrowUp;
		}

		function createDownArrow() {
			var divArrowDown = document.createElement('div');
			divArrowDown.className = 'mtr-arrow down';
			divArrowDown.appendChild(document.createElement('span'));

			divArrowDown.addEventListener('click', function(e) {
				// Prevent blur event
				var input = qSelect(inputValue, '.mtr-input');
				addClass(inputValue, 'arrow-click');
				addClass(divContent, 'mtr-active');

				if (arrowTimeout[elementConfig.name]) {
					window.clearTimeout(arrowTimeout[elementConfig.name]);
				}

				arrowTimeout[elementConfig.name] = setTimeout(function() {
					removeClass(inputValue, 'arrow-click');
					removeClass(divContent, 'mtr-active');
				}, 1000);

				// Change the value with the prev one
				var name = elementConfig.name;
				var currentValue;

				switch(name) {
					case 'hours': currentValue = getHours(); break;
					case 'minutes': currentValue = getMinutes(); break;
					case 'dates': currentValue = getDate(); break;
					case 'months': currentValue = getMonth(); break;
					case 'years': currentValue = getYear(); break;
				}

				var indexInArray = config.defaultValues[name].indexOf(currentValue);
				indexInArray--;

				if (indexInArray < 0) {
					indexInArray = config.defaultValues[name].length - 1;
				}

				switch(name) {
					//case 'hours': setHours(config.defaultValues[name][indexInArray]); break;
					 case 'hours':
						// Check is we have to make a transform of the hour
						var newHour = config.defaultValues[name][indexInArray];
						if (!config.disableAmPm && (getIsPm() && newHour !== 12)) {
							newHour += 12;
						}
						setHours(newHour);
						break;
					case 'minutes': setMinutes(config.defaultValues[name][indexInArray]); break;
					case 'dates': setDate(config.defaultValues[name][indexInArray]); break;
					case 'months': setMonth(config.defaultValues[name][indexInArray]); break;
					case 'years': setYear(config.defaultValues[name][indexInArray]); break;
				}
			}, false);

			return divArrowDown;
		}

		function createInputValue() {
			var inputValue = document.createElement('input');
			inputValue.value = elementConfig.value;
			inputValue.type = 'text';
			inputValue.className = 'mtr-input ' + elementConfig.name;
			inputValue.style.display = 'none';

			// Attach event listeners
			inputValue.addEventListener('blur', function(e) {
				// Blur event has to be calles after specific ammount of time
				// because it can be cause from an arrow button. In this case
				// we shouldn't apple the blur event body
				setTimeout(function() {
					blurEvent();
				}, 500);

				function blurEvent() {
					if (!targetElement) {
						return;
					}

					var newValue = inputValue.value;
					var oldValue = inputValue.getAttribute('data-old-value');

					// If the blur is called after click on arrow we shoulnt update the value
					if (e.target.className.indexOf('arrow-click') > -1) {
						removeClass(e.target, 'arrow-click');
						return;
					}

					// If this is the month input we should decrement it because
					// the months are starting from 0
					if (inputValue.className.indexOf('months') > -1) {
						newValue--;
					}

					// Validate the value
					if (validateValue(elementConfig.name, newValue) === false) {
						inputValue.value = oldValue;
						inputValue.focus();
						return;
					}

					// If the future detection is ON validate the value again
					var target = elementConfig.name.substring(0, elementConfig.name.length-1);
					if (elementConfig.name === 'dates') {
						target = 'day';
					}

					if (config.future && !validateChange(target, newValue, oldValue)) {
						if (elementConfig.name === 'months') {
							oldValue++;
						}

						inputValue.value = oldValue;
						inputValue.focus();
						return;
					}

					inputValue.style.display = 'none';

					switch(elementConfig.name) {
						case 'hours': setHours(newValue); break;
						case 'minutes': setMinutes(newValue); break;
						case 'dates': setDate(newValue); break;
						case 'months': setMonth(newValue); break;
						case 'years': setYear(newValue); break;
					}
				}
			}, false);

			// On wheel scroll we should change the value in the input
			inputValue.addEventListener('wheel ', function(e) {
				e.preventDefault();
				e.stopPropagation();

				// If the user is using the mouse wheel the values should be changed
				var target = e.target;
				var wheelData = e.wheelDeltaY ? e.wheelDeltaY : (e.deltaY * -1);

				var oldValue = parseInt(inputValue.value),
						newValue;

				var configMin = config[elementConfig.name].min,
						configMax = config[elementConfig.name].max,
						configStep = config[elementConfig.name].step;

				if (elementConfig.name === 'months') {
					// If we are scrolling the months we should increment the value
					configMin++;
					configMax++;
				}

				if (direction > 0) { // Scroll up
					if (oldValue < configMax) {
						newValue = oldValue + configStep;
					}
					else {
						newValue = configMin;
					}
				}
				else { // Scroll down
					if (oldValue > configMin) {
						newValue = oldValue - configStep;
					}
					else {
						newValue = configMax;
					}
				}

				inputValue.value = newValue;
				return false;
			}, false);

			return inputValue;
		}

		function createValues(inputValue) {
			var divValues = createElementValues(elementConfig);

			// On swipe, we should cgange the value in the input
			divValues.addEventListener('touchstart', function(e) {
				handleTouchStart(e);
			}, false);
			divValues.addEventListener('touchmove', function(e) {
				handleTouchMove(e, function(direction) {
					var parent = divValues.parentElement.parentElement,
							arrow;

					if (direction > 0) { // Scroll up
						arrow = qSelect(parent, '.mtr-arrow.up');
					}
					else { // Scroll down
						arrow = qSelect(parent, '.mtr-arrow.down');
					}

					arrow.click();
				});
			}, false);

			return divValues;
		}

		return element;
	};

	/**
	 * Create HtmlElement with a radio button control
	 *
	 * @param  {object} elementConfig
	 * @return {HtmlElement}
	 */
	var createRadioInput = function(elementConfig) {
		var element = document.createElement('div');
		element.className = 'mtr-input-radio';
		config.references[elementConfig.name] = config.targetElement + '-input-' + elementConfig.name;
		element.id = config.references[elementConfig.name];

		var formHolder = document.createElement('form');
		formHolder.name = config.references[elementConfig.name];

		// First create the elements
		var radioAm = createInputValue('ampm', 1, 'AM');
		var radioPm = createInputValue('ampm', 0, 'PM');

		formHolder.appendChild(radioAm);
		formHolder.appendChild(radioPm);

		formHolder.ampm.value = getIsAm() ? '1' : '0';

		element.appendChild(formHolder);

		function createInputValue(radioName, radioValue, labelValue) {
			var divHolder = document.createElement('div');
			var label = document.createElement('label');
			var input = document.createElement('input');
			var elementId = config.targetElement + '-radio-' + radioName + '-' + labelValue;

			var innerHtmlSpanValue = document.createElement('span');
			innerHtmlSpanValue.className = 'value';
			innerHtmlSpanValue.appendChild(document.createTextNode(labelValue));

			var innerHtmlSpanRadio = document.createElement('span');
			innerHtmlSpanRadio.className = 'radio';

			label.setAttribute('for', elementId);
			label.appendChild(innerHtmlSpanValue);
			label.appendChild(innerHtmlSpanRadio);

			input.className = 'mtr-input ';
			input.type = 'radio';
			input.name = radioName;
			input.id = elementId;
			input.value = radioValue;

			divHolder.appendChild(input);
			divHolder.appendChild(label);

			// Attach event listeners
			input.addEventListener('change', function(e) {
				var result = setAmPm(radioValue);

				if (!result && config.future) {
					setAmPm(!radioValue);
					e.preventDefault();
					e.stopPropagation();
					return false;
				}
			}, false);

			return divHolder;
		}

		return element;
	};

	/**
	 * This function is creating a new set of HtmlElement which
	 * contains the default values for a specific input
	 *
	 * @param  {obect} elementConfig
	 * @return {HtmlElement}
	 */
	var createElementValues = function(elementConfig) {

		var divValues = document.createElement('div');
		divValues.className = 'mtr-values';

		elementConfig.values.forEach(function(value) {
			var innerHTML = elementConfig.name === 'months' ? value+1 : value;

			var divValueHolder = document.createElement('div');
			divValueHolder.className = 'mtr-default-value-holder';
			divValueHolder.setAttribute('data-value', value);

			var divValue = document.createElement('div');
			divValue.className = 'mtr-default-value';
			divValue.setAttribute('data-value', value);

			if (elementConfig.name === 'minutes' && value === 0) {
				divValue.appendChild(document.createTextNode('00'));
			}
			else {
				divValue.appendChild(document.createTextNode(innerHTML));
			}

			divValueHolder.appendChild(divValue);

			if (elementConfig.valuesNames) {
				var divValueName = document.createElement('div');
				divValueName.className = 'mtr-default-value-name';
				divValueName.appendChild(document.createTextNode(elementConfig.valuesNames[value]));

				divValue.className += ' has-name';

				divValueHolder.appendChild(divValueName);
			}

			divValues.appendChild(divValueHolder);
		});

		// Attach listeners
		var inputClickEventListener = function() {
			// Show the input field for manual setup
			var parent = divValues.parentElement,
					inputValue = qSelect(parent, '.mtr-input');

			// If we are working with months we have to incement the value
			// because the months are starting from 0
			if (inputValue.className.indexOf('months') > -1) {
				inputValue.value = parseInt(inputValue.value) + 1;
			}

			inputValue.style.display = "block";
			inputValue.focus();
		};

		divValues.addEventListener('click', inputClickEventListener, false);
		divValues.addEventListener('touchstart', inputClickEventListener, false);
		divValues.addEventListener('touchend', inputClickEventListener, false);

		divValues.addEventListener('wheel', function(e) {
			e.preventDefault();
			e.stopPropagation();

			if (wheelTimeout) {
				return false;
			}

			// If the user is using the mouse wheel the values should be changed
			var target = e.target;
			var parent = target.parentElement.parentElement.parentElement.parentElement; // value -> values -> content -> input slider
			var values = qSelect(parent, '.mtr-values');
			var input = qSelect(parent, '.mtr-input');
			var wheelData = e.wheelDeltaY ? e.wheelDeltaY : (e.deltaY * -1); // Firefox doesn't support wheelDataY, so we are using deltaY and cghanging the sign of the value

			var arrow;

			if (wheelData > 0) { // Scroll up
				arrow = qSelect(parent, '.mtr-arrow.up');
			}
			else { // Scroll down
				arrow = qSelect(parent, '.mtr-arrow.down');
			}

			wheelTimeout = setTimeout(function() {
				clearWheelTimeout();
			}, 100);

			arrow.click();
			return false;
		}, false);

		divValues.addEventListener('touchstart', function(e) {
			e.preventDefault();
			e.stopPropagation();
			return false;
		}, false);

		divValues.addEventListener('touchmove', function(e) {
			e.preventDefault();
			e.stopPropagation();
			return false;
		}, false);

		return divValues;
	};

	var rebuildElementValues = function(reference, data) {
		var element = byId(reference);
		var elementContent = qSelect(element, '.mtr-content');
		var elementContentValues = qSelect(elementContent, '.mtr-values');

		elementContentValues.parentNode.removeChild(elementContentValues);
		var elementContentNewValues = createElementValues({
			name: data.name,
			values: data.values,
			valuesNames: data.valuesNames
		});

		elementContent.appendChild(elementContentNewValues);
	};

	/**
	 * Updating the date when a month or year is changed
	 * It should realculate the dates in the specific month and check
	 * the postition of the date (if it's bigger than the last date of the month)
	 *
	 * @param  {Number} newMonth
	 * @param  {NUmber} newYar
	 */
	var updateDate = function(newMonth, newYear) {
		newMonth = newMonth !== undefined ? newMonth : getMonth();
		newYear = newYear !== undefined ? newYear : getYear();

		// After month change we should recalculate the range of the dates
		setDatesRange(newMonth, newYear);
		rebuildElementValues(config.references.dates, {
			name: 'dates',
			values: config.defaultValues.dates,
			valuesNames: config.defaultValues.datesNames
		});

		// After the change in the dates of the month we should check is the current date exist
		// because if the current date is 31 and the month has only 30 days it is not correct
		var maxDay = config.defaultValues.dates[config.defaultValues.dates.length-1];
		var currentDate = getDate();

		if (currentDate > maxDay) {
			setDate(maxDay);
		}
	};

	var validateValue = function(type, value) {
		value = parseInt(value);

		// Strict, the value is exact in the array
		return config.defaultValues[type].indexOf(value) > -1 ? true : false;
	};

	/**
	 * This function is validating the change of the date
	 *
	 * If the config.feature is enabled this function will prevent selecting dates
	 * in the past
	 *
	 * @param  {String} target
	 * @param  {Number} newValue
	 * @param  {Number} oldValue
	 * @return {boolean}
	 */
	var validateChange = function(target, newValue, oldValue) {
		if (config.future === false) {
			return true;
		}

		var dateNow = new Date(),
				datePicker = new Date(values.date.getTime());

		switch(target) {
			case 'hour':
				var isAm = getIsAm();
				if (isAm && newValue === 12) {
					newValue = 0;
				}
				// else if (!isAm && newValue < 12) {
				// 	newValue += 12;
				// }
				datePicker.setHours(newValue);
				break;
			case 'minute':	datePicker.setMinutes(newValue); break;
			case 'ampm':
				var currentHours = datePicker.getHours(),
						currentAmPm = (currentHours >= 0 && currentHours <= 11) ? true : false,
						newHours = currentHours;

				if (newValue != oldValue) {
					if (newValue == true && currentHours > 12) { // set AM
						newHours = currentHours - 12;
					}
					else if (newValue == true && currentHours == 12) {
						newHours = 0;
					}
					else if (newValue == false && currentHours < 12) { // Set PM
						newHours = currentHours + 12;
					}
					else if (newValue == false && currentHours == 12) { // Set PM
						newHours = 12;
					}
				}

				datePicker.setHours(newHours);
				break;
			case 'day':	datePicker.setDate(newValue); break;
			case 'month':	datePicker.setMonth(newValue); break;
			case 'year':	datePicker.setFullYear(newValue); break;
		}

		dateNow.setSeconds(0);
		dateNow.setMilliseconds(0);
		datePicker.setSeconds(0);
		datePicker.setMilliseconds(0);

		if (datePicker.getTime() < dateNow.getTime()) {
			return false;
		}
		return true;
	};

	var clearWheelTimeout = function() {
		wheelTimeout = null;
	};

	/*****************************************************************************
	 * A lot of getters and setters now
	 ****************************************************************************/

	var setHours = function(input, preventAnimation) {
		var oldValue = values.date.getHours();
		var isChangeValid = validateChange('hour', input, oldValue);
		var isAm = getIsAm();

		// If the smart hourrs are enabled and we want to gto from 11 Am to 12 PM, we should
		// disable the validation
		if (!config.disableAmPm && (config.smartHours && input === 12 && isAm)) {
			isChangeValid = true;
		}

		if (!config.validateAfter && !isChangeValid) {
			showInputSliderError(config.references.hours);
			return;
		}
		executeChangeEvents('hour', 'beforeChange', input, oldValue);
		var newHour = input;
		if (!config.disableAmPm && input > 12) {
			input -= 12; 			// reduce the values with 12 hours
		}

		updateInputSlider(config.references.hours, input, preventAnimation);

		if (config.validateAfter && !isChangeValid) {
			showInputSliderError(config.references.hours);

			setTimeout(function() {
				if (!config.disableAmPm && oldValue > 12) {
					oldValue -= 12;
				}
				updateInputSlider(config.references.hours, oldValue, preventAnimation);

				executeChangeEvents('hour', 'onChange', input, oldValue);
				executeChangeEvents('hour', 'afterChange', input, oldValue);
			}, config.transitionValidationDelay);
		}
		else {
			values.timestamp = values.date.setHours(newHour);
			if (!config.disableAmPm && (config.smartHours && newHour === 12 && isAm)) {
				values.timestamp = values.date.setHours(12);
				setAmPm(false); 	// set to PM
			}
			else if (!config.disableAmPm && (config.smartHours && (newHour === 23 || newHour === 11) && oldValue === 12 && !isAm)) {
				newHour = 11;
				values.timestamp = values.date.setHours(newHour);
				setAmPm(true); 	// set to AM
			}
			else if (!config.disableAmPm && (!config.smartHours && newHour === 12 && isAm)) {
				values.timestamp = values.date.setHours(0);
			}
			else {
				values.timestamp = values.date.setHours(newHour);
			}

			if (!config.disableAmPm && newHour > 12) {
				newHour -= 12; 			// reduce the values with 12 hours
				setAmPm(false); 	// set to PM
			}

			executeChangeEvents('hour', 'onChange', input, oldValue);
			executeChangeEvents('hour', 'afterChange', input, oldValue);
		}
	};

	var getHours = function() {
		var currentHours = values.date.getHours();

		if (!config.disableAmPm) {
			var isAm = getIsAm();
			if (currentHours === 12 || currentHours === 0) {
				return 12;
			}
			return (currentHours < 12 && isAm) ? currentHours : currentHours - 12;
		}
		else {
			return currentHours;
		}
	};

	var setMinutes = function(input, preventAnimation) {
		var oldValue = values.date.getMinutes();
		var isChangeValid = validateChange('minute', input, oldValue);

		if (!config.validateAfter && !isChangeValid) {
			showInputSliderError(config.references.minutes);
			return;
		}
		executeChangeEvents('minute', 'beforeChange', input, oldValue);
		// TODO: validate
		var defaultValues = config.defaultValues.minutes;
		updateInputSlider(config.references.minutes, input, preventAnimation);

		if (config.validateAfter && !isChangeValid) {
			showInputSliderError(config.references.minutes);
			setTimeout(function() {
				updateInputSlider(config.references.minutes, oldValue, preventAnimation);

				executeChangeEvents('minute', 'onChange', input, oldValue);
				executeChangeEvents('minute', 'afterChange', input, oldValue);
			}, config.transitionValidationDelay);
		}
		else {
			values.timestamp = values.date.setMinutes(input);
			executeChangeEvents('minute', 'onChange', input, oldValue);
			executeChangeEvents('minute', 'afterChange', input, oldValue);
		}
	};

	var getMinutes = function() {
		return values.date.getMinutes();
	};

	var setAmPm = function(setAmPm) {

		if (config.disableAmPm) {
		 return;
		}

		var oldValue = getIsAm();
		if (!validateChange('ampm', setAmPm, oldValue)) {
			showInputRadioError(config.references.ampm, setAmPm);

			if (browser.isSafari) {
				setTimeout(function() {
					setRadioFormValue(config.references.ampm, oldValue);
				}, 10);

			}
			return false;
		}
		executeChangeEvents('ampm', 'beforeChange', setAmPm, oldValue);
		// TODO: validate

		var currentHours = values.date.getHours();
		var currentHoursCalculates = getHours();

		var currentIsAm = getIsAm();

		if (currentIsAm !== setAmPm) {
			if (setAmPm == true && currentHours >= 12 ) { // Set AM
				currentHours -= 12;
				values.timestamp = values.date.setHours(currentHours);
			}
			else if (setAmPm == false && currentHours < 12) { // Set PM
				currentHours += 12;
				values.timestamp = values.date.setHours(currentHours);
			}
		}

		values.ampm = setAmPm;
		setRadioFormValue(config.references.ampm, setAmPm);

		executeChangeEvents('ampm', 'onChange', setAmPm, oldValue);
		executeChangeEvents('ampm', 'afterChange', setAmPm, oldValue);
		return true;
	};

	var setRadioFormValue = function(reference, setAmPm) {

		// If the AM/PM is disabled we don't have t do anything here
		if (config.disableAmPm) {
		 return;
		}

		var divRadioInput = byId(reference);
		var formRadio = qSelect(divRadioInput, 'form');

		formRadio.ampm.value = setAmPm ? '1' : '0';
		var labelAmPm = setAmPm ? 'AM' : 'PM';

		var radioAm = qSelect(formRadio, 'input.mtr-input[type="radio"][value="1"]');
		var radioPm = qSelect(formRadio, 'input.mtr-input[type="radio"][value="0"]');

		var label = qSelect(formRadio, 'label[for="'+config.targetElement+'-radio-ampm-'+labelAmPm+'"]');
		var checkbox = qSelect(label, 'checkbox');

		if (setAmPm) {
			radioAm.setAttribute('checked', '');
			radioAm.checked = true;
			radioPm.removeAttribute('checked');
		}
		else {
			radioPm.setAttribute('checked', '');
			radioPm.checked = true;
			radioAm.removeAttribute('checked');
		}
	};

	var getIsAm = function() {
		var currentHours = values.date.getHours();
		return (currentHours >= 0 && currentHours <= 11) ? true : false;
		//return values.date.toLocaleTimeString().indexOf('AM') > -1 ? 1 : 0;
		//return values.ampm;
	};

	var getIsPm = function() {
		return !getIsAm();
	};

	var setDate = function(newDate, preventAnimation) {
		var oldValue = values.date.getDate();
		var isChangeValid = validateChange('day', newDate, oldValue);

		if (!config.validateAfter && !isChangeValid) {
			showInputSliderError(config.references.dates);
			return;
		}
		executeChangeEvents('day', 'beforeChange', newDate, oldValue);

		// TODO: Validate input
		updateInputSlider(config.references.dates, newDate, preventAnimation);

		if (config.validateAfter && !isChangeValid) {
			showInputSliderError(config.references.dates);
			setTimeout(function() {
				updateInputSlider(config.references.dates, oldValue, preventAnimation);

				executeChangeEvents('day', 'onChange', newDate, oldValue);
				executeChangeEvents('day', 'afterChange', newDate, oldValue);
			}, config.transitionValidationDelay);
		}
		else {
			values.timestamp = values.date.setDate(newDate);
			executeChangeEvents('day', 'onChange', newDate, oldValue);
			executeChangeEvents('day', 'afterChange', newDate, oldValue);
		}
	};

	var getDate = function() {
		return values.date.getDate();
	};

	var setMonth = function(newMonth, preventAnimation) {
		var oldValue = values.date.getMonth();
		var isChangeValid = validateChange('month', newMonth, oldValue);

		if (!config.validateAfter && !isChangeValid) {
			showInputSliderError(config.references.months);
			return;
		}
		executeChangeEvents('month', 'beforeChange', newMonth, oldValue);
		// TODO: Validate input

		// Finally, update the month
		updateInputSlider(config.references.months, newMonth, preventAnimation);

		if (config.validateAfter && !isChangeValid) {
			showInputSliderError(config.references.months);
			setTimeout(function() {
				updateInputSlider(config.references.months, oldValue, preventAnimation);

				executeChangeEvents('month', 'onChange', newMonth, oldValue);
				executeChangeEvents('month', 'afterChange', newMonth, oldValue);
			}, config.transitionValidationDelay);
		}
		else {
			values.timestamp = values.date.setMonth(newMonth);
			updateDate(newMonth);
			executeChangeEvents('month', 'onChange', newMonth, oldValue);
			executeChangeEvents('month', 'afterChange', newMonth, oldValue);
		}

	};

	var getMonth = function() {
		return values.date.getMonth();
	};

	var setYear = function(newYear, preventAnimation) {
		var oldValue = values.date.getFullYear();
		var isChangeValid = validateChange('year', newYear, oldValue);

		if (!config.validateAfter && !isChangeValid) {
			showInputSliderError(config.references.years);
			return;
		}
		executeChangeEvents('year', 'beforeChange', newYear, oldValue);
		// TODO: Validate input
		updateDate(undefined, newYear);
		updateInputSlider(config.references.years, newYear, preventAnimation);

		if (config.validateAfter && !isChangeValid) {
			showInputSliderError(config.references.years);
			setTimeout(function() {
				updateInputSlider(config.references.years, oldValue, preventAnimation);

				executeChangeEvents('year', 'onChange', newYear, oldValue);
				executeChangeEvents('year', 'afterChange', newYear, oldValue);
			}, config.transitionValidationDelay);
		}
		else {
			values.timestamp = values.date.setFullYear(newYear);
			executeChangeEvents('year', 'onChange', newYear, oldValue);
			executeChangeEvents('year', 'afterChange', newYear, oldValue);
		}
	};

	var getYear = function() {
		return values.date.getFullYear();
	};

	// Bigger getter and setters
	var getTime = function() {
		return getHours() + ':' + getMinutes();
	};

	var getFullTime = function() {
		return getHours() + ':' + getMinutes() + ' ' + (getIsAm() ? 'AM' : 'PM');
	};

	var setTimestamp = function(input) {
		var roundedTimestamp = roundUpTimestamp(input);

		values.date = new Date(roundedTimestamp);
		values.timestamp = roundedTimestamp;

		var currentHours = values.date.getHours(),
				currentMinutes = getMinutes(),
				currentAmPm = (currentHours >= 0 && currentHours < 12) ? true : false,
				currentDate = getDate(),
				currentMonth = getMonth(),
				currentYear = getYear();

		currentHours = (currentHours === 0) ? 12 : currentHours;

		setHours(currentHours);
		setMinutes(currentMinutes);
		setMonth(currentMonth);
		setYear(currentYear);
		setDate(currentDate);
		setAmPm(currentAmPm);
	};

	var getTimestamp = function() {
		return values.date.getTime();
	};

	/*****************************************************************************
	 * A lot of actions here (used when event is triggered)
	 ****************************************************************************/

	/**
	 * Update the value of the input slider
	 * @param  {string} reference id to the specific element
	 * @param  {integer} newValue
	 */
	var updateInputSlider = function(reference, newValue, preventAnimation) {
		var element = byId(reference);
		preventAnimation = preventAnimation || false;

		if (!element) {
			return;
		}

		// Find the specific value
		var divValues = qSelect(element, '.mtr-content'),
				divValue = qSelect(element, '.mtr-values .mtr-default-value[data-value="'+newValue+'"]'),
				divArrow = qSelect(element, '.mtr-arrow.up'),
				inputValue = qSelect(element, '.mtr-input');

				scrollTo = getRelativeOffset(divValues, divValue) + divArrow.clientHeight;

		inputValue.value = newValue;
		inputValue.setAttribute('data-old-value', newValue);

		if (config.animations === false || preventAnimation) {
			divValue.scrollIntoView();
		}
		else {
			smooth_scroll_to(divValues, scrollTo, config.transitionDelay);
		}
	};

	/**
	 * Add a error clas to the current input slider when a validation has failed
	 * @param  {String} reference id to the specific element
	 */
	var showInputSliderError = function(reference) {
		var element = byId(reference);
		var divContent = qSelect(element, '.mtr-content');
		addClass(divContent, 'mtr-error');

		setTimeout(function() {
			removeClass(divContent, 'mtr-error');
		}, config.transitionValidationDelay + 300);
	};

	var showInputRadioError = function(reference, value) {
		if (typeof value === 'boolean') {
						value = value === true ? 1 : 0;
				}

		var element = byId(reference);
		var divContent = qSelect(element, '.mtr-input[value="'+value+'"]');
		addClass(divContent, 'mtr-error');

		setTimeout(function() {
			removeClass(divContent, 'mtr-error');
		}, config.transitionValidationDelay + 300);
	};

	var executeChangeEvents = function(target, changeEvent, newValue, oldValue) {

		var callbackFunction = function(callback) {
			callback(target, newValue, oldValue);
		};

		events[changeEvent][target].forEach(function(callback) {
			callbackFunction(callback);
		});

		events[changeEvent].all.forEach(function(callback) {
			callbackFunction(callback);
		});

		switch (target) {
			case 'hour':
			case 'minute':
			case 'ampm':
				events[changeEvent].time.forEach(function(callback) {
					callbackFunction(callback);
				});
				break;
			case 'day':
			case 'month':
			case 'year':
				events[changeEvent].date.forEach(function(callback) {
					callbackFunction(callback);
				});
				break;
		}

	};

	/*****************************************************************************
	 * Some Aliases
	 ****************************************************************************/

	function byId(selector) {
		return document.getElementById(selector);
	}

	function qSelect(element, selector) {
		return element ? element.querySelector(selector) : null;
	}

	function getRelativeOffset(parent, child) {
		if (parent && child) {
			return child.offsetTop - parent.offsetTop;
		}
		return 0;
	}

	/**
	 * A simple function which makes a clone of a specific JS Object
	 * @param  {Object} obj
	 * @return {Object}
	 */
	function clone(obj) {
		var copy;

		// Handle the 3 simple types, and null or undefined
		if (null == obj || "object" != typeof obj) return obj;

		// Handle Array
		if (obj instanceof Array) {
				copy = [];
				for (var i = 0, len = obj.length; i < len; i++) {
						copy[i] = clone(obj[i]);
				}
				return copy;
		}

		// Handle Object
		if (obj instanceof Object) {
				copy = {};
				for (var attr in obj) {
						if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]);
				}
				return copy;
		}

		throw new Error("Unable to copy obj! Its type isn't supported.");
	}

	/**
	 * A simple shortcut function to add a class to specific element
	 * @param {HTMLElement} element
	 * @param {string} className
	 */
	function addClass(element, className) {
		if (!element) {
			return;
		}

		if (element.className.indexOf(className) > -1) {
			return;
		}

		element.className += ' ' + className;
	}

	/**
	 * Short allias for a function which is removing a class name from a specific element
	 * @param {HtmlElement} element
	 * @param {string} className
	 */
	function removeClass(element, className) {
		if (!element) {
			return;
		}

		if (element.className.indexOf(className) === -1) {
			return;
		}

		element.className = element.className.replace(new RegExp(className, 'g'), '');
	}

	/**
	 * Check is a specific input a number
	 * @param  {Number|String}  n
	 * @return {Boolean}
	 */
	function isNumber(input){
	 return Number(input) === input && input % 1 === 0;
	}

	/**
	 * Create array of values for a specific range with a givvent step
	 * @param  {object} settings
	 * @return {array}
	 */
	function createRange(settings) {
		var from = settings.min,
				to = settings.max,
				step = settings.step,
				range = [];

		for (var i=from; i<=to; i+=step) {
			range.push(i);
		}

		return range;
	}

	/**
	 * Create a special range with dates for a specific month
	 */
	function createRangeForDate(month, year) {
		var firstDay = new Date(year, month, 1);
		var lastDay = new Date(year, month + 1, 0);

		var range = {
			values: [],
			names: [],
			min: firstDay.getDate(),
			max: lastDay.getDate(),
			step: 1
		};

		var currentDate;
		for (var i=firstDay.getDate(); i<=lastDay.getDate(); i++) {
			currentDate = new Date(year, month, i);

			range.values.push(i);
			range.names[i] = config.daysNames[currentDate.getDay()];
		}

		return range;
	}

	/**
		Smoothly scroll element to the given target (element.scrollTop)
		for the given duration

		Returns a promise that's fulfilled when done, or rejected if
		interrupted
	*/
	var smooth_scroll_to = function(element, target, duration) {
		target = Math.round(target);
		duration = Math.round(duration);
		if (duration < 0) {
			return;
		}
		if (duration === 0) {
			element.scrollTop = target;
			return;
		}

		var start_time = Date.now();
		var end_time = start_time + duration;

		var start_top = element.scrollTop;
		var distance = target - start_top;

		// https://coderwall.com/p/hujlhg/smooth-scrolling-without-jquery
		// based on http://en.wikipedia.org/wiki/Smoothstep
		var smooth_step = function(start, end, point) {
			if(point <= start) { return 0; }
			if(point >= end) { return 1; }
			var x = (point - start) / (end - start); // interpolation
			return x*x*(3 - 2*x);
		};

		// This is to keep track of where the element's scrollTop is
		// supposed to be, based on what we're doing
		var previous_top = element.scrollTop;

		// This is like a think function from a game loop
		var scroll_frame = function() {
			if(element.scrollTop != previous_top) {
				//reject("interrupted");
				return;
			}

			// set the scrollTop for this frame
			var now = Date.now();
			var point = smooth_step(start_time, end_time, now);
			var frameTop = Math.round(start_top + (distance * point));
			element.scrollTop = frameTop;

			// check if we're done!
			if(now >= end_time) {
				return;
			}

			// If we were supposed to scroll but didn't, then we
			// probably hit the limit, so consider it done; not
			// interrupted.
			if(element.scrollTop === previous_top && element.scrollTop !== frameTop) {
				return;
			}
			previous_top = element.scrollTop;

			// schedule next frame for execution
			setTimeout(function() {
				scroll_frame();
			}, 0);
		};

		// boostrap the animation process
		setTimeout(function() {
			scroll_frame();
		}, 0);
	};

	/**
	 * Round up a timestamp to the closest monutes (11:35 to 11:40)
	 * @param  {Number} timestamp
	 * @return {Number}
	 */
	var roundUpTimestamp = function(timestamp) {
		var border = config.minutes.step * 60 * 1000;
		var delta = 0;

		// We should round up the timestamp only of the minutes step is not set to 1
		if (config.minutes.step > 1) {
			delta = (border - (timestamp % border)) % timestamp;
		}

		return (timestamp + delta);
	};

	/**
	 * Touch Support
	 * http://stackoverflow.com/questions/2264072/detect-a-finger-swipe-through-javascript-on-the-iphone-and-android
	 */

	var xDown = null;
	var yDown = null;

	function handleTouchStart(evt) {
		xDown = evt.touches[0].clientX;
		yDown = evt.touches[0].clientY;
	}

	/**
	 * @param  {Event} evt
	 * @return {Number}
	 */
	function handleTouchMove(evt, callback) {
		if (!xDown || !yDown) {
			return;
		}

		var xUp = evt.touches[0].clientX;
		var yUp = evt.touches[0].clientY;

		var xDiff = xDown - xUp;
		var yDiff = yDown - yUp;

		if (Math.abs(xDiff) > Math.abs(yDiff)) {
			if ( xDiff > 0 ) {
				/* left swipe */
			} else {
				/* right swipe */
			}
		} else {
			if ( yDiff > 0 ) {
				/* up swipe */
				callback(1);
			} else {
				/* down swipe */
				callback(-1);
			}
		}
		/* reset values */
		xDown = null;
		yDown = null;
	}

	/*****************************************************************************
	 * PUBLIC API
	 *
	 * Getters
	 ****************************************************************************/

	// Here is a set of the default Date function
	// We are providing them because the user are familiar with them and
	// maybe this way they will implemet this library easily in their system

	// "Wed Sep 23 2015"
	var toDateString = function() {
		return values.date.toDateString();
	};

	// "Wed, 23 Sep 2015 08:43:47 GMT"
	var toGMTString = function() {
		return values.date.toGMTString();
	};

	// "2015-09-23T08:43:47.284Z"
	var toISOString = function() {
		return values.date.toISOString();
	};

	// "9/23/2015"
	var toLocaleDateString = function() {
		return values.date.toLocaleDateString();
	};

	// "9/23/2015, 11:43:47 AM"
	var toLocaleString = function() {
		return values.date.toLocaleString();
	};

	// "11:43:47 AM"
	var toLocaleTimeString = function() {
		return values.date.toLocaleTimeString();
	};

	// "Wed Sep 23 2015 11:43:47 GMT+0300 (EEST)"
	var toString = function() {
		if (plugins.timezones) {
			return toDateString() + ' ' + toTimeString();
		}

		return values.date.toString();
	};

	// 11:43:47 GMT+0300 (EEST)"
	var toTimeString = function() {
		if (plugins.timezones) {
			var toReturn = '',
					timeString = values.date.toTimeString().split(' ');

			toReturn += timeString[0];
				toReturn += ' GMT' + (config.utcTimezone.offset > 0 ? '+' : '-') + (Math.abs(config.utcTimezone.offset) < 10 ? '0' : '') + Math.abs(config.utcTimezone.offset) + '00';
			toReturn += ' (' + config.utcTimezone.abbr + ')';

			return toReturn;
		}

		return values.date.toTimeString();
	};

	// "Wed, 23 Sep 2015 08:43:47 GMT"
	var toUTCString = function() {
		return values.date.toUTCString();
	};

	/**
	 * Return datetime in specific format
	 * @param  {String} input
	 * @return {String}
	 *
	 * M,MM, MMM
	 * d,D
	 * Y,YY, YYYY
	 *
	 * h, hh
	 * m, mm
	 * a, AA
	 * Z, ZZ
	 */
	var format = function(input) {
		var currentHours = getHours();
		var currentMinutes = getMinutes();
		var currentAmPm = getIsAm();

		var currentDate = getDate();
		var currentMonth = getMonth() + 1;
		var currentYear = getYear();
		var currentTimezone = config.utcTimezone.offset;

		// Dates
		input = specialReplace(input, 'DD', prependZero(currentDate));
		input = specialReplace(input, 'D', currentDate);

		// Years
		input = specialReplace(input, 'YYYY', currentYear);
		input = specialReplace(input, 'YY', currentYear.toString().substr(2));
		input = specialReplace(input, 'Y', currentYear);

		// Hours
		input = specialReplace(input, 'HH', prependZero(transformAmPm(currentHours, currentAmPm)));
		input = specialReplace(input, 'hh', prependZero(currentHours));
		input = specialReplace(input, 'H', transformAmPm(currentHours, currentAmPm));
		input = specialReplace(input, 'h', currentHours);

		// Minutes
		input = specialReplace(input, 'mm', prependZero(currentMinutes));
		input = specialReplace(input, 'm', getMinutes());

		// Am Pm
		input = specialReplace(input, 'a', currentAmPm ? 'am' : 'pm');
		input = specialReplace(input, 'A', currentAmPm ? 'AM' : 'PM');

		// Months
		input = specialReplace(input, 'MMM', config.monthsNames[currentMonth-1]);
		input = specialReplace(input, 'MM', prependZero(currentMonth));
		input = specialReplace(input, 'M', currentMonth);

		input = specialReplace(input, 'ZZ', (currentTimezone > 0 ? '+' : '-') + prependZero(Math.abs(currentTimezone)) + ':00');
		input = specialReplace(input, 'Z', (currentTimezone > 0 ? '+' : '-') + Math.abs(currentTimezone) + ':00');

		input = input.split('#%#').join('');

		function specialReplace(input, selector, value) {
			var specialDelimiter = '#%#';
			var regex = new RegExp(selector+'(?!'+specialDelimiter+')', 'g');
			input = input.replace(regex, value + specialDelimiter);
			return input;
		}

		function prependZero(value) {
			return value <= 9 ? ('0'+value) : value;
		}

		function transformAmPm(hours, ampm) {
			if (!config.disableAmPm) {
				if (hours === 12) {
					return ampm ? 0 : 12;
				}
				return ampm ? hours : hours + 12;
			}
			else {
				return hours;
			}
		}

		return input;
	};

	/*****************************************************************************
	 * PUBLIC API
	 *
	 * Events
	 ****************************************************************************/

	var onChange = function(target, callback) {
		events.onChange[target].push(callback);
	};

	var beforeChange = function(target, callback) {
		events.beforeChange[target].push(callback);
	};

	var afterChange = function(target, callback) {
		events.afterChange[target].push(callback);
	};

	function detectBrowser() {
		var browser = {
			isChrome: false,
			isSafari: false,
			isFirefox: false,
		};

		if (navigator.userAgent.search("Safari") >= 0 && navigator.userAgent.search("Chrome") < 0) {
			browser.isSafari = true;
		}

		return browser;
	}

		/**
	 * Public API here
	 */

	this.init = init;
	this.setConfig = setConfig;

	// Closing these interfaces, use format, instead of them
	// this.getHours = getHours;
	// this.getMinutes = getMinutes;
	// this.getIsAm = getIsAm;
	// this.getIsPm = getIsPm;
	// this.getTime = getTime;
	// this.getDate = getDate;
	// this.getMonth = getMonth;
	// this.getYear = getYear;
	this.getFullTime = getFullTime;
	this.getTimestamp = getTimestamp;

	this.setHours = setHours;
	this.setMinutes = setMinutes;
	this.setAmPm = setAmPm;
	this.setDate = setDate;
	this.setMonth = setMonth;
	this.setYear = setYear;
	this.setTimestamp = setTimestamp;

	this.values = values;

	// Here is the set with the default Date getters
	this.toDateString = toDateString;
	this.toGMTString = toGMTString;
	this.toISOString = toISOString;
	this.toLocaleDateString = toLocaleDateString;
	this.toLocaleString = toLocaleString;
	this.toLocaleTimeString = toLocaleTimeString;
	this.toString = toString;
	this.toTimeString = toTimeString;
	this.toUTCString = toUTCString;
	this.format = format;

	// Here are some events which the api provides
	this.onChange = onChange;
	this.beforeChange = beforeChange;
	this.afterChange = afterChange;

	// Lets init all
	init(inputConfig);

}