jquery.timepicker.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664
  1. // jQuery TimePicker plugin - http://github.com/wvega/timepicker
  2. //
  3. // A jQuery plugin to enhance standard form input fields helping users to select
  4. // (or type) times.
  5. //
  6. // Copyright (c) 2010 Willington Vega <wvega@wvega.com>
  7. //
  8. // Permission is hereby granted, free of charge, to any person obtaining a copy
  9. // of this software and associated documentation files (the "Software"), to deal
  10. // in the Software without restriction, including without limitation the rights
  11. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  12. // copies of the Software, and to permit persons to whom the Software is
  13. // furnished to do so, subject to the following conditions:
  14. //
  15. // The above copyright notice and this permission notice shall be included in
  16. // all copies or substantial portions of the Software.
  17. //
  18. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  19. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  20. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  21. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  22. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  23. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  24. // THE SOFTWARE.
  25. // Define a cross-browser window.console.log method.
  26. // For IE and FF without Firebug, fallback to using an alert.
  27. //if (!window.console) {
  28. // var log = window.opera ? window.opera.postError : alert;
  29. // window.console = { log: function(str) { log(str) } };
  30. //}
  31. if(typeof jQuery != 'undefined') {
  32. (function($, undefined){
  33. function pad(str, ch, length) {
  34. return Array(length + 1 - str.length).join(ch) + str;
  35. }
  36. function normalize() {
  37. if (arguments.length == 1) {
  38. return new Date(1988, 7, 24, arguments[0].getHours(), arguments[0].getMinutes(), 00);
  39. } else if (arguments.length == 2) {
  40. return new Date(1988, 7, 24, arguments[0], arguments[1], 00);
  41. } else {
  42. return new Date(1988, 7, 24);
  43. }
  44. }
  45. $.TimePicker = function() {
  46. var widget = this;
  47. widget.ui = $('ul.ui-timepicker');
  48. if (widget.ui.length == 0) {
  49. widget.ui = $('<ul></ul>').addClass('ui-timepicker ui-timepicker-hidden')
  50. .addClass('ui-widget ui-widget-content ui-menu')
  51. .addClass('ui-corner-all ui-helper-hidden')
  52. .appendTo('body');
  53. if ($.fn.jquery >= '1.4.2') {
  54. widget.ui.delegate('a', 'mouseenter.timepicker', function(event) {
  55. // passing false instead of an instance object tells the function
  56. // to use the current instance
  57. widget.activate(false, $(this).parent());
  58. }).delegate('a', 'mouseleave.timepicker', function(event) {
  59. widget.deactivate(false);
  60. }).delegate('a', 'click.timepicker', function(event) {
  61. event.preventDefault();
  62. widget.select(false, $(this).parent());
  63. });
  64. }
  65. widget.ui.bind('click.timepicker, scroll.timepicker', function(event) {
  66. clearTimeout(widget.closing);
  67. });
  68. }
  69. };
  70. $.TimePicker.count = 0;
  71. $.TimePicker.instance = function() {
  72. if (!$.TimePicker._instance) {
  73. $.TimePicker._instance = new $.TimePicker();
  74. }
  75. return $.TimePicker._instance;
  76. }
  77. $.TimePicker.prototype = {
  78. // extracted from from jQuery UI Core
  79. // http://github,com/jquery/jquery-ui/blob/master/ui/jquery.ui.core.js
  80. keyCode: {
  81. ALT: 18,
  82. BLOQ_MAYUS: 20,
  83. CTRL: 17,
  84. DOWN: 40,
  85. END: 35,
  86. ENTER: 13,
  87. HOME: 36,
  88. LEFT: 37,
  89. NUMPAD_ENTER: 108,
  90. PAGE_DOWN: 34,
  91. PAGE_UP: 33,
  92. RIGHT: 39,
  93. SHIFT: 16,
  94. TAB: 9,
  95. UP: 38
  96. },
  97. _items: function(i, startTime) {
  98. var widget = this, ul = $('<ul></ul>'), item = null, time, end;
  99. if (startTime) {
  100. time = normalize(startTime);
  101. } else if (i.options.startTime) {
  102. time = normalize(i.options.startTime);
  103. } else {
  104. time = normalize(i.options.startHour, i.options.startMinutes)
  105. }
  106. end = new Date(time.getTime() + 24 * 60 * 60 * 1000);
  107. while(time < end) {
  108. if (widget._isValidTime(i, time)) {
  109. item = $('<li>').addClass('ui-menu-item').appendTo(ul);
  110. $('<a>').addClass('ui-corner-all').text($.fn.timepicker.formatTime(i.options.timeFormat, time)).appendTo(item);
  111. item.data('time-value', time);
  112. }
  113. time = new Date(time.getTime() + i.options.interval * 60 * 1000);
  114. }
  115. return ul.children();
  116. },
  117. _isValidTime: function(i, time) {
  118. var min = null, max = null;
  119. time = normalize(time);
  120. if (i.options.minTime !== null) {
  121. min = normalize(i.options.minTime);
  122. } else if (i.options.minHour !== null || i.options.minMinutes !== null) {
  123. min = normalize(i.options.minHour, i.options.minMinutes);
  124. }
  125. if (i.options.maxTime !== null) {
  126. max = normalize(i.options.maxTime)
  127. } else if (i.options.maxHour !== null || i.options.maxMinutes !== null) {
  128. max = normalize(i.options.maxHour, i.options.maxMinutes);
  129. }
  130. if (min !== null && max !== null) {
  131. return time >= min && time <= max;
  132. } else if (min !== null) {
  133. return time >= min;
  134. } else if (max !== null) {
  135. return time <= max;
  136. }
  137. return true;
  138. },
  139. _hasScroll: function() {
  140. return this.ui.height() < this.ui.attr('scrollHeight');
  141. },
  142. /**
  143. * TODO: Write me!
  144. *
  145. * @param i
  146. * @param direction
  147. * @param edge
  148. * */
  149. _move: function(i, direction, edge) {
  150. var widget = this;
  151. if (widget.closed()) {
  152. widget.open(i);
  153. }
  154. if (!widget.active) {
  155. widget.activate(i, widget.ui.children(edge));
  156. return;
  157. }
  158. var next = widget.active[direction + 'All']('.ui-menu-item').eq(0);
  159. if (next.length) {
  160. widget.activate(i, next);
  161. } else {
  162. widget.activate(i, widget.ui.children(edge));
  163. }
  164. },
  165. //
  166. // protected methods
  167. //
  168. register: function(node, options) {
  169. var widget = this, i = {};// timepicker instance object
  170. i.element = $(node);
  171. if (i.element.data('TimePicker')) {return;}
  172. i.element.data('TimePicker', i);
  173. i.options = $.metadata ? $.extend({}, options, i.element.metadata()) : options;
  174. i.widget = widget;
  175. i.selectedTime = $.fn.timepicker.parseTime(i.element.val());
  176. // proxy functions for the exposed api methods
  177. $.extend(i, {
  178. next: function() {return widget.next(i);},
  179. previous: function() {return widget.previous(i);},
  180. first: function() {return widget.first(i);},
  181. last: function() {return widget.last(i);},
  182. selected: function() {return widget.selected(i);},
  183. open: function() {return widget.open(i);},
  184. close: function(force) {return widget.close(i, force);},
  185. closed: function() {return widget.closed(i);},
  186. destroy: function() {return widget.destroy(i)},
  187. getTime: function() {return widget.getTime(i);},
  188. setTime: function(time) {return widget.setTime(i, time);}
  189. });
  190. i.element.bind('keydown.timepicker', function(event) {
  191. switch (event.which || event.keyCode) {
  192. case widget.keyCode.ENTER:
  193. case widget.keyCode.NUMPAD_ENTER:
  194. event.preventDefault();
  195. if (widget.closed()) {
  196. i.element.trigger('change.timepicker');
  197. } else {
  198. widget.select(i, widget.active);
  199. }
  200. break;
  201. case widget.keyCode.UP:
  202. i.previous();
  203. break;
  204. case widget.keyCode.DOWN:
  205. i.next();
  206. break;
  207. default:
  208. if (!widget.closed()) {
  209. i.close(true);
  210. }
  211. break;
  212. }
  213. }).bind('focus.timepicker', function(event) {
  214. i.open();
  215. }).bind('blur.timepicker', function(event) {
  216. i.close();
  217. }).bind('change.timepicker', function(event) {
  218. if (i.closed()) {
  219. i.setTime($.fn.timepicker.parseTime(i.element.val()));
  220. }
  221. });
  222. },
  223. select: function(i, item) {
  224. var widget = this, instance = i === false ? widget.instance : i;
  225. clearTimeout(widget.closing);
  226. widget.setTime(instance, $.fn.timepicker.parseTime(item.children('a').text()));
  227. widget.close(instance, true);
  228. },
  229. activate: function(i, item) {
  230. var widget = this, instance = i === false ? widget.instance : i;
  231. if (instance !== widget.instance) {
  232. return;
  233. } else {
  234. widget.deactivate();
  235. }
  236. if (widget._hasScroll()) {
  237. var offset = item.offset().top - widget.ui.offset().top,
  238. scroll = widget.ui.scrollTop(),
  239. height = widget.ui.height();
  240. if (offset < 0) {
  241. widget.ui.scrollTop(scroll + offset);
  242. } else if (offset >= height) {
  243. widget.ui.scrollTop(scroll + offset - height + item.height());
  244. }
  245. }
  246. widget.active = item.eq(0).children('a').addClass('ui-state-hover')
  247. .attr('id', 'ui-active-item')
  248. .end();
  249. },
  250. deactivate: function() {
  251. var widget = this;
  252. if (!widget.active) {return;}
  253. widget.active.children('a').removeClass('ui-state-hover').removeAttr('id');
  254. widget.active = null;
  255. },
  256. // option: function(key, value) {
  257. // var self = this;
  258. // // TODO: handle variable updates
  259. // if (arguments.length > 1) {
  260. // if (self.options.hasOwnProperty(key)) {
  261. // self.options[key] = value;
  262. // }
  263. // return self;
  264. // }
  265. // return self.options[key];
  266. // },
  267. /**
  268. * _activate, _deactivate, first, last, next, previous, _move and
  269. * _hasScroll were extracted from jQuery UI Menu
  270. * http://github,com/jquery/jquery-ui/blob/menu/ui/jquery.ui.menu.js
  271. */
  272. //
  273. // public methods
  274. //
  275. next: function(i) {
  276. if (this.closed() || this.instance === i) {
  277. this._move(i, 'next', '.ui-menu-item:first');
  278. }
  279. },
  280. previous: function(i) {
  281. if (this.closed() || this.instance === i) {
  282. this._move(i, 'prev', '.ui-menu-item:last');
  283. }
  284. },
  285. first: function(i) {
  286. if (this.instance === i) {
  287. return this.active && !this.active.prevAll('.ui-menu-item').length;
  288. }
  289. return false;
  290. },
  291. last: function(i) {
  292. if (this.instance === i) {
  293. return this.active && !this.active.nextAll('.ui-menu-item').length;
  294. }
  295. return false;
  296. },
  297. selected: function(i) {
  298. if (this.instance === i) {
  299. return this.active ? this.active : null;
  300. }
  301. return null;
  302. },
  303. open: function(i) {
  304. var widget = this, zindex;
  305. // if a date is already selected and options.dynamic is true,
  306. // arrange the items in the list so the first item is
  307. // cronologically right after the selected date.
  308. // TODO: set selectedTime
  309. if (!i.items || (i.options.dynamic && i.selectedTime)) {
  310. i.items = widget._items(i);
  311. }
  312. // remove old li elements but keep associated events, then append
  313. // the new li elements to the ul
  314. if (widget.instance !== i || (i.options.dynamic && i.selectedTime)) {
  315. // handle menu events when using jQuery versions previous to
  316. // 1.4.2 (thanks to Brian Link)
  317. // http://github.com/wvega/timepicker/issues#issue/4
  318. if ($.fn.jquery < '1.4.2') {
  319. widget.ui.children().remove();
  320. widget.ui.append(i.items);
  321. widget.ui.find('a').bind('mouseover.timepicker', function(event) {
  322. widget.activate(i, $(this).parent());
  323. }).bind('mouseout.timepicker', function(event) {
  324. widget.deactivate(i);
  325. }).bind('click.timepicker', function(event) {
  326. event.preventDefault();
  327. widget.select(i, $(this).parent());
  328. });
  329. } else {
  330. widget.ui.children().detach();
  331. widget.ui.append(i.items);
  332. }
  333. }
  334. // theme
  335. widget.ui.removeClass('ui-helper-hidden ui-timepicker-hidden ui-timepicker-standard ui-timepicker-corners').show();
  336. switch (i.options.theme) {
  337. case 'standard':
  338. widget.ui.addClass('ui-timepicker-standard');
  339. break;
  340. case 'standard-rounded-corners':
  341. widget.ui.addClass('ui-timepicker-standard ui-timepicker-corners');
  342. break;
  343. default:
  344. break;
  345. }
  346. // position
  347. // zindex = i.element.offsetParent().css('z-index'); zindex == 'auto' ? 'auto' : parseInt(zindex, 10) + 1
  348. widget.ui.css($.extend(i.element.offset(), {
  349. width: i.element.innerWidth(),
  350. zIndex: i.element.offsetParent().css('z-index')
  351. }));
  352. widget.ui.css('top', parseInt(widget.ui.css('top'), 10) + i.element.outerHeight());
  353. widget.instance = i;
  354. // try to match input field's current value with an item in the
  355. // dropdown
  356. if (i.selectedTime) {
  357. i.items.each(function() {
  358. var item = $(this), time;
  359. if ($.fn.jquery < '1.4.2') {
  360. time = $.fn.timepicker.parseTime(item.find('a').text());
  361. } else {
  362. time = item.data('time-value');
  363. }
  364. if (time.getTime() == i.selectedTime.getTime()) {
  365. widget.activate(i, item);
  366. return false;
  367. }
  368. return true;
  369. });
  370. } else {
  371. widget.deactivate(i);
  372. }
  373. // don't break the chain
  374. return i.element;
  375. },
  376. close: function(i, force) {
  377. var widget = this;
  378. if (widget.closed() || force) {
  379. clearTimeout(widget.closing);
  380. if (widget.instance === i) {
  381. widget.ui.scrollTop(0).addClass('ui-helper-hidden ui-timepicker-hidden').hide();
  382. widget.ui.children().removeClass('ui-state-hover');
  383. }
  384. } else {
  385. widget.closing = setTimeout(function() {
  386. widget.close(i, true);
  387. }, 150);
  388. }
  389. return i.element;
  390. },
  391. closed: function() {
  392. return this.ui.is(':hidden');
  393. },
  394. destroy: function(i) {
  395. var widget = this;
  396. widget.close(i, true);
  397. return i.element.unbind('.timepicker').data('TimePicker', null);
  398. },
  399. getTime: function(i) {
  400. return i.selectedTime ? i.selectedTime : null;
  401. },
  402. setTime: function(i, time) {
  403. var widget = this;
  404. if (time && time.getMinutes) {
  405. i.selectedTime = time;
  406. i.element.val($.fn.timepicker.formatTime(i.options.timeFormat, time));
  407. // custom change event and change callback
  408. i.element.trigger('time-change', [time]);
  409. if ($.isFunction(i.options.change)) {
  410. i.options.change.apply(i.element, [time]);
  411. }
  412. } else {
  413. i.selectedTime = null;
  414. }
  415. }
  416. };
  417. $.TimePicker.defaults = {
  418. timeFormat: 'hh:mm p',
  419. minHour: null,
  420. minMinutes: null,
  421. minTime: null,
  422. maxHour: null,
  423. maxMinutes: null,
  424. maxTime: null,
  425. startHour: null,
  426. startMinutes: null,
  427. startTime: null,
  428. interval: 30,
  429. dynamic: true,
  430. theme: 'standard',
  431. // callbacks
  432. change: function(time) {}
  433. };
  434. $.fn.timepicker = function(options) {
  435. // TODO: see if it works with previous versions
  436. if ($.fn.jquery < '1.3') {
  437. return this;
  438. }
  439. // Calling the constructor again returns a reference to a TimePicker object.
  440. if (this.length == 1 && this.data('TimePicker')) {
  441. return this.data('TimePicker');
  442. }
  443. var globals = $.extend({}, $.TimePicker.defaults, options);
  444. return this.each(function() {
  445. $.TimePicker.instance().register(this, globals);
  446. });
  447. };
  448. /**
  449. * TODO: documentation
  450. */
  451. $.fn.timepicker.formatTime = function(format, time) {
  452. var hours = time.getHours(),
  453. hours12 = hours % 12,
  454. minutes = time.getMinutes(),
  455. seconds = time.getSeconds(),
  456. replacements = {
  457. hh: pad((hours12 === 0 ? 12 : hours12).toString(), '0', 2),
  458. HH: pad(hours.toString(), '0', 2),
  459. mm: pad(minutes.toString(), '0', 2),
  460. ss: pad(seconds.toString(), '0', 2),
  461. h: (hours12 === 0 ? 12 : hours12),
  462. H: hours,
  463. m: minutes,
  464. s: seconds,
  465. p: hours > 11 ? 'PM' : 'AM'
  466. },
  467. str = format, k = '';
  468. for (k in replacements) {
  469. if (replacements.hasOwnProperty(k)) {
  470. str = str.replace(new RegExp(k,'g'), replacements[k]);
  471. }
  472. }
  473. return str;
  474. };
  475. /**
  476. * Convert a string representing a given time into a Date object.
  477. *
  478. * The Date object will have attributes others than hours, minutes and
  479. * seconds set to current local time values. The function will return
  480. * false if given string can't be converted.
  481. *
  482. * If there is an 'a' in the string we set am to true, if there is a 'p'
  483. * we set pm to true, if both are present only am is setted to true.
  484. *
  485. * All non-digit characters are removed from the string before trying to
  486. * parse the time.
  487. *
  488. * '' can't be converted and the function returns false.
  489. * '1' is converted to 01:00:00 am
  490. * '11' is converted to 11:00:00 am
  491. * '111' is converted to 01:11:00 am
  492. * '1111' is converted to 11:11:00 am
  493. * '11111' is converted to 01:11:11 am
  494. * '111111' is converted to 11:11:11 am
  495. *
  496. * Only the first six (or less) characters are considered.
  497. *
  498. * Special case:
  499. *
  500. * When hours is greater than 24 and the last digit is less or equal than 6, and minutes
  501. * and seconds are less or equal than 60, we append a trailing zero and
  502. * start parsing process again. Examples:
  503. *
  504. * '95' is treated as '950' and converted to 09:50:00 am
  505. * '46' is treated as '460' and converted to 05:00:00 am
  506. * '57' can't be converted and the function returns false.
  507. *
  508. * For a detailed list of supported formats check the unit tests at
  509. * http://github.com/wvega/timepicker/tree/master/tests/
  510. */
  511. $.fn.timepicker.parseTime = (function(str) {
  512. var patterns = [
  513. // 1, 12, 123, 1234, 12345, 123456
  514. [/^(\d+)$/, '$1'],
  515. // :1, :2, :3, :4 ... :9
  516. [/^:(\d)$/, '$10'],
  517. // :1, :12, :123, :1234 ...
  518. [/^:(\d+)/, '$1'],
  519. // 6:06, 5:59, 5:8
  520. [/^(\d):([7-9])$/, '0$10$2'],
  521. [/^(\d):(\d\d)$/, '$1$2'],
  522. [/^(\d):(\d{1,})$/, '0$1$20'],
  523. // 10:8, 10:10, 10:34
  524. [/^(\d\d):([7-9])$/, '$10$2'],
  525. [/^(\d\d):(\d)$/, '$1$20'],
  526. [/^(\d\d):(\d*)$/, '$1$2'],
  527. // 123:4, 1234:456
  528. [/^(\d{3,}):(\d)$/, '$10$2'],
  529. [/^(\d{3,}):(\d{2,})/, '$1$2'],
  530. //
  531. [/^(\d):(\d):(\d)$/, '0$10$20$3'],
  532. [/^(\d{1,2}):(\d):(\d\d)/, '$10$2$3']];
  533. return function(str) {
  534. var time = normalize(new Date()),
  535. am = false, pm = false, h = false, m = false, s = false, k = 0;
  536. str = str.toLowerCase();
  537. am = /a/.test(str);
  538. pm = am ? false : /p/.test(str);
  539. str = str.replace(/[^0-9:]/g, '').replace(/:+/g, ':');
  540. for (k in patterns) {
  541. if (patterns[k][0].test(str)) {
  542. str = str.replace(patterns[k][0], patterns[k][1]);
  543. break;
  544. }
  545. }
  546. str = str.replace(/:/g, '');
  547. if (str.length == 1) {
  548. h = str;
  549. } else if (str.length == 2) {
  550. h = str;
  551. } else if (str.length == 3 || str.length == 5) {
  552. h = str.substr(0, 1);
  553. m = str.substr(1, 2);
  554. s = str.substr(3, 2);
  555. } else if (str.length == 4 || str.length > 5) {
  556. h = str.substr(0, 2);
  557. m = str.substr(2, 2);
  558. s = str.substr(4, 2);
  559. }
  560. if (str.length > 0 && str.length < 5) {
  561. if (str.length < 3) {
  562. m = 0;
  563. }
  564. s = 0;
  565. }
  566. if (h === false || m === false || s === false) {
  567. return false;
  568. }
  569. h = parseInt(h, 10);m = parseInt(m, 10);s = parseInt(s, 10);
  570. if (am && h == 12) {
  571. h = 0;
  572. } else if (pm && h < 12) {
  573. h = h + 12;
  574. }
  575. if (h > 24 && (h % 10) <= 6 && m <= 60 && s <= 60) {
  576. return $.fn.timepicker.parseTime(str + '0' + (am ? 'a' : '') + (pm ? 'p' : ''));
  577. } else if (h <= 24 && m <= 60 && s <= 60) {
  578. time.setHours(h, m, s);
  579. return time;
  580. } else {
  581. return false;
  582. }
  583. };
  584. })();
  585. })(jQuery);
  586. }