clockface.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. /**
  2. * Clockface - v1.0.1
  3. * Clockface timepicker for Twitter Bootstrap
  4. *
  5. * Confusion with noon and midnight:
  6. * http://en.wikipedia.org/wiki/12-hour_clock
  7. * Here considered '00:00 am' as midnight and '12:00 pm' as noon.
  8. *
  9. * Author: Vitaliy Potapov
  10. * Project page: http://github.com/vitalets/clockface
  11. * Copyright (c) 2012 Vitaliy Potapov. Released under MIT License.
  12. **/
  13. (function ($) {
  14. var Clockface = function (element, options) {
  15. this.$element = $(element);
  16. this.options = $.extend({}, $.fn.clockface.defaults, options, this.$element.data());
  17. this.init();
  18. };
  19. Clockface.prototype = {
  20. constructor: Clockface,
  21. init: function () {
  22. //apply template
  23. this.$clockface = $($.fn.clockface.template);
  24. this.$clockface.find('.l1 .cell, .left.cell').html('<div class="outer"></div><div class="inner"></div>');
  25. this.$clockface.find('.l5 .cell, .right.cell').html('<div class="inner"></div><div class="outer"></div>');
  26. this.$clockface.hide();
  27. this.$outer = this.$clockface.find('.outer');
  28. this.$inner = this.$clockface.find('.inner');
  29. this.$ampm = this.$clockface.find('.ampm');
  30. //internal vars
  31. this.ampm = null;
  32. this.hour = null;
  33. this.minute = null;
  34. //click am/pm
  35. this.$ampm.click($.proxy(this.clickAmPm, this));
  36. //click cell
  37. this.$clockface.on('click', '.cell', $.proxy(this.click, this));
  38. this.parseFormat();
  39. this.prepareRegexp();
  40. //set ampm text
  41. this.ampmtext = this.is24 ? {am: '12-23', pm: '0-11'} : {am: 'AM', pm: 'PM'};
  42. this.isInline = this.$element.is('div');
  43. if(this.isInline) {
  44. this.$clockface.addClass('clockface-inline').appendTo(this.$element);
  45. } else {
  46. this.$clockface.addClass('dropdown-menu').appendTo('body');
  47. if(this.options.trigger === 'focus') {
  48. this.$element.on('focus.clockface', $.proxy(function(e) { this.show(); }, this));
  49. }
  50. // Click outside hide it. Register single handler for all clockface widgets
  51. $(document).off('click.clockface').on('click.clockface', $.proxy(function (e) {
  52. var $target = $(e.target);
  53. //click inside some clockface --> do nothing
  54. if ($target.closest('.clockface').length) {
  55. return;
  56. }
  57. //iterate all open clockface and close all except current
  58. $('.clockface-open').each(function(){
  59. if(this === e.target) {
  60. return;
  61. }
  62. $(this).clockface('hide');
  63. });
  64. }, this));
  65. }
  66. //fill minutes once
  67. this.fill('minute');
  68. },
  69. /*
  70. Displays widget with specified value
  71. */
  72. show: function(value) {
  73. if(this.$clockface.is(':visible')) {
  74. return;
  75. }
  76. if(!this.isInline) {
  77. if(value === undefined) {
  78. value = this.$element.val();
  79. }
  80. this.$element.addClass('clockface-open');
  81. this.$element.on('keydown.clockface', $.proxy(this.keydown, this));
  82. this.place();
  83. $(window).on('resize.clockface', $.proxy(this.place, this));
  84. }
  85. this.$clockface.show();
  86. this.setTime(value);
  87. //trigger shown event
  88. this.$element.triggerHandler('shown.clockface', this.getTime(true));
  89. },
  90. /*
  91. hides widget
  92. */
  93. hide: function() {
  94. this.$clockface.hide();
  95. if(!this.isInline) {
  96. this.$element.removeClass('clockface-open');
  97. this.$element.off('keydown.clockface');
  98. $(window).off('resize.clockface');
  99. }
  100. //trigger hidden event
  101. this.$element.triggerHandler('hidden.clockface', this.getTime(true));
  102. },
  103. /*
  104. toggles show/hide
  105. */
  106. toggle: function(value) {
  107. if(this.$clockface.is(':visible')) {
  108. this.hide();
  109. } else {
  110. this.show(value);
  111. }
  112. },
  113. /*
  114. Set time of clockface. Am/pm will be set automatically.
  115. Value can be Date object or string
  116. */
  117. setTime: function(value) {
  118. var res, hour, minute, ampm = 'am';
  119. //no new value
  120. if(value === undefined) {
  121. //if ampm null, it;s first showw, need to render hours ('am' by default)
  122. if(this.ampm === null) {
  123. this.setAmPm('am');
  124. }
  125. return;
  126. }
  127. //take value from Date object
  128. if(value instanceof Date) {
  129. hour = value.getHours();
  130. minute = value.getMinutes();
  131. }
  132. //parse value from string
  133. if(typeof value === 'string' && value.length) {
  134. res = this.parseTime(value);
  135. //'24' always '0'
  136. if(res.hour === 24) {
  137. res.hour = 0;
  138. }
  139. hour = res.hour;
  140. minute = res.minute;
  141. ampm = res.ampm;
  142. }
  143. //try to set ampm automatically
  144. if(hour > 11 && hour < 24) {
  145. ampm = 'pm';
  146. //for 12h format substract 12 from value
  147. if(!this.is24 && hour > 12) {
  148. hour -= 12;
  149. }
  150. } else if(hour >= 0 && hour < 11) {
  151. //always set am for 24h and for '0' in 12h
  152. if(this.is24 || hour === 0) {
  153. ampm = 'am';
  154. }
  155. //otherwise ampm should be defined in value itself and retrieved when parsing
  156. }
  157. this.setAmPm(ampm);
  158. this.setHour(hour);
  159. this.setMinute(minute);
  160. },
  161. /*
  162. Set ampm and re-fill hours
  163. */
  164. setAmPm: function(value) {
  165. if(value === this.ampm) {
  166. return;
  167. } else {
  168. this.ampm = value === 'am' ? 'am' : 'pm';
  169. }
  170. //set link's text
  171. this.$ampm.text(this.ampmtext[this.ampm]);
  172. //re-fill and highlight hour
  173. this.fill('hour');
  174. this.highlight('hour');
  175. },
  176. /*
  177. Sets hour value and highlight if possible
  178. */
  179. setHour: function(value) {
  180. value = parseInt(value, 10);
  181. value = isNaN(value) ? null : value;
  182. if(value < 0 || value > 23) {
  183. value = null;
  184. }
  185. if(value === this.hour) {
  186. return;
  187. } else {
  188. this.hour = value;
  189. }
  190. this.highlight('hour');
  191. },
  192. /*
  193. Sets minute value and highlight
  194. */
  195. setMinute: function(value) {
  196. value = parseInt(value, 10);
  197. value = isNaN(value) ? null : value;
  198. if(value < 0 || value > 59) {
  199. value = null;
  200. }
  201. if(value === this.minute) {
  202. return;
  203. } else {
  204. this.minute = value;
  205. }
  206. this.highlight('minute');
  207. },
  208. /*
  209. Highlights hour/minute
  210. */
  211. highlight: function(what) {
  212. var index,
  213. values = this.getValues(what),
  214. value = what === 'minute' ? this.minute : this.hour,
  215. $cells = what === 'minute' ? this.$outer : this.$inner;
  216. $cells.removeClass('active');
  217. //find index of value and highlight if possible
  218. index = $.inArray(value, values);
  219. if(index >= 0) {
  220. $cells.eq(index).addClass('active');
  221. }
  222. },
  223. /*
  224. Fill values around
  225. */
  226. fill: function(what) {
  227. var values = this.getValues(what),
  228. $cells = what === 'minute' ? this.$outer : this.$inner,
  229. leadZero = what === 'minute';
  230. $cells.each(function(i){
  231. var v = values[i];
  232. if(leadZero && v < 10) {
  233. v = '0' + v;
  234. }
  235. $(this).text(v);
  236. });
  237. },
  238. /*
  239. returns values of hours or minutes, depend on ampm and 24/12 format (0-11, 12-23, 00-55, etc)
  240. param what: 'hour'/'minute'
  241. */
  242. getValues: function(what) {
  243. var values = [11, 0, 1, 10, 2, 9, 3, 8, 4, 7, 6, 5],
  244. result = values.slice();
  245. //minutes
  246. if(what === 'minute') {
  247. $.each(values, function(i, v) { result[i] = v*5; });
  248. } else {
  249. //hours
  250. if(!this.is24) {
  251. result[1] = 12; //need this to show '12' instead of '00' for 12h am/pm
  252. }
  253. if(this.is24 && this.ampm === 'pm') {
  254. $.each(values, function(i, v) { result[i] = v+12; });
  255. }
  256. }
  257. return result;
  258. },
  259. /*
  260. Click cell handler.
  261. Stores hour/minute and highlights.
  262. On second click deselect value
  263. */
  264. click: function(e) {
  265. var $target = $(e.target),
  266. value = $target.hasClass('active') ? null : $target.text();
  267. if($target.hasClass('inner')) {
  268. this.setHour(value);
  269. } else {
  270. this.setMinute(value);
  271. }
  272. //update value in input
  273. if(!this.isInline) {
  274. this.$element.val(this.getTime());
  275. }
  276. //trigger pick event
  277. this.$element.triggerHandler('pick.clockface', this.getTime(true));
  278. },
  279. /*
  280. Click handler on ampm link
  281. */
  282. clickAmPm: function(e) {
  283. e.preventDefault();
  284. //toggle am/pm
  285. this.setAmPm(this.ampm === 'am' ? 'pm' : 'am');
  286. //update value in input
  287. if(!this.isInline && !this.is24) {
  288. this.$element.val(this.getTime());
  289. }
  290. //trigger pick event
  291. this.$element.triggerHandler('pick.clockface', this.getTime(true));
  292. },
  293. /*
  294. Place widget below input
  295. */
  296. place: function(){
  297. var zIndex = parseInt(this.$element.parents().filter(function() {
  298. return $(this).css('z-index') != 'auto';
  299. }).first().css('z-index'), 10)+10,
  300. offset = this.$element.offset();
  301. this.$clockface.css({
  302. top: offset.top + this.$element.outerHeight(),
  303. left: offset.left,
  304. zIndex: zIndex
  305. });
  306. },
  307. /*
  308. keydown handler (for not inline mode)
  309. */
  310. keydown: function(e) {
  311. //tab, escape, enter --> hide
  312. if(/^(9|27|13)$/.test(e.which)) {
  313. this.hide();
  314. return;
  315. }
  316. clearTimeout(this.timer);
  317. this.timer = setTimeout($.proxy(function(){
  318. this.setTime(this.$element.val());
  319. }, this), 500);
  320. },
  321. /*
  322. Parse format from options and set this.is24
  323. */
  324. parseFormat: function() {
  325. var format = this.options.format,
  326. hFormat = 'HH',
  327. mFormat = 'mm';
  328. //hour format
  329. $.each(['HH', 'hh', 'H', 'h'], function(i, f){
  330. if(format.indexOf(f) !== -1) {
  331. hFormat = f;
  332. return false;
  333. }
  334. });
  335. //minute format
  336. $.each(['mm', 'm'], function(i, f){
  337. if(format.indexOf(f) !== -1) {
  338. mFormat = f;
  339. return false;
  340. }
  341. });
  342. //is 24 hour format
  343. this.is24 = hFormat.indexOf('H') !== -1;
  344. this.hFormat = hFormat;
  345. this.mFormat = mFormat;
  346. },
  347. /*
  348. Parse value passed as string or Date object
  349. */
  350. parseTime: function(value) {
  351. var hour = null,
  352. minute = null,
  353. ampm = 'am',
  354. parts = [], digits;
  355. value = $.trim(value);
  356. //try parse time from string assuming separator exist
  357. if(this.regexpSep) {
  358. parts = value.match(this.regexpSep);
  359. }
  360. if(parts && parts.length) {
  361. hour = parts[1] ? parseInt(parts[1], 10) : null;
  362. minute = parts[2] ? parseInt(parts[2], 10): null;
  363. ampm = (!parts[3] || parts[3].toLowerCase() === 'a') ? 'am' : 'pm';
  364. } else {
  365. //if parse with separator failed, search for 1,4-digit block and process it
  366. //use reversed string to start from end (usefull with full dates)
  367. //see http://stackoverflow.com/questions/141348/what-is-the-best-way-to-parse-a-time-into-a-date-object-from-user-input-in-javas
  368. value = value.split('').reverse().join('').replace(/\s/g, '');
  369. parts = value.match(this.regexpNoSep);
  370. if(parts && parts.length) {
  371. ampm = (!parts[1] || parts[1].toLowerCase() === 'a') ? 'am' : 'pm';
  372. //reverse back
  373. digits = parts[2].split('').reverse().join('');
  374. //use smart analyzing to detect hours and minutes
  375. switch(digits.length) {
  376. case 1:
  377. hour = parseInt(digits, 10); //e.g. '6'
  378. break;
  379. case 2:
  380. hour = parseInt(digits, 10); //e.g. '16'
  381. //if((this.is24 && hour > 24) || (!this.is24 && hour > 12)) { //e.g. 26
  382. if(hour > 24) { //e.g. 26
  383. hour = parseInt(digits[0], 10);
  384. minute = parseInt(digits[1], 10);
  385. }
  386. break;
  387. case 3:
  388. hour = parseInt(digits[0], 10); //e.g. 105
  389. minute = parseInt(digits[1]+digits[2], 10);
  390. if(minute > 59) {
  391. hour = parseInt(digits[0]+digits[1], 10); //e.g. 195
  392. minute = parseInt(digits[2], 10);
  393. if(hour > 24) {
  394. hour = null;
  395. minute = null;
  396. }
  397. }
  398. break;
  399. case 4:
  400. hour = parseInt(digits[0]+digits[1], 10); //e.g. 2006
  401. minute = parseInt(digits[2]+digits[3], 10);
  402. if(hour > 24) {
  403. hour = null;
  404. }
  405. if(minute > 59) {
  406. minute = null;
  407. }
  408. }
  409. }
  410. }
  411. return {hour: hour, minute: minute, ampm: ampm};
  412. },
  413. prepareRegexp: function() {
  414. //take separator from format
  415. var sep = this.options.format.match(/h\s*([^hm]?)\s*m/i); //HH-mm, HH:mm
  416. if(sep && sep.length) {
  417. sep = sep[1];
  418. }
  419. //sep can be null for HH, and '' for HHmm
  420. this.separator = sep;
  421. //parse from string
  422. //use reversed string and regexp to parse 2-digit minutes first
  423. //see http://stackoverflow.com/questions/141348/what-is-the-best-way-to-parse-a-time-into-a-date-object-from-user-input-in-javas
  424. //this.regexp = new RegExp('(a|p)?\\s*((\\d\\d?)' + sep + ')?(\\d\\d?)', 'i');
  425. //regexp, used with separator
  426. this.regexpSep = (this.separator && this.separator.length) ? new RegExp('(\\d\\d?)\\s*\\' + this.separator + '\\s*(\\d?\\d?)\\s*(a|p)?', 'i') : null;
  427. //second regexp applied if previous has no result or separator is empty (to reversed string)
  428. this.regexpNoSep = new RegExp('(a|p)?\\s*(\\d{1,4})', 'i');
  429. },
  430. /*
  431. Returns time as string in specified format
  432. */
  433. getTime: function(asObject) {
  434. if(asObject === true) {
  435. return {
  436. hour: this.hour,
  437. minute: this.minute,
  438. ampm: this.ampm
  439. };
  440. }
  441. var hour = this.hour !== null ? this.hour + '' : '',
  442. minute = this.minute !== null ? this.minute + '' : '',
  443. result = this.options.format;
  444. if(!hour.length && !minute.length) {
  445. return '';
  446. }
  447. if(this.hFormat.length > 1 && hour.length === 1) {
  448. hour = '0' + hour;
  449. }
  450. if(this.mFormat.length > 1 && minute.length === 1) {
  451. minute = '0' + minute;
  452. }
  453. //delete separator if no minutes
  454. if(!minute.length && this.separator) {
  455. result = result.replace(this.separator, '');
  456. }
  457. result = result.replace(this.hFormat, hour).replace(this.mFormat, minute);
  458. if(!this.is24) {
  459. if(result.indexOf('A') !== -1) {
  460. result = result.replace('A', this.ampm.toUpperCase());
  461. } else {
  462. result = result.replace('a', this.ampm);
  463. }
  464. }
  465. return result;
  466. },
  467. /*
  468. Removes widget and detach events
  469. */
  470. destroy: function() {
  471. this.hide();
  472. this.$clockface.remove();
  473. if(!this.isInline && this.options.trigger === 'focus') {
  474. this.$element.off('focus.clockface');
  475. }
  476. }
  477. };
  478. $.fn.clockface = function ( option ) {
  479. var d, args = Array.apply(null, arguments);
  480. args.shift();
  481. //getTime returns string (not jQuery onject)
  482. if(option === 'getTime' && this.length && (d = this.eq(0).data('clockface'))) {
  483. return d.getTime.apply(d, args);
  484. }
  485. return this.each(function () {
  486. var $this = $(this),
  487. data = $this.data('clockface'),
  488. options = typeof option == 'object' && option;
  489. if (!data) {
  490. $this.data('clockface', (data = new Clockface(this, options)));
  491. }
  492. if (typeof option == 'string' && typeof data[option] == 'function') {
  493. data[option].apply(data, args);
  494. }
  495. });
  496. };
  497. $.fn.clockface.defaults = {
  498. //see http://momentjs.com/docs/#/displaying/format/
  499. format: 'H:mm',
  500. trigger: 'focus' //focus|manual
  501. };
  502. $.fn.clockface.template = ''+
  503. '<div class="clockface">' +
  504. '<div class="l1">' +
  505. '<div class="cell"></div>' +
  506. '<div class="cell"></div>' +
  507. '<div class="cell"></div>' +
  508. '</div>' +
  509. '<div class="l2">' +
  510. '<div class="cell left"></div>' +
  511. '<div class="cell right"></div>' +
  512. '</div>'+
  513. '<div class="l3">' +
  514. '<div class="cell left"></div>' +
  515. '<div class="cell right"></div>' +
  516. '<div class="center"><a href="#" class="ampm"></a></div>' +
  517. '</div>'+
  518. '<div class="l4">' +
  519. '<div class="cell left"></div>' +
  520. '<div class="cell right"></div>' +
  521. '</div>'+
  522. '<div class="l5">' +
  523. '<div class="cell"></div>' +
  524. '<div class="cell"></div>' +
  525. '<div class="cell"></div>' +
  526. '</div>'+
  527. '</div>';
  528. }(window.jQuery));