diff --git a/README.md b/README.md index 2365792..a1f15e5 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ -**I will not continue to maintain this gem as I don't use Rails asset pipeline anymore, if you are interested, [contact me](mailto:tkrotoff@gmail.com).** - # jQuery timepicker for Rails +[![Gem Version](https://badge.fury.io/rb/jquery-timepicker-rails.png)](http://badge.fury.io/rb/jquery-timepicker-rails) -jquery-timepicker packaged for the Rails 3.1+ asset pipeline. -Check [jquery-timepicker project home page](http://github.com/jonthornton/jquery-timepicker). +jquery-timepicker and Datepair.js packaged for the Rails 3.1+ asset pipeline. +See the [jquery-timepicker project home page](http://github.com/jonthornton/jquery-timepicker) and the [Datepair.js home page](https://github.com/jonthornton/Datepair.js). ## Installation @@ -29,16 +28,16 @@ Add the following stylesheet file to `app/assets/stylesheets/application.css`: *= require jquery.timepicker.css -Optionally, you can also use `datepair.js`: - - //= require datepair.js +You can also include `Datepair.js`, and optionally it's accompanying jquery plugin, `jquery.datepair.js`: -Most people will prefer to copy-paste this file in order to customize it. + //= require Datepair + //= require jquery.datepair.js -jquery-timepicker depends on jQuery and [bootstrap-datepicker](http://github.com/eternicode/bootstrap-datepicker) or [jQuery UI Datepicker](http://jqueryui.com/demos/datepicker/). +Datepair.js depends on [bootstrap-datepicker](http://github.com/eternicode/bootstrap-datepicker) or [jQuery UI Datepicker](http://jqueryui.com/demos/datepicker/). +jquery-timepicker depends on `jQuery`. ## License jquery-timepicker is being developed by [Jon Thornton](http://jonthornton.com/) and is under [MIT license](http://en.wikipedia.org/wiki/MIT_License). -This gem is also licensed under [MIT license](https://raw.github.com/tkrotoff/jquery-timepicker-rails/master/LICENSE). +This gem is also licensed under [MIT license](https://raw.github.com/tkrotoff/jquery-timepicker-rails/master/LICENSE.txt). diff --git a/jquery-timepicker-rails.gemspec b/jquery-timepicker-rails.gemspec index 1572d95..cb5be28 100644 --- a/jquery-timepicker-rails.gemspec +++ b/jquery-timepicker-rails.gemspec @@ -6,11 +6,11 @@ require 'jquery-timepicker-rails/version' Gem::Specification.new do |spec| spec.name = "jquery-timepicker-rails" spec.version = Jquery::Timepicker::Rails::VERSION - spec.authors = ["Tanguy Krotoff (jQuery plugin by Jon Thornton)"] + spec.authors = ["Tanguy Krotoff (jQuery plugin by Jon Thornton)", "Fabio Cantoni"] spec.email = ["tkrotoff@gmail.com"] spec.description = %q{A jQuery timepicker plugin inspired by Google Calendar} spec.summary = %q{jquery-timepicker packaged for the Rails 3.1+ asset pipeline} - spec.homepage = "http://github.com/tkrotoff/jquery-timepicker-rails" + spec.homepage = "https://github.com/cover/jquery-timepicker-rails" spec.license = "MIT" spec.files = `git ls-files`.split($/) diff --git a/lib/jquery-timepicker-rails/version.rb b/lib/jquery-timepicker-rails/version.rb index 05e8354..4134b04 100644 --- a/lib/jquery-timepicker-rails/version.rb +++ b/lib/jquery-timepicker-rails/version.rb @@ -1,7 +1,7 @@ module Jquery module Timepicker module Rails - VERSION = "1.2.5.0" + VERSION = '1.11.10' end end end diff --git a/vendor/assets/javascripts/Datepair.js b/vendor/assets/javascripts/Datepair.js new file mode 100644 index 0000000..e57da6a --- /dev/null +++ b/vendor/assets/javascripts/Datepair.js @@ -0,0 +1,361 @@ +/*! + * datepair.js v0.4.15 - A javascript plugin for intelligently selecting date and time ranges inspired by Google Calendar. + * Copyright (c) 2016 Jon Thornton - http://jonthornton.github.com/Datepair.js + * License: MIT + */ + +(function(window, document) { + + 'use strict'; + + var _ONE_DAY = 86400000; + var jq = window.Zepto || window.jQuery; + + function simpleExtend(obj1, obj2) { + var out = obj2 || {}; + + for (var i in obj1) { + if (!(i in out)) { + out[i] = obj1[i]; + } + } + + return out; + } + + // IE's custom event support is totally borked. + // Use jQuery if possible + function triggerSimpleCustomEvent(el, eventName) { + if (jq) { + jq(el).trigger(eventName); + } else { + var event = document.createEvent('CustomEvent'); + event.initCustomEvent(eventName, true, true, {}); + el.dispatchEvent(event); + } + } + + // el.classList not supported by < IE10 + // use jQuery if available + function hasClass(el, className) { + if (jq) { + return jq(el).hasClass(className); + } else { + return el.classList.contains(className); + } + } + + function Datepair(container, options) { + this.dateDelta = null; + this.timeDelta = null; + this._defaults = { + startClass: 'start', + endClass: 'end', + timeClass: 'time', + dateClass: 'date', + defaultDateDelta: 0, + defaultTimeDelta: 3600000, + anchor: 'start', + + // defaults for jquery-timepicker; override when using other input widgets + parseTime: function(input){ + return jq(input).timepicker('getTime'); + }, + updateTime: function(input, dateObj){ + jq(input).timepicker('setTime', dateObj); + }, + setMinTime: function(input, dateObj){ + jq(input).timepicker('option', 'minTime', dateObj); + }, + + // defaults for bootstrap datepicker; override when using other input widgets + parseDate: function(input){ + return input.value && jq(input).datepicker('getDate'); + }, + updateDate: function(input, dateObj){ + jq(input).datepicker('update', dateObj); + } + }; + + this.container = container; + this.settings = simpleExtend(this._defaults, options); + + this.startDateInput = this.container.querySelector('.'+this.settings.startClass+'.'+this.settings.dateClass); + this.endDateInput = this.container.querySelector('.'+this.settings.endClass+'.'+this.settings.dateClass); + this.startTimeInput = this.container.querySelector('.'+this.settings.startClass+'.'+this.settings.timeClass); + this.endTimeInput = this.container.querySelector('.'+this.settings.endClass+'.'+this.settings.timeClass); + + // initialize date and time deltas + this.refresh(); + + // init starts here + this._bindChangeHandler(); + } + + Datepair.prototype = { + constructor: Datepair, + + option: function(key, value) + { + if (typeof key == 'object') { + this.settings = simpleExtend(this.settings, key); + + } else if (typeof key == 'string' && typeof value != 'undefined') { + this.settings[key] = value; + + } else if (typeof key == 'string') { + return this.settings[key]; + } + + this._updateEndMintime(); + }, + + getTimeDiff: function() + { + // due to the fact that times can wrap around, timeDiff for any + // time-only pair will always be >= 0 + var delta = this.dateDelta + this.timeDelta; + if (delta < 0 && (!this.startDateInput || !this.endDateInput) ) { + delta += _ONE_DAY; + } + + return delta; + }, + + refresh: function() + { + if (this.startDateInput && this.startDateInput.value && this.endDateInput && this.endDateInput.value) { + var startDate = this.settings.parseDate(this.startDateInput); + var endDate = this.settings.parseDate(this.endDateInput); + if (startDate && endDate) { + this.dateDelta = endDate.getTime() - startDate.getTime(); + } + } + if (this.startTimeInput && this.startTimeInput.value && this.endTimeInput && this.endTimeInput.value) { + var startTime = this.settings.parseTime(this.startTimeInput); + var endTime = this.settings.parseTime(this.endTimeInput); + if (startTime && endTime) { + this.timeDelta = endTime.getTime() - startTime.getTime(); + this._updateEndMintime(); + } + } + }, + + remove: function() + { + this._unbindChangeHandler(); + }, + + _bindChangeHandler: function(){ + // addEventListener doesn't work with synthetic "change" events + // fired by jQuery's trigger() functioin. If jQuery is present, + // use that for event binding + if (jq) { + jq(this.container).on('change.datepair', jq.proxy(this.handleEvent, this)); + } else { + this.container.addEventListener('change', this, false); + } + }, + + _unbindChangeHandler: function(){ + if (jq) { + jq(this.container).off('change.datepair'); + } else { + this.container.removeEventListener('change', this, false); + } + }, + + // This function will be called when passing 'this' to addEventListener + handleEvent: function(e){ + // temporarily unbind the change handler to prevent triggering this + // if we update other inputs + this._unbindChangeHandler(); + + if (hasClass(e.target, this.settings.dateClass)) { + if (e.target.value != '') { + this._dateChanged(e.target); + this._timeChanged(e.target); + } else { + this.dateDelta = null; + } + + } else if (hasClass(e.target, this.settings.timeClass)) { + if (e.target.value != '') { + this._timeChanged(e.target); + } else { + this.timeDelta = null; + } + } + + this._validateRanges(); + this._updateEndMintime(); + this._bindChangeHandler(); + }, + + _dateChanged: function(target){ + if (!this.startDateInput || !this.endDateInput) { + return; + } + + var startDate = this.settings.parseDate(this.startDateInput); + var endDate = this.settings.parseDate(this.endDateInput); + + if (!startDate || !endDate) { + if (this.settings.defaultDateDelta !== null) { + if (startDate) { + var newEnd = new Date(startDate.getTime() + this.settings.defaultDateDelta * _ONE_DAY); + this.settings.updateDate(this.endDateInput, newEnd); + + } else if (endDate) { + var newStart = new Date(endDate.getTime() - this.settings.defaultDateDelta * _ONE_DAY); + this.settings.updateDate(this.startDateInput, newStart); + } + + this.dateDelta = this.settings.defaultDateDelta * _ONE_DAY; + } else { + this.dateDelta = null; + } + + return; + } + + if (this.settings.anchor == 'start' && hasClass(target, this.settings.startClass)) { + var newDate = new Date(startDate.getTime() + this.dateDelta); + this.settings.updateDate(this.endDateInput, newDate); + } else if (this.settings.anchor == 'end' && hasClass(target, this.settings.endClass)) { + var newDate = new Date(endDate.getTime() - this.dateDelta); + this.settings.updateDate(this.startDateInput, newDate); + } else { + if (endDate < startDate) { + var otherInput = hasClass(target, this.settings.startClass) ? this.endDateInput : this.startDateInput; + var selectedDate = this.settings.parseDate(target); + this.dateDelta = 0; + this.settings.updateDate(otherInput, selectedDate); + } else { + this.dateDelta = endDate.getTime() - startDate.getTime(); + } + } + }, + + _timeChanged: function(target){ + if (!this.startTimeInput || !this.endTimeInput) { + return; + } + + var startTime = this.settings.parseTime(this.startTimeInput); + var endTime = this.settings.parseTime(this.endTimeInput); + + if (!startTime || !endTime) { + if (this.settings.defaultTimeDelta !== null) { + if (startTime) { + var newEnd = new Date(startTime.getTime() + this.settings.defaultTimeDelta); + this.settings.updateTime(this.endTimeInput, newEnd); + } else if (endTime) { + var newStart = new Date(endTime.getTime() - this.settings.defaultTimeDelta); + this.settings.updateTime(this.startTimeInput, newStart); + } + + this.timeDelta = this.settings.defaultTimeDelta; + } else { + this.timeDelta = null; + } + + return; + } + + if (this.settings.anchor == 'start' && hasClass(target, this.settings.startClass)) { + var newTime = new Date(startTime.getTime() + this.timeDelta); + this.settings.updateTime(this.endTimeInput, newTime); + endTime = this.settings.parseTime(this.endTimeInput); + + this._doMidnightRollover(startTime, endTime); + } else if (this.settings.anchor == 'end' && hasClass(target, this.settings.endClass)) { + var newTime = new Date(endTime.getTime() - this.timeDelta); + this.settings.updateTime(this.startTimeInput, newTime); + startTime = this.settings.parseTime(this.startTimeInput); + + this._doMidnightRollover(startTime, endTime); + } else { + this._doMidnightRollover(startTime, endTime); + + var startDate, endDate; + if (this.startDateInput && this.endDateInput) { + startDate = this.settings.parseDate(this.startDateInput); + endDate = this.settings.parseDate(this.endDateInput); + } + + if ((+startDate == +endDate) && (endTime < startTime)) { + var thisInput = hasClass(target, this.settings.endClass) ? this.endTimeInput : this.startTimeInput; + var otherInput = hasClass(target, this.settings.startClass) ? this.endTimeInput : this.startTimeInput; + var selectedTime = this.settings.parseTime(thisInput); + this.timeDelta = 0; + this.settings.updateTime(otherInput, selectedTime); + } else { + this.timeDelta = endTime.getTime() - startTime.getTime(); + } + } + + + }, + + _doMidnightRollover: function(startTime, endTime) { + if (!this.startDateInput || !this.endDateInput) { + return; + } + + var endDate = this.settings.parseDate(this.endDateInput); + var startDate = this.settings.parseDate(this.startDateInput); + var newDelta = endTime.getTime() - startTime.getTime(); + var offset = (endTime < startTime) ? _ONE_DAY : -1 * _ONE_DAY; + + if (this.dateDelta !== null + && this.dateDelta + this.timeDelta <= _ONE_DAY + && this.dateDelta + newDelta != 0 + && (offset > 0 || this.dateDelta != 0) + && ((newDelta >= 0 && this.timeDelta < 0) || (newDelta < 0 && this.timeDelta >= 0))) { + + if (this.settings.anchor == 'start') { + this.settings.updateDate(this.endDateInput, new Date(endDate.getTime() + offset)); + this._dateChanged(this.endDateInput); + } else if (this.settings.anchor == 'end') { + this.settings.updateDate(this.startDateInput, new Date(startDate.getTime() - offset)); + this._dateChanged(this.startDateInput); + } + } + this.timeDelta = newDelta; + }, + + _updateEndMintime: function(){ + if (typeof this.settings.setMinTime != 'function') return; + + var baseTime = null; + if (this.settings.anchor == 'start' && (!this.dateDelta || this.dateDelta < _ONE_DAY || (this.timeDelta && this.dateDelta + this.timeDelta < _ONE_DAY))) { + baseTime = this.settings.parseTime(this.startTimeInput); + } + + this.settings.setMinTime(this.endTimeInput, baseTime); + }, + + _validateRanges: function(){ + if (this.startTimeInput && this.endTimeInput && this.timeDelta === null) { + triggerSimpleCustomEvent(this.container, 'rangeIncomplete'); + return; + } + + if (this.startDateInput && this.endDateInput && this.dateDelta === null) { + triggerSimpleCustomEvent(this.container, 'rangeIncomplete'); + return; + } + + // due to the fact that times can wrap around, any time-only pair will be considered valid + if (!this.startDateInput || !this.endDateInput || this.dateDelta + this.timeDelta >= 0) { + triggerSimpleCustomEvent(this.container, 'rangeSelected'); + } else { + triggerSimpleCustomEvent(this.container, 'rangeError'); + } + } + }; + + window.Datepair = Datepair; + +}(window, document)); \ No newline at end of file diff --git a/vendor/assets/javascripts/datepair.js b/vendor/assets/javascripts/datepair.js deleted file mode 100755 index d01d796..0000000 --- a/vendor/assets/javascripts/datepair.js +++ /dev/null @@ -1,226 +0,0 @@ -$(function() { - var DATEPICKER_FORMAT = 'yyyy-m-d'; - var TIMEPICKER_FORMAT = 'g:ia'; - var DATE_FORMAT = 'Y-n-j'; // for this format see http://php.net/manual/function.date.php - - $('.datepair input.date').each(function(){ - var $this = $(this); - - $this.datepicker({ - 'format': DATEPICKER_FORMAT, - 'autoclose': true - }); - - if ($this.hasClass('start') || $this.hasClass('end')) { - $this.on('changeDate change', doDatepair); - } - - }); - - $('.datepair input.time').each(function() { - var $this = $(this); - - $this.timepicker({ - 'showDuration': true, - 'timeFormat': TIMEPICKER_FORMAT, - 'scrollDefaultNow': true - }); - - if ($this.hasClass('start') || $this.hasClass('end')) { - $this.on('changeTime change', doDatepair); - } - - if ($this.hasClass('end')) { - $this.on('focus', function(){$('.ui-timepicker-with-duration').scrollTop(0);}); - } - - }); - - $('.datepair').each(initDatepair); - - function initDatepair() - { - var container = $(this); - - var startDateInput = container.find('input.start.date'); - var endDateInput = container.find('input.end.date'); - var dateDelta = 0; - - if (startDateInput.length && endDateInput.length) { - var startDate = parseDate(startDateInput.val(), DATEPICKER_FORMAT); - var endDate = parseDate(endDateInput.val(), DATEPICKER_FORMAT); - - dateDelta = endDate.getTime() - startDate.getTime(); - container.data('dateDelta', dateDelta); - } - - var startTimeInput = container.find('input.start.time'); - var endTimeInput = container.find('input.end.time'); - - if (startTimeInput.length && endTimeInput.length) { - var startInt = startTimeInput.timepicker('getSecondsFromMidnight'); - var endInt = endTimeInput.timepicker('getSecondsFromMidnight'); - - container.data('timeDelta', endInt - startInt); - - if (dateDelta < 86400000) { - endTimeInput.timepicker('option', 'minTime', startInt); - } - } - } - - function doDatepair() - { - var target = $(this); - if (target.val() == '') { - return; - } - - var container = target.closest('.datepair'); - - if (target.hasClass('date')) { - updateDatePair(target, container); - - } else if (target.hasClass('time')) { - updateTimePair(target, container); - } - } - - function updateDatePair(target, container) - { - var start = container.find('input.start.date'); - var end = container.find('input.end.date'); - if (!start.length || !end.length) { - return; - } - - var startDate = parseDate(start.val(), DATEPICKER_FORMAT); - var endDate = parseDate(end.val(), DATEPICKER_FORMAT); - - var oldDelta = container.data('dateDelta'); - - if (!isNaN(oldDelta) && oldDelta !== null && target.hasClass('start')) { - var newEnd = new Date(startDate.getTime()+oldDelta); - end.val(newEnd.format(DATE_FORMAT)); - end.datepicker('update'); - return; - - } else { - var newDelta = endDate.getTime() - startDate.getTime(); - - if (newDelta < 0) { - newDelta = 0; - - if (target.hasClass('start')) { - end.val(start.val()); - end.datepicker('update'); - } else if (target.hasClass('end')) { - start.val(end.val()); - start.datepicker('update'); - } - } - - if (newDelta < 86400000) { - var startTimeVal = container.find('input.start.time').val(); - - if (startTimeVal) { - container.find('input.end.time').timepicker('option', {'minTime': startTimeVal}); - } - } else { - container.find('input.end.time').timepicker('option', {'minTime': null}); - } - - container.data('dateDelta', newDelta); - } - } - - function updateTimePair(target, container) - { - var start = container.find('input.start.time'); - var end = container.find('input.end.time'); - - if (!start.length) { - return; - } - - var startInt = start.timepicker('getSecondsFromMidnight'); - var dateDelta = container.data('dateDelta'); - - if (target.hasClass('start') && (!dateDelta || dateDelta < 86400000)) { - end.timepicker('option', 'minTime', startInt); - } - - if (!end.length) { - return; - } - - var endInt = end.timepicker('getSecondsFromMidnight'); - var oldDelta = container.data('timeDelta'); - - var endDateAdvance = 0; - var newDelta; - - if (oldDelta && target.hasClass('start')) { - // lock the duration and advance the end time - - var newEnd = (startInt+oldDelta)%86400; - - if (newEnd < 0) { - newEnd += 86400; - } - - end.timepicker('setTime', newEnd); - newDelta = newEnd - startInt; - } else if (startInt !== null && endInt !== null) { - newDelta = endInt - startInt; - } else { - return; - } - - container.data('timeDelta', newDelta); - - if (newDelta < 0 && (!oldDelta || oldDelta > 0)) { - // overnight time span. advance the end date 1 day - endDateAdvance = 86400000; - - } else if (newDelta > 0 && oldDelta < 0) { - // switching from overnight to same-day time span. decrease the end date 1 day - endDateAdvance = -86400000; - } - - var startInput = container.find('.start.date'); - var endInput = container.find('.end.date'); - - if (startInput.val() && !endInput.val()) { - endInput.val(startInput.val()); - endInput.datepicker('update'); - dateDelta = 0; - container.data('dateDelta', 0); - } - - if (endDateAdvance != 0) { - if (dateDelta || dateDelta === 0) { - var endDate = parseDate(endInput.val(), DATEPICKER_FORMAT); - var newEnd = new Date(endDate.getTime() + endDateAdvance); - endInput.val(newEnd.format(DATE_FORMAT)); - endInput.datepicker('update'); - container.data('dateDelta', dateDelta + endDateAdvance); - } - } - } -}); - -function parseDate(input, format) { - if (input == '') - return new Date(''); - - format = format || 'yyyy-mm-dd'; // default format - var parts = input.match(/(\d+)/g), i = 0, fmt = {}; - // extract date-part indexes from the format - format.replace(/(yyyy|dd?|mm?)/g, function(part) { fmt[part] = i++; }); - - return new Date(parts[fmt['yyyy']], parts[fmt['mm'] == undefined ? fmt['m'] : fmt['mm']]-1, parts[fmt['dd'] == undefined ? fmt['d'] : fmt['dd']]); -} - -// Simulates PHP's date function -Date.prototype.format=function(format){var returnStr='';var replace=Date.replaceChars;for(var i=0;i= settings.step*30) { + // if offset is larger than a half step, round up + seconds += (settings.step*60) - offset; + } else { + // round down + seconds -= offset; + } + + return _moduloSeconds(seconds, settings); + } + }, + scrollDefault: null, + selectOnBlur: false, + show2400: false, + showDuration: false, + showOn: ['click', 'focus'], + showOnFocus: true, + step: 30, + stopScrollPropagation: false, + timeFormat: 'g:ia', + typeaheadHighlight: true, + useSelect: false, + wrapHours: true + }; + + var methods = { init: function(options) { return this.each(function() { var self = $(this); - // convert dropdowns to text input - if (self[0].tagName == 'SELECT') { - var attrs = { 'type': 'text', 'value': self.val() }; - var raw_attrs = self[0].attributes; - - for (var i=0; i < raw_attrs.length; i++) { - attrs[raw_attrs[i].nodeName] = raw_attrs[i].nodeValue; + // pick up settings from data attributes + var attributeOptions = []; + for (var key in _DEFAULTS) { + if (self.data(key)) { + attributeOptions[key] = self.data(key); } - - var input = $('', attrs); - self.replaceWith(input); - self = input; } - var settings = $.extend({}, _defaults); - - if (options) { - settings = $.extend(settings, options); - } + var settings = $.extend({}, _DEFAULTS, options, attributeOptions); if (settings.lang) { _lang = $.extend(_lang, settings.lang); } settings = _parseSettings(settings); - self.data('timepicker-settings', settings); - self.prop('autocomplete', 'off'); - self.on('click.timepicker focus.timepicker', methods.show); - self.on('change.timepicker', _formatValue); - self.on('keydown.timepicker', _keydownhandler); - self.on('keyup.timepicker', _keyuphandler); self.addClass('ui-timepicker-input'); - _formatValue.call(self.get(0)); + if (settings.useSelect) { + _render(self); + } else { + self.prop('autocomplete', 'off'); + if (settings.showOn) { + for (var i in settings.showOn) { + self.on(settings.showOn[i]+'.timepicker', methods.show); + } + } + self.on('change.timepicker', _formatValue); + self.on('keydown.timepicker', _keydownhandler); + self.on('keyup.timepicker', _keyuphandler); + if (settings.disableTextInput) { + self.on('keydown.timepicker', _disableTextInputHandler); + } + + _formatValue.call(self.get(0), null, 'initial'); + } }); }, @@ -93,6 +133,15 @@ requires jQuery 1.7+ var self = $(this); var settings = self.data('timepicker-settings'); + if (e) { + e.preventDefault(); + } + + if (settings.useSelect) { + self.data('timepicker-list').focus(); + return; + } + if (_hideKeyboard(self)) { // block the keyboard on mobile devices self.blur(); @@ -111,39 +160,60 @@ requires jQuery 1.7+ list = self.data('timepicker-list'); } - if (list.is(':visible')) { + if (_isVisible(list)) { return; } + self.data('ui-timepicker-value', self.val()); + _setSelected(self, list); + // make sure other pickers are hidden methods.hide(); + // position the dropdown relative to the input list.show(); + var listOffset = {}; + + if (settings.orientation.match(/r/)) { + // right-align the dropdown + listOffset.left = self.offset().left + self.outerWidth() - list.outerWidth() + parseInt(list.css('marginLeft').replace('px', ''), 10); + } else { + // left-align the dropdown + listOffset.left = self.offset().left + parseInt(list.css('marginLeft').replace('px', ''), 10); + } + + var verticalOrientation; + if (settings.orientation.match(/t/)) { + verticalOrientation = 't'; + } else if (settings.orientation.match(/b/)) { + verticalOrientation = 'b'; + } else if ((self.offset().top + self.outerHeight(true) + list.outerHeight()) > $(window).height() + $(window).scrollTop()) { + verticalOrientation = 't'; + } else { + verticalOrientation = 'b'; + } - if ((self.offset().top + self.outerHeight(true) + list.outerHeight()) > $(window).height() + $(window).scrollTop()) { + if (verticalOrientation == 't') { // position the dropdown on top - list.offset({ - 'left': self.offset().left + parseInt(list.css('marginLeft').replace('px', ''), 10), - 'top': self.offset().top - list.outerHeight() + parseInt(list.css('marginTop').replace('px', ''), 10) - }); + list.addClass('ui-timepicker-positioned-top'); + listOffset.top = self.offset().top - list.outerHeight() + parseInt(list.css('marginTop').replace('px', ''), 10); } else { // put it under the input - list.offset({ - 'left':self.offset().left + parseInt(list.css('marginLeft').replace('px', ''), 10), - 'top': self.offset().top + self.outerHeight() + parseInt(list.css('marginTop').replace('px', ''), 10) - }); + list.removeClass('ui-timepicker-positioned-top'); + listOffset.top = self.offset().top + self.outerHeight() + parseInt(list.css('marginTop').replace('px', ''), 10); } + list.offset(listOffset); + // position scrolling var selected = list.find('.ui-timepicker-selected'); if (!selected.length) { - if (_getTimeValue(self)) { - selected = _findRow(self, list, _time2int(_getTimeValue(self))); - } else if (settings.scrollDefaultNow) { - selected = _findRow(self, list, _time2int(new Date())); - } else if (settings.scrollDefaultTime !== false) { - selected = _findRow(self, list, _time2int(settings.scrollDefaultTime)); + var timeInt = _time2int(_getTimeValue(self)); + if (timeInt !== null) { + selected = _findRow(self, list, timeInt); + } else if (settings.scrollDefault) { + selected = _findRow(self, list, settings.scrollDefault()); } } @@ -154,15 +224,42 @@ requires jQuery 1.7+ list.scrollTop(0); } - _attachCloseHandler(settings); + // prevent scroll propagation + if(settings.stopScrollPropagation) { + $(document).on('wheel.ui-timepicker', '.ui-timepicker-wrapper', function(e){ + e.preventDefault(); + var currentScroll = $(this).scrollTop(); + $(this).scrollTop(currentScroll + e.originalEvent.deltaY); + }); + } + + // attach close handlers + $(document).on('touchstart.ui-timepicker mousedown.ui-timepicker', _closeHandler); + $(window).on('resize.ui-timepicker', _closeHandler); + if (settings.closeOnWindowScroll) { + $(document).on('scroll.ui-timepicker', _closeHandler); + } self.trigger('showTimepicker'); + + return this; }, hide: function(e) { - $('.ui-timepicker-wrapper:visible').each(function() { + var self = $(this); + var settings = self.data('timepicker-settings'); + + if (settings && settings.useSelect) { + self.blur(); + } + + $('.ui-timepicker-wrapper').each(function() { var list = $(this); + if (!_isVisible(list)) { + return; + } + var self = list.data('timepicker-input'); var settings = self.data('timepicker-settings'); @@ -173,34 +270,42 @@ requires jQuery 1.7+ list.hide(); self.trigger('hideTimepicker'); }); + + return this; }, option: function(key, value) { - var self = this; - var settings = self.data('timepicker-settings'); - var list = self.data('timepicker-list'); + if (typeof key == 'string' && typeof value == 'undefined') { + return $(this).data('timepicker-settings')[key]; + } - if (typeof key == 'object') { - settings = $.extend(settings, key); + return this.each(function(){ + var self = $(this); + var settings = self.data('timepicker-settings'); + var list = self.data('timepicker-list'); - } else if (typeof key == 'string' && typeof value != 'undefined') { - settings[key] = value; + if (typeof key == 'object') { + settings = $.extend(settings, key); + } else if (typeof key == 'string') { + settings[key] = value; + } - } else if (typeof key == 'string') { - return settings[key]; - } + settings = _parseSettings(settings); - settings = _parseSettings(settings); + self.data('timepicker-settings', settings); - self.data('timepicker-settings', settings); + _formatValue.call(self.get(0), {'type':'change'}, 'initial'); - if (list) { - list.remove(); - self.data('timepicker-list', false); - } + if (list) { + list.remove(); + self.data('timepicker-list', false); + } - return self; + if (settings.useSelect) { + _render(self); + } + }); }, getSecondsFromMidnight: function() @@ -211,23 +316,58 @@ requires jQuery 1.7+ getTime: function(relative_date) { var self = this; + + var time_string = _getTimeValue(self); + if (!time_string) { + return null; + } + + var offset = _time2int(time_string); + if (offset === null) { + return null; + } + if (!relative_date) { relative_date = new Date(); } - relative_date.setHours(0, 0, 0, 0); - return new Date(relative_date.valueOf() + (_time2int(_getTimeValue(self))*1000)); + // construct a Date from relative date, and offset's time + var time = new Date(relative_date); + time.setHours(offset / 3600); + time.setMinutes(offset % 3600 / 60); + time.setSeconds(offset % 60); + time.setMilliseconds(0); + + return time; + }, + + isVisible: function() { + var self = this; + var list = self.data('timepicker-list'); + return !!(list && _isVisible(list)); }, setTime: function(value) { var self = this; - var prettyTime = _int2time(_time2int(value), self.data('timepicker-settings').timeFormat); + var settings = self.data('timepicker-settings'); + + if (settings.forceRoundTime) { + var prettyTime = _roundAndFormatTime(_time2int(value), settings) + } else { + var prettyTime = _int2time(_time2int(value), settings); + } + + if (value && prettyTime === null && settings.noneOption) { + prettyTime = value; + } _setTimeValue(self, prettyTime); if (self.data('timepicker-list')) { _setSelected(self, self.data('timepicker-list')); } + + return this; }, remove: function() @@ -239,6 +379,7 @@ requires jQuery 1.7+ return; } + var settings = self.data('timepicker-settings'); self.removeAttr('autocomplete', 'off'); self.removeClass('ui-timepicker-input'); self.removeData('timepicker-settings'); @@ -249,12 +390,24 @@ requires jQuery 1.7+ self.data('timepicker-list').remove(); } + if (settings.useSelect) { + self.show(); + } + self.removeData('timepicker-list'); + + return this; } }; // private methods + function _isVisible(elem) + { + var el = elem[0]; + return el.offsetWidth > 0 && el.offsetHeight > 0; + } + function _parseSettings(settings) { if (settings.minTime) { @@ -269,6 +422,29 @@ requires jQuery 1.7+ settings.durationTime = _time2int(settings.durationTime); } + if (settings.scrollDefault == 'now') { + settings.scrollDefault = function() { + return settings.roundingFunction(_time2int(new Date()), settings); + } + } else if (settings.scrollDefault && typeof settings.scrollDefault != 'function') { + var val = settings.scrollDefault; + settings.scrollDefault = function() { + return settings.roundingFunction(_time2int(val), settings); + } + } else if (settings.minTime) { + settings.scrollDefault = function() { + return settings.roundingFunction(settings.minTime, settings); + } + } + + if ($.type(settings.timeFormat) === "string" && settings.timeFormat.match(/[gh]/)) { + settings._twelveHourTime = true; + } + + if (settings.showOnFocus === false && settings.showOn.indexOf('focus') != -1) { + settings.showOn.splice(settings.showOn.indexOf('focus'), 1); + } + if (settings.disableTimeRanges.length > 0) { // convert string times to integers for (var i in settings.disableTimeRanges) { @@ -282,6 +458,17 @@ requires jQuery 1.7+ settings.disableTimeRanges = settings.disableTimeRanges.sort(function(a, b){ return a[0] - b[0]; }); + + // merge any overlapping ranges + for (var i = settings.disableTimeRanges.length-1; i > 0; i--) { + if (settings.disableTimeRanges[i][0] <= settings.disableTimeRanges[i-1][1]) { + settings.disableTimeRanges[i-1] = [ + Math.min(settings.disableTimeRanges[i][0], settings.disableTimeRanges[i-1][0]), + Math.max(settings.disableTimeRanges[i][1], settings.disableTimeRanges[i-1][1]) + ]; + settings.disableTimeRanges.splice(i, 1); + } + } } return settings; @@ -297,18 +484,42 @@ requires jQuery 1.7+ self.data('timepicker-list', false); } - list = $('