timepicker.js 20 KB

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