From 9bc5207b2c650f5bba81a7bbf8c22e0d9778d58c Mon Sep 17 00:00:00 2001 From: tbekos Date: Fri, 26 Apr 2013 16:44:45 +0300 Subject: [PATCH] feat(timepicker): add timepicker directive --- src/timepicker/docs/demo.html | 12 + src/timepicker/docs/demo.js | 27 ++ src/timepicker/docs/readme.md | 33 ++ src/timepicker/test/timepicker.spec.js | 621 +++++++++++++++++++++++++ src/timepicker/timepicker.js | 217 +++++++++ template/timepicker/timepicker.html | 20 + 6 files changed, 930 insertions(+) create mode 100644 src/timepicker/docs/demo.html create mode 100644 src/timepicker/docs/demo.js create mode 100644 src/timepicker/docs/readme.md create mode 100644 src/timepicker/test/timepicker.spec.js create mode 100644 src/timepicker/timepicker.js create mode 100644 template/timepicker/timepicker.html diff --git a/src/timepicker/docs/demo.html b/src/timepicker/docs/demo.html new file mode 100644 index 0000000000..172e4b6888 --- /dev/null +++ b/src/timepicker/docs/demo.html @@ -0,0 +1,12 @@ +
+ + +
Time is: {{mytime | date:'shortTime' }}
+ +
Hours step is:
+
Minutes step is:
+ + + + +
\ No newline at end of file diff --git a/src/timepicker/docs/demo.js b/src/timepicker/docs/demo.js new file mode 100644 index 0000000000..1c456b855c --- /dev/null +++ b/src/timepicker/docs/demo.js @@ -0,0 +1,27 @@ +var TimepickerDemoCtrl = function ($scope) { + $scope.mytime = new Date(); + + $scope.hstep = 1; + $scope.mstep = 15; + + $scope.options = { + hstep: [1, 2, 3], + mstep: [1, 5, 10, 15, 25, 30] + }; + + $scope.ismeridian = true; + $scope.toggleMode = function() { + $scope.ismeridian = ! $scope.ismeridian; + }; + + $scope.update = function() { + var d = new Date(); + d.setHours( 14 ); + d.setMinutes( 0 ); + $scope.mytime = d; + }; + + $scope.clear = function() { + $scope.mytime = null; + }; +}; diff --git a/src/timepicker/docs/readme.md b/src/timepicker/docs/readme.md new file mode 100644 index 0000000000..3ed75a6abb --- /dev/null +++ b/src/timepicker/docs/readme.md @@ -0,0 +1,33 @@ +A lightweight & configurable timepicker directive. + +### Settings ### + +All settings can be provided as attributes in the `` or globally configured through the `timepickerConfig`. + + * `ng-model` + : + The Date object that provides the time state. + + * `hour-step` + _(Defaults: 1)_ : + Number of hours to increase or decrease when using a button. + + * `minute-step` + _(Defaults: 1)_ : + Number of minutes to increase or decrease when using a button. + + * `show-meridian` + _(Defaults: true)_ : + Whether to display 12H or 24H mode. + + * `meridians` + _(Defaults: ['AM', 'PM'])_ : + Meridian labels + + * `readonly-input` + _(Defaults: false)_ : + Whether user can type inside the hours & minutes input. + + * `mousewheel` + _(Defaults: true)_ : + Whether user can scroll inside the hours & minutes input to increase or decrease it's values. diff --git a/src/timepicker/test/timepicker.spec.js b/src/timepicker/test/timepicker.spec.js new file mode 100644 index 0000000000..bfa6114529 --- /dev/null +++ b/src/timepicker/test/timepicker.spec.js @@ -0,0 +1,621 @@ +describe('timepicker directive', function () { + var $rootScope, element; + + beforeEach(module('ui.bootstrap.timepicker')); + beforeEach(module('template/timepicker/timepicker.html')); + beforeEach(inject(function(_$compile_, _$rootScope_) { + $compile = _$compile_; + $rootScope = _$rootScope_; + $rootScope.time = newTime(14, 40); + + element = $compile('')($rootScope); + $rootScope.$digest(); + })); + + function newTime(hours, minutes) { + var time = new Date(); + time.setHours(hours); + time.setMinutes(minutes); + return time; + } + + function getTimeState(withoutMeridian) { + var inputs = element.find('input'); + + var state = []; + for (var i = 0; i < 2; i ++) { + state.push(inputs.eq(i).val()); + } + if ( withoutMeridian !== true ) { + state.push( getMeridianButton().text() ); + } + return state; + } + + function getModelState() { + return [ $rootScope.time.getHours(), $rootScope.time.getMinutes() ]; + } + + function getArrow(isUp, tdIndex) { + return element.find('tr').eq( (isUp) ? 0 : 2 ).find('td').eq( tdIndex ).find('a').eq(0); + } + + function getHoursButton(isUp) { + return getArrow(isUp, 0); + } + + function getMinutesButton(isUp) { + return getArrow(isUp, 2); + } + + function getMeridianButton() { + return element.find('button').eq(0); + } + + function doClick(button, n) { + for (var i = 0, max = n || 1; i < max; i++) { + button.click(); + $rootScope.$digest(); + } + } + + function wheelThatMouse(delta) { + var e = $.Event('mousewheel'); + e.wheelDelta = delta; + return e; + } + + it('contains three row & three input elements', function() { + expect(element.find('tr').length).toBe(3); + expect(element.find('input').length).toBe(2); + expect(element.find('button').length).toBe(1); + }); + + it('has initially the correct time & meridian', function() { + expect(getTimeState()).toEqual(['02', '40', 'PM']); + expect(getModelState()).toEqual([14, 40]); + }); + + it('changes inputs when model changes value', function() { + $rootScope.time = newTime(11, 50); + $rootScope.$digest(); + expect(getTimeState()).toEqual(['11', '50', 'AM']); + expect(getModelState()).toEqual([11, 50]); + + $rootScope.time = newTime(16, 40); + $rootScope.$digest(); + expect(getTimeState()).toEqual(['04', '40', 'PM']); + expect(getModelState()).toEqual([16, 40]); + }); + + it('increases / decreases hours when arrows are clicked', function() { + var up = getHoursButton(true); + var down = getHoursButton(false); + + doClick(up); + expect(getTimeState()).toEqual(['03', '40', 'PM']); + expect(getModelState()).toEqual([15, 40]); + + doClick(down); + expect(getTimeState()).toEqual(['02', '40', 'PM']); + expect(getModelState()).toEqual([14, 40]); + + doClick(down); + expect(getTimeState()).toEqual(['01', '40', 'PM']); + expect(getModelState()).toEqual([13, 40]); + }); + + it('increase / decreases minutes by default step when arrows are clicked', function() { + var up = getMinutesButton(true); + var down = getMinutesButton(false); + + doClick(up); + expect(getTimeState()).toEqual(['02', '41', 'PM']); + expect(getModelState()).toEqual([14, 41]); + + doClick(down); + expect(getTimeState()).toEqual(['02', '40', 'PM']); + expect(getModelState()).toEqual([14, 40]); + + doClick(down); + expect(getTimeState()).toEqual(['02', '39', 'PM']); + expect(getModelState()).toEqual([14, 39]); + }); + + it('toggles meridian when arrows are clicked', function() { + var button = getMeridianButton(); + + doClick(button); + expect(getTimeState()).toEqual(['02', '40', 'AM']); + expect(getModelState()).toEqual([2, 40]); + + doClick(button); + expect(getTimeState()).toEqual(['02', '40', 'PM']); + expect(getModelState()).toEqual([14, 40]); + + doClick(button); + expect(getTimeState()).toEqual(['02', '40', 'AM']); + expect(getModelState()).toEqual([2, 40]); + }); + + it('has minutes "connected" to hours', function() { + var up = getMinutesButton(true); + var down = getMinutesButton(false); + + doClick(up, 10); + expect(getTimeState()).toEqual(['02', '50', 'PM']); + expect(getModelState()).toEqual([14, 50]); + + doClick(up, 10); + expect(getTimeState()).toEqual(['03', '00', 'PM']); + expect(getModelState()).toEqual([15, 0]); + + doClick(up, 10); + $rootScope.$digest(); + expect(getTimeState()).toEqual(['03', '10', 'PM']); + expect(getModelState()).toEqual([15, 10]); + + doClick(down, 10); + $rootScope.$digest(); + expect(getTimeState()).toEqual(['03', '00', 'PM']); + expect(getModelState()).toEqual([15, 0]); + + doClick(down, 10); + $rootScope.$digest(); + expect(getTimeState()).toEqual(['02', '50', 'PM']); + expect(getModelState()).toEqual([14, 50]); + }); + + it('has hours "connected" to meridian', function() { + var up = getHoursButton(true); + var down = getHoursButton(false); + + // AM -> PM + $rootScope.time = newTime(11, 0); + $rootScope.$digest(); + expect(getTimeState()).toEqual(['11', '00', 'AM']); + expect(getModelState()).toEqual([11, 0]); + + doClick(up); + expect(getTimeState()).toEqual(['12', '00', 'PM']); + expect(getModelState()).toEqual([12, 0]); + + doClick(up); + expect(getTimeState()).toEqual(['01', '00', 'PM']); + expect(getModelState()).toEqual([13, 0]); + + doClick(down); + expect(getTimeState()).toEqual(['12', '00', 'PM']); + expect(getModelState()).toEqual([12, 0]); + + doClick(down); + expect(getTimeState()).toEqual(['11', '00', 'AM']); + expect(getModelState()).toEqual([11, 0]); + + // PM -> AM + $rootScope.time = newTime(23, 0); + $rootScope.$digest(); + expect(getTimeState()).toEqual(['11', '00', 'PM']); + expect(getModelState()).toEqual([23, 0]); + + doClick(up); + expect(getTimeState()).toEqual(['12', '00', 'AM']); + expect(getModelState()).toEqual([0, 0]); + + doClick(up); + expect(getTimeState()).toEqual(['01', '00', 'AM']); + expect(getModelState()).toEqual([01, 0]); + + doClick(down); + expect(getTimeState()).toEqual(['12', '00', 'AM']); + expect(getModelState()).toEqual([0, 0]); + + doClick(down); + expect(getTimeState()).toEqual(['11', '00', 'PM']); + expect(getModelState()).toEqual([23, 0]); + }); + + it('changes only the time part', function() { + $rootScope.time = newTime(23, 50); + $rootScope.$digest(); + + var date = $rootScope.time.getDate(); + var up = getHoursButton(true); + doClick(up); + + expect(getTimeState()).toEqual(['12', '50', 'AM']); + expect(getModelState()).toEqual([0, 50]); + expect(date).toEqual($rootScope.time.getDate()); + }); + + it('responds properly on "mousewheel" events', function() { + var inputs = element.find('input'); + var hoursEl = inputs.eq(0), minutesEl = inputs.eq(1); + var upMouseWheelEvent = wheelThatMouse(1); + var downMouseWheelEvent = wheelThatMouse(-1); + + expect(getTimeState()).toEqual(['02', '40', 'PM']); + expect(getModelState()).toEqual([14, 40]); + + // UP + hoursEl.trigger( upMouseWheelEvent ); + $rootScope.$digest(); + expect(getTimeState()).toEqual(['03', '40', 'PM']); + expect(getModelState()).toEqual([15, 40]); + + hoursEl.trigger( upMouseWheelEvent ); + $rootScope.$digest(); + expect(getTimeState()).toEqual(['04', '40', 'PM']); + expect(getModelState()).toEqual([16, 40]); + + minutesEl.trigger( upMouseWheelEvent ); + $rootScope.$digest(); + expect(getTimeState()).toEqual(['04', '41', 'PM']); + expect(getModelState()).toEqual([16, 41]); + + minutesEl.trigger( upMouseWheelEvent ); + $rootScope.$digest(); + expect(getTimeState()).toEqual(['04', '42', 'PM']); + expect(getModelState()).toEqual([16, 42]); + + // DOWN + minutesEl.trigger( downMouseWheelEvent ); + $rootScope.$digest(); + expect(getTimeState()).toEqual(['04', '41', 'PM']); + expect(getModelState()).toEqual([16, 41]); + + minutesEl.trigger( downMouseWheelEvent ); + $rootScope.$digest(); + expect(getTimeState()).toEqual(['04', '40', 'PM']); + expect(getModelState()).toEqual([16, 40]); + + hoursEl.trigger( downMouseWheelEvent ); + $rootScope.$digest(); + expect(getTimeState()).toEqual(['03', '40', 'PM']); + expect(getModelState()).toEqual([15, 40]); + + hoursEl.trigger( downMouseWheelEvent ); + $rootScope.$digest(); + expect(getTimeState()).toEqual(['02', '40', 'PM']); + expect(getModelState()).toEqual([14, 40]); + }); + + describe('attributes', function () { + beforeEach(function() { + $rootScope.hstep = 2; + $rootScope.mstep = 30; + $rootScope.time = newTime(14, 0); + element = $compile('')($rootScope); + $rootScope.$digest(); + }); + + it('increases / decreases hours by configurable step', function() { + var up = getHoursButton(true); + var down = getHoursButton(false); + + expect(getTimeState()).toEqual(['02', '00', 'PM']); + expect(getModelState()).toEqual([14, 0]); + + doClick(up); + expect(getTimeState()).toEqual(['04', '00', 'PM']); + expect(getModelState()).toEqual([16, 0]); + + doClick(down); + expect(getTimeState()).toEqual(['02', '00', 'PM']); + expect(getModelState()).toEqual([14, 0]); + + doClick(down); + expect(getTimeState()).toEqual(['12', '00', 'PM']); + expect(getModelState()).toEqual([12, 0]); + + // Change step + $rootScope.hstep = 3; + $rootScope.$digest(); + + doClick(up); + expect(getTimeState()).toEqual(['03', '00', 'PM']); + expect(getModelState()).toEqual([15, 0]); + + doClick(down); + expect(getTimeState()).toEqual(['12', '00', 'PM']); + expect(getModelState()).toEqual([12, 0]); + }); + + it('increases / decreases minutes by configurable step', function() { + var up = getMinutesButton(true); + var down = getMinutesButton(false); + + doClick(up); + expect(getTimeState()).toEqual(['02', '30', 'PM']); + expect(getModelState()).toEqual([14, 30]); + + doClick(up); + expect(getTimeState()).toEqual(['03', '00', 'PM']); + expect(getModelState()).toEqual([15, 0]); + + doClick(down); + expect(getTimeState()).toEqual(['02', '30', 'PM']); + expect(getModelState()).toEqual([14, 30]); + + doClick(down); + expect(getTimeState()).toEqual(['02', '00', 'PM']); + expect(getModelState()).toEqual([14, 0]); + + // Change step + $rootScope.mstep = 15; + $rootScope.$digest(); + + doClick(up); + expect(getTimeState()).toEqual(['02', '15', 'PM']); + expect(getModelState()).toEqual([14, 15]); + + doClick(down); + expect(getTimeState()).toEqual(['02', '00', 'PM']); + expect(getModelState()).toEqual([14, 0]); + + doClick(down); + expect(getTimeState()).toEqual(['01', '45', 'PM']); + expect(getModelState()).toEqual([13, 45]); + }); + + it('responds properly on "mousewheel" events with configurable steps', function() { + var inputs = element.find('input'); + var hoursEl = inputs.eq(0), minutesEl = inputs.eq(1); + var upMouseWheelEvent = wheelThatMouse(1); + var downMouseWheelEvent = wheelThatMouse(-1); + + expect(getTimeState()).toEqual(['02', '00', 'PM']); + expect(getModelState()).toEqual([14, 0]); + + // UP + hoursEl.trigger( upMouseWheelEvent ); + $rootScope.$digest(); + expect(getTimeState()).toEqual(['04', '00', 'PM']); + expect(getModelState()).toEqual([16, 0]); + + minutesEl.trigger( upMouseWheelEvent ); + $rootScope.$digest(); + expect(getTimeState()).toEqual(['04', '30', 'PM']); + expect(getModelState()).toEqual([16, 30]); + + // DOWN + minutesEl.trigger( downMouseWheelEvent ); + $rootScope.$digest(); + expect(getTimeState()).toEqual(['04', '00', 'PM']); + expect(getModelState()).toEqual([16, 0]); + + hoursEl.trigger( downMouseWheelEvent ); + $rootScope.$digest(); + expect(getTimeState()).toEqual(['02', '00', 'PM']); + expect(getModelState()).toEqual([14, 0]); + }); + + it('can handle strings as steps', function() { + var upHours = getHoursButton(true); + var upMinutes = getMinutesButton(true); + + expect(getTimeState()).toEqual(['02', '00', 'PM']); + expect(getModelState()).toEqual([14, 0]); + + $rootScope.hstep = '4'; + $rootScope.mstep = '20'; + $rootScope.$digest(); + + doClick(upHours); + expect(getTimeState()).toEqual(['06', '00', 'PM']); + expect(getModelState()).toEqual([18, 0]); + + doClick(upMinutes); + expect(getTimeState()).toEqual(['06', '20', 'PM']); + expect(getModelState()).toEqual([18, 20]); + }); + + }); + + describe('12 / 24 hour mode', function () { + beforeEach(function() { + $rootScope.meridian = false; + $rootScope.time = newTime(14, 10); + element = $compile('')($rootScope); + $rootScope.$digest(); + }); + + function getMeridianTd() { + return element.find('tr').eq(1).find('td').eq(3); + } + + it('initially displays correct time when `show-meridian` is false', function() { + expect(getTimeState(true)).toEqual(['14', '10']); + expect(getModelState()).toEqual([14, 10]); + expect(getMeridianTd().css('display')).toBe('none'); + }); + + it('toggles correctly between different modes', function() { + expect(getTimeState(true)).toEqual(['14', '10']); + + $rootScope.meridian = true; + $rootScope.$digest(); + expect(getTimeState()).toEqual(['02', '10', 'PM']); + expect(getModelState()).toEqual([14, 10]); + expect(getMeridianTd().css('display')).not.toBe('none'); + + $rootScope.meridian = false; + $rootScope.$digest(); + expect(getTimeState(true)).toEqual(['14', '10']); + expect(getModelState()).toEqual([14, 10]); + expect(getMeridianTd().css('display')).toBe('none'); + }); + }); + + describe('setting timepickerConfig steps', function() { + var originalConfig = {}; + beforeEach(inject(function(_$compile_, _$rootScope_, timepickerConfig) { + angular.extend(originalConfig, timepickerConfig); + timepickerConfig.hourStep = 2; + timepickerConfig.minuteStep = 10; + timepickerConfig.showMeridian = false; + element = $compile('')($rootScope); + $rootScope.$digest(); + })); + afterEach(inject(function(timepickerConfig) { + // return it to the original state + angular.extend(timepickerConfig, originalConfig); + })); + + it('does not affect the initial value', function () { + expect(getTimeState(true)).toEqual(['14', '40']); + expect(getModelState()).toEqual([14, 40]); + }); + + it('increases / decreases hours with configured step', function() { + var up = getHoursButton(true); + var down = getHoursButton(false); + + doClick(up, 2); + expect(getTimeState(true)).toEqual(['18', '40']); + expect(getModelState()).toEqual([18, 40]); + + doClick(down, 3); + expect(getTimeState(true)).toEqual(['12', '40']); + expect(getModelState()).toEqual([12, 40]); + }); + + it('increases / decreases minutes with configured step', function() { + var up = getMinutesButton(true); + var down = getMinutesButton(false); + + doClick(up); + expect(getTimeState(true)).toEqual(['14', '50']); + expect(getModelState()).toEqual([14, 50]); + + doClick(down, 3); + expect(getTimeState(true)).toEqual(['14', '20']); + expect(getModelState()).toEqual([14, 20]); + }); + }); + + describe('setting timepickerConfig meridian labels', function() { + var originalConfig = {}; + beforeEach(inject(function(_$compile_, _$rootScope_, timepickerConfig) { + angular.extend(originalConfig, timepickerConfig); + timepickerConfig.meridians = ['π.μ.', 'μ.μ.']; + timepickerConfig.showMeridian = true; + element = $compile('')($rootScope); + $rootScope.$digest(); + })); + afterEach(inject(function(timepickerConfig) { + // return it to the original state + angular.extend(timepickerConfig, originalConfig); + })); + + it('displays correctly', function () { + expect(getTimeState()).toEqual(['02', '40', 'μ.μ.']); + expect(getModelState()).toEqual([14, 40]); + }); + + it('toggles correctly', function () { + $rootScope.time = newTime(2, 40); + $rootScope.$digest(); + + expect(getTimeState()).toEqual(['02', '40', 'π.μ.']); + expect(getModelState()).toEqual([2, 40]); + }); + }); + + describe('user input validation', function () { + + var changeInputValueTo; + + beforeEach(inject(function(_$compile_, _$rootScope_, $sniffer) { + changeInputValueTo = function (inputEl, value) { + inputEl.val(value); + inputEl.trigger($sniffer.hasEvent('input') ? 'input' : 'change'); + $rootScope.$digest(); + }; + })); + + function getHoursInputEl() { + return element.find('input').eq(0); + } + + function getMinutesInputEl() { + return element.find('input').eq(1); + } + + it('has initially the correct time & meridian', function() { + expect(getTimeState()).toEqual(['02', '40', 'PM']); + expect(getModelState()).toEqual([14, 40]); + }); + + it('updates hours & pads on input blur', function() { + var el = getHoursInputEl(); + + changeInputValueTo(el, 5); + expect(getTimeState()).toEqual(['5', '40', 'PM']); + expect(getModelState()).toEqual([17, 40]); + + el.blur(); + expect(getTimeState()).toEqual(['05', '40', 'PM']); + expect(getModelState()).toEqual([17, 40]); + }); + + it('updates minutes & pads on input blur', function() { + var el = getMinutesInputEl(); + + changeInputValueTo(el, 9); + expect(getTimeState()).toEqual(['02', '9', 'PM']); + expect(getModelState()).toEqual([14, 9]); + + el.blur(); + expect(getTimeState()).toEqual(['02', '09', 'PM']); + expect(getModelState()).toEqual([14, 9]); + }); + + it('clears model when input hours is invalid & alerts the UI', function() { + var el = getHoursInputEl(); + + changeInputValueTo(el, 'pizza'); + expect($rootScope.time).toBe(null); + expect(el.parent().hasClass('error')).toBe(true); + + changeInputValueTo(el, 8); + el.blur(); + $rootScope.$digest(); + expect(getTimeState()).toEqual(['08', '40', 'PM']); + expect(getModelState()).toEqual([20, 40]); + expect(el.parent().hasClass('error')).toBe(false); + }); + + it('clears model when input minutes is invalid & alerts the UI', function() { + var el = getMinutesInputEl(); + + changeInputValueTo(el, 'pizza'); + expect($rootScope.time).toBe(null); + expect(el.parent().hasClass('error')).toBe(true); + + changeInputValueTo(el, 22); + expect(getTimeState()).toEqual(['02', '22', 'PM']); + expect(getModelState()).toEqual([14, 22]); + expect(el.parent().hasClass('error')).toBe(false); + }); + + it('handles 12/24H mode change', function() { + $rootScope.meridian = true; + element = $compile('')($rootScope); + $rootScope.$digest(); + + var el = getHoursInputEl(); + + changeInputValueTo(el, '16'); + expect($rootScope.time).toBe(null); + expect(el.parent().hasClass('error')).toBe(true); + + $rootScope.meridian = false; + $rootScope.$digest(); + expect(getTimeState(true)).toEqual(['16', '40']); + expect(getModelState()).toEqual([16, 40]); + }); + }); + +}); + diff --git a/src/timepicker/timepicker.js b/src/timepicker/timepicker.js new file mode 100644 index 0000000000..f9fac966d3 --- /dev/null +++ b/src/timepicker/timepicker.js @@ -0,0 +1,217 @@ +angular.module('ui.bootstrap.timepicker', []) + +.filter('pad', function() { + return function(input) { + if ( angular.isDefined(input) && input.toString().length < 2 ) { + input = '0' + input; + } + return input; + }; +}) + +.constant('timepickerConfig', { + hourStep: 1, + minuteStep: 1, + showMeridian: true, + meridians: ['AM', 'PM'], + readonlyInput: false, + mousewheel: true +}) + +.directive('timepicker', ['padFilter', '$parse', 'timepickerConfig', function (padFilter, $parse, timepickerConfig) { + return { + restrict: 'EA', + require:'ngModel', + replace: true, + templateUrl: 'template/timepicker/timepicker.html', + scope: { + model: '=ngModel' + }, + link: function(scope, element, attrs, ngModelCtrl) { + var selected = new Date(), meridians = timepickerConfig.meridians; + + var hourStep = timepickerConfig.hourStep; + if (attrs.hourStep) { + scope.$parent.$watch($parse(attrs.hourStep), function(value) { + hourStep = parseInt(value, 10); + }); + } + + var minuteStep = timepickerConfig.minuteStep; + if (attrs.minuteStep) { + scope.$parent.$watch($parse(attrs.minuteStep), function(value) { + minuteStep = parseInt(value, 10); + }); + } + + // 12H / 24H mode + scope.showMeridian = timepickerConfig.showMeridian; + if (attrs.showMeridian) { + scope.$parent.$watch($parse(attrs.showMeridian), function(value) { + scope.showMeridian = !! value; + + if ( ! scope.model ) { + // Reset + var dt = new Date( selected ); + var hours = getScopeHours(); + if (angular.isDefined( hours )) { + dt.setHours( hours ); + } + scope.model = new Date( dt ); + } else { + refreshTemplate(); + } + }); + } + + // Get scope.hours in 24H mode if valid + function getScopeHours ( ) { + var hours = parseInt( scope.hours, 10 ); + var valid = ( scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24); + if ( !valid ) { + return; + } + + if ( scope.showMeridian ) { + if ( hours === 12 ) { + hours = 0; + } + if ( scope.meridian === meridians[1] ) { + hours = hours + 12; + } + } + return hours; + } + + // Input elements + var inputs = element.find('input'); + var hoursInputEl = inputs.eq(0), minutesInputEl = inputs.eq(1); + + // Respond on mousewheel spin + var mousewheel = (angular.isDefined(attrs.mousewheel)) ? scope.$eval(attrs.mousewheel) : timepickerConfig.mousewheel; + if ( mousewheel ) { + + var isScrollingUp = function(e) { + if (e.originalEvent) { + e = e.originalEvent; + } + return (e.detail || e.wheelDelta > 0); + }; + + hoursInputEl.bind('mousewheel', function(e) { + scope.$apply( (isScrollingUp(e)) ? scope.incrementHours() : scope.decrementHours() ); + e.preventDefault(); + }); + + minutesInputEl.bind('mousewheel', function(e) { + scope.$apply( (isScrollingUp(e)) ? scope.incrementMinutes() : scope.decrementMinutes() ); + e.preventDefault(); + }); + } + + var keyboardChange = false; + scope.readonlyInput = (angular.isDefined(attrs.readonlyInput)) ? scope.$eval(attrs.readonlyInput) : timepickerConfig.readonlyInput; + if ( ! scope.readonlyInput ) { + scope.updateHours = function() { + var hours = getScopeHours(); + + if ( angular.isDefined(hours) ) { + keyboardChange = 'h'; + if ( scope.model === null ) { + scope.model = new Date( selected ); + } + scope.model.setHours( hours ); + } else { + scope.model = null; + scope.validHours = false; + } + }; + + hoursInputEl.bind('blur', function(e) { + if ( scope.validHours && scope.hours < 10) { + scope.$apply( function() { + scope.hours = padFilter( scope.hours ); + }); + } + }); + + scope.updateMinutes = function() { + var minutes = parseInt(scope.minutes, 10); + if ( minutes >= 0 && minutes < 60 ) { + keyboardChange = 'm'; + if ( scope.model === null ) { + scope.model = new Date( selected ); + } + scope.model.setMinutes( minutes ); + } else { + scope.model = null; + scope.validMinutes = false; + } + }; + + minutesInputEl.bind('blur', function(e) { + if ( scope.validMinutes && scope.minutes < 10 ) { + scope.$apply( function() { + scope.minutes = padFilter( scope.minutes ); + }); + } + }); + } else { + scope.updateHours = angular.noop; + scope.updateMinutes = angular.noop; + } + + scope.$watch( function getModelTimestamp() { + return +scope.model; + }, function( timestamp ) { + if ( !isNaN( timestamp ) && timestamp > 0 ) { + selected = new Date( timestamp ); + refreshTemplate(); + } + }); + + function refreshTemplate() { + var hours = selected.getHours(); + if ( scope.showMeridian ) { + // Convert 24 to 12 hour system + hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; + } + scope.hours = ( keyboardChange === 'h' ) ? hours : padFilter(hours); + scope.validHours = true; + + var minutes = selected.getMinutes(); + scope.minutes = ( keyboardChange === 'm' ) ? minutes : padFilter(minutes); + scope.validMinutes = true; + + scope.meridian = ( scope.showMeridian ) ? (( selected.getHours() < 12 ) ? meridians[0] : meridians[1]) : ''; + + keyboardChange = false; + } + + function addMinutes( minutes ) { + var dt = new Date( selected.getTime() + minutes * 60000 ); + if ( dt.getDate() !== selected.getDate()) { + dt.setDate( dt.getDate() - 1 ); + } + selected.setTime( dt.getTime() ); + scope.model = new Date( selected ); + } + + scope.incrementHours = function() { + addMinutes( hourStep * 60 ); + }; + scope.decrementHours = function() { + addMinutes( - hourStep * 60 ); + }; + scope.incrementMinutes = function() { + addMinutes( minuteStep ); + }; + scope.decrementMinutes = function() { + addMinutes( - minuteStep ); + }; + scope.toggleMeridian = function() { + addMinutes( 12 * 60 * (( selected.getHours() < 12 ) ? 1 : -1) ); + }; + } + }; +}]); \ No newline at end of file diff --git a/template/timepicker/timepicker.html b/template/timepicker/timepicker.html new file mode 100644 index 0000000000..5ef9d0083a --- /dev/null +++ b/template/timepicker/timepicker.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + +
 
:
 
\ No newline at end of file