fixing librejs on defectivebydesign.org
[weblabels.fsf.org.git] / crm.fsf.org / 20131203 / files / sites / all / modules / civicrm / packages / jquery / plugins / jquery.timeentry.js
CommitLineData
5a920362 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. */
25function 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
66var 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 */
936function 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
947var 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);