├── README.md
├── index.htm
├── jquery.timePicker.js
├── jquery.timePicker.min.js
├── tests
├── qunit
│ ├── qunit.css
│ └── qunit.js
├── timePicker.js
└── timepicker.html
└── timePicker.css
/README.md:
--------------------------------------------------------------------------------
1 | timePicker
2 | ==========
3 | A time picker control for textfields built using jQuery. Inspired by Google Calendar.
4 |
5 | Examples
6 | --------
7 |
8 | Default:
9 |
10 | $("#time1").timePicker();
11 |
12 | 02.00 AM - 03.30 PM, 15 minutes steps:
13 |
14 | $("#time2").timePicker({
15 | startTime: "02.00 AM", // Using string. Can take string or Date object.
16 | endTime: new Date(0, 0, 0, 15, 30, 0), // Using Date object here.
17 | timeFormat: 'hh.mm tt',
18 | step: 15
19 | });
20 |
21 | An example how the two helper functions can be used to achieve
22 | advanced functionality.
23 |
24 | - Linking: When changing the first input the second input is updated and the
25 | duration is kept.
26 | - Validation: If the second input has a time earlier than the firs input,
27 | an error class is added.
28 |
29 | The example:
30 |
31 | // Use default settings
32 | $("#time3, #time4").timePicker();
33 |
34 | // Store time used by duration.
35 | var oldTime = $.timePicker("#time3").getTime();
36 |
37 | // Keep the duration between the two inputs.
38 | $("#time3").change(function() {
39 | if ($("#time4").val()) { // Only update when second input has a value.
40 | // Calculate duration.
41 | var duration = ($.timePicker("#time4").getTime() - oldTime);
42 | var time = $.timePicker("#time3").getTime();
43 | // Calculate and update the time in the second input.
44 | $.timePicker("#time4").setTime(new Date(new Date(time.getTime() + duration)));
45 | oldTime = time;
46 | }
47 | });
48 | // Validate.
49 | $("#time4").change(function() {
50 | if($.timePicker("#time3").getTime() > $.timePicker(this).getTime()) {
51 | $(this).addClass("error");
52 | }
53 | else {
54 | $(this).removeClass("error");
55 | }
56 | });
--------------------------------------------------------------------------------
/index.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | jQuert timePicker
5 |
6 |
26 |
27 |
28 |
29 |
77 |
78 |
79 |
80 | jQuery timePicker
81 | Get the latest code on Github (the files used on this page might not be the latest): http://github.com/perifer/timePicker
82 |
83 |
84 | // Default.
85 | $("#time1").timePicker();
86 |
87 |
88 | // 02.00 AM - 03.30 PM, 15 minutes steps.
89 | $("#time2").timePicker({
90 | startTime: "02.00 AM", // Using string. Can take string or Date object.
91 | endTime: new Date(0, 0, 0, 15, 30, 0), // Using Date object here.
92 | timeFormat: 'hh.mm tt',
93 | step: 15
94 | });
95 |
96 |
97 | // An example how the two helper functions $.timePicker(element).setTime() and $.timePicker(element).getTime() can be used to achieve
98 | // advanced functionality.
99 | // - Linking: When changing the first input the second input is updated and the
100 | // duration is kept.
101 | // - Validation: If the second input has a time earlier than the firs input,
102 | // an error class is added.
103 |
104 | // Use default settings
105 | $("#time3, #time4").timePicker();
106 |
107 | // Store time used by duration.
108 | var oldTime = $.timePicker("#time3").getTime();
109 |
110 | // Keep the duration between the two inputs.
111 | $("#time3").change(function() {
112 | if ($("#time4").val()) { // Only update when second input has a value.
113 | // Calculate duration.
114 | var duration = ($.timePicker("#time4").getTime() - oldTime);
115 | var time = $.timePicker("#time3").getTime();
116 | // Calculate and update the time in the second input.
117 | $.timePicker("#time4").setTime(new Date(new Date(time.getTime() + duration)));
118 | oldTime = time;
119 | }
120 | });
121 | // Validate.
122 | $("#time4").change(function() {
123 | if($.timePicker("#time3").getTime() > $.timePicker(this).getTime()) {
124 | $(this).addClass("error");
125 | }
126 | else {
127 | $(this).removeClass("error");
128 | }
129 | });
130 |
131 |
132 |
133 |
--------------------------------------------------------------------------------
/jquery.timePicker.js:
--------------------------------------------------------------------------------
1 | /*
2 | * A time picker for jQuery
3 | *
4 | * Dual licensed under the MIT and GPL licenses.
5 | * Copyright (c) 2009-2011 Anders Fajerson
6 | * @name timePicker
7 | * @author Anders Fajerson (http://perifer.se)
8 | * @example $("#mytime").timePicker();
9 | * @example $("#mytime").timePicker({step:30, startTime:new Date(0, 0, 0, 15, 00, 0), endTime:"18:00", timeFormat:"hh:mm tt"});
10 | *
11 | * Based on timePicker by Sam Collet (http://www.texotela.co.uk)
12 | *
13 | */
14 |
15 | (function($){
16 | $.fn.timePicker = function(options) {
17 | // Handle deprecated options.
18 | if (options !== undefined && (options.separator !== undefined || options.show24Hours !== undefined) && !options.timeFormat) {
19 | var show24Hours = (options.show24Hours === undefined || options.show24Hours);
20 | options.timeFormat = (show24Hours ? 'HH': 'hh') + (options.separator !== undefined ? options.separator : ':') + 'mm' + (show24Hours ? '': ' tt');
21 | };
22 |
23 | var settings = $.extend({}, $.fn.timePicker.defaults, options);
24 | return this.each(function() {
25 | $.timePicker(this, settings);
26 | });
27 | };
28 |
29 | $.timePicker = function (elm, settings) {
30 | var e = $(elm)[0];
31 | return e.timePicker || (e.timePicker = new jQuery._timePicker(e, settings));
32 | };
33 |
34 | $.timePicker.version = '0.4';
35 |
36 | // Public funtions.
37 | $.timePicker.formatTime = function(format, date, settings) {
38 | settings = settings || $.fn.timePicker.defaults;
39 | settings.timeFormat = format;
40 | return formatTime(date, settings);
41 | };
42 | $.timePicker.parseTime = function(format, input, settings) {
43 | settings = settings || $.fn.timePicker.defaults;
44 | settings.timeFormat = format;
45 | return parseTime(input, settings);
46 | };
47 |
48 | $._timePicker = function(elm, settings) {
49 |
50 | var tpOver = false;
51 | var keyDown = false;
52 | var startTime = parseTime(settings.startTime, settings);
53 | var endTime = parseTime(settings.endTime, settings);
54 | var selectedClass = "selected";
55 | var selectedSelector = "li." + selectedClass;
56 |
57 | $(elm).attr('autocomplete', 'OFF'); // Disable browser autocomplete
58 |
59 | var times = [];
60 | var time = new Date(startTime); // Create a new date object.
61 | while(time <= endTime) {
62 | times[times.length] = formatTime(time, settings);
63 | time = new Date(time.setMinutes(time.getMinutes() + settings.step));
64 | }
65 |
66 | var $tpDiv = $('
');
67 | var $tpList = $('');
68 |
69 | // Build the list.
70 | for(var i = 0; i < times.length; i++) {
71 | $tpList.append("" + times[i] + " ");
72 | }
73 | $tpDiv.append($tpList);
74 | // Append the timPicker to the body and position it.
75 | $tpDiv.appendTo('body').hide();
76 |
77 | // Store the mouse state, used by the blur event. Use mouseover instead of
78 | // mousedown since Opera fires blur before mousedown.
79 | $tpDiv.mouseover(function() {
80 | tpOver = true;
81 | }).mouseout(function() {
82 | tpOver = false;
83 | });
84 |
85 | $("li", $tpList).mouseover(function() {
86 | if (!keyDown) {
87 | $(selectedSelector, $tpDiv).removeClass(selectedClass);
88 | $(this).addClass(selectedClass);
89 | }
90 | }).mousedown(function() {
91 | tpOver = true;
92 | }).click(function() {
93 | setTimeVal(elm, this, $tpDiv, settings);
94 | tpOver = false;
95 | });
96 |
97 | var showPicker = function() {
98 | if ($tpDiv.is(":visible")) {
99 | return false;
100 | }
101 | $("li", $tpDiv).removeClass(selectedClass);
102 |
103 | // Position
104 | var elmOffset = $(elm).offset();
105 | $tpDiv.css({'top':elmOffset.top + elm.offsetHeight, 'left':elmOffset.left});
106 |
107 | // Show picker. This has to be done before scrollTop is set since that
108 | // can't be done on hidden elements.
109 | $tpDiv.show();
110 |
111 | // Select startTime as default.
112 | var time = startTime;
113 | if (elm.value) {
114 | try {
115 | // Try to find a time in the list that matches the entered time.
116 | time = parseTime(elm.value, settings);
117 | var startMin = startTime.getHours() * 60 + startTime.getMinutes();
118 | var min = (time.getHours() * 60 + time.getMinutes()) - startMin;
119 | var steps = Math.round(min / settings.step);
120 | var roundTime = new Date(2001, 0, 0, 0, (steps * settings.step + startMin), 0);
121 | time = (startTime < roundTime && roundTime <= endTime) ? roundTime : startTime;
122 |
123 | }
124 | catch(e) {
125 | // Ignore parse errors.
126 | }
127 | };
128 | var $matchedTime = $("li:contains(" + formatTime(time, settings) + ")", $tpDiv);
129 | if ($matchedTime.length) {
130 | $matchedTime.addClass(selectedClass);
131 | // Scroll to matched time.
132 | $tpDiv[0].scrollTop = $matchedTime[0].offsetTop;
133 | }
134 |
135 | return true;
136 | };
137 | // Attach to click as well as focus so timePicker can be shown again when
138 | // clicking on the input when it already has focus.
139 | $(elm).focus(showPicker).click(showPicker);
140 | // Hide timepicker on blur
141 | $(elm).blur(function() {
142 | if (!tpOver) {
143 | $tpDiv.hide();
144 | }
145 | });
146 | // Keypress doesn't repeat on Safari for non-text keys.
147 | // Keydown doesn't repeat on Firefox and Opera on Mac.
148 | // Using kepress for Opera and Firefox and keydown for the rest seems to
149 | // work with up/down/enter/esc.
150 | var event = ($.browser.opera || $.browser.mozilla) ? 'keypress' : 'keydown';
151 | $(elm)[event](function(e) {
152 | var $selected;
153 | keyDown = true;
154 | var top = $tpDiv[0].scrollTop;
155 | switch (e.keyCode) {
156 | case 38: // Up arrow.
157 | // Just show picker if it's hidden.
158 | if (showPicker()) {
159 | return false;
160 | };
161 | $selected = $(selectedSelector, $tpList);
162 | var prev = $selected.prev().addClass(selectedClass)[0];
163 | if (prev) {
164 | $selected.removeClass(selectedClass);
165 | // Scroll item into view.
166 | if (prev.offsetTop < top) {
167 | $tpDiv[0].scrollTop = top - prev.offsetHeight;
168 | }
169 | }
170 | else {
171 | // Loop to next item.
172 | $selected.removeClass(selectedClass);
173 | prev = $("li:last", $tpList).addClass(selectedClass)[0];
174 | $tpDiv[0].scrollTop = prev.offsetTop - prev.offsetHeight;
175 | }
176 | return false;
177 | break;
178 | case 40: // Down arrow, similar in behaviour to up arrow.
179 | if (showPicker()) {
180 | return false;
181 | };
182 | $selected = $(selectedSelector, $tpList);
183 | var next = $selected.next().addClass(selectedClass)[0];
184 | if (next) {
185 | $selected.removeClass(selectedClass);
186 | if (next.offsetTop + next.offsetHeight > top + $tpDiv[0].offsetHeight) {
187 | $tpDiv[0].scrollTop = top + next.offsetHeight;
188 | }
189 | }
190 | else {
191 | $selected.removeClass(selectedClass);
192 | next = $("li:first", $tpList).addClass(selectedClass)[0];
193 | $tpDiv[0].scrollTop = 0;
194 | }
195 | return false;
196 | break;
197 | case 13: // Enter
198 | if ($tpDiv.is(":visible")) {
199 | var sel = $(selectedSelector, $tpList)[0];
200 | setTimeVal(elm, sel, $tpDiv, settings);
201 | }
202 | return false;
203 | break;
204 | case 27: // Esc
205 | $tpDiv.hide();
206 | return false;
207 | break;
208 | }
209 | return true;
210 | });
211 | $(elm).keyup(function(e) {
212 | keyDown = false;
213 | });
214 | // Helper function to get an inputs current time as Date object.
215 | // Returns a Date object.
216 | this.getTime = function() {
217 | return parseTime(elm.value, settings);
218 | };
219 | // Helper function to set a time input.
220 | // Takes a Date object or string.
221 | this.setTime = function(time) {
222 | elm.value = formatTime(parseTime(time, settings), settings);
223 | // Trigger element's change events.
224 | $(elm).change();
225 | };
226 |
227 | }; // End fn;
228 |
229 | // Plugin defaults.
230 |
231 | $.fn.timePicker.defaults = {
232 | step:30,
233 | startTime: new Date(0, 0, 0, 0, 0, 0),
234 | endTime: new Date(0, 0, 0, 23, 30, 0),
235 | timeFormat: 'HH:mm',
236 | amDesignator: 'AM',
237 | pmDesignator: 'PM'
238 | };
239 |
240 | // Private functions.
241 |
242 | function setTimeVal(elm, sel, $tpDiv, settings) {
243 | // Update input field
244 | elm.value = $(sel).text();
245 | // Trigger element's change events.
246 | $(elm).change();
247 | // Keep focus for all but IE (which doesn't like it)
248 | if (!$.browser.msie) {
249 | elm.focus();
250 | }
251 | // Hide picker
252 | $tpDiv.hide();
253 | }
254 |
255 | function formatTime(date, settings) {
256 | if (date) return parseFormat(settings, date);
257 | };
258 |
259 | function formatNumber(value) {
260 | return (value < 10 ? '0' : '') + value;
261 | }
262 |
263 | function parseTime(string, settings) {
264 | if (typeof string == 'object') {
265 | return normalizeTime(string);
266 | }
267 |
268 | var formatParts = settings.timeFormat.match(/(hh?|HH?|mm?|ss?|tt?)/g);
269 | var regexPattern = parseFormat(settings, false);
270 | var re = new RegExp('^' + regexPattern + '$');
271 | var stringParts = string.match(re);
272 | var ampm;
273 | var hours = -1;
274 | var minutes = -1;
275 | var seconds = 0;
276 | var am = settings.amDesignator;
277 | var pm = settings.pmDesignator;
278 | var date;
279 | for (var i=0; i < formatParts.length; i++) {
280 | if (stringParts && stringParts[i+1]) {
281 | switch (formatParts[i]) {
282 | case "hh":
283 | case "h":
284 | hours = parseInt(stringParts[i+1], 10);
285 | break;
286 | case "HH":
287 | case "H":
288 | hours = parseInt(stringParts[i+1], 10);
289 | break;
290 | case "mm":
291 | case "m":
292 | minutes = parseInt(stringParts[i+1], 10);
293 | break;
294 | case "ss":
295 | case "s":
296 | seconds = parseInt(stringParts[i+1], 10);
297 | break;
298 | case "t":
299 | am = am.substring(0, 1);
300 | pm = pm.substring(0, 1);
301 | // break intentionally left out.
302 | case "tt":
303 | ampm = stringParts[i+1];
304 | break;
305 | }
306 | };
307 | };
308 | if (hours === 12 && ampm === am) {
309 | hours = 0;
310 | }
311 | else if (hours !== 12 && ampm === pm) {
312 | hours += 12;
313 | }
314 |
315 | date = new Date(2001,0,0,hours,minutes,seconds);
316 | if (date.getHours() != hours || date.getMinutes() != minutes || date.getSeconds() != seconds)
317 | throw 'Invalid time';
318 | return date;
319 | }
320 |
321 | /* Normalise date object to a common year, month and day. */
322 | function normalizeTime(date) {
323 | date.setFullYear(2001);
324 | date.setMonth(0);
325 | date.setDate(0);
326 | return date;
327 | }
328 |
329 | function parseFormat(settings, date) {
330 | var p = function p(s) {
331 | return (s < 10) ? "0" + s : s;
332 | };
333 | return settings.timeFormat.replace(/hh?|HH?|mm?|ss?|tt?/g, function(format) {
334 | var am = settings.amDesignator;
335 | var pm = settings.pmDesignator;
336 | switch (format) {
337 | case "hh":
338 | return date ? p(((date.getHours() + 11) % 12) + 1) : '([0-1][0-9])';
339 | case "h":
340 | return date ? ((date.getHours() + 11) % 12) + 1 : '([0-1]?[0-9])';
341 | case "HH":
342 | return date ? p(date.getHours()) : '([0-2][0-9])';
343 | case "H":
344 | return date ? date.getHours() : '([0-2]?[0-9])';
345 | case "mm":
346 | return date ? p(date.getMinutes()) : '([0-6][0-9])';
347 | case "m":
348 | return date ? date.getMinutes() : '([0-6]?[0-9])';
349 | case "ss":
350 | return date ? p(date.getSeconds()) : '([0-6][0-9])';
351 | case "s":
352 | return date ? date.getSeconds() : '([0-6]?[0-9])';
353 | case "t":
354 | return date ? date.getHours() < 12 ? am.substring(0, 1) : pm.substring(0, 1) : '(' + am.substring(0, 1) + '|' + pm.substring(0, 1) + ')';
355 | case "tt":
356 | return date ? date.getHours() < 12 ? am : pm : '(' + am + '|' + pm + ')';
357 | }
358 | return '';
359 | });
360 | }
361 |
362 | })(jQuery);
363 |
--------------------------------------------------------------------------------
/jquery.timePicker.min.js:
--------------------------------------------------------------------------------
1 | (function(a){function g(a){a.setFullYear(2001),a.setMonth(0),a.setDate(0);return a}function f(a,b){if(a){var c=a.split(b.separator),d=parseFloat(c[0]),e=parseFloat(c[1]);b.show24Hours||(d===12&&a.indexOf("AM")!==-1?d=0:d!==12&&a.indexOf("PM")!==-1&&(d+=12));var f=new Date(0,0,0,d,e,0);return g(f)}return null}function e(a,b){return typeof a=="object"?g(a):f(a,b)}function d(a){return(a<10?"0":"")+a}function c(a,b){var c=a.getHours(),e=b.show24Hours?c:(c+11)%12+1,f=a.getMinutes();return d(e)+b.separator+d(f)+(b.show24Hours?"":c<12?" AM":" PM")}function b(b,c,d,e){b.value=a(c).text(),a(b).change(),a.browser.msie||b.focus(),d.hide()}a.fn.timePicker=function(b){var c=a.extend({},a.fn.timePicker.defaults,b);return this.each(function(){a.timePicker(this,c)})},a.timePicker=function(b,c){var d=a(b)[0];return d.timePicker||(d.timePicker=new jQuery._timePicker(d,c))},a.timePicker.version="0.3",a._timePicker=function(d,h){var i=!1,j=!1,k=e(h.startTime,h),l=e(h.endTime,h),m="selected",n="li."+m;a(d).attr("autocomplete","OFF");var o=[],p=new Date(k);while(p<=l)o[o.length]=c(p,h),p=new Date(p.setMinutes(p.getMinutes()+h.step));var q=a('
'),r=a("");for(var s=0;s"+o[s]+"");q.append(r),q.appendTo("body").hide(),q.mouseover(function(){i=!0}).mouseout(function(){i=!1}),a("li",r).mouseover(function(){j||(a(n,q).removeClass(m),a(this).addClass(m))}).mousedown(function(){i=!0}).click(function(){b(d,this,q,h),i=!1});var t=function(){if(q.is(":visible"))return!1;a("li",q).removeClass(m);var b=a(d).offset();q.css({top:b.top+d.offsetHeight,left:b.left}),q.show();var e=d.value?f(d.value,h):k,i=k.getHours()*60+k.getMinutes(),j=e.getHours()*60+e.getMinutes()-i,n=Math.round(j/h.step),o=g(new Date(0,0,0,0,n*h.step+i,0));o=kf+q[0].offsetHeight&&(q[0].scrollTop=f+i.offsetHeight)):(e.removeClass(m),i=a("li:first",r).addClass(m)[0],q[0].scrollTop=0);return!1;case 13:if(q.is(":visible")){var k=a(n,r)[0];b(d,k,q,h)}return!1;case 27:q.hide();return!1}return!0}),a(d).keyup(function(a){j=!1}),this.getTime=function(){return f(d.value,h)},this.setTime=function(b){d.value=c(e(b,h),h),a(d).change()}},a.fn.timePicker.defaults={step:30,startTime:new Date(0,0,0,0,0,0),endTime:new Date(0,0,0,23,30,0),separator:":",show24Hours:!0}})(jQuery)
--------------------------------------------------------------------------------
/tests/qunit/qunit.css:
--------------------------------------------------------------------------------
1 | /** Font Family and Sizes */
2 |
3 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
4 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
5 | }
6 |
7 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
8 | #qunit-tests { font-size: smaller; }
9 |
10 |
11 | /** Resets */
12 |
13 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult {
14 | margin: 0;
15 | padding: 0;
16 | }
17 |
18 |
19 | /** Header */
20 |
21 | #qunit-header {
22 | padding: 0.5em 0 0.5em 1em;
23 |
24 | color: #8699a4;
25 | background-color: #0d3349;
26 |
27 | font-size: 1.5em;
28 | line-height: 1em;
29 | font-weight: normal;
30 |
31 | border-radius: 15px 15px 0 0;
32 | -moz-border-radius: 15px 15px 0 0;
33 | -webkit-border-top-right-radius: 15px;
34 | -webkit-border-top-left-radius: 15px;
35 | }
36 |
37 | #qunit-header a {
38 | text-decoration: none;
39 | color: #c2ccd1;
40 | }
41 |
42 | #qunit-header a:hover,
43 | #qunit-header a:focus {
44 | color: #fff;
45 | }
46 |
47 | #qunit-banner {
48 | height: 5px;
49 | }
50 |
51 | #qunit-testrunner-toolbar {
52 | padding: 0.5em 0 0.5em 2em;
53 | color: #5E740B;
54 | background-color: #eee;
55 | }
56 |
57 | #qunit-userAgent {
58 | padding: 0.5em 0 0.5em 2.5em;
59 | background-color: #2b81af;
60 | color: #fff;
61 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
62 | }
63 |
64 |
65 | /** Tests: Pass/Fail */
66 |
67 | #qunit-tests {
68 | list-style-position: inside;
69 | }
70 |
71 | #qunit-tests li {
72 | padding: 0.4em 0.5em 0.4em 2.5em;
73 | border-bottom: 1px solid #fff;
74 | list-style-position: inside;
75 | }
76 |
77 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
78 | display: none;
79 | }
80 |
81 | #qunit-tests li strong {
82 | cursor: pointer;
83 | }
84 |
85 | #qunit-tests li a {
86 | padding: 0.5em;
87 | color: #c2ccd1;
88 | text-decoration: none;
89 | }
90 | #qunit-tests li a:hover,
91 | #qunit-tests li a:focus {
92 | color: #000;
93 | }
94 |
95 | #qunit-tests ol {
96 | margin-top: 0.5em;
97 | padding: 0.5em;
98 |
99 | background-color: #fff;
100 |
101 | border-radius: 15px;
102 | -moz-border-radius: 15px;
103 | -webkit-border-radius: 15px;
104 |
105 | box-shadow: inset 0px 2px 13px #999;
106 | -moz-box-shadow: inset 0px 2px 13px #999;
107 | -webkit-box-shadow: inset 0px 2px 13px #999;
108 | }
109 |
110 | #qunit-tests table {
111 | border-collapse: collapse;
112 | margin-top: .2em;
113 | }
114 |
115 | #qunit-tests th {
116 | text-align: right;
117 | vertical-align: top;
118 | padding: 0 .5em 0 0;
119 | }
120 |
121 | #qunit-tests td {
122 | vertical-align: top;
123 | }
124 |
125 | #qunit-tests pre {
126 | margin: 0;
127 | white-space: pre-wrap;
128 | word-wrap: break-word;
129 | }
130 |
131 | #qunit-tests del {
132 | background-color: #e0f2be;
133 | color: #374e0c;
134 | text-decoration: none;
135 | }
136 |
137 | #qunit-tests ins {
138 | background-color: #ffcaca;
139 | color: #500;
140 | text-decoration: none;
141 | }
142 |
143 | /*** Test Counts */
144 |
145 | #qunit-tests b.counts { color: black; }
146 | #qunit-tests b.passed { color: #5E740B; }
147 | #qunit-tests b.failed { color: #710909; }
148 |
149 | #qunit-tests li li {
150 | margin: 0.5em;
151 | padding: 0.4em 0.5em 0.4em 0.5em;
152 | background-color: #fff;
153 | border-bottom: none;
154 | list-style-position: inside;
155 | }
156 |
157 | /*** Passing Styles */
158 |
159 | #qunit-tests li li.pass {
160 | color: #5E740B;
161 | background-color: #fff;
162 | border-left: 26px solid #C6E746;
163 | }
164 |
165 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
166 | #qunit-tests .pass .test-name { color: #366097; }
167 |
168 | #qunit-tests .pass .test-actual,
169 | #qunit-tests .pass .test-expected { color: #999999; }
170 |
171 | #qunit-banner.qunit-pass { background-color: #C6E746; }
172 |
173 | /*** Failing Styles */
174 |
175 | #qunit-tests li li.fail {
176 | color: #710909;
177 | background-color: #fff;
178 | border-left: 26px solid #EE5757;
179 | }
180 |
181 | #qunit-tests > li:last-child {
182 | border-radius: 0 0 15px 15px;
183 | -moz-border-radius: 0 0 15px 15px;
184 | -webkit-border-bottom-right-radius: 15px;
185 | -webkit-border-bottom-left-radius: 15px;
186 | }
187 |
188 | #qunit-tests .fail { color: #000000; background-color: #EE5757; }
189 | #qunit-tests .fail .test-name,
190 | #qunit-tests .fail .module-name { color: #000000; }
191 |
192 | #qunit-tests .fail .test-actual { color: #EE5757; }
193 | #qunit-tests .fail .test-expected { color: green; }
194 |
195 | #qunit-banner.qunit-fail { background-color: #EE5757; }
196 |
197 |
198 | /** Result */
199 |
200 | #qunit-testresult {
201 | padding: 0.5em 0.5em 0.5em 2.5em;
202 |
203 | color: #2b81af;
204 | background-color: #D2E0E6;
205 |
206 | border-bottom: 1px solid white;
207 | }
208 |
209 | /** Fixture */
210 |
211 | #qunit-fixture {
212 | position: absolute;
213 | top: -10000px;
214 | left: -10000px;
215 | }
216 |
--------------------------------------------------------------------------------
/tests/qunit/qunit.js:
--------------------------------------------------------------------------------
1 | /*
2 | * QUnit - A JavaScript Unit Testing Framework
3 | *
4 | * http://docs.jquery.com/QUnit
5 | *
6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer
7 | * Dual licensed under the MIT (MIT-LICENSE.txt)
8 | * or GPL (GPL-LICENSE.txt) licenses.
9 | */
10 |
11 | (function(window) {
12 |
13 | var defined = {
14 | setTimeout: typeof window.setTimeout !== "undefined",
15 | sessionStorage: (function() {
16 | try {
17 | return !!sessionStorage.getItem;
18 | } catch(e){
19 | return false;
20 | }
21 | })()
22 | };
23 |
24 | var testId = 0;
25 |
26 | var Test = function(name, testName, expected, testEnvironmentArg, async, callback) {
27 | this.name = name;
28 | this.testName = testName;
29 | this.expected = expected;
30 | this.testEnvironmentArg = testEnvironmentArg;
31 | this.async = async;
32 | this.callback = callback;
33 | this.assertions = [];
34 | };
35 | Test.prototype = {
36 | init: function() {
37 | var tests = id("qunit-tests");
38 | if (tests) {
39 | var b = document.createElement("strong");
40 | b.innerHTML = "Running " + this.name;
41 | var li = document.createElement("li");
42 | li.appendChild( b );
43 | li.className = "running";
44 | li.id = this.id = "test-output" + testId++;
45 | tests.appendChild( li );
46 | }
47 | },
48 | setup: function() {
49 | if (this.module != config.previousModule) {
50 | if ( config.previousModule ) {
51 | QUnit.moduleDone( {
52 | name: config.previousModule,
53 | failed: config.moduleStats.bad,
54 | passed: config.moduleStats.all - config.moduleStats.bad,
55 | total: config.moduleStats.all
56 | } );
57 | }
58 | config.previousModule = this.module;
59 | config.moduleStats = { all: 0, bad: 0 };
60 | QUnit.moduleStart( {
61 | name: this.module
62 | } );
63 | }
64 |
65 | config.current = this;
66 | this.testEnvironment = extend({
67 | setup: function() {},
68 | teardown: function() {}
69 | }, this.moduleTestEnvironment);
70 | if (this.testEnvironmentArg) {
71 | extend(this.testEnvironment, this.testEnvironmentArg);
72 | }
73 |
74 | QUnit.testStart( {
75 | name: this.testName
76 | } );
77 |
78 | // allow utility functions to access the current test environment
79 | // TODO why??
80 | QUnit.current_testEnvironment = this.testEnvironment;
81 |
82 | try {
83 | if ( !config.pollution ) {
84 | saveGlobal();
85 | }
86 |
87 | this.testEnvironment.setup.call(this.testEnvironment);
88 | } catch(e) {
89 | QUnit.ok( false, "Setup failed on " + this.testName + ": " + e.message );
90 | }
91 | },
92 | run: function() {
93 | if ( this.async ) {
94 | QUnit.stop();
95 | }
96 |
97 | if ( config.notrycatch ) {
98 | this.callback.call(this.testEnvironment);
99 | return;
100 | }
101 | try {
102 | this.callback.call(this.testEnvironment);
103 | } catch(e) {
104 | fail("Test " + this.testName + " died, exception and test follows", e, this.callback);
105 | QUnit.ok( false, "Died on test #" + (this.assertions.length + 1) + ": " + e.message + " - " + QUnit.jsDump.parse(e) );
106 | // else next test will carry the responsibility
107 | saveGlobal();
108 |
109 | // Restart the tests if they're blocking
110 | if ( config.blocking ) {
111 | start();
112 | }
113 | }
114 | },
115 | teardown: function() {
116 | try {
117 | checkPollution();
118 | this.testEnvironment.teardown.call(this.testEnvironment);
119 | } catch(e) {
120 | QUnit.ok( false, "Teardown failed on " + this.testName + ": " + e.message );
121 | }
122 | },
123 | finish: function() {
124 | if ( this.expected && this.expected != this.assertions.length ) {
125 | QUnit.ok( false, "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run" );
126 | }
127 |
128 | var good = 0, bad = 0,
129 | tests = id("qunit-tests");
130 |
131 | config.stats.all += this.assertions.length;
132 | config.moduleStats.all += this.assertions.length;
133 |
134 | if ( tests ) {
135 | var ol = document.createElement("ol");
136 |
137 | for ( var i = 0; i < this.assertions.length; i++ ) {
138 | var assertion = this.assertions[i];
139 |
140 | var li = document.createElement("li");
141 | li.className = assertion.result ? "pass" : "fail";
142 | li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed");
143 | ol.appendChild( li );
144 |
145 | if ( assertion.result ) {
146 | good++;
147 | } else {
148 | bad++;
149 | config.stats.bad++;
150 | config.moduleStats.bad++;
151 | }
152 | }
153 |
154 | // store result when possible
155 | if ( QUnit.config.reorder && defined.sessionStorage ) {
156 | if (bad) {
157 | sessionStorage.setItem("qunit-" + this.module + "-" + this.testName, bad)
158 | } else {
159 | sessionStorage.removeItem("qunit-" + this.testName);
160 | }
161 | }
162 |
163 | if (bad == 0) {
164 | ol.style.display = "none";
165 | }
166 |
167 | var b = document.createElement("strong");
168 | b.innerHTML = this.name + " (" + bad + " , " + good + " , " + this.assertions.length + ") ";
169 |
170 | var a = document.createElement("a");
171 | a.innerHTML = "Rerun";
172 | a.href = QUnit.url({ filter: getText([b]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") });
173 |
174 | addEvent(b, "click", function() {
175 | var next = b.nextSibling.nextSibling,
176 | display = next.style.display;
177 | next.style.display = display === "none" ? "block" : "none";
178 | });
179 |
180 | addEvent(b, "dblclick", function(e) {
181 | var target = e && e.target ? e.target : window.event.srcElement;
182 | if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) {
183 | target = target.parentNode;
184 | }
185 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) {
186 | window.location = QUnit.url({ filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") });
187 | }
188 | });
189 |
190 | var li = id(this.id);
191 | li.className = bad ? "fail" : "pass";
192 | li.removeChild( li.firstChild );
193 | li.appendChild( b );
194 | li.appendChild( a );
195 | li.appendChild( ol );
196 |
197 | } else {
198 | for ( var i = 0; i < this.assertions.length; i++ ) {
199 | if ( !this.assertions[i].result ) {
200 | bad++;
201 | config.stats.bad++;
202 | config.moduleStats.bad++;
203 | }
204 | }
205 | }
206 |
207 | try {
208 | QUnit.reset();
209 | } catch(e) {
210 | fail("reset() failed, following Test " + this.testName + ", exception and reset fn follows", e, QUnit.reset);
211 | }
212 |
213 | QUnit.testDone( {
214 | name: this.testName,
215 | failed: bad,
216 | passed: this.assertions.length - bad,
217 | total: this.assertions.length
218 | } );
219 | },
220 |
221 | queue: function() {
222 | var test = this;
223 | synchronize(function() {
224 | test.init();
225 | });
226 | function run() {
227 | // each of these can by async
228 | synchronize(function() {
229 | test.setup();
230 | });
231 | synchronize(function() {
232 | test.run();
233 | });
234 | synchronize(function() {
235 | test.teardown();
236 | });
237 | synchronize(function() {
238 | test.finish();
239 | });
240 | }
241 | // defer when previous test run passed, if storage is available
242 | var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-" + this.module + "-" + this.testName);
243 | if (bad) {
244 | run();
245 | } else {
246 | synchronize(run);
247 | };
248 | }
249 |
250 | };
251 |
252 | var QUnit = {
253 |
254 | // call on start of module test to prepend name to all tests
255 | module: function(name, testEnvironment) {
256 | config.currentModule = name;
257 | config.currentModuleTestEnviroment = testEnvironment;
258 | },
259 |
260 | asyncTest: function(testName, expected, callback) {
261 | if ( arguments.length === 2 ) {
262 | callback = expected;
263 | expected = 0;
264 | }
265 |
266 | QUnit.test(testName, expected, callback, true);
267 | },
268 |
269 | test: function(testName, expected, callback, async) {
270 | var name = '' + testName + ' ', testEnvironmentArg;
271 |
272 | if ( arguments.length === 2 ) {
273 | callback = expected;
274 | expected = null;
275 | }
276 | // is 2nd argument a testEnvironment?
277 | if ( expected && typeof expected === 'object') {
278 | testEnvironmentArg = expected;
279 | expected = null;
280 | }
281 |
282 | if ( config.currentModule ) {
283 | name = '' + config.currentModule + " : " + name;
284 | }
285 |
286 | if ( !validTest(config.currentModule + ": " + testName) ) {
287 | return;
288 | }
289 |
290 | var test = new Test(name, testName, expected, testEnvironmentArg, async, callback);
291 | test.module = config.currentModule;
292 | test.moduleTestEnvironment = config.currentModuleTestEnviroment;
293 | test.queue();
294 | },
295 |
296 | /**
297 | * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through.
298 | */
299 | expect: function(asserts) {
300 | config.current.expected = asserts;
301 | },
302 |
303 | /**
304 | * Asserts true.
305 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" );
306 | */
307 | ok: function(a, msg) {
308 | a = !!a;
309 | var details = {
310 | result: a,
311 | message: msg
312 | };
313 | msg = escapeHtml(msg);
314 | QUnit.log(details);
315 | config.current.assertions.push({
316 | result: a,
317 | message: msg
318 | });
319 | },
320 |
321 | /**
322 | * Checks that the first two arguments are equal, with an optional message.
323 | * Prints out both actual and expected values.
324 | *
325 | * Prefered to ok( actual == expected, message )
326 | *
327 | * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." );
328 | *
329 | * @param Object actual
330 | * @param Object expected
331 | * @param String message (optional)
332 | */
333 | equal: function(actual, expected, message) {
334 | QUnit.push(expected == actual, actual, expected, message);
335 | },
336 |
337 | notEqual: function(actual, expected, message) {
338 | QUnit.push(expected != actual, actual, expected, message);
339 | },
340 |
341 | deepEqual: function(actual, expected, message) {
342 | QUnit.push(QUnit.equiv(actual, expected), actual, expected, message);
343 | },
344 |
345 | notDeepEqual: function(actual, expected, message) {
346 | QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message);
347 | },
348 |
349 | strictEqual: function(actual, expected, message) {
350 | QUnit.push(expected === actual, actual, expected, message);
351 | },
352 |
353 | notStrictEqual: function(actual, expected, message) {
354 | QUnit.push(expected !== actual, actual, expected, message);
355 | },
356 |
357 | raises: function(block, expected, message) {
358 | var actual, ok = false;
359 |
360 | if (typeof expected === 'string') {
361 | message = expected;
362 | expected = null;
363 | }
364 |
365 | try {
366 | block();
367 | } catch (e) {
368 | actual = e;
369 | }
370 |
371 | if (actual) {
372 | // we don't want to validate thrown error
373 | if (!expected) {
374 | ok = true;
375 | // expected is a regexp
376 | } else if (QUnit.objectType(expected) === "regexp") {
377 | ok = expected.test(actual);
378 | // expected is a constructor
379 | } else if (actual instanceof expected) {
380 | ok = true;
381 | // expected is a validation function which returns true is validation passed
382 | } else if (expected.call({}, actual) === true) {
383 | ok = true;
384 | }
385 | }
386 |
387 | QUnit.ok(ok, message);
388 | },
389 |
390 | start: function() {
391 | config.semaphore--;
392 | if (config.semaphore > 0) {
393 | // don't start until equal number of stop-calls
394 | return;
395 | }
396 | if (config.semaphore < 0) {
397 | // ignore if start is called more often then stop
398 | config.semaphore = 0;
399 | }
400 | // A slight delay, to avoid any current callbacks
401 | if ( defined.setTimeout ) {
402 | window.setTimeout(function() {
403 | if ( config.timeout ) {
404 | clearTimeout(config.timeout);
405 | }
406 |
407 | config.blocking = false;
408 | process();
409 | }, 13);
410 | } else {
411 | config.blocking = false;
412 | process();
413 | }
414 | },
415 |
416 | stop: function(timeout) {
417 | config.semaphore++;
418 | config.blocking = true;
419 |
420 | if ( timeout && defined.setTimeout ) {
421 | clearTimeout(config.timeout);
422 | config.timeout = window.setTimeout(function() {
423 | QUnit.ok( false, "Test timed out" );
424 | QUnit.start();
425 | }, timeout);
426 | }
427 | }
428 | };
429 |
430 | // Backwards compatibility, deprecated
431 | QUnit.equals = QUnit.equal;
432 | QUnit.same = QUnit.deepEqual;
433 |
434 | // Maintain internal state
435 | var config = {
436 | // The queue of tests to run
437 | queue: [],
438 |
439 | // block until document ready
440 | blocking: true,
441 |
442 | // by default, run previously failed tests first
443 | // very useful in combination with "Hide passed tests" checked
444 | reorder: true,
445 |
446 | noglobals: false,
447 | notrycatch: false
448 | };
449 |
450 | // Load paramaters
451 | (function() {
452 | var location = window.location || { search: "", protocol: "file:" },
453 | params = location.search.slice( 1 ).split( "&" ),
454 | length = params.length,
455 | urlParams = {},
456 | current;
457 |
458 | if ( params[ 0 ] ) {
459 | for ( var i = 0; i < length; i++ ) {
460 | current = params[ i ].split( "=" );
461 | current[ 0 ] = decodeURIComponent( current[ 0 ] );
462 | // allow just a key to turn on a flag, e.g., test.html?noglobals
463 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true;
464 | urlParams[ current[ 0 ] ] = current[ 1 ];
465 | if ( current[ 0 ] in config ) {
466 | config[ current[ 0 ] ] = current[ 1 ];
467 | }
468 | }
469 | }
470 |
471 | QUnit.urlParams = urlParams;
472 | config.filter = urlParams.filter;
473 |
474 | // Figure out if we're running the tests from a server or not
475 | QUnit.isLocal = !!(location.protocol === 'file:');
476 | })();
477 |
478 | // Expose the API as global variables, unless an 'exports'
479 | // object exists, in that case we assume we're in CommonJS
480 | if ( typeof exports === "undefined" || typeof require === "undefined" ) {
481 | extend(window, QUnit);
482 | window.QUnit = QUnit;
483 | } else {
484 | extend(exports, QUnit);
485 | exports.QUnit = QUnit;
486 | }
487 |
488 | // define these after exposing globals to keep them in these QUnit namespace only
489 | extend(QUnit, {
490 | config: config,
491 |
492 | // Initialize the configuration options
493 | init: function() {
494 | extend(config, {
495 | stats: { all: 0, bad: 0 },
496 | moduleStats: { all: 0, bad: 0 },
497 | started: +new Date,
498 | updateRate: 1000,
499 | blocking: false,
500 | autostart: true,
501 | autorun: false,
502 | filter: "",
503 | queue: [],
504 | semaphore: 0
505 | });
506 |
507 | var tests = id( "qunit-tests" ),
508 | banner = id( "qunit-banner" ),
509 | result = id( "qunit-testresult" );
510 |
511 | if ( tests ) {
512 | tests.innerHTML = "";
513 | }
514 |
515 | if ( banner ) {
516 | banner.className = "";
517 | }
518 |
519 | if ( result ) {
520 | result.parentNode.removeChild( result );
521 | }
522 |
523 | if ( tests ) {
524 | result = document.createElement( "p" );
525 | result.id = "qunit-testresult";
526 | result.className = "result";
527 | tests.parentNode.insertBefore( result, tests );
528 | result.innerHTML = 'Running... ';
529 | }
530 | },
531 |
532 | /**
533 | * Resets the test setup. Useful for tests that modify the DOM.
534 | *
535 | * If jQuery is available, uses jQuery's html(), otherwise just innerHTML.
536 | */
537 | reset: function() {
538 | if ( window.jQuery ) {
539 | jQuery( "#main, #qunit-fixture" ).html( config.fixture );
540 | } else {
541 | var main = id( 'main' ) || id( 'qunit-fixture' );
542 | if ( main ) {
543 | main.innerHTML = config.fixture;
544 | }
545 | }
546 | },
547 |
548 | /**
549 | * Trigger an event on an element.
550 | *
551 | * @example triggerEvent( document.body, "click" );
552 | *
553 | * @param DOMElement elem
554 | * @param String type
555 | */
556 | triggerEvent: function( elem, type, event ) {
557 | if ( document.createEvent ) {
558 | event = document.createEvent("MouseEvents");
559 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView,
560 | 0, 0, 0, 0, 0, false, false, false, false, 0, null);
561 | elem.dispatchEvent( event );
562 |
563 | } else if ( elem.fireEvent ) {
564 | elem.fireEvent("on"+type);
565 | }
566 | },
567 |
568 | // Safe object type checking
569 | is: function( type, obj ) {
570 | return QUnit.objectType( obj ) == type;
571 | },
572 |
573 | objectType: function( obj ) {
574 | if (typeof obj === "undefined") {
575 | return "undefined";
576 |
577 | // consider: typeof null === object
578 | }
579 | if (obj === null) {
580 | return "null";
581 | }
582 |
583 | var type = Object.prototype.toString.call( obj )
584 | .match(/^\[object\s(.*)\]$/)[1] || '';
585 |
586 | switch (type) {
587 | case 'Number':
588 | if (isNaN(obj)) {
589 | return "nan";
590 | } else {
591 | return "number";
592 | }
593 | case 'String':
594 | case 'Boolean':
595 | case 'Array':
596 | case 'Date':
597 | case 'RegExp':
598 | case 'Function':
599 | return type.toLowerCase();
600 | }
601 | if (typeof obj === "object") {
602 | return "object";
603 | }
604 | return undefined;
605 | },
606 |
607 | push: function(result, actual, expected, message) {
608 | var details = {
609 | result: result,
610 | message: message,
611 | actual: actual,
612 | expected: expected
613 | };
614 |
615 | message = escapeHtml(message) || (result ? "okay" : "failed");
616 | message = '' + message + " ";
617 | expected = escapeHtml(QUnit.jsDump.parse(expected));
618 | actual = escapeHtml(QUnit.jsDump.parse(actual));
619 | var output = message + 'Expected: ' + expected + ' ';
620 | if (actual != expected) {
621 | output += 'Result: ' + actual + ' ';
622 | output += 'Diff: ' + QUnit.diff(expected, actual) +' ';
623 | }
624 | if (!result) {
625 | var source = sourceFromStacktrace();
626 | if (source) {
627 | details.source = source;
628 | output += 'Source: ' + source +' ';
629 | }
630 | }
631 | output += "
";
632 |
633 | QUnit.log(details);
634 |
635 | config.current.assertions.push({
636 | result: !!result,
637 | message: output
638 | });
639 | },
640 |
641 | url: function( params ) {
642 | params = extend( extend( {}, QUnit.urlParams ), params );
643 | var querystring = "?",
644 | key;
645 | for ( key in params ) {
646 | querystring += encodeURIComponent( key ) + "=" +
647 | encodeURIComponent( params[ key ] ) + "&";
648 | }
649 | return window.location.pathname + querystring.slice( 0, -1 );
650 | },
651 |
652 | // Logging callbacks; all receive a single argument with the listed properties
653 | // run test/logs.html for any related changes
654 | begin: function() {},
655 | // done: { failed, passed, total, runtime }
656 | done: function() {},
657 | // log: { result, actual, expected, message }
658 | log: function() {},
659 | // testStart: { name }
660 | testStart: function() {},
661 | // testDone: { name, failed, passed, total }
662 | testDone: function() {},
663 | // moduleStart: { name }
664 | moduleStart: function() {},
665 | // moduleDone: { name, failed, passed, total }
666 | moduleDone: function() {}
667 | });
668 |
669 | if ( typeof document === "undefined" || document.readyState === "complete" ) {
670 | config.autorun = true;
671 | }
672 |
673 | addEvent(window, "load", function() {
674 | QUnit.begin({});
675 |
676 | // Initialize the config, saving the execution queue
677 | var oldconfig = extend({}, config);
678 | QUnit.init();
679 | extend(config, oldconfig);
680 |
681 | config.blocking = false;
682 |
683 | var userAgent = id("qunit-userAgent");
684 | if ( userAgent ) {
685 | userAgent.innerHTML = navigator.userAgent;
686 | }
687 | var banner = id("qunit-header");
688 | if ( banner ) {
689 | banner.innerHTML = ' ' + banner.innerHTML + ' ' +
690 | ' noglobals ' +
691 | ' notrycatch ';
692 | addEvent( banner, "change", function( event ) {
693 | var params = {};
694 | params[ event.target.name ] = event.target.checked ? true : undefined;
695 | window.location = QUnit.url( params );
696 | });
697 | }
698 |
699 | var toolbar = id("qunit-testrunner-toolbar");
700 | if ( toolbar ) {
701 | var filter = document.createElement("input");
702 | filter.type = "checkbox";
703 | filter.id = "qunit-filter-pass";
704 | addEvent( filter, "click", function() {
705 | var ol = document.getElementById("qunit-tests");
706 | if ( filter.checked ) {
707 | ol.className = ol.className + " hidepass";
708 | } else {
709 | var tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " ";
710 | ol.className = tmp.replace(/ hidepass /, " ");
711 | }
712 | if ( defined.sessionStorage ) {
713 | if (filter.checked) {
714 | sessionStorage.setItem("qunit-filter-passed-tests", "true");
715 | } else {
716 | sessionStorage.removeItem("qunit-filter-passed-tests");
717 | }
718 | }
719 | });
720 | if ( defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) {
721 | filter.checked = true;
722 | var ol = document.getElementById("qunit-tests");
723 | ol.className = ol.className + " hidepass";
724 | }
725 | toolbar.appendChild( filter );
726 |
727 | var label = document.createElement("label");
728 | label.setAttribute("for", "qunit-filter-pass");
729 | label.innerHTML = "Hide passed tests";
730 | toolbar.appendChild( label );
731 | }
732 |
733 | var main = id('main') || id('qunit-fixture');
734 | if ( main ) {
735 | config.fixture = main.innerHTML;
736 | }
737 |
738 | if (config.autostart) {
739 | QUnit.start();
740 | }
741 | });
742 |
743 | function done() {
744 | config.autorun = true;
745 |
746 | // Log the last module results
747 | if ( config.currentModule ) {
748 | QUnit.moduleDone( {
749 | name: config.currentModule,
750 | failed: config.moduleStats.bad,
751 | passed: config.moduleStats.all - config.moduleStats.bad,
752 | total: config.moduleStats.all
753 | } );
754 | }
755 |
756 | var banner = id("qunit-banner"),
757 | tests = id("qunit-tests"),
758 | runtime = +new Date - config.started,
759 | passed = config.stats.all - config.stats.bad,
760 | html = [
761 | 'Tests completed in ',
762 | runtime,
763 | ' milliseconds. ',
764 | '',
765 | passed,
766 | ' tests of ',
767 | config.stats.all,
768 | ' passed, ',
769 | config.stats.bad,
770 | ' failed.'
771 | ].join('');
772 |
773 | if ( banner ) {
774 | banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass");
775 | }
776 |
777 | if ( tests ) {
778 | id( "qunit-testresult" ).innerHTML = html;
779 | }
780 |
781 | QUnit.done( {
782 | failed: config.stats.bad,
783 | passed: passed,
784 | total: config.stats.all,
785 | runtime: runtime
786 | } );
787 | }
788 |
789 | function validTest( name ) {
790 | var filter = config.filter,
791 | run = false;
792 |
793 | if ( !filter ) {
794 | return true;
795 | }
796 |
797 | not = filter.charAt( 0 ) === "!";
798 | if ( not ) {
799 | filter = filter.slice( 1 );
800 | }
801 |
802 | if ( name.indexOf( filter ) !== -1 ) {
803 | return !not;
804 | }
805 |
806 | if ( not ) {
807 | run = true;
808 | }
809 |
810 | return run;
811 | }
812 |
813 | // so far supports only Firefox, Chrome and Opera (buggy)
814 | // could be extended in the future to use something like https://github.com/csnover/TraceKit
815 | function sourceFromStacktrace() {
816 | try {
817 | throw new Error();
818 | } catch ( e ) {
819 | if (e.stacktrace) {
820 | // Opera
821 | return e.stacktrace.split("\n")[6];
822 | } else if (e.stack) {
823 | // Firefox, Chrome
824 | return e.stack.split("\n")[4];
825 | }
826 | }
827 | }
828 |
829 | function escapeHtml(s) {
830 | if (!s) {
831 | return "";
832 | }
833 | s = s + "";
834 | return s.replace(/[\&"<>\\]/g, function(s) {
835 | switch(s) {
836 | case "&": return "&";
837 | case "\\": return "\\\\";
838 | case '"': return '\"';
839 | case "<": return "<";
840 | case ">": return ">";
841 | default: return s;
842 | }
843 | });
844 | }
845 |
846 | function synchronize( callback ) {
847 | config.queue.push( callback );
848 |
849 | if ( config.autorun && !config.blocking ) {
850 | process();
851 | }
852 | }
853 |
854 | function process() {
855 | var start = (new Date()).getTime();
856 |
857 | while ( config.queue.length && !config.blocking ) {
858 | if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) {
859 | config.queue.shift()();
860 | } else {
861 | window.setTimeout( process, 13 );
862 | break;
863 | }
864 | }
865 | if (!config.blocking && !config.queue.length) {
866 | done();
867 | }
868 | }
869 |
870 | function saveGlobal() {
871 | config.pollution = [];
872 |
873 | if ( config.noglobals ) {
874 | for ( var key in window ) {
875 | config.pollution.push( key );
876 | }
877 | }
878 | }
879 |
880 | function checkPollution( name ) {
881 | var old = config.pollution;
882 | saveGlobal();
883 |
884 | var newGlobals = diff( old, config.pollution );
885 | if ( newGlobals.length > 0 ) {
886 | ok( false, "Introduced global variable(s): " + newGlobals.join(", ") );
887 | config.current.expected++;
888 | }
889 |
890 | var deletedGlobals = diff( config.pollution, old );
891 | if ( deletedGlobals.length > 0 ) {
892 | ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") );
893 | config.current.expected++;
894 | }
895 | }
896 |
897 | // returns a new Array with the elements that are in a but not in b
898 | function diff( a, b ) {
899 | var result = a.slice();
900 | for ( var i = 0; i < result.length; i++ ) {
901 | for ( var j = 0; j < b.length; j++ ) {
902 | if ( result[i] === b[j] ) {
903 | result.splice(i, 1);
904 | i--;
905 | break;
906 | }
907 | }
908 | }
909 | return result;
910 | }
911 |
912 | function fail(message, exception, callback) {
913 | if ( typeof console !== "undefined" && console.error && console.warn ) {
914 | console.error(message);
915 | console.error(exception);
916 | console.warn(callback.toString());
917 |
918 | } else if ( window.opera && opera.postError ) {
919 | opera.postError(message, exception, callback.toString);
920 | }
921 | }
922 |
923 | function extend(a, b) {
924 | for ( var prop in b ) {
925 | if ( b[prop] === undefined ) {
926 | delete a[prop];
927 | } else {
928 | a[prop] = b[prop];
929 | }
930 | }
931 |
932 | return a;
933 | }
934 |
935 | function addEvent(elem, type, fn) {
936 | if ( elem.addEventListener ) {
937 | elem.addEventListener( type, fn, false );
938 | } else if ( elem.attachEvent ) {
939 | elem.attachEvent( "on" + type, fn );
940 | } else {
941 | fn();
942 | }
943 | }
944 |
945 | function id(name) {
946 | return !!(typeof document !== "undefined" && document && document.getElementById) &&
947 | document.getElementById( name );
948 | }
949 |
950 | // Test for equality any JavaScript type.
951 | // Discussions and reference: http://philrathe.com/articles/equiv
952 | // Test suites: http://philrathe.com/tests/equiv
953 | // Author: Philippe Rathé
954 | QUnit.equiv = function () {
955 |
956 | var innerEquiv; // the real equiv function
957 | var callers = []; // stack to decide between skip/abort functions
958 | var parents = []; // stack to avoiding loops from circular referencing
959 |
960 | // Call the o related callback with the given arguments.
961 | function bindCallbacks(o, callbacks, args) {
962 | var prop = QUnit.objectType(o);
963 | if (prop) {
964 | if (QUnit.objectType(callbacks[prop]) === "function") {
965 | return callbacks[prop].apply(callbacks, args);
966 | } else {
967 | return callbacks[prop]; // or undefined
968 | }
969 | }
970 | }
971 |
972 | var callbacks = function () {
973 |
974 | // for string, boolean, number and null
975 | function useStrictEquality(b, a) {
976 | if (b instanceof a.constructor || a instanceof b.constructor) {
977 | // to catch short annotaion VS 'new' annotation of a declaration
978 | // e.g. var i = 1;
979 | // var j = new Number(1);
980 | return a == b;
981 | } else {
982 | return a === b;
983 | }
984 | }
985 |
986 | return {
987 | "string": useStrictEquality,
988 | "boolean": useStrictEquality,
989 | "number": useStrictEquality,
990 | "null": useStrictEquality,
991 | "undefined": useStrictEquality,
992 |
993 | "nan": function (b) {
994 | return isNaN(b);
995 | },
996 |
997 | "date": function (b, a) {
998 | return QUnit.objectType(b) === "date" && a.valueOf() === b.valueOf();
999 | },
1000 |
1001 | "regexp": function (b, a) {
1002 | return QUnit.objectType(b) === "regexp" &&
1003 | a.source === b.source && // the regex itself
1004 | a.global === b.global && // and its modifers (gmi) ...
1005 | a.ignoreCase === b.ignoreCase &&
1006 | a.multiline === b.multiline;
1007 | },
1008 |
1009 | // - skip when the property is a method of an instance (OOP)
1010 | // - abort otherwise,
1011 | // initial === would have catch identical references anyway
1012 | "function": function () {
1013 | var caller = callers[callers.length - 1];
1014 | return caller !== Object &&
1015 | typeof caller !== "undefined";
1016 | },
1017 |
1018 | "array": function (b, a) {
1019 | var i, j, loop;
1020 | var len;
1021 |
1022 | // b could be an object literal here
1023 | if ( ! (QUnit.objectType(b) === "array")) {
1024 | return false;
1025 | }
1026 |
1027 | len = a.length;
1028 | if (len !== b.length) { // safe and faster
1029 | return false;
1030 | }
1031 |
1032 | //track reference to avoid circular references
1033 | parents.push(a);
1034 | for (i = 0; i < len; i++) {
1035 | loop = false;
1036 | for(j=0;j= 0) {
1181 | type = "array";
1182 | } else {
1183 | type = typeof obj;
1184 | }
1185 | return type;
1186 | },
1187 | separator:function() {
1188 | return this.multiline ? this.HTML ? ' ' : '\n' : this.HTML ? ' ' : ' ';
1189 | },
1190 | indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing
1191 | if ( !this.multiline )
1192 | return '';
1193 | var chr = this.indentChar;
1194 | if ( this.HTML )
1195 | chr = chr.replace(/\t/g,' ').replace(/ /g,' ');
1196 | return Array( this._depth_ + (extra||0) ).join(chr);
1197 | },
1198 | up:function( a ) {
1199 | this._depth_ += a || 1;
1200 | },
1201 | down:function( a ) {
1202 | this._depth_ -= a || 1;
1203 | },
1204 | setParser:function( name, parser ) {
1205 | this.parsers[name] = parser;
1206 | },
1207 | // The next 3 are exposed so you can use them
1208 | quote:quote,
1209 | literal:literal,
1210 | join:join,
1211 | //
1212 | _depth_: 1,
1213 | // This is the list of parsers, to modify them, use jsDump.setParser
1214 | parsers:{
1215 | window: '[Window]',
1216 | document: '[Document]',
1217 | error:'[ERROR]', //when no parser is found, shouldn't happen
1218 | unknown: '[Unknown]',
1219 | 'null':'null',
1220 | 'undefined':'undefined',
1221 | 'function':function( fn ) {
1222 | var ret = 'function',
1223 | name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE
1224 | if ( name )
1225 | ret += ' ' + name;
1226 | ret += '(';
1227 |
1228 | ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join('');
1229 | return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' );
1230 | },
1231 | array: array,
1232 | nodelist: array,
1233 | arguments: array,
1234 | object:function( map ) {
1235 | var ret = [ ];
1236 | QUnit.jsDump.up();
1237 | for ( var key in map )
1238 | ret.push( QUnit.jsDump.parse(key,'key') + ': ' + QUnit.jsDump.parse(map[key]) );
1239 | QUnit.jsDump.down();
1240 | return join( '{', ret, '}' );
1241 | },
1242 | node:function( node ) {
1243 | var open = QUnit.jsDump.HTML ? '<' : '<',
1244 | close = QUnit.jsDump.HTML ? '>' : '>';
1245 |
1246 | var tag = node.nodeName.toLowerCase(),
1247 | ret = open + tag;
1248 |
1249 | for ( var a in QUnit.jsDump.DOMAttrs ) {
1250 | var val = node[QUnit.jsDump.DOMAttrs[a]];
1251 | if ( val )
1252 | ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' );
1253 | }
1254 | return ret + close + open + '/' + tag + close;
1255 | },
1256 | functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function
1257 | var l = fn.length;
1258 | if ( !l ) return '';
1259 |
1260 | var args = Array(l);
1261 | while ( l-- )
1262 | args[l] = String.fromCharCode(97+l);//97 is 'a'
1263 | return ' ' + args.join(', ') + ' ';
1264 | },
1265 | key:quote, //object calls it internally, the key part of an item in a map
1266 | functionCode:'[code]', //function calls it internally, it's the content of the function
1267 | attribute:quote, //node calls it internally, it's an html attribute value
1268 | string:quote,
1269 | date:quote,
1270 | regexp:literal, //regex
1271 | number:literal,
1272 | 'boolean':literal
1273 | },
1274 | DOMAttrs:{//attributes to dump from nodes, name=>realName
1275 | id:'id',
1276 | name:'name',
1277 | 'class':'className'
1278 | },
1279 | HTML:false,//if true, entities are escaped ( <, >, \t, space and \n )
1280 | indentChar:' ',//indentation unit
1281 | multiline:true //if true, items in a collection, are separated by a \n, else just a space.
1282 | };
1283 |
1284 | return jsDump;
1285 | })();
1286 |
1287 | // from Sizzle.js
1288 | function getText( elems ) {
1289 | var ret = "", elem;
1290 |
1291 | for ( var i = 0; elems[i]; i++ ) {
1292 | elem = elems[i];
1293 |
1294 | // Get the text from text nodes and CDATA nodes
1295 | if ( elem.nodeType === 3 || elem.nodeType === 4 ) {
1296 | ret += elem.nodeValue;
1297 |
1298 | // Traverse everything else, except comment nodes
1299 | } else if ( elem.nodeType !== 8 ) {
1300 | ret += getText( elem.childNodes );
1301 | }
1302 | }
1303 |
1304 | return ret;
1305 | };
1306 |
1307 | /*
1308 | * Javascript Diff Algorithm
1309 | * By John Resig (http://ejohn.org/)
1310 | * Modified by Chu Alan "sprite"
1311 | *
1312 | * Released under the MIT license.
1313 | *
1314 | * More Info:
1315 | * http://ejohn.org/projects/javascript-diff-algorithm/
1316 | *
1317 | * Usage: QUnit.diff(expected, actual)
1318 | *
1319 | * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over"
1320 | */
1321 | QUnit.diff = (function() {
1322 | function diff(o, n){
1323 | var ns = new Object();
1324 | var os = new Object();
1325 |
1326 | for (var i = 0; i < n.length; i++) {
1327 | if (ns[n[i]] == null)
1328 | ns[n[i]] = {
1329 | rows: new Array(),
1330 | o: null
1331 | };
1332 | ns[n[i]].rows.push(i);
1333 | }
1334 |
1335 | for (var i = 0; i < o.length; i++) {
1336 | if (os[o[i]] == null)
1337 | os[o[i]] = {
1338 | rows: new Array(),
1339 | n: null
1340 | };
1341 | os[o[i]].rows.push(i);
1342 | }
1343 |
1344 | for (var i in ns) {
1345 | if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) {
1346 | n[ns[i].rows[0]] = {
1347 | text: n[ns[i].rows[0]],
1348 | row: os[i].rows[0]
1349 | };
1350 | o[os[i].rows[0]] = {
1351 | text: o[os[i].rows[0]],
1352 | row: ns[i].rows[0]
1353 | };
1354 | }
1355 | }
1356 |
1357 | for (var i = 0; i < n.length - 1; i++) {
1358 | if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null &&
1359 | n[i + 1] == o[n[i].row + 1]) {
1360 | n[i + 1] = {
1361 | text: n[i + 1],
1362 | row: n[i].row + 1
1363 | };
1364 | o[n[i].row + 1] = {
1365 | text: o[n[i].row + 1],
1366 | row: i + 1
1367 | };
1368 | }
1369 | }
1370 |
1371 | for (var i = n.length - 1; i > 0; i--) {
1372 | if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null &&
1373 | n[i - 1] == o[n[i].row - 1]) {
1374 | n[i - 1] = {
1375 | text: n[i - 1],
1376 | row: n[i].row - 1
1377 | };
1378 | o[n[i].row - 1] = {
1379 | text: o[n[i].row - 1],
1380 | row: i - 1
1381 | };
1382 | }
1383 | }
1384 |
1385 | return {
1386 | o: o,
1387 | n: n
1388 | };
1389 | }
1390 |
1391 | return function(o, n){
1392 | o = o.replace(/\s+$/, '');
1393 | n = n.replace(/\s+$/, '');
1394 | var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/));
1395 |
1396 | var str = "";
1397 |
1398 | var oSpace = o.match(/\s+/g);
1399 | if (oSpace == null) {
1400 | oSpace = [" "];
1401 | }
1402 | else {
1403 | oSpace.push(" ");
1404 | }
1405 | var nSpace = n.match(/\s+/g);
1406 | if (nSpace == null) {
1407 | nSpace = [" "];
1408 | }
1409 | else {
1410 | nSpace.push(" ");
1411 | }
1412 |
1413 | if (out.n.length == 0) {
1414 | for (var i = 0; i < out.o.length; i++) {
1415 | str += '' + out.o[i] + oSpace[i] + "";
1416 | }
1417 | }
1418 | else {
1419 | if (out.n[0].text == null) {
1420 | for (n = 0; n < out.o.length && out.o[n].text == null; n++) {
1421 | str += '' + out.o[n] + oSpace[n] + "";
1422 | }
1423 | }
1424 |
1425 | for (var i = 0; i < out.n.length; i++) {
1426 | if (out.n[i].text == null) {
1427 | str += '' + out.n[i] + nSpace[i] + " ";
1428 | }
1429 | else {
1430 | var pre = "";
1431 |
1432 | for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) {
1433 | pre += '' + out.o[n] + oSpace[n] + "";
1434 | }
1435 | str += " " + out.n[i].text + nSpace[i] + pre;
1436 | }
1437 | }
1438 | }
1439 |
1440 | return str;
1441 | };
1442 | })();
1443 |
1444 | })(this);
1445 |
--------------------------------------------------------------------------------
/tests/timePicker.js:
--------------------------------------------------------------------------------
1 | function equalsDate(d1, d2, message) {
2 | if (!d1 || !d2) {
3 | ok(false, message + ' - missing date');
4 | return;
5 | }
6 | d1 = new Date(d1.getFullYear(), d1.getMonth(), d1.getDate());
7 | d2 = new Date(d2.getFullYear(), d2.getMonth(), d2.getDate());
8 | equals(d1.toString(), d2.toString(), message);
9 | }
10 |
11 | module("timePicker");
12 |
13 | test('formatTime', function() {
14 | equals($.timePicker.formatTime('HH:mm', new Date(0, 0, 0, 13, 30)),
15 | '13:30', 'Format time HH:mm');
16 | equals($.timePicker.formatTime('HH:mm tt', new Date(2022, 0, 0, 13, 30)),
17 | '13:30 PM', 'Format time HH:mm');
18 | equals($.timePicker.formatTime('hh:mm tt', new Date(2022, 0, 0, 13, 30)),
19 | '01:30 PM', 'Format time hh:mm tt');
20 | equals($.timePicker.formatTime('h:mm t', new Date(2022, 0, 0, 4, 30)),
21 | '4:30 A', 'Format time h:m t');
22 | equals($.timePicker.formatTime('hh:mm tt', new Date(2022, 0, 0, 0, 0)),
23 | '12:00 AM', 'Format time hh:mm tt');
24 | equals($.timePicker.formatTime('h:m t', new Date(2022, 0, 0, 0, 0)),
25 | '12:0 A', 'Format time h:m t');
26 | });
27 |
28 | test('parseTime', function() {
29 | equalsDate($.timePicker.parseTime('HH:mm', '13:30'),
30 | new Date(2001,0,0,13,30,0), 'Format time HH:mm');
31 | equalsDate($.timePicker.parseTime('h:m', '2:1'),
32 | new Date(2001,0,0,2,1,0), 'Format time h:m');
33 | equalsDate($.timePicker.parseTime('hh:mm tt', '12:00 AM'),
34 | new Date(2001,0,0,0,0,0), 'Format time hh:mm tt midnight');
35 | });
36 |
--------------------------------------------------------------------------------
/tests/timepicker.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | jQuery timePicker Test Suite
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/timePicker.css:
--------------------------------------------------------------------------------
1 | div.time-picker {
2 | position: absolute;
3 | height: 191px;
4 | width:4em; /* needed for IE */
5 | overflow: auto;
6 | background: #fff;
7 | border: 1px solid #aaa;
8 | z-index: 99;
9 | margin: 0;
10 | }
11 | div.time-picker-12hours {
12 | width:6em; /* needed for IE */
13 | }
14 |
15 | div.time-picker ul {
16 | list-style-type: none;
17 | margin: 0;
18 | padding: 0;
19 | }
20 | div.time-picker li {
21 | cursor: pointer;
22 | height: 10px;
23 | font: 12px/1 Helvetica, Arial, sans-serif;
24 | padding: 4px 3px;
25 | }
26 | div.time-picker li.selected {
27 | background: #0063CE;
28 | color: #fff;
29 | }
--------------------------------------------------------------------------------