| 1 | /* http://keith-wood.name/timeEntry.html |
| 2 | Time entry for jQuery v1.4.9. |
| 3 | Written by Keith Wood (kbwood{at}iinet.com.au) June 2007. |
| 4 | Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and |
| 5 | MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses. |
| 6 | Please attribute the author if you use it. */ |
| 7 | |
| 8 | /* Turn an input field into an entry point for a time value. |
| 9 | The time can be entered via directly typing the value, |
| 10 | via the arrow keys, or via spinner buttons. |
| 11 | It is configurable to show 12 or 24-hour time, to show or hide seconds, |
| 12 | to enforce a minimum and/or maximum time, to change the spinner image, |
| 13 | and to constrain the time to steps, e.g. only on the quarter hours. |
| 14 | Attach it with $('input selector').timeEntry(); for default settings, |
| 15 | or configure it with options like: |
| 16 | $('input selector').timeEntry( |
| 17 | {spinnerImage: 'spinnerSquare.png', spinnerSize: [20, 20, 0]}); */ |
| 18 | |
| 19 | (function($) { // Hide scope, no $ conflict |
| 20 | |
| 21 | /* TimeEntry manager. |
| 22 | Use the singleton instance of this class, $.timeEntry, to interact with the time entry |
| 23 | functionality. Settings for (groups of) fields are maintained in an instance object |
| 24 | (TimeEntryInstance), allowing multiple different settings on the same page. */ |
| 25 | function TimeEntry() { |
| 26 | this._disabledInputs = []; // List of time entry inputs that have been disabled |
| 27 | this.regional = []; // Available regional settings, indexed by language code |
| 28 | this.regional[''] = { // Default regional settings |
| 29 | show24Hours: false, // True to use 24 hour time, false for 12 hour (AM/PM) |
| 30 | separator: ':', // The separator between time fields |
| 31 | ampmPrefix: '', // The separator before the AM/PM text |
| 32 | ampmNames: ['AM', 'PM'], // Names of morning/evening markers |
| 33 | spinnerTexts: ['Now', 'Previous field', 'Next field', 'Increment', 'Decrement'] |
| 34 | // The popup texts for the spinner image areas |
| 35 | }; |
| 36 | this._defaults = { |
| 37 | appendText: '', // Display text following the input box, e.g. showing the format |
| 38 | showSeconds: false, // True to show seconds as well, false for hours/minutes only |
| 39 | timeSteps: [1, 1, 1], // Steps for each of hours/minutes/seconds when incrementing/decrementing |
| 40 | initialField: 0, // The field to highlight initially, 0 = hours, 1 = minutes, ... |
| 41 | useMouseWheel: true, // True to use mouse wheel for increment/decrement if possible, |
| 42 | // false to never use it |
| 43 | defaultTime: null, // The time to use if none has been set, leave at null for now |
| 44 | minTime: null, // The earliest selectable time, or null for no limit |
| 45 | maxTime: null, // The latest selectable time, or null for no limit |
| 46 | spinnerImage: 'spinnerDefault.png', // The URL of the images to use for the time spinner |
| 47 | // Seven images packed horizontally for normal, each button pressed, and disabled |
| 48 | spinnerSize: [20, 20, 8], // The width and height of the spinner image, |
| 49 | // and size of centre button for current time |
| 50 | spinnerBigImage: '', // The URL of the images to use for the expanded time spinner |
| 51 | // Seven images packed horizontally for normal, each button pressed, and disabled |
| 52 | spinnerBigSize: [40, 40, 16], // The width and height of the expanded spinner image, |
| 53 | // and size of centre button for current time |
| 54 | spinnerIncDecOnly: false, // True for increment/decrement buttons only, false for all |
| 55 | spinnerRepeat: [500, 250], // Initial and subsequent waits in milliseconds |
| 56 | // for repeats on the spinner buttons |
| 57 | beforeShow: null, // Function that takes an input field and |
| 58 | // returns a set of custom settings for the time entry |
| 59 | beforeSetTime: null // Function that runs before updating the time, |
| 60 | // takes the old and new times, and minimum and maximum times as parameters, |
| 61 | // and returns an adjusted time if necessary |
| 62 | }; |
| 63 | $.extend(this._defaults, this.regional['']); |
| 64 | } |
| 65 | |
| 66 | var PROP_NAME = 'timeEntry'; |
| 67 | |
| 68 | $.extend(TimeEntry.prototype, { |
| 69 | /* Class name added to elements to indicate already configured with time entry. */ |
| 70 | markerClassName: 'hasTimeEntry', |
| 71 | |
| 72 | /* Override the default settings for all instances of the time entry. |
| 73 | @param options (object) the new settings to use as defaults (anonymous object) |
| 74 | @return (DateEntry) this object */ |
| 75 | setDefaults: function(options) { |
| 76 | extendRemove(this._defaults, options || {}); |
| 77 | return this; |
| 78 | }, |
| 79 | |
| 80 | /* Attach the time entry handler to an input field. |
| 81 | @param target (element) the field to attach to |
| 82 | @param options (object) custom settings for this instance */ |
| 83 | _connectTimeEntry: function(target, options) { |
| 84 | var input = $(target); |
| 85 | if (input.hasClass(this.markerClassName)) { |
| 86 | return; |
| 87 | } |
| 88 | var inst = {}; |
| 89 | inst.options = $.extend({}, options); |
| 90 | inst._selectedHour = 0; // The currently selected hour |
| 91 | inst._selectedMinute = 0; // The currently selected minute |
| 92 | inst._selectedSecond = 0; // The currently selected second |
| 93 | inst._field = 0; // The selected subfield |
| 94 | inst.input = $(target); // The attached input field |
| 95 | $.data(target, PROP_NAME, inst); |
| 96 | var spinnerImage = this._get(inst, 'spinnerImage'); |
| 97 | var spinnerText = this._get(inst, 'spinnerText'); |
| 98 | var spinnerSize = this._get(inst, 'spinnerSize'); |
| 99 | var appendText = this._get(inst, 'appendText'); |
| 100 | var spinner = (!spinnerImage ? null : |
| 101 | $('<span class="timeEntry_control" style="display: inline-block; ' + |
| 102 | 'background: url(\'' + spinnerImage + '\') 0 0 no-repeat; ' + |
| 103 | 'width: ' + spinnerSize[0] + 'px; height: ' + spinnerSize[1] + 'px;' + |
| 104 | ($.browser.mozilla && $.browser.version < '1.9' ? // FF 2- (Win) |
| 105 | ' padding-left: ' + spinnerSize[0] + 'px; padding-bottom: ' + |
| 106 | (spinnerSize[1] - 18) + 'px;' : '') + '"></span>')); |
| 107 | input.wrap('<span class="timeEntry_wrap"></span>'). |
| 108 | after(appendText ? '<span class="timeEntry_append">' + appendText + '</span>' : ''). |
| 109 | after(spinner || ''); |
| 110 | input.addClass(this.markerClassName).bind('focus.timeEntry', this._doFocus). |
| 111 | bind('blur.timeEntry', this._doBlur).bind('click.timeEntry', this._doClick). |
| 112 | bind('keydown.timeEntry', this._doKeyDown).bind('keypress.timeEntry', this._doKeyPress); |
| 113 | // Check pastes |
| 114 | if ($.browser.mozilla) { |
| 115 | input.bind('input.timeEntry', function(event) { $.timeEntry._parseTime(inst); }); |
| 116 | } |
| 117 | if ($.browser.msie) { |
| 118 | input.bind('paste.timeEntry', |
| 119 | function(event) { setTimeout(function() { $.timeEntry._parseTime(inst); }, 1); }); |
| 120 | } |
| 121 | // Allow mouse wheel usage |
| 122 | if (this._get(inst, 'useMouseWheel') && $.fn.mousewheel) { |
| 123 | input.mousewheel(this._doMouseWheel); |
| 124 | } |
| 125 | if (spinner) { |
| 126 | spinner.mousedown(this._handleSpinner).mouseup(this._endSpinner). |
| 127 | mouseover(this._expandSpinner).mouseout(this._endSpinner). |
| 128 | mousemove(this._describeSpinner); |
| 129 | } |
| 130 | }, |
| 131 | |
| 132 | /* Enable a time entry input and any associated spinner. |
| 133 | @param input (element) single input field */ |
| 134 | _enableTimeEntry: function(input) { |
| 135 | this._enableDisable(input, false); |
| 136 | }, |
| 137 | |
| 138 | /* Disable a time entry input and any associated spinner. |
| 139 | @param input (element) single input field */ |
| 140 | _disableTimeEntry: function(input) { |
| 141 | this._enableDisable(input, true); |
| 142 | }, |
| 143 | |
| 144 | /* Enable or disable a time entry input and any associated spinner. |
| 145 | @param input (element) single input field |
| 146 | @param disable (boolean) true to disable, false to enable */ |
| 147 | _enableDisable: function(input, disable) { |
| 148 | var inst = $.data(input, PROP_NAME); |
| 149 | if (!inst) { |
| 150 | return; |
| 151 | } |
| 152 | input.disabled = disable; |
| 153 | if (input.nextSibling && input.nextSibling.nodeName.toLowerCase() == 'span') { |
| 154 | $.timeEntry._changeSpinner(inst, input.nextSibling, (disable ? 5 : -1)); |
| 155 | } |
| 156 | $.timeEntry._disabledInputs = $.map($.timeEntry._disabledInputs, |
| 157 | function(value) { return (value == input ? null : value); }); // Delete entry |
| 158 | if (disable) { |
| 159 | $.timeEntry._disabledInputs.push(input); |
| 160 | } |
| 161 | }, |
| 162 | |
| 163 | /* Check whether an input field has been disabled. |
| 164 | @param input (element) input field to check |
| 165 | @return (boolean) true if this field has been disabled, false if it is enabled */ |
| 166 | _isDisabledTimeEntry: function(input) { |
| 167 | return $.inArray(input, this._disabledInputs) > -1; |
| 168 | }, |
| 169 | |
| 170 | /* Reconfigure the settings for a time entry field. |
| 171 | @param input (element) input field to change |
| 172 | @param options (object) new settings to add or |
| 173 | (string) an individual setting name |
| 174 | @param value (any) the individual setting's value */ |
| 175 | _changeTimeEntry: function(input, options, value) { |
| 176 | var inst = $.data(input, PROP_NAME); |
| 177 | if (inst) { |
| 178 | if (typeof options == 'string') { |
| 179 | var name = options; |
| 180 | options = {}; |
| 181 | options[name] = value; |
| 182 | } |
| 183 | var currentTime = this._extractTime(inst); |
| 184 | extendRemove(inst.options, options || {}); |
| 185 | if (currentTime) { |
| 186 | this._setTime(inst, new Date(0, 0, 0, |
| 187 | currentTime[0], currentTime[1], currentTime[2])); |
| 188 | } |
| 189 | } |
| 190 | $.data(input, PROP_NAME, inst); |
| 191 | }, |
| 192 | |
| 193 | /* Remove the time entry functionality from an input. |
| 194 | @param input (element) input field to affect */ |
| 195 | _destroyTimeEntry: function(input) { |
| 196 | $input = $(input); |
| 197 | if (!$input.hasClass(this.markerClassName)) { |
| 198 | return; |
| 199 | } |
| 200 | $input.removeClass(this.markerClassName).unbind('.timeEntry'); |
| 201 | if ($.fn.mousewheel) { |
| 202 | $input.unmousewheel(); |
| 203 | } |
| 204 | this._disabledInputs = $.map(this._disabledInputs, |
| 205 | function(value) { return (value == input ? null : value); }); // Delete entry |
| 206 | $input.parent().replaceWith($input); |
| 207 | $.removeData(input, PROP_NAME); |
| 208 | }, |
| 209 | |
| 210 | /* Initialise the current time for a time entry input field. |
| 211 | @param input (element) input field to update |
| 212 | @param time (Date) the new time (year/month/day ignored) or null for now */ |
| 213 | _setTimeTimeEntry: function(input, time) { |
| 214 | var inst = $.data(input, PROP_NAME); |
| 215 | if (inst) { |
| 216 | if (time === null || time === '') { |
| 217 | inst.input.val(''); |
| 218 | } |
| 219 | else { |
| 220 | this._setTime(inst, time ? (typeof time == 'object' ? |
| 221 | new Date(time.getTime()) : time) : null); |
| 222 | } |
| 223 | } |
| 224 | }, |
| 225 | |
| 226 | /* Retrieve the current time for a time entry input field. |
| 227 | @param input (element) input field to examine |
| 228 | @return (Date) current time (year/month/day zero) or null if none */ |
| 229 | _getTimeTimeEntry: function(input) { |
| 230 | var inst = $.data(input, PROP_NAME); |
| 231 | var currentTime = (inst ? this._extractTime(inst) : null); |
| 232 | return (!currentTime ? null : |
| 233 | new Date(0, 0, 0, currentTime[0], currentTime[1], currentTime[2])); |
| 234 | }, |
| 235 | |
| 236 | /* Retrieve the millisecond offset for the current time. |
| 237 | @param input (element) input field to examine |
| 238 | @return (number) the time as milliseconds offset or zero if none */ |
| 239 | _getOffsetTimeEntry: function(input) { |
| 240 | var inst = $.data(input, PROP_NAME); |
| 241 | var currentTime = (inst ? this._extractTime(inst) : null); |
| 242 | return (!currentTime ? 0 : |
| 243 | (currentTime[0] * 3600 + currentTime[1] * 60 + currentTime[2]) * 1000); |
| 244 | }, |
| 245 | |
| 246 | /* Initialise time entry. |
| 247 | @param target (element) the input field or |
| 248 | (event) the focus event */ |
| 249 | _doFocus: function(target) { |
| 250 | var input = (target.nodeName && target.nodeName.toLowerCase() == 'input' ? target : this); |
| 251 | if ($.timeEntry._lastInput == input || $.timeEntry._isDisabledTimeEntry(input)) { |
| 252 | $.timeEntry._focussed = false; |
| 253 | return; |
| 254 | } |
| 255 | var inst = $.data(input, PROP_NAME); |
| 256 | $.timeEntry._focussed = true; |
| 257 | $.timeEntry._lastInput = input; |
| 258 | $.timeEntry._blurredInput = null; |
| 259 | var beforeShow = $.timeEntry._get(inst, 'beforeShow'); |
| 260 | extendRemove(inst.options, (beforeShow ? beforeShow.apply(input, [input]) : {})); |
| 261 | $.data(input, PROP_NAME, inst); |
| 262 | $.timeEntry._parseTime(inst); |
| 263 | setTimeout(function() { $.timeEntry._showField(inst); }, 10); |
| 264 | }, |
| 265 | |
| 266 | /* Note that the field has been exited. |
| 267 | @param event (event) the blur event */ |
| 268 | _doBlur: function(event) { |
| 269 | $.timeEntry._blurredInput = $.timeEntry._lastInput; |
| 270 | $.timeEntry._lastInput = null; |
| 271 | }, |
| 272 | |
| 273 | /* Select appropriate field portion on click, if already in the field. |
| 274 | @param event (event) the click event */ |
| 275 | _doClick: function(event) { |
| 276 | var input = event.target; |
| 277 | var inst = $.data(input, PROP_NAME); |
| 278 | if (!$.timeEntry._focussed) { |
| 279 | var fieldSize = $.timeEntry._get(inst, 'separator').length + 2; |
| 280 | inst._field = 0; |
| 281 | if (input.selectionStart != null) { // Use input select range |
| 282 | for (var field = 0; field <= Math.max(1, inst._secondField, inst._ampmField); field++) { |
| 283 | var end = (field != inst._ampmField ? (field * fieldSize) + 2 : |
| 284 | (inst._ampmField * fieldSize) + $.timeEntry._get(inst, 'ampmPrefix').length + |
| 285 | $.timeEntry._get(inst, 'ampmNames')[0].length); |
| 286 | inst._field = field; |
| 287 | if (input.selectionStart < end) { |
| 288 | break; |
| 289 | } |
| 290 | } |
| 291 | } |
| 292 | else if (input.createTextRange) { // Check against bounding boxes |
| 293 | var src = $(event.srcElement); |
| 294 | var range = input.createTextRange(); |
| 295 | var convert = function(value) { |
| 296 | return {thin: 2, medium: 4, thick: 6}[value] || value; |
| 297 | }; |
| 298 | var offsetX = event.clientX + document.documentElement.scrollLeft - |
| 299 | (src.offset().left + parseInt(convert(src.css('border-left-width')), 10)) - |
| 300 | range.offsetLeft; // Position - left edge - alignment |
| 301 | for (var field = 0; field <= Math.max(1, inst._secondField, inst._ampmField); field++) { |
| 302 | var end = (field != inst._ampmField ? (field * fieldSize) + 2 : |
| 303 | (inst._ampmField * fieldSize) + $.timeEntry._get(inst, 'ampmPrefix').length + |
| 304 | $.timeEntry._get(inst, 'ampmNames')[0].length); |
| 305 | range.collapse(); |
| 306 | range.moveEnd('character', end); |
| 307 | inst._field = field; |
| 308 | if (offsetX < range.boundingWidth) { // And compare |
| 309 | break; |
| 310 | } |
| 311 | } |
| 312 | } |
| 313 | } |
| 314 | $.data(input, PROP_NAME, inst); |
| 315 | $.timeEntry._showField(inst); |
| 316 | $.timeEntry._focussed = false; |
| 317 | }, |
| 318 | |
| 319 | /* Handle keystrokes in the field. |
| 320 | @param event (event) the keydown event |
| 321 | @return (boolean) true to continue, false to stop processing */ |
| 322 | _doKeyDown: function(event) { |
| 323 | if (event.keyCode >= 48) { // >= '0' |
| 324 | return true; |
| 325 | } |
| 326 | var inst = $.data(event.target, PROP_NAME); |
| 327 | switch (event.keyCode) { |
| 328 | case 9: return (event.shiftKey ? |
| 329 | // Move to previous time field, or out if at the beginning |
| 330 | $.timeEntry._changeField(inst, -1, true) : |
| 331 | // Move to next time field, or out if at the end |
| 332 | $.timeEntry._changeField(inst, +1, true)); |
| 333 | case 35: if (event.ctrlKey) { // Clear time on ctrl+end |
| 334 | $.timeEntry._setValue(inst, ''); |
| 335 | } |
| 336 | else { // Last field on end |
| 337 | inst._field = Math.max(1, inst._secondField, inst._ampmField); |
| 338 | $.timeEntry._adjustField(inst, 0); |
| 339 | } |
| 340 | break; |
| 341 | case 36: if (event.ctrlKey) { // Current time on ctrl+home |
| 342 | $.timeEntry._setTime(inst); |
| 343 | } |
| 344 | else { // First field on home |
| 345 | inst._field = 0; |
| 346 | $.timeEntry._adjustField(inst, 0); |
| 347 | } |
| 348 | break; |
| 349 | case 37: $.timeEntry._changeField(inst, -1, false); break; // Previous field on left |
| 350 | case 38: $.timeEntry._adjustField(inst, +1); break; // Increment time field on up |
| 351 | case 39: $.timeEntry._changeField(inst, +1, false); break; // Next field on right |
| 352 | case 40: $.timeEntry._adjustField(inst, -1); break; // Decrement time field on down |
| 353 | case 46: $.timeEntry._setValue(inst, ''); break; // Clear time on delete |
| 354 | } |
| 355 | return false; |
| 356 | }, |
| 357 | |
| 358 | /* Disallow unwanted characters. |
| 359 | @param event (event) the keypress event |
| 360 | @return (boolean) true to continue, false to stop processing */ |
| 361 | _doKeyPress: function(event) { |
| 362 | var chr = String.fromCharCode(event.charCode == undefined ? event.keyCode : event.charCode); |
| 363 | if (chr < ' ') { |
| 364 | return true; |
| 365 | } |
| 366 | var inst = $.data(event.target, PROP_NAME); |
| 367 | $.timeEntry._handleKeyPress(inst, chr); |
| 368 | return false; |
| 369 | }, |
| 370 | |
| 371 | /* Increment/decrement on mouse wheel activity. |
| 372 | @param event (event) the mouse wheel event |
| 373 | @param delta (number) the amount of change */ |
| 374 | _doMouseWheel: function(event, delta) { |
| 375 | if ($.timeEntry._isDisabledTimeEntry(event.target)) { |
| 376 | return; |
| 377 | } |
| 378 | delta = ($.browser.opera ? -delta / Math.abs(delta) : |
| 379 | ($.browser.safari ? delta / Math.abs(delta) : delta)); |
| 380 | var inst = $.data(event.target, PROP_NAME); |
| 381 | inst.input.focus(); |
| 382 | if (!inst.input.val()) { |
| 383 | $.timeEntry._parseTime(inst); |
| 384 | } |
| 385 | $.timeEntry._adjustField(inst, delta); |
| 386 | event.preventDefault(); |
| 387 | }, |
| 388 | |
| 389 | /* Expand the spinner, if possible, to make it easier to use. |
| 390 | @param event (event) the mouse over event */ |
| 391 | _expandSpinner: function(event) { |
| 392 | var spinner = $.timeEntry._getSpinnerTarget(event); |
| 393 | var inst = $.data($.timeEntry._getInput(spinner), PROP_NAME); |
| 394 | if ($.timeEntry._isDisabledTimeEntry(inst.input[0])) { |
| 395 | return; |
| 396 | } |
| 397 | var spinnerBigImage = $.timeEntry._get(inst, 'spinnerBigImage'); |
| 398 | if (spinnerBigImage) { |
| 399 | inst._expanded = true; |
| 400 | var offset = $(spinner).offset(); |
| 401 | var relative = null; |
| 402 | $(spinner).parents().each(function() { |
| 403 | var parent = $(this); |
| 404 | if (parent.css('position') == 'relative' || |
| 405 | parent.css('position') == 'absolute') { |
| 406 | relative = parent.offset(); |
| 407 | } |
| 408 | return !relative; |
| 409 | }); |
| 410 | var spinnerSize = $.timeEntry._get(inst, 'spinnerSize'); |
| 411 | var spinnerBigSize = $.timeEntry._get(inst, 'spinnerBigSize'); |
| 412 | $('<div class="timeEntry_expand" style="position: absolute; left: ' + |
| 413 | (offset.left - (spinnerBigSize[0] - spinnerSize[0]) / 2 - |
| 414 | (relative ? relative.left : 0)) + 'px; top: ' + (offset.top - |
| 415 | (spinnerBigSize[1] - spinnerSize[1]) / 2 - (relative ? relative.top : 0)) + |
| 416 | 'px; width: ' + spinnerBigSize[0] + 'px; height: ' + |
| 417 | spinnerBigSize[1] + 'px; background: transparent url(' + |
| 418 | spinnerBigImage + ') no-repeat 0px 0px; z-index: 10;"></div>'). |
| 419 | mousedown($.timeEntry._handleSpinner).mouseup($.timeEntry._endSpinner). |
| 420 | mouseout($.timeEntry._endExpand).mousemove($.timeEntry._describeSpinner). |
| 421 | insertAfter(spinner); |
| 422 | } |
| 423 | }, |
| 424 | |
| 425 | /* Locate the actual input field from the spinner. |
| 426 | @param spinner (element) the current spinner |
| 427 | @return (element) the corresponding input */ |
| 428 | _getInput: function(spinner) { |
| 429 | return $(spinner).siblings('.' + $.timeEntry.markerClassName)[0]; |
| 430 | }, |
| 431 | |
| 432 | /* Change the title based on position within the spinner. |
| 433 | @param event (event) the mouse move event */ |
| 434 | _describeSpinner: function(event) { |
| 435 | var spinner = $.timeEntry._getSpinnerTarget(event); |
| 436 | var inst = $.data($.timeEntry._getInput(spinner), PROP_NAME); |
| 437 | spinner.title = $.timeEntry._get(inst, 'spinnerTexts') |
| 438 | [$.timeEntry._getSpinnerRegion(inst, event)]; |
| 439 | }, |
| 440 | |
| 441 | /* Handle a click on the spinner. |
| 442 | @param event (event) the mouse click event */ |
| 443 | _handleSpinner: function(event) { |
| 444 | var spinner = $.timeEntry._getSpinnerTarget(event); |
| 445 | var input = $.timeEntry._getInput(spinner); |
| 446 | if ($.timeEntry._isDisabledTimeEntry(input)) { |
| 447 | return; |
| 448 | } |
| 449 | if (input == $.timeEntry._blurredInput) { |
| 450 | $.timeEntry._lastInput = input; |
| 451 | $.timeEntry._blurredInput = null; |
| 452 | } |
| 453 | var inst = $.data(input, PROP_NAME); |
| 454 | $.timeEntry._doFocus(input); |
| 455 | var region = $.timeEntry._getSpinnerRegion(inst, event); |
| 456 | $.timeEntry._changeSpinner(inst, spinner, region); |
| 457 | $.timeEntry._actionSpinner(inst, region); |
| 458 | $.timeEntry._timer = null; |
| 459 | $.timeEntry._handlingSpinner = true; |
| 460 | var spinnerRepeat = $.timeEntry._get(inst, 'spinnerRepeat'); |
| 461 | if (region >= 3 && spinnerRepeat[0]) { // Repeat increment/decrement |
| 462 | $.timeEntry._timer = setTimeout( |
| 463 | function() { $.timeEntry._repeatSpinner(inst, region); }, |
| 464 | spinnerRepeat[0]); |
| 465 | $(spinner).one('mouseout', $.timeEntry._releaseSpinner). |
| 466 | one('mouseup', $.timeEntry._releaseSpinner); |
| 467 | } |
| 468 | }, |
| 469 | |
| 470 | /* Action a click on the spinner. |
| 471 | @param inst (object) the instance settings |
| 472 | @param region (number) the spinner "button" */ |
| 473 | _actionSpinner: function(inst, region) { |
| 474 | if (!inst.input.val()) { |
| 475 | $.timeEntry._parseTime(inst); |
| 476 | } |
| 477 | switch (region) { |
| 478 | case 0: this._setTime(inst); break; |
| 479 | case 1: this._changeField(inst, -1, false); break; |
| 480 | case 2: this._changeField(inst, +1, false); break; |
| 481 | case 3: this._adjustField(inst, +1); break; |
| 482 | case 4: this._adjustField(inst, -1); break; |
| 483 | } |
| 484 | }, |
| 485 | |
| 486 | /* Repeat a click on the spinner. |
| 487 | @param inst (object) the instance settings |
| 488 | @param region (number) the spinner "button" */ |
| 489 | _repeatSpinner: function(inst, region) { |
| 490 | if (!$.timeEntry._timer) { |
| 491 | return; |
| 492 | } |
| 493 | $.timeEntry._lastInput = $.timeEntry._blurredInput; |
| 494 | this._actionSpinner(inst, region); |
| 495 | this._timer = setTimeout( |
| 496 | function() { $.timeEntry._repeatSpinner(inst, region); }, |
| 497 | this._get(inst, 'spinnerRepeat')[1]); |
| 498 | }, |
| 499 | |
| 500 | /* Stop a spinner repeat. |
| 501 | @param event (event) the mouse event */ |
| 502 | _releaseSpinner: function(event) { |
| 503 | clearTimeout($.timeEntry._timer); |
| 504 | $.timeEntry._timer = null; |
| 505 | }, |
| 506 | |
| 507 | /* Tidy up after an expanded spinner. |
| 508 | @param event (event) the mouse event */ |
| 509 | _endExpand: function(event) { |
| 510 | $.timeEntry._timer = null; |
| 511 | var spinner = $.timeEntry._getSpinnerTarget(event); |
| 512 | var input = $.timeEntry._getInput(spinner); |
| 513 | var inst = $.data(input, PROP_NAME); |
| 514 | $(spinner).remove(); |
| 515 | inst._expanded = false; |
| 516 | }, |
| 517 | |
| 518 | /* Tidy up after a spinner click. |
| 519 | @param event (event) the mouse event */ |
| 520 | _endSpinner: function(event) { |
| 521 | $.timeEntry._timer = null; |
| 522 | var spinner = $.timeEntry._getSpinnerTarget(event); |
| 523 | var input = $.timeEntry._getInput(spinner); |
| 524 | var inst = $.data(input, PROP_NAME); |
| 525 | if (!$.timeEntry._isDisabledTimeEntry(input)) { |
| 526 | $.timeEntry._changeSpinner(inst, spinner, -1); |
| 527 | } |
| 528 | if ($.timeEntry._handlingSpinner) { |
| 529 | $.timeEntry._lastInput = $.timeEntry._blurredInput; |
| 530 | } |
| 531 | if ($.timeEntry._lastInput && $.timeEntry._handlingSpinner) { |
| 532 | $.timeEntry._showField(inst); |
| 533 | } |
| 534 | $.timeEntry._handlingSpinner = false; |
| 535 | }, |
| 536 | |
| 537 | /* Retrieve the spinner from the event. |
| 538 | @param event (event) the mouse click event |
| 539 | @return (element) the target field */ |
| 540 | _getSpinnerTarget: function(event) { |
| 541 | return event.target || event.srcElement; |
| 542 | }, |
| 543 | |
| 544 | /* Determine which "button" within the spinner was clicked. |
| 545 | @param inst (object) the instance settings |
| 546 | @param event (event) the mouse event |
| 547 | @return (number) the spinner "button" number */ |
| 548 | _getSpinnerRegion: function(inst, event) { |
| 549 | var spinner = this._getSpinnerTarget(event); |
| 550 | var pos = ($.browser.opera || $.browser.safari ? |
| 551 | $.timeEntry._findPos(spinner) : $(spinner).offset()); |
| 552 | var scrolled = ($.browser.safari ? $.timeEntry._findScroll(spinner) : |
| 553 | [document.documentElement.scrollLeft || document.body.scrollLeft, |
| 554 | document.documentElement.scrollTop || document.body.scrollTop]); |
| 555 | var spinnerIncDecOnly = this._get(inst, 'spinnerIncDecOnly'); |
| 556 | var left = (spinnerIncDecOnly ? 99 : event.clientX + scrolled[0] - |
| 557 | pos.left - ($.browser.msie ? 2 : 0)); |
| 558 | var top = event.clientY + scrolled[1] - pos.top - ($.browser.msie ? 2 : 0); |
| 559 | var spinnerSize = this._get(inst, (inst._expanded ? 'spinnerBigSize' : 'spinnerSize')); |
| 560 | var right = (spinnerIncDecOnly ? 99 : spinnerSize[0] - 1 - left); |
| 561 | var bottom = spinnerSize[1] - 1 - top; |
| 562 | if (spinnerSize[2] > 0 && Math.abs(left - right) <= spinnerSize[2] && |
| 563 | Math.abs(top - bottom) <= spinnerSize[2]) { |
| 564 | return 0; // Centre button |
| 565 | } |
| 566 | var min = Math.min(left, top, right, bottom); |
| 567 | return (min == left ? 1 : (min == right ? 2 : (min == top ? 3 : 4))); // Nearest edge |
| 568 | }, |
| 569 | |
| 570 | /* Change the spinner image depending on button clicked. |
| 571 | @param inst (object) the instance settings |
| 572 | @param spinner (element) the spinner control |
| 573 | @param region (number) the spinner "button" */ |
| 574 | _changeSpinner: function(inst, spinner, region) { |
| 575 | $(spinner).css('background-position', '-' + ((region + 1) * |
| 576 | this._get(inst, (inst._expanded ? 'spinnerBigSize' : 'spinnerSize'))[0]) + 'px 0px'); |
| 577 | }, |
| 578 | |
| 579 | /* Find an object's position on the screen. |
| 580 | @param obj (element) the control |
| 581 | @return (object) position as .left and .top */ |
| 582 | _findPos: function(obj) { |
| 583 | var curLeft = curTop = 0; |
| 584 | if (obj.offsetParent) { |
| 585 | curLeft = obj.offsetLeft; |
| 586 | curTop = obj.offsetTop; |
| 587 | while (obj = obj.offsetParent) { |
| 588 | var origCurLeft = curLeft; |
| 589 | curLeft += obj.offsetLeft; |
| 590 | if (curLeft < 0) { |
| 591 | curLeft = origCurLeft; |
| 592 | } |
| 593 | curTop += obj.offsetTop; |
| 594 | } |
| 595 | } |
| 596 | return {left: curLeft, top: curTop}; |
| 597 | }, |
| 598 | |
| 599 | /* Find an object's scroll offset on the screen. |
| 600 | @param obj (element) the control |
| 601 | @return (number[]) offset as [left, top] */ |
| 602 | _findScroll: function(obj) { |
| 603 | var isFixed = false; |
| 604 | $(obj).parents().each(function() { |
| 605 | isFixed |= $(this).css('position') == 'fixed'; |
| 606 | }); |
| 607 | if (isFixed) { |
| 608 | return [0, 0]; |
| 609 | } |
| 610 | var scrollLeft = obj.scrollLeft; |
| 611 | var scrollTop = obj.scrollTop; |
| 612 | while (obj = obj.parentNode) { |
| 613 | scrollLeft += obj.scrollLeft || 0; |
| 614 | scrollTop += obj.scrollTop || 0; |
| 615 | } |
| 616 | return [scrollLeft, scrollTop]; |
| 617 | }, |
| 618 | |
| 619 | /* Get a setting value, defaulting if necessary. |
| 620 | @param inst (object) the instance settings |
| 621 | @param name (string) the setting name |
| 622 | @return (any) the setting value */ |
| 623 | _get: function(inst, name) { |
| 624 | return (inst.options[name] != null ? |
| 625 | inst.options[name] : $.timeEntry._defaults[name]); |
| 626 | }, |
| 627 | |
| 628 | /* Extract the time value from the input field, or default to now. |
| 629 | @param inst (object) the instance settings */ |
| 630 | _parseTime: function(inst) { |
| 631 | var currentTime = this._extractTime(inst); |
| 632 | var showSeconds = this._get(inst, 'showSeconds'); |
| 633 | if (currentTime) { |
| 634 | inst._selectedHour = currentTime[0]; |
| 635 | inst._selectedMinute = currentTime[1]; |
| 636 | inst._selectedSecond = currentTime[2]; |
| 637 | } |
| 638 | else { |
| 639 | var now = this._constrainTime(inst); |
| 640 | inst._selectedHour = now[0]; |
| 641 | inst._selectedMinute = now[1]; |
| 642 | inst._selectedSecond = (showSeconds ? now[2] : 0); |
| 643 | } |
| 644 | inst._secondField = (showSeconds ? 2 : -1); |
| 645 | inst._ampmField = (this._get(inst, 'show24Hours') ? -1 : (showSeconds ? 3 : 2)); |
| 646 | inst._lastChr = ''; |
| 647 | inst._field = Math.max(0, Math.min( |
| 648 | Math.max(1, inst._secondField, inst._ampmField), this._get(inst, 'initialField'))); |
| 649 | if (inst.input.val() != '') { |
| 650 | this._showTime(inst); |
| 651 | } |
| 652 | }, |
| 653 | |
| 654 | /* Extract the time value from a string as an array of values, or default to null. |
| 655 | @param inst (object) the instance settings |
| 656 | @param value (string) the time value to parse |
| 657 | @return (number[3]) the time components (hours, minutes, seconds) |
| 658 | or null if no value */ |
| 659 | _extractTime: function(inst, value) { |
| 660 | value = value || inst.input.val(); |
| 661 | var separator = this._get(inst, 'separator'); |
| 662 | var currentTime = value.split(separator); |
| 663 | if (separator == '' && value != '') { |
| 664 | currentTime[0] = value.substring(0, 2); |
| 665 | currentTime[1] = value.substring(2, 4); |
| 666 | currentTime[2] = value.substring(4, 6); |
| 667 | } |
| 668 | var ampmNames = this._get(inst, 'ampmNames'); |
| 669 | var show24Hours = this._get(inst, 'show24Hours'); |
| 670 | if (currentTime.length >= 2) { |
| 671 | var isAM = !show24Hours && (value.indexOf(ampmNames[0]) > -1); |
| 672 | var isPM = !show24Hours && (value.indexOf(ampmNames[1]) > -1); |
| 673 | var hour = parseInt(currentTime[0], 10); |
| 674 | hour = (isNaN(hour) ? 0 : hour); |
| 675 | hour = ((isAM || isPM) && hour == 12 ? 0 : hour) + (isPM ? 12 : 0); |
| 676 | var minute = parseInt(currentTime[1], 10); |
| 677 | minute = (isNaN(minute) ? 0 : minute); |
| 678 | var second = (currentTime.length >= 3 ? |
| 679 | parseInt(currentTime[2], 10) : 0); |
| 680 | second = (isNaN(second) || !this._get(inst, 'showSeconds') ? 0 : second); |
| 681 | return this._constrainTime(inst, [hour, minute, second]); |
| 682 | } |
| 683 | return null; |
| 684 | }, |
| 685 | |
| 686 | /* Constrain the given/current time to the time steps. |
| 687 | @param inst (object) the instance settings |
| 688 | @param fields (number[3]) the current time components (hours, minutes, seconds) |
| 689 | @return (number[3]) the constrained time components (hours, minutes, seconds) */ |
| 690 | _constrainTime: function(inst, fields) { |
| 691 | var specified = (fields != null); |
| 692 | if (!specified) { |
| 693 | var now = this._determineTime(inst, this._get(inst, 'defaultTime')) || new Date(); |
| 694 | fields = [now.getHours(), now.getMinutes(), now.getSeconds()]; |
| 695 | } |
| 696 | var reset = false; |
| 697 | var timeSteps = this._get(inst, 'timeSteps'); |
| 698 | for (var i = 0; i < timeSteps.length; i++) { |
| 699 | if (reset) { |
| 700 | fields[i] = 0; |
| 701 | } |
| 702 | else if (timeSteps[i] > 1) { |
| 703 | fields[i] = Math.round(fields[i] / timeSteps[i]) * timeSteps[i]; |
| 704 | reset = true; |
| 705 | } |
| 706 | } |
| 707 | return fields; |
| 708 | }, |
| 709 | |
| 710 | /* Set the selected time into the input field. |
| 711 | @param inst (object) the instance settings */ |
| 712 | _showTime: function(inst) { |
| 713 | var show24Hours = this._get(inst, 'show24Hours'); |
| 714 | var separator = this._get(inst, 'separator'); |
| 715 | var currentTime = (this._formatNumber(show24Hours ? inst._selectedHour : |
| 716 | ((inst._selectedHour + 11) % 12) + 1) + separator + |
| 717 | this._formatNumber(inst._selectedMinute) + |
| 718 | (this._get(inst, 'showSeconds') ? separator + |
| 719 | this._formatNumber(inst._selectedSecond) : '') + |
| 720 | (show24Hours ? '' : this._get(inst, 'ampmPrefix') + |
| 721 | this._get(inst, 'ampmNames')[(inst._selectedHour < 12 ? 0 : 1)])); |
| 722 | this._setValue(inst, currentTime); |
| 723 | this._showField(inst); |
| 724 | }, |
| 725 | |
| 726 | /* Highlight the current time field. |
| 727 | @param inst (object) the instance settings */ |
| 728 | _showField: function(inst) { |
| 729 | var input = inst.input[0]; |
| 730 | if (inst.input.is(':hidden') || $.timeEntry._lastInput != input) { |
| 731 | return; |
| 732 | } |
| 733 | var separator = this._get(inst, 'separator'); |
| 734 | var fieldSize = separator.length + 2; |
| 735 | var start = (inst._field != inst._ampmField ? (inst._field * fieldSize) : |
| 736 | (inst._ampmField * fieldSize) - separator.length + this._get(inst, 'ampmPrefix').length); |
| 737 | var end = start + (inst._field != inst._ampmField ? 2 : this._get(inst, 'ampmNames')[0].length); |
| 738 | if (input.setSelectionRange) { // Mozilla |
| 739 | input.setSelectionRange(start, end); |
| 740 | } |
| 741 | else if (input.createTextRange) { // IE |
| 742 | var range = input.createTextRange(); |
| 743 | range.moveStart('character', start); |
| 744 | range.moveEnd('character', end - inst.input.val().length); |
| 745 | range.select(); |
| 746 | } |
| 747 | if (!input.disabled) { |
| 748 | input.focus(); |
| 749 | } |
| 750 | }, |
| 751 | |
| 752 | /* Ensure displayed single number has a leading zero. |
| 753 | @param value (number) current value |
| 754 | @return (string) number with at least two digits */ |
| 755 | _formatNumber: function(value) { |
| 756 | return (value < 10 ? '0' : '') + value; |
| 757 | }, |
| 758 | |
| 759 | /* Update the input field and notify listeners. |
| 760 | @param inst (object) the instance settings |
| 761 | @param value (string) the new value */ |
| 762 | _setValue: function(inst, value) { |
| 763 | if (value != inst.input.val()) { |
| 764 | inst.input.val(value).trigger('change'); |
| 765 | } |
| 766 | }, |
| 767 | |
| 768 | /* Move to previous/next field, or out of field altogether if appropriate. |
| 769 | @param inst (object) the instance settings |
| 770 | @param offset (number) the direction of change (-1, +1) |
| 771 | @param moveOut (boolean) true if can move out of the field |
| 772 | @return (boolean) true if exitting the field, false if not */ |
| 773 | _changeField: function(inst, offset, moveOut) { |
| 774 | var atFirstLast = (inst.input.val() == '' || inst._field == |
| 775 | (offset == -1 ? 0 : Math.max(1, inst._secondField, inst._ampmField))); |
| 776 | if (!atFirstLast) { |
| 777 | inst._field += offset; |
| 778 | } |
| 779 | this._showField(inst); |
| 780 | inst._lastChr = ''; |
| 781 | $.data(inst.input[0], PROP_NAME, inst); |
| 782 | return (atFirstLast && moveOut); |
| 783 | }, |
| 784 | |
| 785 | /* Update the current field in the direction indicated. |
| 786 | @param inst (object) the instance settings |
| 787 | @param offset (number) the amount to change by */ |
| 788 | _adjustField: function(inst, offset) { |
| 789 | if (inst.input.val() == '') { |
| 790 | offset = 0; |
| 791 | } |
| 792 | var timeSteps = this._get(inst, 'timeSteps'); |
| 793 | this._setTime(inst, new Date(0, 0, 0, |
| 794 | inst._selectedHour + (inst._field == 0 ? offset * timeSteps[0] : 0) + |
| 795 | (inst._field == inst._ampmField ? offset * 12 : 0), |
| 796 | inst._selectedMinute + (inst._field == 1 ? offset * timeSteps[1] : 0), |
| 797 | inst._selectedSecond + (inst._field == inst._secondField ? offset * timeSteps[2] : 0))); |
| 798 | }, |
| 799 | |
| 800 | /* Check against minimum/maximum and display time. |
| 801 | @param inst (object) the instance settings |
| 802 | @param time (Date) an actual time or |
| 803 | (number) offset in seconds from now or |
| 804 | (string) units and periods of offsets from now */ |
| 805 | _setTime: function(inst, time) { |
| 806 | time = this._determineTime(inst, time); |
| 807 | var fields = this._constrainTime(inst, time ? |
| 808 | [time.getHours(), time.getMinutes(), time.getSeconds()] : null); |
| 809 | time = new Date(0, 0, 0, fields[0], fields[1], fields[2]); |
| 810 | // Normalise to base date |
| 811 | var time = this._normaliseTime(time); |
| 812 | var minTime = this._normaliseTime(this._determineTime(inst, this._get(inst, 'minTime'))); |
| 813 | var maxTime = this._normaliseTime(this._determineTime(inst, this._get(inst, 'maxTime'))); |
| 814 | // Ensure it is within the bounds set |
| 815 | time = (minTime && time < minTime ? minTime : |
| 816 | (maxTime && time > maxTime ? maxTime : time)); |
| 817 | var beforeSetTime = this._get(inst, 'beforeSetTime'); |
| 818 | // Perform further restrictions if required |
| 819 | if (beforeSetTime) { |
| 820 | time = beforeSetTime.apply(inst.input[0], |
| 821 | [this._getTimeTimeEntry(inst.input[0]), time, minTime, maxTime]); |
| 822 | } |
| 823 | inst._selectedHour = time.getHours(); |
| 824 | inst._selectedMinute = time.getMinutes(); |
| 825 | inst._selectedSecond = time.getSeconds(); |
| 826 | this._showTime(inst); |
| 827 | $.data(inst.input[0], PROP_NAME, inst); |
| 828 | }, |
| 829 | |
| 830 | /* Normalise time object to a common date. |
| 831 | @param time (Date) the original time |
| 832 | @return (Date) the normalised time */ |
| 833 | _normaliseTime: function(time) { |
| 834 | if (!time) { |
| 835 | return null; |
| 836 | } |
| 837 | time.setFullYear(1900); |
| 838 | time.setMonth(0); |
| 839 | time.setDate(0); |
| 840 | return time; |
| 841 | }, |
| 842 | |
| 843 | /* A time may be specified as an exact value or a relative one. |
| 844 | @param inst (object) the instance settings |
| 845 | @param setting (Date) an actual time or |
| 846 | (number) offset in seconds from now or |
| 847 | (string) units and periods of offsets from now |
| 848 | @return (Date) the calculated time */ |
| 849 | _determineTime: function(inst, setting) { |
| 850 | var offsetNumeric = function(offset) { // E.g. +300, -2 |
| 851 | var time = new Date(); |
| 852 | time.setTime(time.getTime() + offset * 1000); |
| 853 | return time; |
| 854 | }; |
| 855 | var offsetString = function(offset) { // E.g. '+2m', '-4h', '+3h +30m' or '12:34:56PM' |
| 856 | var fields = $.timeEntry._extractTime(inst, offset); // Actual time? |
| 857 | var time = new Date(); |
| 858 | var hour = (fields ? fields[0] : time.getHours()); |
| 859 | var minute = (fields ? fields[1] : time.getMinutes()); |
| 860 | var second = (fields ? fields[2] : time.getSeconds()); |
| 861 | if (!fields) { |
| 862 | var pattern = /([+-]?[0-9]+)\s*(s|S|m|M|h|H)?/g; |
| 863 | var matches = pattern.exec(offset); |
| 864 | while (matches) { |
| 865 | switch (matches[2] || 's') { |
| 866 | case 's' : case 'S' : |
| 867 | second += parseInt(matches[1], 10); break; |
| 868 | case 'm' : case 'M' : |
| 869 | minute += parseInt(matches[1], 10); break; |
| 870 | case 'h' : case 'H' : |
| 871 | hour += parseInt(matches[1], 10); break; |
| 872 | } |
| 873 | matches = pattern.exec(offset); |
| 874 | } |
| 875 | } |
| 876 | time = new Date(0, 0, 10, hour, minute, second, 0); |
| 877 | if (/^!/.test(offset)) { // No wrapping |
| 878 | if (time.getDate() > 10) { |
| 879 | time = new Date(0, 0, 10, 23, 59, 59); |
| 880 | } |
| 881 | else if (time.getDate() < 10) { |
| 882 | time = new Date(0, 0, 10, 0, 0, 0); |
| 883 | } |
| 884 | } |
| 885 | return time; |
| 886 | }; |
| 887 | return (setting ? (typeof setting == 'string' ? offsetString(setting) : |
| 888 | (typeof setting == 'number' ? offsetNumeric(setting) : setting)) : null); |
| 889 | }, |
| 890 | |
| 891 | /* Update time based on keystroke entered. |
| 892 | @param inst (object) the instance settings |
| 893 | @param chr (ch) the new character */ |
| 894 | _handleKeyPress: function(inst, chr) { |
| 895 | if (chr == this._get(inst, 'separator')) { |
| 896 | this._changeField(inst, +1, false); |
| 897 | } |
| 898 | else if (chr >= '0' && chr <= '9') { // Allow direct entry of time |
| 899 | var key = parseInt(chr, 10); |
| 900 | var value = parseInt(inst._lastChr + chr, 10); |
| 901 | var show24Hours = this._get(inst, 'show24Hours'); |
| 902 | var hour = (inst._field != 0 ? inst._selectedHour : |
| 903 | (show24Hours ? (value < 24 ? value : key) : |
| 904 | (value >= 1 && value <= 12 ? value : |
| 905 | (key > 0 ? key : inst._selectedHour)) % 12 + |
| 906 | (inst._selectedHour >= 12 ? 12 : 0))); |
| 907 | var minute = (inst._field != 1 ? inst._selectedMinute : |
| 908 | (value < 60 ? value : key)); |
| 909 | var second = (inst._field != inst._secondField ? inst._selectedSecond : |
| 910 | (value < 60 ? value : key)); |
| 911 | var fields = this._constrainTime(inst, [hour, minute, second]); |
| 912 | this._setTime(inst, new Date(0, 0, 0, fields[0], fields[1], fields[2])); |
| 913 | inst._lastChr = chr; |
| 914 | } |
| 915 | else if (!this._get(inst, 'show24Hours')) { // Set am/pm based on first char of names |
| 916 | chr = chr.toLowerCase(); |
| 917 | var ampmNames = this._get(inst, 'ampmNames'); |
| 918 | if ((chr == ampmNames[0].substring(0, 1).toLowerCase() && |
| 919 | inst._selectedHour >= 12) || |
| 920 | (chr == ampmNames[1].substring(0, 1).toLowerCase() && |
| 921 | inst._selectedHour < 12)) { |
| 922 | var saveField = inst._field; |
| 923 | inst._field = inst._ampmField; |
| 924 | this._adjustField(inst, +1); |
| 925 | inst._field = saveField; |
| 926 | this._showField(inst); |
| 927 | } |
| 928 | } |
| 929 | } |
| 930 | }); |
| 931 | |
| 932 | /* jQuery extend now ignores nulls! |
| 933 | @param target (object) the object to update |
| 934 | @param props (object) the new settings |
| 935 | @return (object) the updated object */ |
| 936 | function extendRemove(target, props) { |
| 937 | $.extend(target, props); |
| 938 | for (var name in props) { |
| 939 | if (props[name] == null) { |
| 940 | target[name] = null; |
| 941 | } |
| 942 | } |
| 943 | return target; |
| 944 | } |
| 945 | |
| 946 | // Commands that don't return a jQuery object |
| 947 | var getters = ['getOffset', 'getTime', 'isDisabled']; |
| 948 | |
| 949 | /* Attach the time entry functionality to a jQuery selection. |
| 950 | @param command (string) the command to run (optional, default 'attach') |
| 951 | @param options (object) the new settings to use for these countdown instances (optional) |
| 952 | @return (jQuery) for chaining further calls */ |
| 953 | $.fn.timeEntry = function(options) { |
| 954 | var otherArgs = Array.prototype.slice.call(arguments, 1); |
| 955 | if (typeof options == 'string' && $.inArray(options, getters) > -1) { |
| 956 | return $.timeEntry['_' + options + 'TimeEntry'].apply($.timeEntry, [this[0]].concat(otherArgs)); |
| 957 | } |
| 958 | return this.each(function() { |
| 959 | var nodeName = this.nodeName.toLowerCase(); |
| 960 | if (nodeName == 'input') { |
| 961 | if (typeof options == 'string') { |
| 962 | $.timeEntry['_' + options + 'TimeEntry'].apply($.timeEntry, [this].concat(otherArgs)); |
| 963 | } |
| 964 | else { |
| 965 | // Check for settings on the control itself |
| 966 | var inlineSettings = ($.fn.metadata ? $(this).metadata() : {}); |
| 967 | $.timeEntry._connectTimeEntry(this, $.extend(inlineSettings, options)); |
| 968 | } |
| 969 | } |
| 970 | }); |
| 971 | }; |
| 972 | |
| 973 | /* Initialise the time entry functionality. */ |
| 974 | $.timeEntry = new TimeEntry(); // Singleton instance |
| 975 | |
| 976 | })(jQuery); |