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
 */
// eslint-disable-next-line no-unused-vars
function MtrDatepicker (inputConfig) {
  // The real implementation of the library starts here

  // The main configuration properties
  // All of them can be overridden 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
    },

    // Responsible for the transition of the sliders - animated or static
    animations: true,

    // Make auto switch between AM/PM when moving from 11AM to 12PM or backwards
    smartHours: false,

    // Validate the date to be only in the future
    future: false,

    // Disable the 12 hours time format and go to a full 24 hours experience
    disableAmPm: false,

    // perform the future validation after the date change
    validateAfter: true,

    // change the local timezone to a specific one
    utcTimezone: 0,

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

    // Show the fields related with the date
    datepicker: true,

    // Show the fields related with the time
    timepicker: true,

    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 errors in the config.');
      return;
    }

    setConfig(inputConfig);

    targetElement = byId(config.targetElement);

    setDatesRange();
    createMarkup();
  };

  /**
   * Detach all event listeners and clear the markup of the component
   */
  var destroy = function () {
    // Clear all attached events
    Object.keys(events).forEach(function (eventType) {
      Object.keys(events[eventType]).forEach(function (fieldEvent) {
        events[eventType][fieldEvent] = [];
      });
    });

    // Clear the markup
    while (targetElement.firstChild) {
      targetElement.removeChild(targetElement.firstChild);
    }
  }

  /**
   * Attaching the user input config settings to override 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;
    config.datepicker = input.datepicker !== undefined ? input.datepicker : config.datepicker;
    config.timepicker = input.timepicker !== undefined ? input.timepicker : config.timepicker;

    // Change the defaults 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;

    if (input.datepicker !== undefined && input.timepicker !== undefined) {
      if (!input.datepicker && !input.timepicker) {
        console.error('Invalid configuration: cannot disable both datepicker and timepicker at the same time.');
        result = false;
      }
    }

    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 enabled, 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 errors 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 occurred during the initialization of the datepicker.'));

      targetElement.appendChild(errorElement);
    }

    return result;
  };

  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 switching 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
    if (config.timepicker) {
      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
    if (config.datepicker) {
      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';

      if (Array.isArray(config.datepicker)) {
        // If provided use the desired order of the date fields
        config.datepicker.forEach(function (field) {
          switch (field) {
            case 'months': rowDate.appendChild(monthElement); break;
            case 'dates': rowDate.appendChild(dateElement); break;
            case 'years': rowDate.appendChild(yearElement); break;
            default: break;
          }
        });
      } else {
        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 definitions 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':
            // 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':
            // 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 called after specific amount of time
        // because it can be caused 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 shouldn't 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;
          }

          // Trim the leading zero
          newValue = parseInt(newValue);

          // 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);

      // Accept the new values on <Enter>
      inputValue.addEventListener('keyup', function (e) {
        if (e.keyCode === 13) {
          e.preventDefault();
          inputValue.blur();
        }
      }, 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);
        var newValue;

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

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

        if (wheelData > 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 change 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;
          var 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  {object} 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;
      var inputValue = qSelect(parent, '.mtr-input');

      // If we are working with months we have to increment 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 changing 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 recalculate the dates in the specific month and check
   * the position 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);

    if (config.datepicker && (
      Array.isArray(config.datepicker) && config.datepicker.indexOf('dates') > -1
    )) {
      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;
  };

  /**
   * 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();
    var datePicker = new Date(values.date.getTime());

    switch (target) {
      case 'hour':
        var isAm = getIsAm();
        if (isAm && newValue === 12) {
          newValue = 0;
        }

        datePicker.setHours(newValue);
        break;
      case 'minute': datePicker.setMinutes(newValue); break;
      case 'ampm':
        var currentHours = datePicker.getHours();
        var 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 hours 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;
    }

    setAmPm = !!setAmPm;

    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 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 timepicker or the AM/PM are disabled we don't have t do anything here
    if (!config.timepicker || config.disableAmPm) {
      return;
    }

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

    formRadio.ampm.value = setAmPm ? '1' : '0';

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

    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;
  };

  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);

    // TODO: Validate the number of days in the new month
    // Currently fails when transition from Mar 30 to Feb

    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 () {
    var hours = getHours();
    var minutes = getMinutes();
    var amPm = getIsAm() ? 'AM' : 'PM';

    if (minutes <= 9) {
      minutes = '0' + minutes;
    }

    return hours + ':' + minutes + ' ' + amPm;
  };

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

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

    var currentHours = values.date.getHours();
    var currentMinutes = getMinutes();
    var currentAmPm = currentHours >= 0 && currentHours < 12;
    var currentDate = getDate();
    var currentMonth = getMonth();
    var 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');
    var divValue = qSelect(element, '.mtr-values .mtr-default-value[data-value="' + newValue + '"]');
    var divArrow = qSelect(element, '.mtr-arrow.up');
    var inputValue = qSelect(element, '.mtr-input');

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

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

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

  /**
   * Add an error class 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 (obj === null || typeof obj !== 'object') 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) {
        // eslint-disable-next-line no-prototype-builtins
        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 alias 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 given step
   * @param  {object} settings
   * @return {array}
   */
  function createRange (settings) {
    var from = settings.min;
    var to = settings.max;
    var step = settings.step;
    var 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 smoothScrollTo = function (element, target, duration) {
    target = Math.round(target);
    duration = Math.round(duration);
    if (duration < 0) {
      return;
    }
    if (duration === 0) {
      element.scrollTop = target;
      return;
    }

    var startTime = Date.now();
    var endTime = startTime + duration;

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

    // https://coderwall.com/p/hujlhg/smooth-scrolling-without-jquery
    // based on http://en.wikipedia.org/wiki/Smoothstep
    var smoothStep = 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 previousTop = element.scrollTop;

    // This is like a think function from a game loop
    var scrollFrame = function () {
      if (element.scrollTop !== previousTop) {
        return;
      }

      // set the scrollTop for this frame
      var now = Date.now();
      var point = smoothStep(startTime, endTime, now);
      var frameTop = Math.round(startTop + (distance * point));
      element.scrollTop = frameTop;

      // check if we're done!
      if (now >= endTime) {
        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 === previousTop && element.scrollTop !== frameTop) {
        return;
      }
      previousTop = element.scrollTop;

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

    // bootstrap the animation process
    setTimeout(function () {
      scrollFrame();
    }, 0);
  };

  /**
   * Round up a timestamp to the closest minutes (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 */
        // eslint-disable-next-line standard/no-callback-literal
        callback(1);
      } else {
        /* down swipe */
        // eslint-disable-next-line standard/no-callback-literal
        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 implement 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 () {
    // Remove the offsets of the timezone
    var localTimezoneOffsetTimestamp = values.date.getTime() - (values.date.getTimezoneOffset() * 60 * 1000);
    var timezoneOffsetTimestamp = localTimezoneOffsetTimestamp - (config.utcTimezone.offset * 60 * 60 * 1000);

    var date = new Date(timezoneOffsetTimestamp);

    return 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 = '';
      var 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 () {
    // Remove the offsets of the timezone
    var localTimezoneOffsetTimestamp = values.date.getTime() - (values.date.getTimezoneOffset() * 60 * 1000);
    var timezoneOffsetTimestamp = localTimezoneOffsetTimestamp - (config.utcTimezone.offset * 60 * 60 * 1000);

    var date = new Date(timezoneOffsetTimestamp);

    return date.toUTCString();
  };

  /**
   * Return datetime in specific format
   * @param  {String} input
   * @return {String}
   *
   * M,MM, MMM
   * D,DD
   * 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
    // TODO: Make a special case for month with M (March, May)
    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;
  this.destroy = destroy;

  // 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);
}