/* http://keith-wood.name/timeEntry.html Time entry for jQuery v1.4.9. Written by Keith Wood (kbwood{at}iinet.com.au) June 2007. 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, or via spinner buttons. 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, and to constrain the time to steps, e.g. only on the quarter hours. Attach it with $('input selector').timeEntry(); for default settings, or configure it with options like: $('input selector').timeEntry( {spinnerImage: 'spinnerSquare.png', spinnerSize: [20, 20, 0]}); */ (function($) { // Hide scope, no $ conflict /* TimeEntry manager. Use the singleton instance of this class, $.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.regional = []; // Available regional settings, indexed by language code this.regional[''] = { // Default regional settings show24Hours: false, // True to use 24 hour time, false for 12 hour (AM/PM) separator: ':', // The separator between time fields ampmPrefix: '', // The separator before the AM/PM text ampmNames: ['AM', 'PM'], // Names of morning/evening markers spinnerTexts: ['Now', 'Previous field', 'Next field', 'Increment', 'Decrement'] // The popup texts for the spinner image areas }; this._defaults = { appendText: '', // Display text following the input box, e.g. showing the format showSeconds: false, // True to show seconds as well, false for hours/minutes only timeSteps: [1, 1, 1], // Steps for each of hours/minutes/seconds when incrementing/decrementing initialField: 0, // The field to highlight initially, 0 = hours, 1 = minutes, ... useMouseWheel: true, // True to use mouse wheel for increment/decrement if possible, // false to never use it 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 spinnerImage: 'spinnerDefault.png', // The URL of the images to use for the time spinner // Seven images packed horizontally for normal, each button pressed, and disabled spinnerSize: [20, 20, 8], // The width and height of the spinner image, // and size of centre button for current time spinnerBigImage: '', // The URL of the images to use for the expanded time spinner // Seven images packed horizontally for normal, each button pressed, and disabled spinnerBigSize: [40, 40, 16], // The width and height of the expanded spinner image, // and size of centre button for current time spinnerIncDecOnly: false, // True for increment/decrement buttons only, false for all spinnerRepeat: [500, 250], // Initial and subsequent waits in milliseconds // for repeats on the spinner buttons beforeShow: null, // Function that takes an input field and // returns a set of custom settings for the time entry beforeSetTime: null // Function that runs before updating the time, // takes the old and new times, and minimum and maximum times as parameters, // and returns an adjusted time if necessary }; $.extend(this._defaults, this.regional['']); } var PROP_NAME = 'timeEntry'; $.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 = $(target); if (input.hasClass(this.markerClassName)) { return; } var inst = {}; inst.options = $.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 = $(target); // The attached input field $.data(target, PROP_NAME, inst); var spinnerImage = this._get(inst, 'spinnerImage'); var spinnerText = this._get(inst, 'spinnerText'); var spinnerSize = this._get(inst, 'spinnerSize'); var appendText = this._get(inst, 'appendText'); var spinner = (!spinnerImage ? null : $('')); input.wrap(''). after(appendText ? '' + appendText + '' : ''). after(spinner || ''); 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 ($.browser.mozilla) { input.bind('input.timeEntry', function(event) { $.timeEntry._parseTime(inst); }); } if ($.browser.msie) { input.bind('paste.timeEntry', function(event) { setTimeout(function() { $.timeEntry._parseTime(inst); }, 1); }); } // Allow mouse wheel usage if (this._get(inst, 'useMouseWheel') && $.fn.mousewheel) { input.mousewheel(this._doMouseWheel); } if (spinner) { spinner.mousedown(this._handleSpinner).mouseup(this._endSpinner). mouseover(this._expandSpinner).mouseout(this._endSpinner). mousemove(this._describeSpinner); } }, /* Enable a time entry input and any associated spinner. @param input (element) single input field */ _enableTimeEntry: function(input) { this._enableDisable(input, false); }, /* Disable a time entry input and any associated spinner. @param input (element) single input field */ _disableTimeEntry: function(input) { this._enableDisable(input, true); }, /* Enable or disable a time entry input and any associated spinner. @param input (element) single input field @param disable (boolean) true to disable, false to enable */ _enableDisable: function(input, disable) { var inst = $.data(input, PROP_NAME); if (!inst) { return; } input.disabled = disable; if (input.nextSibling && input.nextSibling.nodeName.toLowerCase() == 'span') { $.timeEntry._changeSpinner(inst, input.nextSibling, (disable ? 5 : -1)); } $.timeEntry._disabledInputs = $.map($.timeEntry._disabledInputs, function(value) { return (value == input ? null : value); }); // Delete entry if (disable) { $.timeEntry._disabledInputs.push(input); } }, /* 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 $.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 = $.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])); } } $.data(input, PROP_NAME, inst); }, /* Remove the time entry functionality from an input. @param input (element) input field to affect */ _destroyTimeEntry: function(input) { $input = $(input); if (!$input.hasClass(this.markerClassName)) { return; } $input.removeClass(this.markerClassName).unbind('.timeEntry'); if ($.fn.mousewheel) { $input.unmousewheel(); } this._disabledInputs = $.map(this._disabledInputs, function(value) { return (value == input ? null : value); }); // Delete entry $input.parent().replaceWith($input); $.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 = $.data(input, PROP_NAME); if (inst) { if (time === null || time === '') { inst.input.val(''); } else { 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 = $.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 = $.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 ($.timeEntry._lastInput == input || $.timeEntry._isDisabledTimeEntry(input)) { $.timeEntry._focussed = false; return; } var inst = $.data(input, PROP_NAME); $.timeEntry._focussed = true; $.timeEntry._lastInput = input; $.timeEntry._blurredInput = null; var beforeShow = $.timeEntry._get(inst, 'beforeShow'); extendRemove(inst.options, (beforeShow ? beforeShow.apply(input, [input]) : {})); $.data(input, PROP_NAME, inst); $.timeEntry._parseTime(inst); setTimeout(function() { $.timeEntry._showField(inst); }, 10); }, /* Note that the field has been exited. @param event (event) the blur event */ _doBlur: function(event) { $.timeEntry._blurredInput = $.timeEntry._lastInput; $.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 = $.data(input, PROP_NAME); if (!$.timeEntry._focussed) { var fieldSize = $.timeEntry._get(inst, 'separator').length + 2; 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) + $.timeEntry._get(inst, 'ampmPrefix').length + $.timeEntry._get(inst, 'ampmNames')[0].length); inst._field = field; if (input.selectionStart < end) { break; } } } else if (input.createTextRange) { // Check against bounding boxes var src = $(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) + $.timeEntry._get(inst, 'ampmPrefix').length + $.timeEntry._get(inst, 'ampmNames')[0].length); range.collapse(); range.moveEnd('character', end); inst._field = field; if (offsetX < range.boundingWidth) { // And compare break; } } } } $.data(input, PROP_NAME, inst); $.timeEntry._showField(inst); $.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) { // >= '0' return true; } var inst = $.data(event.target, PROP_NAME); switch (event.keyCode) { case 9: return (event.shiftKey ? // Move to previous time field, or out if at the beginning $.timeEntry._changeField(inst, -1, true) : // Move to next time field, or out if at the end $.timeEntry._changeField(inst, +1, true)); case 35: if (event.ctrlKey) { // Clear time on ctrl+end $.timeEntry._setValue(inst, ''); } else { // Last field on end inst._field = Math.max(1, inst._secondField, inst._ampmField); $.timeEntry._adjustField(inst, 0); } break; case 36: if (event.ctrlKey) { // Current time on ctrl+home $.timeEntry._setTime(inst); } else { // First field on home inst._field = 0; $.timeEntry._adjustField(inst, 0); } break; case 37: $.timeEntry._changeField(inst, -1, false); break; // Previous field on left case 38: $.timeEntry._adjustField(inst, +1); break; // Increment time field on up case 39: $.timeEntry._changeField(inst, +1, false); break; // Next field on right case 40: $.timeEntry._adjustField(inst, -1); break; // Decrement time field on down case 46: $.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 = $.data(event.target, PROP_NAME); $.timeEntry._handleKeyPress(inst, chr); return false; }, /* Increment/decrement on mouse wheel activity. @param event (event) the mouse wheel event @param delta (number) the amount of change */ _doMouseWheel: function(event, delta) { if ($.timeEntry._isDisabledTimeEntry(event.target)) { return; } delta = ($.browser.opera ? -delta / Math.abs(delta) : ($.browser.safari ? delta / Math.abs(delta) : delta)); var inst = $.data(event.target, PROP_NAME); inst.input.focus(); if (!inst.input.val()) { $.timeEntry._parseTime(inst); } $.timeEntry._adjustField(inst, delta); event.preventDefault(); }, /* Expand the spinner, if possible, to make it easier to use. @param event (event) the mouse over event */ _expandSpinner: function(event) { var spinner = $.timeEntry._getSpinnerTarget(event); var inst = $.data($.timeEntry._getInput(spinner), PROP_NAME); if ($.timeEntry._isDisabledTimeEntry(inst.input[0])) { return; } var spinnerBigImage = $.timeEntry._get(inst, 'spinnerBigImage'); if (spinnerBigImage) { inst._expanded = true; var offset = $(spinner).offset(); var relative = null; $(spinner).parents().each(function() { var parent = $(this); if (parent.css('position') == 'relative' || parent.css('position') == 'absolute') { relative = parent.offset(); } return !relative; }); var spinnerSize = $.timeEntry._get(inst, 'spinnerSize'); var spinnerBigSize = $.timeEntry._get(inst, 'spinnerBigSize'); $('
'). mousedown($.timeEntry._handleSpinner).mouseup($.timeEntry._endSpinner). mouseout($.timeEntry._endExpand).mousemove($.timeEntry._describeSpinner). insertAfter(spinner); } }, /* Locate the actual input field from the spinner. @param spinner (element) the current spinner @return (element) the corresponding input */ _getInput: function(spinner) { return $(spinner).siblings('.' + $.timeEntry.markerClassName)[0]; }, /* Change the title based on position within the spinner. @param event (event) the mouse move event */ _describeSpinner: function(event) { var spinner = $.timeEntry._getSpinnerTarget(event); var inst = $.data($.timeEntry._getInput(spinner), PROP_NAME); spinner.title = $.timeEntry._get(inst, 'spinnerTexts') [$.timeEntry._getSpinnerRegion(inst, event)]; }, /* Handle a click on the spinner. @param event (event) the mouse click event */ _handleSpinner: function(event) { var spinner = $.timeEntry._getSpinnerTarget(event); var input = $.timeEntry._getInput(spinner); if ($.timeEntry._isDisabledTimeEntry(input)) { return; } if (input == $.timeEntry._blurredInput) { $.timeEntry._lastInput = input; $.timeEntry._blurredInput = null; } var inst = $.data(input, PROP_NAME); $.timeEntry._doFocus(input); var region = $.timeEntry._getSpinnerRegion(inst, event); $.timeEntry._changeSpinner(inst, spinner, region); $.timeEntry._actionSpinner(inst, region); $.timeEntry._timer = null; $.timeEntry._handlingSpinner = true; var spinnerRepeat = $.timeEntry._get(inst, 'spinnerRepeat'); if (region >= 3 && spinnerRepeat[0]) { // Repeat increment/decrement $.timeEntry._timer = setTimeout( function() { $.timeEntry._repeatSpinner(inst, region); }, spinnerRepeat[0]); $(spinner).one('mouseout', $.timeEntry._releaseSpinner). one('mouseup', $.timeEntry._releaseSpinner); } }, /* Action a click on the spinner. @param inst (object) the instance settings @param region (number) the spinner "button" */ _actionSpinner: function(inst, region) { if (!inst.input.val()) { $.timeEntry._parseTime(inst); } switch (region) { case 0: this._setTime(inst); break; case 1: this._changeField(inst, -1, false); break; case 2: this._changeField(inst, +1, false); break; case 3: this._adjustField(inst, +1); break; case 4: this._adjustField(inst, -1); break; } }, /* Repeat a click on the spinner. @param inst (object) the instance settings @param region (number) the spinner "button" */ _repeatSpinner: function(inst, region) { if (!$.timeEntry._timer) { return; } $.timeEntry._lastInput = $.timeEntry._blurredInput; this._actionSpinner(inst, region); this._timer = setTimeout( function() { $.timeEntry._repeatSpinner(inst, region); }, this._get(inst, 'spinnerRepeat')[1]); }, /* Stop a spinner repeat. @param event (event) the mouse event */ _releaseSpinner: function(event) { clearTimeout($.timeEntry._timer); $.timeEntry._timer = null; }, /* Tidy up after an expanded spinner. @param event (event) the mouse event */ _endExpand: function(event) { $.timeEntry._timer = null; var spinner = $.timeEntry._getSpinnerTarget(event); var input = $.timeEntry._getInput(spinner); var inst = $.data(input, PROP_NAME); $(spinner).remove(); inst._expanded = false; }, /* Tidy up after a spinner click. @param event (event) the mouse event */ _endSpinner: function(event) { $.timeEntry._timer = null; var spinner = $.timeEntry._getSpinnerTarget(event); var input = $.timeEntry._getInput(spinner); var inst = $.data(input, PROP_NAME); if (!$.timeEntry._isDisabledTimeEntry(input)) { $.timeEntry._changeSpinner(inst, spinner, -1); } if ($.timeEntry._handlingSpinner) { $.timeEntry._lastInput = $.timeEntry._blurredInput; } if ($.timeEntry._lastInput && $.timeEntry._handlingSpinner) { $.timeEntry._showField(inst); } $.timeEntry._handlingSpinner = false; }, /* Retrieve the spinner from the event. @param event (event) the mouse click event @return (element) the target field */ _getSpinnerTarget: function(event) { return event.target || event.srcElement; }, /* Determine which "button" within the spinner was clicked. @param inst (object) the instance settings @param event (event) the mouse event @return (number) the spinner "button" number */ _getSpinnerRegion: function(inst, event) { var spinner = this._getSpinnerTarget(event); var pos = ($.browser.opera || $.browser.safari ? $.timeEntry._findPos(spinner) : $(spinner).offset()); var scrolled = ($.browser.safari ? $.timeEntry._findScroll(spinner) : [document.documentElement.scrollLeft || document.body.scrollLeft, document.documentElement.scrollTop || document.body.scrollTop]); var spinnerIncDecOnly = this._get(inst, 'spinnerIncDecOnly'); var left = (spinnerIncDecOnly ? 99 : event.clientX + scrolled[0] - pos.left - ($.browser.msie ? 2 : 0)); var top = event.clientY + scrolled[1] - pos.top - ($.browser.msie ? 2 : 0); var spinnerSize = this._get(inst, (inst._expanded ? 'spinnerBigSize' : 'spinnerSize')); var right = (spinnerIncDecOnly ? 99 : spinnerSize[0] - 1 - left); var bottom = spinnerSize[1] - 1 - top; if (spinnerSize[2] > 0 && Math.abs(left - right) <= spinnerSize[2] && Math.abs(top - bottom) <= spinnerSize[2]) { return 0; // Centre button } var min = Math.min(left, top, right, bottom); return (min == left ? 1 : (min == right ? 2 : (min == top ? 3 : 4))); // Nearest edge }, /* Change the spinner image depending on button clicked. @param inst (object) the instance settings @param spinner (element) the spinner control @param region (number) the spinner "button" */ _changeSpinner: function(inst, spinner, region) { $(spinner).css('background-position', '-' + ((region + 1) * this._get(inst, (inst._expanded ? 'spinnerBigSize' : 'spinnerSize'))[0]) + 'px 0px'); }, /* Find an object's position on the screen. @param obj (element) the control @return (object) position as .left and .top */ _findPos: function(obj) { var curLeft = curTop = 0; if (obj.offsetParent) { curLeft = obj.offsetLeft; curTop = obj.offsetTop; while (obj = obj.offsetParent) { var origCurLeft = curLeft; curLeft += obj.offsetLeft; if (curLeft < 0) { curLeft = origCurLeft; } curTop += obj.offsetTop; } } return {left: curLeft, top: curTop}; }, /* Find an object's scroll offset on the screen. @param obj (element) the control @return (number[]) offset as [left, top] */ _findScroll: function(obj) { var isFixed = false; $(obj).parents().each(function() { isFixed |= $(this).css('position') == 'fixed'; }); if (isFixed) { return [0, 0]; } var scrollLeft = obj.scrollLeft; var scrollTop = obj.scrollTop; while (obj = obj.parentNode) { scrollLeft += obj.scrollLeft || 0; scrollTop += obj.scrollTop || 0; } return [scrollLeft, scrollTop]; }, /* 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] : $.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), this._get(inst, 'initialField'))); 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 separator = this._get(inst, 'separator'); var currentTime = value.split(separator); if (separator == '' && value != '') { currentTime[0] = value.substring(0, 2); currentTime[1] = value.substring(2, 4); currentTime[2] = value.substring(4, 6); } 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()]; } var reset = false; var timeSteps = this._get(inst, 'timeSteps'); for (var i = 0; i < timeSteps.length; i++) { if (reset) { fields[i] = 0; } else if (timeSteps[i] > 1) { fields[i] = Math.round(fields[i] / timeSteps[i]) * timeSteps[i]; reset = true; } } 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 separator = this._get(inst, 'separator'); var currentTime = (this._formatNumber(show24Hours ? inst._selectedHour : ((inst._selectedHour + 11) % 12) + 1) + separator + this._formatNumber(inst._selectedMinute) + (this._get(inst, 'showSeconds') ? separator + this._formatNumber(inst._selectedSecond) : '') + (show24Hours ? '' : this._get(inst, 'ampmPrefix') + 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') || $.timeEntry._lastInput != input) { return; } var separator = this._get(inst, 'separator'); var fieldSize = separator.length + 2; var start = (inst._field != inst._ampmField ? (inst._field * fieldSize) : (inst._ampmField * fieldSize) - separator.length + this._get(inst, 'ampmPrefix').length); var end = start + (inst._field != inst._ampmField ? 2 : this._get(inst, 'ampmNames')[0].length); 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 = ''; $.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; } var timeSteps = this._get(inst, 'timeSteps'); this._setTime(inst, new Date(0, 0, 0, inst._selectedHour + (inst._field == 0 ? offset * timeSteps[0] : 0) + (inst._field == inst._ampmField ? offset * 12 : 0), inst._selectedMinute + (inst._field == 1 ? offset * timeSteps[1] : 0), inst._selectedSecond + (inst._field == inst._secondField ? offset * timeSteps[2] : 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)); var beforeSetTime = this._get(inst, 'beforeSetTime'); // Perform further restrictions if required if (beforeSetTime) { time = beforeSetTime.apply(inst.input[0], [this._getTimeTimeEntry(inst.input[0]), time, minTime, maxTime]); } inst._selectedHour = time.getHours(); inst._selectedMinute = time.getMinutes(); inst._selectedSecond = time.getSeconds(); this._showTime(inst); $.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 = $.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._get(inst, 'separator')) { 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) { $.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 */ $.fn.timeEntry = function(options) { var otherArgs = Array.prototype.slice.call(arguments, 1); if (typeof options == 'string' && $.inArray(options, getters) > -1) { return $.timeEntry['_' + options + 'TimeEntry'].apply($.timeEntry, [this[0]].concat(otherArgs)); } return this.each(function() { var nodeName = this.nodeName.toLowerCase(); if (nodeName == 'input') { if (typeof options == 'string') { $.timeEntry['_' + options + 'TimeEntry'].apply($.timeEntry, [this].concat(otherArgs)); } else { // Check for settings on the control itself var inlineSettings = ($.fn.metadata ? $(this).metadata() : {}); $.timeEntry._connectTimeEntry(this, $.extend(inlineSettings, options)); } } }); }; /* Initialise the time entry functionality. */ $.timeEntry = new TimeEntry(); // Singleton instance })(jQuery);