timepicker.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. /**
  2. * angular-strap
  3. * @version v2.3.9 - 2016-06-10
  4. * @link http://mgcrea.github.io/angular-strap
  5. * @author Olivier Louvignes <olivier@mg-crea.com> (https://github.com/mgcrea)
  6. * @license MIT License, http://www.opensource.org/licenses/MIT
  7. */
  8. 'use strict';
  9. angular.module('mgcrea.ngStrap.timepicker', [ 'mgcrea.ngStrap.helpers.dateParser', 'mgcrea.ngStrap.helpers.dateFormatter', 'mgcrea.ngStrap.tooltip' ]).provider('$timepicker', function() {
  10. var defaults = this.defaults = {
  11. animation: 'am-fade',
  12. defaultDate: 'auto',
  13. prefixClass: 'timepicker',
  14. placement: 'bottom-left',
  15. templateUrl: 'timepicker/timepicker.tpl.html',
  16. trigger: 'focus',
  17. container: false,
  18. keyboard: true,
  19. html: false,
  20. delay: 0,
  21. useNative: true,
  22. timeType: 'date',
  23. timeFormat: 'shortTime',
  24. timezone: null,
  25. modelTimeFormat: null,
  26. autoclose: false,
  27. minTime: -Infinity,
  28. maxTime: +Infinity,
  29. length: 5,
  30. hourStep: 1,
  31. minuteStep: 5,
  32. secondStep: 5,
  33. roundDisplay: false,
  34. iconUp: 'glyphicon glyphicon-chevron-up',
  35. iconDown: 'glyphicon glyphicon-chevron-down',
  36. arrowBehavior: 'pager'
  37. };
  38. this.$get = [ '$window', '$document', '$rootScope', '$sce', '$dateFormatter', '$tooltip', '$timeout', function($window, $document, $rootScope, $sce, $dateFormatter, $tooltip, $timeout) {
  39. var isNative = /(ip[ao]d|iphone|android)/gi.test($window.navigator.userAgent);
  40. var isTouch = 'createTouch' in $window.document && isNative;
  41. if (!defaults.lang) {
  42. defaults.lang = $dateFormatter.getDefaultLocale();
  43. }
  44. function timepickerFactory(element, controller, config) {
  45. var $timepicker = $tooltip(element, angular.extend({}, defaults, config));
  46. var parentScope = config.scope;
  47. var options = $timepicker.$options;
  48. var scope = $timepicker.$scope;
  49. var lang = options.lang;
  50. var formatDate = function(date, format, timezone) {
  51. return $dateFormatter.formatDate(date, format, lang, timezone);
  52. };
  53. function floorMinutes(time) {
  54. var coeff = 1e3 * 60 * options.minuteStep;
  55. return new Date(Math.floor(time.getTime() / coeff) * coeff);
  56. }
  57. var selectedIndex = 0;
  58. var defaultDate = options.roundDisplay ? floorMinutes(new Date()) : new Date();
  59. var startDate = controller.$dateValue || defaultDate;
  60. var viewDate = {
  61. hour: startDate.getHours(),
  62. meridian: startDate.getHours() < 12,
  63. minute: startDate.getMinutes(),
  64. second: startDate.getSeconds(),
  65. millisecond: startDate.getMilliseconds()
  66. };
  67. var format = $dateFormatter.getDatetimeFormat(options.timeFormat, lang);
  68. var hoursFormat = $dateFormatter.hoursFormat(format);
  69. var timeSeparator = $dateFormatter.timeSeparator(format);
  70. var minutesFormat = $dateFormatter.minutesFormat(format);
  71. var secondsFormat = $dateFormatter.secondsFormat(format);
  72. var showSeconds = $dateFormatter.showSeconds(format);
  73. var showAM = $dateFormatter.showAM(format);
  74. scope.$iconUp = options.iconUp;
  75. scope.$iconDown = options.iconDown;
  76. scope.$select = function(date, index) {
  77. $timepicker.select(date, index);
  78. };
  79. scope.$moveIndex = function(value, index) {
  80. $timepicker.$moveIndex(value, index);
  81. };
  82. scope.$switchMeridian = function(date) {
  83. $timepicker.switchMeridian(date);
  84. };
  85. $timepicker.update = function(date) {
  86. if (angular.isDate(date) && !isNaN(date.getTime())) {
  87. $timepicker.$date = date;
  88. angular.extend(viewDate, {
  89. hour: date.getHours(),
  90. minute: date.getMinutes(),
  91. second: date.getSeconds(),
  92. millisecond: date.getMilliseconds()
  93. });
  94. $timepicker.$build();
  95. } else if (!$timepicker.$isBuilt) {
  96. $timepicker.$build();
  97. }
  98. };
  99. $timepicker.select = function(date, index, keep) {
  100. if (!controller.$dateValue || isNaN(controller.$dateValue.getTime())) {
  101. controller.$dateValue = options.defaultDate === 'today' ? new Date() : new Date(1970, 0, 1);
  102. }
  103. if (!angular.isDate(date)) date = new Date(date);
  104. if (index === 0) controller.$dateValue.setHours(date.getHours()); else if (index === 1) controller.$dateValue.setMinutes(date.getMinutes()); else if (index === 2) controller.$dateValue.setSeconds(date.getSeconds());
  105. controller.$setViewValue(angular.copy(controller.$dateValue));
  106. controller.$render();
  107. if (options.autoclose && !keep) {
  108. $timeout(function() {
  109. $timepicker.hide(true);
  110. });
  111. }
  112. };
  113. $timepicker.switchMeridian = function(date) {
  114. if (!controller.$dateValue || isNaN(controller.$dateValue.getTime())) {
  115. return;
  116. }
  117. var hours = (date || controller.$dateValue).getHours();
  118. controller.$dateValue.setHours(hours < 12 ? hours + 12 : hours - 12);
  119. controller.$setViewValue(angular.copy(controller.$dateValue));
  120. controller.$render();
  121. };
  122. $timepicker.$build = function() {
  123. var i;
  124. var midIndex = scope.midIndex = parseInt(options.length / 2, 10);
  125. var hours = [];
  126. var hour;
  127. for (i = 0; i < options.length; i++) {
  128. hour = new Date(1970, 0, 1, viewDate.hour - (midIndex - i) * options.hourStep);
  129. hours.push({
  130. date: hour,
  131. label: formatDate(hour, hoursFormat),
  132. selected: $timepicker.$date && $timepicker.$isSelected(hour, 0),
  133. disabled: $timepicker.$isDisabled(hour, 0)
  134. });
  135. }
  136. var minutes = [];
  137. var minute;
  138. for (i = 0; i < options.length; i++) {
  139. minute = new Date(1970, 0, 1, 0, viewDate.minute - (midIndex - i) * options.minuteStep);
  140. minutes.push({
  141. date: minute,
  142. label: formatDate(minute, minutesFormat),
  143. selected: $timepicker.$date && $timepicker.$isSelected(minute, 1),
  144. disabled: $timepicker.$isDisabled(minute, 1)
  145. });
  146. }
  147. var seconds = [];
  148. var second;
  149. for (i = 0; i < options.length; i++) {
  150. second = new Date(1970, 0, 1, 0, 0, viewDate.second - (midIndex - i) * options.secondStep);
  151. seconds.push({
  152. date: second,
  153. label: formatDate(second, secondsFormat),
  154. selected: $timepicker.$date && $timepicker.$isSelected(second, 2),
  155. disabled: $timepicker.$isDisabled(second, 2)
  156. });
  157. }
  158. var rows = [];
  159. for (i = 0; i < options.length; i++) {
  160. if (showSeconds) {
  161. rows.push([ hours[i], minutes[i], seconds[i] ]);
  162. } else {
  163. rows.push([ hours[i], minutes[i] ]);
  164. }
  165. }
  166. scope.rows = rows;
  167. scope.showSeconds = showSeconds;
  168. scope.showAM = showAM;
  169. scope.isAM = ($timepicker.$date || hours[midIndex].date).getHours() < 12;
  170. scope.timeSeparator = timeSeparator;
  171. $timepicker.$isBuilt = true;
  172. };
  173. $timepicker.$isSelected = function(date, index) {
  174. if (!$timepicker.$date) return false; else if (index === 0) {
  175. return date.getHours() === $timepicker.$date.getHours();
  176. } else if (index === 1) {
  177. return date.getMinutes() === $timepicker.$date.getMinutes();
  178. } else if (index === 2) {
  179. return date.getSeconds() === $timepicker.$date.getSeconds();
  180. }
  181. };
  182. $timepicker.$isDisabled = function(date, index) {
  183. var selectedTime;
  184. if (index === 0) {
  185. selectedTime = date.getTime() + viewDate.minute * 6e4 + viewDate.second * 1e3;
  186. } else if (index === 1) {
  187. selectedTime = date.getTime() + viewDate.hour * 36e5 + viewDate.second * 1e3;
  188. } else if (index === 2) {
  189. selectedTime = date.getTime() + viewDate.hour * 36e5 + viewDate.minute * 6e4;
  190. }
  191. return selectedTime < options.minTime * 1 || selectedTime > options.maxTime * 1;
  192. };
  193. scope.$arrowAction = function(value, index) {
  194. if (options.arrowBehavior === 'picker') {
  195. $timepicker.$setTimeByStep(value, index);
  196. } else {
  197. $timepicker.$moveIndex(value, index);
  198. }
  199. };
  200. $timepicker.$setTimeByStep = function(value, index) {
  201. var newDate = new Date($timepicker.$date || startDate);
  202. var hours = newDate.getHours();
  203. var minutes = newDate.getMinutes();
  204. var seconds = newDate.getSeconds();
  205. if (index === 0) {
  206. newDate.setHours(hours - parseInt(options.hourStep, 10) * value);
  207. } else if (index === 1) {
  208. newDate.setMinutes(minutes - parseInt(options.minuteStep, 10) * value);
  209. } else if (index === 2) {
  210. newDate.setSeconds(seconds - parseInt(options.secondStep, 10) * value);
  211. }
  212. $timepicker.select(newDate, index, true);
  213. };
  214. $timepicker.$moveIndex = function(value, index) {
  215. var targetDate;
  216. if (index === 0) {
  217. targetDate = new Date(1970, 0, 1, viewDate.hour + value * options.length, viewDate.minute, viewDate.second);
  218. angular.extend(viewDate, {
  219. hour: targetDate.getHours()
  220. });
  221. } else if (index === 1) {
  222. targetDate = new Date(1970, 0, 1, viewDate.hour, viewDate.minute + value * options.length * options.minuteStep, viewDate.second);
  223. angular.extend(viewDate, {
  224. minute: targetDate.getMinutes()
  225. });
  226. } else if (index === 2) {
  227. targetDate = new Date(1970, 0, 1, viewDate.hour, viewDate.minute, viewDate.second + value * options.length * options.secondStep);
  228. angular.extend(viewDate, {
  229. second: targetDate.getSeconds()
  230. });
  231. }
  232. $timepicker.$build();
  233. };
  234. $timepicker.$onMouseDown = function(evt) {
  235. if (evt.target.nodeName.toLowerCase() !== 'input') evt.preventDefault();
  236. evt.stopPropagation();
  237. if (isTouch) {
  238. var targetEl = angular.element(evt.target);
  239. if (targetEl[0].nodeName.toLowerCase() !== 'button') {
  240. targetEl = targetEl.parent();
  241. }
  242. targetEl.triggerHandler('click');
  243. }
  244. };
  245. $timepicker.$onKeyDown = function(evt) {
  246. if (!/(38|37|39|40|13)/.test(evt.keyCode) || evt.shiftKey || evt.altKey) return;
  247. evt.preventDefault();
  248. evt.stopPropagation();
  249. if (evt.keyCode === 13) {
  250. $timepicker.hide(true);
  251. return;
  252. }
  253. var newDate = new Date($timepicker.$date);
  254. var hours = newDate.getHours();
  255. var hoursLength = formatDate(newDate, hoursFormat).length;
  256. var minutes = newDate.getMinutes();
  257. var minutesLength = formatDate(newDate, minutesFormat).length;
  258. var seconds = newDate.getSeconds();
  259. var secondsLength = formatDate(newDate, secondsFormat).length;
  260. var sepLength = 1;
  261. var lateralMove = /(37|39)/.test(evt.keyCode);
  262. var count = 2 + showSeconds * 1 + showAM * 1;
  263. if (lateralMove) {
  264. if (evt.keyCode === 37) selectedIndex = selectedIndex < 1 ? count - 1 : selectedIndex - 1; else if (evt.keyCode === 39) selectedIndex = selectedIndex < count - 1 ? selectedIndex + 1 : 0;
  265. }
  266. var selectRange = [ 0, hoursLength ];
  267. var incr = 0;
  268. if (evt.keyCode === 38) incr = -1;
  269. if (evt.keyCode === 40) incr = +1;
  270. var isSeconds = selectedIndex === 2 && showSeconds;
  271. var isMeridian = selectedIndex === 2 && !showSeconds || selectedIndex === 3 && showSeconds;
  272. if (selectedIndex === 0) {
  273. newDate.setHours(hours + incr * parseInt(options.hourStep, 10));
  274. hoursLength = formatDate(newDate, hoursFormat).length;
  275. selectRange = [ 0, hoursLength ];
  276. } else if (selectedIndex === 1) {
  277. newDate.setMinutes(minutes + incr * parseInt(options.minuteStep, 10));
  278. minutesLength = formatDate(newDate, minutesFormat).length;
  279. selectRange = [ hoursLength + sepLength, minutesLength ];
  280. } else if (isSeconds) {
  281. newDate.setSeconds(seconds + incr * parseInt(options.secondStep, 10));
  282. secondsLength = formatDate(newDate, secondsFormat).length;
  283. selectRange = [ hoursLength + sepLength + minutesLength + sepLength, secondsLength ];
  284. } else if (isMeridian) {
  285. if (!lateralMove) $timepicker.switchMeridian();
  286. selectRange = [ hoursLength + sepLength + minutesLength + sepLength + (secondsLength + sepLength) * showSeconds, 2 ];
  287. }
  288. $timepicker.select(newDate, selectedIndex, true);
  289. createSelection(selectRange[0], selectRange[1]);
  290. parentScope.$digest();
  291. };
  292. function createSelection(start, length) {
  293. var end = start + length;
  294. if (element[0].createTextRange) {
  295. var selRange = element[0].createTextRange();
  296. selRange.collapse(true);
  297. selRange.moveStart('character', start);
  298. selRange.moveEnd('character', end);
  299. selRange.select();
  300. } else if (element[0].setSelectionRange) {
  301. element[0].setSelectionRange(start, end);
  302. } else if (angular.isUndefined(element[0].selectionStart)) {
  303. element[0].selectionStart = start;
  304. element[0].selectionEnd = end;
  305. }
  306. }
  307. function focusElement() {
  308. element[0].focus();
  309. }
  310. var _init = $timepicker.init;
  311. $timepicker.init = function() {
  312. if (isNative && options.useNative) {
  313. element.prop('type', 'time');
  314. element.css('-webkit-appearance', 'textfield');
  315. return;
  316. } else if (isTouch) {
  317. element.prop('type', 'text');
  318. element.attr('readonly', 'true');
  319. element.on('click', focusElement);
  320. }
  321. _init();
  322. };
  323. var _destroy = $timepicker.destroy;
  324. $timepicker.destroy = function() {
  325. if (isNative && options.useNative) {
  326. element.off('click', focusElement);
  327. }
  328. _destroy();
  329. };
  330. var _show = $timepicker.show;
  331. $timepicker.show = function() {
  332. if (!isTouch && element.attr('readonly') || element.attr('disabled')) return;
  333. _show();
  334. $timeout(function() {
  335. if ($timepicker.$element) $timepicker.$element.on(isTouch ? 'touchstart' : 'mousedown', $timepicker.$onMouseDown);
  336. if (options.keyboard) {
  337. if (element) element.on('keydown', $timepicker.$onKeyDown);
  338. }
  339. }, 0, false);
  340. };
  341. var _hide = $timepicker.hide;
  342. $timepicker.hide = function(blur) {
  343. if (!$timepicker.$isShown) return;
  344. if ($timepicker.$element) $timepicker.$element.off(isTouch ? 'touchstart' : 'mousedown', $timepicker.$onMouseDown);
  345. if (options.keyboard) {
  346. if (element) element.off('keydown', $timepicker.$onKeyDown);
  347. }
  348. _hide(blur);
  349. };
  350. return $timepicker;
  351. }
  352. timepickerFactory.defaults = defaults;
  353. return timepickerFactory;
  354. } ];
  355. }).directive('bsTimepicker', [ '$window', '$parse', '$q', '$dateFormatter', '$dateParser', '$timepicker', function($window, $parse, $q, $dateFormatter, $dateParser, $timepicker) {
  356. var defaults = $timepicker.defaults;
  357. var isNative = /(ip[ao]d|iphone|android)/gi.test($window.navigator.userAgent);
  358. return {
  359. restrict: 'EAC',
  360. require: 'ngModel',
  361. link: function postLink(scope, element, attr, controller) {
  362. var options = {
  363. scope: scope
  364. };
  365. angular.forEach([ 'template', 'templateUrl', 'controller', 'controllerAs', 'placement', 'container', 'delay', 'trigger', 'keyboard', 'html', 'animation', 'autoclose', 'timeType', 'timeFormat', 'timezone', 'modelTimeFormat', 'useNative', 'hourStep', 'minuteStep', 'secondStep', 'length', 'arrowBehavior', 'iconUp', 'iconDown', 'roundDisplay', 'id', 'prefixClass', 'prefixEvent', 'defaultDate' ], function(key) {
  366. if (angular.isDefined(attr[key])) options[key] = attr[key];
  367. });
  368. var falseValueRegExp = /^(false|0|)$/i;
  369. angular.forEach([ 'html', 'container', 'autoclose', 'useNative', 'roundDisplay' ], function(key) {
  370. if (angular.isDefined(attr[key]) && falseValueRegExp.test(attr[key])) {
  371. options[key] = false;
  372. }
  373. });
  374. angular.forEach([ 'onBeforeShow', 'onShow', 'onBeforeHide', 'onHide' ], function(key) {
  375. var bsKey = 'bs' + key.charAt(0).toUpperCase() + key.slice(1);
  376. if (angular.isDefined(attr[bsKey])) {
  377. options[key] = scope.$eval(attr[bsKey]);
  378. }
  379. });
  380. if (isNative && (options.useNative || defaults.useNative)) options.timeFormat = 'HH:mm';
  381. var timepicker = $timepicker(element, controller, options);
  382. options = timepicker.$options;
  383. var lang = options.lang;
  384. var formatDate = function(date, format, timezone) {
  385. return $dateFormatter.formatDate(date, format, lang, timezone);
  386. };
  387. if (attr.bsShow) {
  388. scope.$watch(attr.bsShow, function(newValue, oldValue) {
  389. if (!timepicker || !angular.isDefined(newValue)) return;
  390. if (angular.isString(newValue)) newValue = !!newValue.match(/true|,?(timepicker),?/i);
  391. if (newValue === true) {
  392. timepicker.show();
  393. } else {
  394. timepicker.hide();
  395. }
  396. });
  397. }
  398. var dateParser = $dateParser({
  399. format: options.timeFormat,
  400. lang: lang
  401. });
  402. angular.forEach([ 'minTime', 'maxTime' ], function(key) {
  403. if (angular.isDefined(attr[key])) {
  404. attr.$observe(key, function(newValue) {
  405. timepicker.$options[key] = dateParser.getTimeForAttribute(key, newValue);
  406. if (!isNaN(timepicker.$options[key])) timepicker.$build();
  407. validateAgainstMinMaxTime(controller.$dateValue);
  408. });
  409. }
  410. });
  411. scope.$watch(attr.ngModel, function(newValue, oldValue) {
  412. timepicker.update(controller.$dateValue);
  413. }, true);
  414. function validateAgainstMinMaxTime(parsedTime) {
  415. if (!angular.isDate(parsedTime)) return;
  416. var isMinValid = isNaN(options.minTime) || new Date(parsedTime.getTime()).setFullYear(1970, 0, 1) >= options.minTime;
  417. var isMaxValid = isNaN(options.maxTime) || new Date(parsedTime.getTime()).setFullYear(1970, 0, 1) <= options.maxTime;
  418. var isValid = isMinValid && isMaxValid;
  419. controller.$setValidity('date', isValid);
  420. controller.$setValidity('min', isMinValid);
  421. controller.$setValidity('max', isMaxValid);
  422. if (!isValid) {
  423. return;
  424. }
  425. controller.$dateValue = parsedTime;
  426. }
  427. controller.$parsers.unshift(function(viewValue) {
  428. var date;
  429. if (!viewValue) {
  430. controller.$setValidity('date', true);
  431. return null;
  432. }
  433. var parsedTime = angular.isDate(viewValue) ? viewValue : dateParser.parse(viewValue, controller.$dateValue);
  434. if (!parsedTime || isNaN(parsedTime.getTime())) {
  435. controller.$setValidity('date', false);
  436. return undefined;
  437. }
  438. validateAgainstMinMaxTime(parsedTime);
  439. if (options.timeType === 'string') {
  440. date = dateParser.timezoneOffsetAdjust(parsedTime, options.timezone, true);
  441. return formatDate(date, options.modelTimeFormat || options.timeFormat);
  442. }
  443. date = dateParser.timezoneOffsetAdjust(controller.$dateValue, options.timezone, true);
  444. if (options.timeType === 'number') {
  445. return date.getTime();
  446. } else if (options.timeType === 'unix') {
  447. return date.getTime() / 1e3;
  448. } else if (options.timeType === 'iso') {
  449. return date.toISOString();
  450. }
  451. return new Date(date);
  452. });
  453. controller.$formatters.push(function(modelValue) {
  454. var date;
  455. if (angular.isUndefined(modelValue) || modelValue === null) {
  456. date = NaN;
  457. } else if (angular.isDate(modelValue)) {
  458. date = modelValue;
  459. } else if (options.timeType === 'string') {
  460. date = dateParser.parse(modelValue, null, options.modelTimeFormat);
  461. } else if (options.timeType === 'unix') {
  462. date = new Date(modelValue * 1e3);
  463. } else {
  464. date = new Date(modelValue);
  465. }
  466. controller.$dateValue = dateParser.timezoneOffsetAdjust(date, options.timezone);
  467. return getTimeFormattedString();
  468. });
  469. controller.$render = function() {
  470. element.val(getTimeFormattedString());
  471. };
  472. function getTimeFormattedString() {
  473. return !controller.$dateValue || isNaN(controller.$dateValue.getTime()) ? '' : formatDate(controller.$dateValue, options.timeFormat);
  474. }
  475. scope.$on('$destroy', function() {
  476. if (timepicker) timepicker.destroy();
  477. options = null;
  478. timepicker = null;
  479. });
  480. }
  481. };
  482. } ]);