/*
http://keith-wood.name/timeEntry.html
Time entry for jQuery v1.4.8.
Written by Keith Wood (kbwood{at}iinet.com.au) June 2007.
Minor changes by Massimo Di Pierro Nov 2010 (simplified and changed behavior)
Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and
MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses.
Please attribute the author if you use it.
Turn an input field into an entry point for a time value.
The time can be entered via directly typing the value,
via the arrow keys.
It is configurable to show 12 or 24-hour time, to show or hide seconds,
to enforce a minimum and/or maximum time, to change the spinner image.
Example: jQuery('input.time').timeEntry();
*/
(function(jQuery) { // Hide scope, no jQuery conflict
var PROP_NAME = 'timeEntry';
/* TimeEntry manager.
Use the singleton instance of this class, jQuery.timeEntry, to interact with the time entry
functionality. Settings for (groups of) fields are maintained in an instance object
(TimeEntryInstance), allowing multiple different settings on the same page.
*/
function TimeEntry() {
this._disabledInputs = []; // List of time entry inputs that have been disabled
this._defaults = {
showSeconds: true, // True to show seconds as well, false for hours/minutes only
defaultTime: null, // The time to use if none has been set, leave at null for now
minTime: null, // The earliest selectable time, or null for no limit
maxTime: null, // The latest selectable time, or null for no limit
show24Hours: true, // True to use 24 hour time, false for 12 hour (AM/PM)
ampmNames: ['am', 'pm'] // Names of morning/evening markers
};
jQuery.extend(this._defaults);
}
jQuery.extend(TimeEntry.prototype, {
/*
Class name added to elements to indicate already configured with time entry.
*/
markerClassName: 'hasTimeEntry',
/* Override the default settings for all instances of the time entry.
@param options (object) the new settings to use as defaults (anonymous object)
@return (DateEntry) this object
*/
setDefaults: function(options) {
extendRemove(this._defaults, options || {});
return this;
},
/* Attach the time entry handler to an input field.
@param target (element) the field to attach to
@param options (object) custom settings for this instance
*/
_connectTimeEntry: function(target, options) {
var input = jQuery(target);
if (input.hasClass(this.markerClassName)) {
return;
}
var inst = {};
inst.options = jQuery.extend({}, options);
inst._selectedHour = 0; // The currently selected hour
inst._selectedMinute = 0; // The currently selected minute
inst._selectedSecond = 0; // The currently selected second
inst._field = 0; // The selected subfield
inst.input = jQuery(target); // The attached input field
jQuery.data(target, PROP_NAME, inst);
input.addClass(this.markerClassName).bind('focus.timeEntry', this._doFocus).
bind('blur.timeEntry', this._doBlur).bind('click.timeEntry', this._doClick).
bind('keydown.timeEntry', this._doKeyDown).bind('keypress.timeEntry', this._doKeyPress);
// Check pastes
if (jQuery.browser.mozilla)
input.bind('input.timeEntry', function(event) { jQuery.timeEntry._parseTime(inst); });
if (jQuery.browser.msie)
input.bind('paste.timeEntry', function(event) { setTimeout(function() { jQuery.timeEntry._parseTime(inst); }, 1); });
},
/* Check whether an input field has been disabled.
@param input (element) input field to check
@return (boolean) true if this field has been disabled, false if it is enabled
*/
_isDisabledTimeEntry: function(input) {
return jQuery.inArray(input, this._disabledInputs) > -1;
},
/* Reconfigure the settings for a time entry field.
@param input (element) input field to change
@param options (object) new settings to add or
(string) an individual setting name
@param value (any) the individual setting's value
*/
_changeTimeEntry: function(input, options, value) {
var inst = jQuery.data(input, PROP_NAME);
if (inst) {
if (typeof options == 'string') {
var name = options;
options = {};
options[name] = value;
}
var currentTime = this._extractTime(inst);
extendRemove(inst.options, options || {});
if (currentTime)
this._setTime(inst, new Date(0, 0, 0,
currentTime[0], currentTime[1], currentTime[2]));
}
jQuery.data(input, PROP_NAME, inst);
},
/* Remove the time entry functionality from an input.
@param input (element) input field to affect
*/
_destroyTimeEntry: function(input) {
jQueryinput = jQuery(input);
if (!jQueryinput.hasClass(this.markerClassName)) return;
jQueryinput.removeClass(this.markerClassName).unbind('.timeEntry');
this._disabledInputs = jQuery.map(this._disabledInputs, function(value) { return (value == input ? null : value); }); // Delete entry
jQueryinput.parent().replaceWith(jQueryinput);
jQuery.removeData(input, PROP_NAME);
},
/* Initialise the current time for a time entry input field.
@param input (element) input field to update
@param time (Date) the new time (year/month/day ignored) or null for now
*/
_setTimeTimeEntry: function(input, time) {
var inst = jQuery.data(input, PROP_NAME);
if (inst) this._setTime(inst, time ? (typeof time == 'object' ? new Date(time.getTime()) : time) : null);
},
/* Retrieve the current time for a time entry input field.
@param input (element) input field to examine
@return (Date) current time (year/month/day zero) or null if none
*/
_getTimeTimeEntry: function(input) {
var inst = jQuery.data(input, PROP_NAME);
var currentTime = (inst ? this._extractTime(inst) : null);
return (!currentTime ? null : new Date(0, 0, 0, currentTime[0], currentTime[1], currentTime[2]));
},
/* Retrieve the millisecond offset for the current time.
@param input (element) input field to examine
@return (number) the time as milliseconds offset or zero if none
*/
_getOffsetTimeEntry: function(input) {
var inst = jQuery.data(input, PROP_NAME);
var currentTime = (inst ? this._extractTime(inst) : null);
return (!currentTime ? 0 : (currentTime[0] * 3600 + currentTime[1] * 60 + currentTime[2]) * 1000);
},
/* Initialise time entry.
@param target (element) the input field or (event) the focus event
*/
_doFocus: function(target) {
var input = (target.nodeName && target.nodeName.toLowerCase() == 'input' ? target : this);
if (jQuery.timeEntry._lastInput == input || jQuery.timeEntry._isDisabledTimeEntry(input)) {
jQuery.timeEntry._focussed = false;
return;
}
var inst = jQuery.data(input, PROP_NAME);
jQuery.timeEntry._focussed = true;
jQuery.timeEntry._lastInput = input;
jQuery.timeEntry._blurredInput = null;
jQuery.data(input, PROP_NAME, inst);
jQuery.timeEntry._parseTime(inst);
setTimeout(function() { jQuery.timeEntry._showField(inst); }, 10);
},
/* Note that the field has been exited.
@param event (event) the blur event
*/
_doBlur: function(event) {
jQuery.timeEntry._blurredInput = jQuery.timeEntry._lastInput;
jQuery.timeEntry._lastInput = null;
},
/* Select appropriate field portion on click, if already in the field.
@param event (event) the click event
*/
_doClick: function(event) {
var input = event.target;
var inst = jQuery.data(input, PROP_NAME);
if (!jQuery.timeEntry._focussed) {
var fieldSize = 3;
inst._field = 0;
if (input.selectionStart != null) { // Use input select range
for (var field = 0; field <= Math.max(1, inst._secondField, inst._ampmField); field++) {
var end = (field != inst._ampmField ? (field * fieldSize) + 2 : (inst._ampmField * fieldSize) + 2);
inst._field = field;
if (input.selectionStart < end) break;
}
} else if (input.createTextRange) { // Check against bounding boxes
var src = jQuery(event.srcElement);
var range = input.createTextRange();
var convert = function(value) {
return {thin: 2, medium: 4, thick: 6}[value] || value;
};
var offsetX = event.clientX + document.documentElement.scrollLeft -
(src.offset().left + parseInt(convert(src.css('border-left-width')), 10)) -
range.offsetLeft; // Position - left edge - alignment
for (var field = 0; field <= Math.max(1, inst._secondField, inst._ampmField); field++) {
var end = (field != inst._ampmField ? (field * fieldSize) + 2 : (inst._ampmField * fieldSize) + 2);
range.collapse();
range.moveEnd('character', end);
inst._field = field;
if (offsetX < range.boundingWidth) break; // And compare
}
}
}
jQuery.data(input, PROP_NAME, inst);
jQuery.timeEntry._showField(inst);
jQuery.timeEntry._focussed = false;
},
/* Handle keystrokes in the field.
@param event (event) the keydown event
@return (boolean) true to continue, false to stop processing
*/
_doKeyDown: function(event) {
if (event.keyCode >= 48) return true;
var inst = jQuery.data(event.target, PROP_NAME);
switch (event.keyCode) {
case 9:
var its = jQuery(':input');
its.eq(its.index(this)+(event.shiftKey?-1:+1)).focus();
break;
case 37: jQuery.timeEntry._changeField(inst, -1, false); break; // Previous field on left
case 38: jQuery.timeEntry._adjustField(inst, -1); break; // Increment time field on down
case 16: if(!event.shiftKey) jQuery.timeEntry._changeField(inst, +1, false); break; // Next field on right
case 39: jQuery.timeEntry._changeField(inst, +1, false); break; // Next field on right
case 40: jQuery.timeEntry._adjustField(inst, +1); break; // Decrement time field on up
case 32: case 46: jQuery.timeEntry._setValue(inst, ''); break; // Clear time on delete
}
return false;
},
/* Disallow unwanted characters.
@param event (event) the keypress event
@return (boolean) true to continue, false to stop processing
*/
_doKeyPress: function(event) {
var chr = String.fromCharCode(event.charCode == undefined ? event.keyCode : event.charCode);
if (chr < ' ') return true;
var inst = jQuery.data(event.target, PROP_NAME);
jQuery.timeEntry._handleKeyPress(inst, chr);
return false;
},
/* Get a setting value, defaulting if necessary.
@param inst (object) the instance settings
@param name (string) the setting name
@return (any) the setting value
*/
_get: function(inst, name) {
return (inst.options[name] != null ? inst.options[name] : jQuery.timeEntry._defaults[name]);
},
/* Extract the time value from the input field, or default to now.
@param inst (object) the instance settings
*/
_parseTime: function(inst) {
var currentTime = this._extractTime(inst);
var showSeconds = this._get(inst, 'showSeconds');
if (currentTime) {
inst._selectedHour = currentTime[0];
inst._selectedMinute = currentTime[1];
inst._selectedSecond = currentTime[2];
}
else {
var now = this._constrainTime(inst);
inst._selectedHour = now[0];
inst._selectedMinute = now[1];
inst._selectedSecond = (showSeconds ? now[2] : 0);
}
inst._secondField = (showSeconds ? 2 : -1);
inst._ampmField = (this._get(inst, 'show24Hours') ? -1 : (showSeconds ? 3 : 2));
inst._lastChr = '';
inst._field = Math.max(0, Math.min(Math.max(1, inst._secondField, inst._ampmField), 0));
if (inst.input.val() != '') this._showTime(inst);
},
/* Extract the time value from a string as an array of values, or default to null.
@param inst (object) the instance settings
@param value (string) the time value to parse
@return (number[3]) the time components (hours, minutes, seconds)
or null if no value
*/
_extractTime: function(inst, value) {
value = value || inst.input.val();
var currentTime = value.split(':');
var ampmNames = this._get(inst, 'ampmNames');
var show24Hours = this._get(inst, 'show24Hours');
if (currentTime.length >= 2) {
var isAM = !show24Hours && (value.indexOf(ampmNames[0]) > -1);
var isPM = !show24Hours && (value.indexOf(ampmNames[1]) > -1);
var hour = parseInt(currentTime[0], 10);
hour = (isNaN(hour) ? 0 : hour);
hour = ((isAM || isPM) && hour == 12 ? 0 : hour) + (isPM ? 12 : 0);
var minute = parseInt(currentTime[1], 10);
minute = (isNaN(minute) ? 0 : minute);
var second = (currentTime.length >= 3 ?
parseInt(currentTime[2], 10) : 0);
second = (isNaN(second) || !this._get(inst, 'showSeconds') ? 0 : second);
return this._constrainTime(inst, [hour, minute, second]);
}
return null;
},
/* Constrain the given/current time to the time steps.
@param inst (object) the instance settings
@param fields (number[3]) the current time components (hours, minutes, seconds)
@return (number[3]) the constrained time components (hours, minutes, seconds)
*/
_constrainTime: function(inst, fields) {
var specified = (fields != null);
if (!specified) {
var now = this._determineTime(inst, this._get(inst, 'defaultTime')) || new Date();
fields = [now.getHours(), now.getMinutes(), now.getSeconds()];
}
return fields;
},
/* Set the selected time into the input field.
@param inst (object) the instance settings
*/
_showTime: function(inst) {
var show24Hours = this._get(inst, 'show24Hours');
var currentTime = (this._formatNumber(show24Hours ? inst._selectedHour :
((inst._selectedHour + 11) % 12) + 1) + ':' +
this._formatNumber(inst._selectedMinute) +
(this._get(inst, 'showSeconds') ? ':' +
this._formatNumber(inst._selectedSecond) : '') +
(show24Hours ? '' : this._get(inst, 'ampmNames')[(inst._selectedHour < 12 ? 0 : 1)]));
this._setValue(inst, currentTime);
this._showField(inst);
},
/* Highlight the current time field.
@param inst (object) the instance settings
*/
_showField: function(inst) {
var input = inst.input[0];
if (inst.input.is(':hidden') || jQuery.timeEntry._lastInput != input) return;
var fieldSize = 3;
var start = (inst._field == inst._ampmField ? (inst._ampmField * fieldSize) - 1 : (inst._field * fieldSize));
var end = start + (inst._field == inst._ampmField ? 2 : 2);
if (input.setSelectionRange) { // Mozilla
input.setSelectionRange(start, end);
}
else if (input.createTextRange) { // IE
var range = input.createTextRange();
range.moveStart('character', start);
range.moveEnd('character', end - inst.input.val().length);
range.select();
}
if (!input.disabled) input.focus();
},
/* Ensure displayed single number has a leading zero.
@param value (number) current value
@return (string) number with at least two digits
*/
_formatNumber: function(value) {
return (value < 10 ? '0' : '') + value;
},
/* Update the input field and notify listeners.
@param inst (object) the instance settings
@param value (string) the new value
*/
_setValue: function(inst, value) {
if (value != inst.input.val()) inst.input.val(value).trigger('change');
},
/* Move to previous/next field, or out of field altogether if appropriate.
@param inst (object) the instance settings
@param offset (number) the direction of change (-1, +1)
@param moveOut (boolean) true if can move out of the field
@return (boolean) true if exitting the field, false if not
*/
_changeField: function(inst, offset, moveOut) {
var atFirstLast = (inst.input.val() == '' || inst._field == (offset == -1 ? 0 : Math.max(1, inst._secondField, inst._ampmField)));
if (!atFirstLast) inst._field += offset;
this._showField(inst);
inst._lastChr = '';
jQuery.data(inst.input[0], PROP_NAME, inst);
return (atFirstLast && moveOut);
},
/* Update the current field in the direction indicated.
@param inst (object) the instance settings
@param offset (number) the amount to change by
*/
_adjustField: function(inst, offset) {
if (inst.input.val() == '') offset = 0;
this._setTime(inst, new Date(0, 0, 0,
inst._selectedHour + (inst._field == 0 ? offset : 0) +
(inst._field == inst._ampmField ? offset * 12 : 0),
inst._selectedMinute + (inst._field == 1 ? offset : 0),
inst._selectedSecond + (inst._field == inst._secondField ? offset : 0)));
},
/* Check against minimum/maximum and display time.
@param inst (object) the instance settings
@param time (Date) an actual time or
(number) offset in seconds from now or
(string) units and periods of offsets from now
*/
_setTime: function(inst, time) {
time = this._determineTime(inst, time);
var fields = this._constrainTime(inst, time ?
[time.getHours(), time.getMinutes(), time.getSeconds()] : null);
time = new Date(0, 0, 0, fields[0], fields[1], fields[2]);
// Normalise to base date
var time = this._normaliseTime(time);
var minTime = this._normaliseTime(this._determineTime(inst, this._get(inst, 'minTime')));
var maxTime = this._normaliseTime(this._determineTime(inst, this._get(inst, 'maxTime')));
// Ensure it is within the bounds set
time = (minTime && time < minTime ? minTime :
(maxTime && time > maxTime ? maxTime : time));
inst._selectedHour = time.getHours();
inst._selectedMinute = time.getMinutes();
inst._selectedSecond = time.getSeconds();
this._showTime(inst);
jQuery.data(inst.input[0], PROP_NAME, inst);
},
/* Normalise time object to a common date.
@param time (Date) the original time
@return (Date) the normalised time
*/
_normaliseTime: function(time) {
if (!time) return null;
time.setFullYear(1900);
time.setMonth(0);
time.setDate(0);
return time;
},
/* A time may be specified as an exact value or a relative one.
@param inst (object) the instance settings
@param setting (Date) an actual time or
(number) offset in seconds from now or
(string) units and periods of offsets from now
@return (Date) the calculated time
*/
_determineTime: function(inst, setting) {
var offsetNumeric = function(offset) { // E.g. +300, -2
var time = new Date();
time.setTime(time.getTime() + offset * 1000);
return time;
};
var offsetString = function(offset) { // E.g. '+2m', '-4h', '+3h +30m' or '12:34:56PM'
var fields = jQuery.timeEntry._extractTime(inst, offset); // Actual time?
var time = new Date();
var hour = (fields ? fields[0] : time.getHours());
var minute = (fields ? fields[1] : time.getMinutes());
var second = (fields ? fields[2] : time.getSeconds());
if (!fields) {
var pattern = /([+-]?[0-9]+)\s*(s|S|m|M|h|H)?/g;
var matches = pattern.exec(offset);
while (matches) {
switch (matches[2] || 's') {
case 's' : case 'S' : second += parseInt(matches[1], 10); break;
case 'm' : case 'M' : minute += parseInt(matches[1], 10); break;
case 'h' : case 'H' : hour += parseInt(matches[1], 10); break;
}
matches = pattern.exec(offset);
}
}
time = new Date(0, 0, 10, hour, minute, second, 0);
if (/^!/.test(offset)) { // No wrapping
if (time.getDate() > 10)
time = new Date(0, 0, 10, 23, 59, 59);
else if (time.getDate() < 10)
time = new Date(0, 0, 10, 0, 0, 0);
}
return time;
};
return (setting ? (typeof setting == 'string' ? offsetString(setting) :
(typeof setting == 'number' ? offsetNumeric(setting) : setting)) : null);
},
/* Update time based on keystroke entered.
@param inst (object) the instance settings
@param chr (ch) the new character
*/
_handleKeyPress: function(inst, chr) {
if (chr == ':') this._changeField(inst, +1, false);
else if (chr >= '0' && chr <= '9') { // Allow direct entry of time
var key = parseInt(chr, 10);
var value = parseInt(inst._lastChr + chr, 10);
var show24Hours = this._get(inst, 'show24Hours');
var hour = (inst._field != 0 ? inst._selectedHour :
(show24Hours ? (value < 24 ? value : key) :
(value >= 1 && value <= 12 ? value :
(key > 0 ? key : inst._selectedHour)) % 12 +
(inst._selectedHour >= 12 ? 12 : 0)));
var minute = (inst._field != 1 ? inst._selectedMinute :
(value < 60 ? value : key));
var second = (inst._field != inst._secondField ? inst._selectedSecond :
(value < 60 ? value : key));
var fields = this._constrainTime(inst, [hour, minute, second]);
this._setTime(inst, new Date(0, 0, 0, fields[0], fields[1], fields[2]));
inst._lastChr = chr;
}
else if (!this._get(inst, 'show24Hours')) { // Set am/pm based on first char of names
chr = chr.toLowerCase();
var ampmNames = this._get(inst, 'ampmNames');
if ((chr == ampmNames[0].substring(0, 1).toLowerCase() && inst._selectedHour >= 12) ||
(chr == ampmNames[1].substring(0, 1).toLowerCase() && inst._selectedHour < 12)) {
var saveField = inst._field;
inst._field = inst._ampmField;
this._adjustField(inst, +1);
inst._field = saveField;
this._showField(inst);
}
}
}
});
/* jQuery extend now ignores nulls!
@param target (object) the object to update
@param props (object) the new settings
@return (object) the updated object
*/
function extendRemove(target, props) {
jQuery.extend(target, props);
for (var name in props) if (props[name] == null) target[name] = null;
return target;
}
// Commands that don't return a jQuery object
var getters = ['getOffset', 'getTime', 'isDisabled'];
/* Attach the time entry functionality to a jQuery selection.
@param command (string) the command to run (optional, default 'attach')
@param options (object) the new settings to use for these countdown instances (optional)
@return (jQuery) for chaining further calls
*/
jQuery.fn.timeEntry = function(options) {
var otherArgs = Array.prototype.slice.call(arguments, 1);
if (typeof options == 'string' && jQuery.inArray(options, getters) > -1) {
return jQuery.timeEntry['_' + options + 'TimeEntry'].apply(jQuery.timeEntry, [this[0]].concat(otherArgs));
}
return this.each(function() {
var nodeName = this.nodeName.toLowerCase();
if (nodeName == 'input') {
if (typeof options == 'string')
jQuery.timeEntry['_' + options + 'TimeEntry'].apply(jQuery.timeEntry, [this].concat(otherArgs));
else {
// Check for settings on the control itself
var inlineSettings = (jQuery.fn.metadata ? jQuery(this).metadata() : {});
jQuery.timeEntry._connectTimeEntry(this, jQuery.extend(inlineSettings, options));
}
}
});
};
/* Initialise the time entry functionality. */
jQuery.timeEntry = new TimeEntry(); // Singleton instance
})(jQuery);