andela/eventx

View on GitHub
app/assets/javascripts/materialize.clockpicker.js

Summary

Maintainability
F
5 days
Test Coverage
/* jshint ignore:start */
/*!
 * ClockPicker v0.0.7 (http://weareoutman.github.io/clockpicker/)
 * Copyright 2014 Wang Shenwei.
 * Licensed under MIT (https://github.com/weareoutman/clockpicker/blob/gh-pages/LICENSE)
 *
 * Further modified
 * Copyright 2015 Ching Yaw Hao.
 */
(function () {
  var canvas, svg;
  var $ = window.jQuery, $win = $(window), $doc = $(document);
  // Can I use inline svg ?
  var svgNS = 'http://www.w3.org/2000/svg', svgSupported = 'SVGAngle' in window && function () {
      var supported, el = document.createElement('div');
      el.innerHTML = '<svg/>';
      supported = (el.firstChild && el.firstChild.namespaceURI) == svgNS;
      el.innerHTML = '';
      return supported;
    }();
  // Can I use transition ?
  var transitionSupported = function () {
    var style = document.createElement('div').style;
    return 'transition' in style || 'WebkitTransition' in style || 'MozTransition' in style || 'msTransition' in style || 'OTransition' in style;
  }();
  // Listen touch events in touch screen device, instead of mouse events in desktop.
  var touchSupported = 'ontouchstart' in window, mousedownEvent = 'mousedown' + (touchSupported ? ' touchstart' : ''), mousemoveEvent = 'mousemove.clockpicker' + (touchSupported ? ' touchmove.clockpicker' : ''), mouseupEvent = 'mouseup.clockpicker' + (touchSupported ? ' touchend.clockpicker' : '');
  // Vibrate the device if supported
  var vibrate = navigator.vibrate ? 'vibrate' : navigator.webkitVibrate ? 'webkitVibrate' : null;
  function createSvgElement(name) {
    return document.createElementNS(svgNS, name);
  }
  function leadingZero(num) {
    return (num < 10 ? '0' : '') + num;
  }
  // Get a unique id
  var idCounter = 0;
  function uniqueId(prefix) {
    var id = ++idCounter + '';
    return prefix ? prefix + id : id;
  }
  // Clock size
  var dialRadius = 135, outerRadius = 110,
    // innerRadius = 80 on 12 hour clock
    innerRadius = 80, tickRadius = 20, diameter = dialRadius * 2, duration = transitionSupported ? 350 : 1;
  // Popover template
  var tpl = [
    '<div class="clockpicker picker">',
    '<div class="picker__holder">',
    '<div class="picker__frame">',
    '<div class="picker__wrap">',
    '<div class="picker__box">',
    '<div class="picker__date-display">',
    '<div class="clockpicker-display">',
    '<div class="clockpicker-display-column">',
    '<span class="clockpicker-span-hours text-primary"></span>',
    ':',
    '<span class="clockpicker-span-minutes"></span>',
    '</div>',
    '<div class="clockpicker-display-column clockpicker-display-am-pm">',
    '<div class="clockpicker-span-am-pm"></div>',
    '</div>',
    '</div>',
    '</div>',
    '<div class="picker__calendar-container">',
    '<div class="clockpicker-plate">',
    '<div class="clockpicker-canvas"></div>',
    '<div class="clockpicker-dial clockpicker-hours"></div>',
    '<div class="clockpicker-dial clockpicker-minutes clockpicker-dial-out"></div>',
    '</div>',
    '<div class="clockpicker-am-pm-block">',
    '</div>',
    '</div>',
    '<div class="picker__footer">',
    '</div>',
    '</div>',
    '</div>',
    '</div>',
    '</div>',
    '</div>'
  ].join('');
  // ClockPicker
  function ClockPicker(element, options) {
    var popover = $(tpl), plate = popover.find('.clockpicker-plate'), holder = popover.find('.picker__holder'), hoursView = popover.find('.clockpicker-hours'), minutesView = popover.find('.clockpicker-minutes'), amPmBlock = popover.find('.clockpicker-am-pm-block'), isInput = element.prop('tagName') === 'INPUT', input = isInput ? element : element.find('input'), label = $('label[for=' + input.attr('id') + ']'), self = this, timer;
    this.id = uniqueId('cp');
    this.element = element;
    this.holder = holder;
    this.options = options;
    this.isAppended = false;
    this.isShown = false;
    this.currentView = 'hours';
    this.isInput = isInput;
    this.input = input;
    this.label = label;
    this.popover = popover;
    this.plate = plate;
    this.hoursView = hoursView;
    this.minutesView = minutesView;
    this.amPmBlock = amPmBlock;
    this.spanHours = popover.find('.clockpicker-span-hours');
    this.spanMinutes = popover.find('.clockpicker-span-minutes');
    this.spanAmPm = popover.find('.clockpicker-span-am-pm');
    this.footer = popover.find('.picker__footer');
    this.amOrPm = 'PM';
    // Setup for for 12 hour clock if option is selected
    if (options.twelvehour) {
      var amPmButtonsTemplate = [
        '<div class="clockpicker-am-pm-block">',
        '<button type="button" class="btn-floating btn-flat clockpicker-button clockpicker-am-button">',
        'AM',
        '</button>',
        '<button type="button" class="btn-floating btn-flat clockpicker-button clockpicker-pm-button">',
        'PM',
        '</button>',
        '</div>'
      ].join('');
      var amPmButtons = $(amPmButtonsTemplate);
      if (!options.ampmclickable) {
        $('<button type="button" class="btn-floating btn-flat clockpicker-button am-button" tabindex="1">' + 'AM' + '</button>').on('click', function () {
          self.amOrPm = 'AM';
          self.amPmBlock.children('.pm-button').removeClass('active');
          self.amPmBlock.children('.am-button').addClass('active');
          self.spanAmPm.empty().append('AM');
        }).appendTo(this.amPmBlock);
        $('<button type="button" class="btn-floating btn-flat clockpicker-button pm-button" tabindex="2">' + 'PM' + '</button>').on('click', function () {
          self.amOrPm = 'PM';
          self.amPmBlock.children('.am-button').removeClass('active');
          self.amPmBlock.children('.pm-button').addClass('active');
          self.spanAmPm.empty().append('PM');
        }).appendTo(this.amPmBlock);
      } else {
        this.spanAmPm.empty();
        $('<div id="click-am">AM</div>').on('click', function () {
          self.spanAmPm.children('#click-am').addClass('text-primary');
          self.spanAmPm.children('#click-pm').removeClass('text-primary');
          self.amOrPm = 'AM';
        }).appendTo(this.spanAmPm);
        $('<div id="click-pm">PM</div>').on('click', function () {
          self.spanAmPm.children('#click-pm').addClass('text-primary');
          self.spanAmPm.children('#click-am').removeClass('text-primary');
          self.amOrPm = 'PM';
        }).appendTo(this.spanAmPm);
      }
    }
    if (options.darktheme) {
      popover.addClass('darktheme');
    }
    // If autoclose is not setted, append a button
    $('<button type="button" class="btn-flat clockpicker-button" tabindex="' + (options.twelvehour ? '3' : '1') + '">' + options.donetext + '</button>').click($.proxy(this.done, this)).appendTo(this.footer);
    this.spanHours.click($.proxy(this.toggleView, this, 'hours'));
    this.spanMinutes.click($.proxy(this.toggleView, this, 'minutes'));
    // Show or toggle
    input.on('focus.clockpicker click.clockpicker', $.proxy(this.show, this));
    // Build ticks
    var tickTpl = $('<div class="clockpicker-tick"></div>'), i, tick, radian, radius;
    // Hours view
    if (options.twelvehour) {
      for (i = 1; i < 13; i += 1) {
        tick = tickTpl.clone();
        radian = i / 6 * Math.PI;
        radius = outerRadius;
        tick.css('font-size', '140%');
        tick.css({
          left: dialRadius + Math.sin(radian) * radius - tickRadius,
          top: dialRadius - Math.cos(radian) * radius - tickRadius
        });
        tick.html(i === 0 ? '00' : i);
        hoursView.append(tick);
        tick.on(mousedownEvent, mousedown);
      }
    } else {
      for (i = 0; i < 24; i += 1) {
        tick = tickTpl.clone();
        radian = i / 6 * Math.PI;
        var inner = i > 0 && i < 13;
        radius = inner ? innerRadius : outerRadius;
        tick.css({
          left: dialRadius + Math.sin(radian) * radius - tickRadius,
          top: dialRadius - Math.cos(radian) * radius - tickRadius
        });
        if (inner) {
          tick.css('font-size', '120%');
        }
        tick.html(i === 0 ? '00' : i);
        hoursView.append(tick);
        tick.on(mousedownEvent, mousedown);
      }
    }
    // Minutes view
    for (i = 0; i < 60; i += 5) {
      tick = tickTpl.clone();
      radian = i / 30 * Math.PI;
      tick.css({
        left: dialRadius + Math.sin(radian) * outerRadius - tickRadius,
        top: dialRadius - Math.cos(radian) * outerRadius - tickRadius
      });
      tick.css('font-size', '140%');
      tick.html(leadingZero(i));
      minutesView.append(tick);
      tick.on(mousedownEvent, mousedown);
    }
    // Clicking on minutes view space
    plate.on(mousedownEvent, function (e) {
      if ($(e.target).closest('.clockpicker-tick').length === 0) {
        mousedown(e, true);
      }
    });
    // Mousedown or touchstart
    function mousedown(e, space) {
      var offset = plate.offset(), isTouch = /^touch/.test(e.type), x0 = offset.left + dialRadius, y0 = offset.top + dialRadius, dx = (isTouch ? e.originalEvent.touches[0] : e).pageX - x0, dy = (isTouch ? e.originalEvent.touches[0] : e).pageY - y0, z = Math.sqrt(dx * dx + dy * dy), moved = false;
      // When clicking on minutes view space, check the mouse position
      if (space && (z < outerRadius - tickRadius || z > outerRadius + tickRadius)) {
        return;
      }
      e.preventDefault();
      // Set cursor style of body after 200ms
      var movingTimer = setTimeout(function () {
        self.popover.addClass('clockpicker-moving');
      }, 200);
      // Place the canvas to top
      if (svgSupported) {
        plate.append(self.canvas);
      }
      // Clock
      self.setHand(dx, dy, !space, true);
      // Mousemove on document
      $doc.off(mousemoveEvent).on(mousemoveEvent, function (e) {
        e.preventDefault();
        var isTouch = /^touch/.test(e.type), x = (isTouch ? e.originalEvent.touches[0] : e).pageX - x0, y = (isTouch ? e.originalEvent.touches[0] : e).pageY - y0;
        if (!moved && x === dx && y === dy) {
          // Clicking in chrome on windows will trigger a mousemove event
          return;
        }
        moved = true;
        self.setHand(x, y, false, true);
      });
      // Mouseup on document
      $doc.off(mouseupEvent).on(mouseupEvent, function (e) {
        $doc.off(mouseupEvent);
        e.preventDefault();
        var isTouch = /^touch/.test(e.type), x = (isTouch ? e.originalEvent.changedTouches[0] : e).pageX - x0, y = (isTouch ? e.originalEvent.changedTouches[0] : e).pageY - y0;
        if ((space || moved) && x === dx && y === dy) {
          self.setHand(x, y);
        }
        if (self.currentView === 'hours') {
          self.toggleView('minutes', duration / 2);
        } else if (options.autoclose) {
          self.minutesView.addClass('clockpicker-dial-out');
          setTimeout(function () {
            self.done();
          }, duration / 2);
        }
        plate.prepend(canvas);
        // Reset cursor style of body
        clearTimeout(movingTimer);
        self.popover.removeClass('clockpicker-moving');
        // Unbind mousemove event
        $doc.off(mousemoveEvent);
      });
    }
    if (svgSupported) {
      // Draw clock hands and others
      canvas = popover.find('.clockpicker-canvas');
      svg = createSvgElement('svg');
      svg.setAttribute('class', 'clockpicker-svg');
      svg.setAttribute('width', diameter);
      svg.setAttribute('height', diameter);
      var g = createSvgElement('g');
      g.setAttribute('transform', 'translate(' + dialRadius + ',' + dialRadius + ')');
      var bearing = createSvgElement('circle');
      bearing.setAttribute('class', 'clockpicker-canvas-bearing');
      bearing.setAttribute('cx', 0);
      bearing.setAttribute('cy', 0);
      bearing.setAttribute('r', 2);
      var hand = createSvgElement('line');
      hand.setAttribute('x1', 0);
      hand.setAttribute('y1', 0);
      var bg = createSvgElement('circle');
      bg.setAttribute('class', 'clockpicker-canvas-bg');
      bg.setAttribute('r', tickRadius);
      var fg = createSvgElement('circle');
      fg.setAttribute('class', 'clockpicker-canvas-fg');
      fg.setAttribute('r', 5);
      g.appendChild(hand);
      g.appendChild(bg);
      g.appendChild(fg);
      g.appendChild(bearing);
      svg.appendChild(g);
      canvas.append(svg);
      this.hand = hand;
      this.bg = bg;
      this.fg = fg;
      this.bearing = bearing;
      this.g = g;
      this.canvas = canvas;
    }
    raiseCallback(this.options.init);
  }
  function raiseCallback(callbackFunction) {
    if (callbackFunction && typeof callbackFunction === 'function') {
      callbackFunction();
    }
  }
  // Default options
  ClockPicker.DEFAULTS = {
    'default': '',
    // default time, 'now' or '13:14' e.g.
    fromnow: 0,
    // set default time to * milliseconds from now (using with default = 'now')
    donetext: 'Done',
    // done button text
    autoclose: false,
    // auto close when minute is selected
    ampmclickable: false,
    // set am/pm button on itself
    darktheme: false,
    // set to dark theme
    twelvehour: true,
    // change to 12 hour AM/PM clock from 24 hour
    vibrate: true  // vibrate the device when dragging clock hand
  };
  // Show or hide popover
  ClockPicker.prototype.toggle = function () {
    this[this.isShown ? 'hide' : 'show']();
  };
  // Set popover position
  ClockPicker.prototype.locate = function () {
    var element = this.element, popover = this.popover, offset = element.offset(), width = element.outerWidth(), height = element.outerHeight(), align = this.options.align, self = this;
    popover.show();
  };
  // Show popover
  ClockPicker.prototype.show = function (e) {
    // Not show again
    if (this.isShown) {
      return;
    }
    raiseCallback(this.options.beforeShow);
    $(':input').each(function () {
      $(this).attr('tabindex', -1);
    });
    var self = this;
    // Initialize
    this.input.blur();
    this.popover.addClass('picker--opened');
    this.input.addClass('picker__input picker__input--active');
    $(document.body).css('overflow', 'hidden');
    if (!this.isAppended) {
      // Append popover to body
      this.popover.insertAfter(this.input);
      if (this.options.twelvehour) {
        this.amOrPm = 'PM';
        if (!this.options.ampmclickable) {
          this.amPmBlock.children('.am-button').removeClass('active');
          this.amPmBlock.children('.pm-button').addClass('active');
          this.spanAmPm.empty().append('PM');
        } else {
          this.spanAmPm.children('#click-pm').addClass('text-primary');
          this.spanAmPm.children('#click-am').removeClass('text-primary');
        }
      }
      // Reset position when resize
      $win.on('resize.clockpicker' + this.id, function () {
        if (self.isShown) {
          self.locate();
        }
      });
      this.isAppended = true;
    }
    // Get the time
    var value = ((this.input.prop('value') || this.options['default'] || '') + '').split(':');
    if (this.options.twelvehour && typeof value[1] !== 'undefined') {
      value[1] = value[1].replace('AM', '').replace('PM', '');
    }
    if (value[0] === 'now') {
      var now = new Date(+new Date() + this.options.fromnow);
      value = [
        now.getHours(),
        now.getMinutes()
      ];
    }
    this.hours = +value[0] || 0;
    this.minutes = +value[1] || 0;
    this.spanHours.html(leadingZero(this.hours));
    this.spanMinutes.html(leadingZero(this.minutes));
    // Toggle to hours view
    this.toggleView('hours');
    // Set position
    this.locate();
    this.isShown = true;
    // Hide when clicking or tabbing on any element except the clock and input
    $doc.on('click.clockpicker.' + this.id + ' focusin.clockpicker.' + this.id, function (e) {
      var target = $(e.target);
      if (target.closest(self.popover.find('.picker__wrap')).length === 0 && target.closest(self.input).length === 0) {
        self.hide();
      }
    });
    // Hide when ESC is pressed
    $doc.on('keyup.clockpicker.' + this.id, function (e) {
      if (e.keyCode === 27) {
        self.hide();
      }
    });
    raiseCallback(this.options.afterShow);
  };
  // Hide popover
  ClockPicker.prototype.hide = function () {
    raiseCallback(this.options.beforeHide);
    this.input.removeClass('picker__input picker__input--active');
    this.popover.removeClass('picker--opened');
    $(document.body).css('overflow', 'visible');
    this.isShown = false;
    $(':input').each(function (index) {
      $(this).attr('tabindex', index + 1);
    });
    // Unbinding events on document
    $doc.off('click.clockpicker.' + this.id + ' focusin.clockpicker.' + this.id);
    $doc.off('keyup.clockpicker.' + this.id);
    this.popover.hide();
    raiseCallback(this.options.afterHide);
  };
  // Toggle to hours or minutes view
  ClockPicker.prototype.toggleView = function (view, delay) {
    var raiseAfterHourSelect = false;
    if (view === 'minutes' && $(this.hoursView).css('visibility') === 'visible') {
      raiseCallback(this.options.beforeHourSelect);
      raiseAfterHourSelect = true;
    }
    var isHours = view === 'hours', nextView = isHours ? this.hoursView : this.minutesView, hideView = isHours ? this.minutesView : this.hoursView;
    this.currentView = view;
    this.spanHours.toggleClass('text-primary', isHours);
    this.spanMinutes.toggleClass('text-primary', !isHours);
    // Let's make transitions
    hideView.addClass('clockpicker-dial-out');
    nextView.css('visibility', 'visible').removeClass('clockpicker-dial-out');
    // Reset clock hand
    this.resetClock(delay);
    // After transitions ended
    clearTimeout(this.toggleViewTimer);
    this.toggleViewTimer = setTimeout(function () {
      hideView.css('visibility', 'hidden');
    }, duration);
    if (raiseAfterHourSelect) {
      raiseCallback(this.options.afterHourSelect);
    }
  };
  // Reset clock hand
  ClockPicker.prototype.resetClock = function (delay) {
    var view = this.currentView, value = this[view], isHours = view === 'hours', unit = Math.PI / (isHours ? 6 : 30), radian = value * unit, radius = isHours && value > 0 && value < 13 ? innerRadius : outerRadius, x = Math.sin(radian) * radius, y = -Math.cos(radian) * radius, self = this;
    if (svgSupported && delay) {
      self.canvas.addClass('clockpicker-canvas-out');
      setTimeout(function () {
        self.canvas.removeClass('clockpicker-canvas-out');
        self.setHand(x, y);
      }, delay);
    } else {
      this.setHand(x, y);
    }
  };
  // Set clock hand to (x, y)
  ClockPicker.prototype.setHand = function (x, y, roundBy5, dragging) {
    var radian = Math.atan2(x, -y), isHours = this.currentView === 'hours', unit = Math.PI / (isHours || roundBy5 ? 6 : 30), z = Math.sqrt(x * x + y * y), options = this.options, inner = isHours && z < (outerRadius + innerRadius) / 2, radius = inner ? innerRadius : outerRadius, value;
    if (options.twelvehour) {
      radius = outerRadius;
    }
    // Radian should in range [0, 2PI]
    if (radian < 0) {
      radian = Math.PI * 2 + radian;
    }
    // Get the round value
    value = Math.round(radian / unit);
    // Get the round radian
    radian = value * unit;
    // Correct the hours or minutes
    if (options.twelvehour) {
      if (isHours) {
        if (value === 0) {
          value = 12;
        }
      } else {
        if (roundBy5) {
          value *= 5;
        }
        if (value === 60) {
          value = 0;
        }
      }
    } else {
      if (isHours) {
        if (value === 12) {
          value = 0;
        }
        value = inner ? value === 0 ? 12 : value : value === 0 ? 0 : value + 12;
      } else {
        if (roundBy5) {
          value *= 5;
        }
        if (value === 60) {
          value = 0;
        }
      }
    }
    if (isHours) {
      this.fg.setAttribute('class', 'clockpicker-canvas-fg');
    } else {
      if (value % 5 === 0) {
        this.fg.setAttribute('class', 'clockpicker-canvas-fg');
      } else {
        this.fg.setAttribute('class', 'clockpicker-canvas-fg active');
      }
    }
    // Once hours or minutes changed, vibrate the device
    if (this[this.currentView] !== value) {
      if (vibrate && this.options.vibrate) {
        // Do not vibrate too frequently
        if (!this.vibrateTimer) {
          navigator[vibrate](10);
          this.vibrateTimer = setTimeout($.proxy(function () {
            this.vibrateTimer = null;
          }, this), 100);
        }
      }
    }
    this[this.currentView] = value;
    this[isHours ? 'spanHours' : 'spanMinutes'].html(leadingZero(value));
    // If svg is not supported, just add an active class to the tick
    if (!svgSupported) {
      this[isHours ? 'hoursView' : 'minutesView'].find('.clockpicker-tick').each(function () {
        var tick = $(this);
        tick.toggleClass('active', value === +tick.html());
      });
      return;
    }
    // Place clock hand at the top when dragging
    if (dragging || !isHours && value % 5) {
      this.g.insertBefore(this.hand, this.bearing);
      this.g.insertBefore(this.bg, this.fg);
      this.bg.setAttribute('class', 'clockpicker-canvas-bg clockpicker-canvas-bg-trans');
    } else {
      // Or place it at the bottom
      this.g.insertBefore(this.hand, this.bg);
      this.g.insertBefore(this.fg, this.bg);
      this.bg.setAttribute('class', 'clockpicker-canvas-bg');
    }
    // Set clock hand and others' position
    var cx1 = Math.sin(radian) * (radius - tickRadius), cy1 = -Math.cos(radian) * (radius - tickRadius), cx2 = Math.sin(radian) * radius, cy2 = -Math.cos(radian) * radius;
    this.hand.setAttribute('x2', cx1);
    this.hand.setAttribute('y2', cy1);
    this.bg.setAttribute('cx', cx2);
    this.bg.setAttribute('cy', cy2);
    this.fg.setAttribute('cx', cx2);
    this.fg.setAttribute('cy', cy2);
  };
  // Hours and minutes are selected
  ClockPicker.prototype.done = function () {
    raiseCallback(this.options.beforeDone);
    this.hide();
    this.label.addClass('active');
    var last = this.input.prop('value'), value = leadingZero(this.hours) + ':' + leadingZero(this.minutes);
    if (this.options.twelvehour) {
      value = value + this.amOrPm;
    }
    this.input.prop('value', value);
    if (value !== last) {
      this.input.triggerHandler('change');
      if (!this.isInput) {
        this.element.trigger('change');
      }
    }
    if (this.options.autoclose) {
      this.input.trigger('blur');
    }
    raiseCallback(this.options.afterDone);
  };
  // Remove clockpicker from input
  ClockPicker.prototype.remove = function () {
    this.element.removeData('clockpicker');
    this.input.off('focus.clockpicker click.clockpicker');
    if (this.isShown) {
      this.hide();
    }
    if (this.isAppended) {
      $win.off('resize.clockpicker' + this.id);
      this.popover.remove();
    }
  };
  // Extends $.fn.clockpicker
  $.fn.pickatime = function (option) {
    var args = Array.prototype.slice.call(arguments, 1);
    return this.each(function () {
      var $this = $(this), data = $this.data('clockpicker');
      if (!data) {
        var options = $.extend({}, ClockPicker.DEFAULTS, $this.data(), typeof option == 'object' && option);
        $this.data('clockpicker', new ClockPicker($this, options));
      } else {
        // Manual operatsions. show, hide, remove, e.g.
        if (typeof data[option] === 'function') {
          data[option].apply(data, args);
        }
      }
    });
  };
}());  /* jshint ignore:end */