├── less
└── angular-clockpicker.less
├── .travis.yml
├── .gitignore
├── .jscsrc
├── example
├── app.js
└── index.html
├── .jshintrc
├── bower.json
├── LICENSE
├── package.json
├── test
├── config
│ └── karma.conf.js
└── angular-clockpicker.js
├── README.md
├── Gruntfile.js
└── lib
└── angular-clockpicker.js
/less/angular-clockpicker.less:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "0.10.36"
4 | before_script:
5 | - npm install -g bower
6 | - bower install
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | third_party
2 | node_modules
3 | bower_components
4 | .idea/
5 | .tmp
6 | *.log
7 | frontend/**/*.css
8 | frontend/**/*.css.map
9 | gjslint.xml
10 | jshint.xml
11 | npm-debug.log
12 | dist/
13 | package-lock.json
--------------------------------------------------------------------------------
/.jscsrc:
--------------------------------------------------------------------------------
1 | {
2 | "preset": "airbnb",
3 | "requirePaddingNewLinesAfterBlocks": null,
4 | "requireTrailingComma": null,
5 | "disallowTrailingComma": true,
6 | "disallowMultipleVarDecl": null,
7 | "requirePaddingNewLinesBeforeLineComments": null,
8 | "safeContextKeyword": ["self"],
9 | "requireCamelCaseOrUpperCaseIdentifiers": null
10 | }
11 |
--------------------------------------------------------------------------------
/example/app.js:
--------------------------------------------------------------------------------
1 | ;(function () {
2 | 'use strict';
3 |
4 | var app = angular.module('clockpicker.example', ['angular-clockpicker']);
5 |
6 | app.controller('dateCtrl', function($scope, moment){
7 | $scope.date = moment('2013-09-29 18:42');
8 |
9 | $scope.options = {
10 | done: 'Ok !!',
11 | twelvehour: true,
12 | nativeOnMobile: true
13 | };
14 |
15 | function randomInt(maxExcluded) {
16 | return Math.floor(Math.random()*maxExcluded);
17 | }
18 |
19 | $scope.randomTime = function () {
20 | $scope.date.hour(randomInt(24));
21 | $scope.date.minute(randomInt(60));
22 | };
23 |
24 | });
25 | })();
26 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "node": true,
3 | "browser": true,
4 | "esnext": true,
5 | "bitwise": true,
6 | "curly": true,
7 | "eqeqeq": true,
8 | "immed": true,
9 | "indent": 2,
10 | "latedef": true,
11 | "newcap": true,
12 | "noarg": true,
13 | "quotmark": "single",
14 | "regexp": true,
15 | "undef": true,
16 | "unused": "vars",
17 | "strict": true,
18 | "trailing": true,
19 | "smarttabs": true,
20 | "white": false,
21 | "expr": true,
22 | "globals": {
23 | "angular": false,
24 | "after": false,
25 | "afterEach": false,
26 | "before": false,
27 | "beforeEach": false,
28 | "browser": false,
29 | "describe": false,
30 | "it": false
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-clockpicker",
3 | "version": "1.2.0",
4 | "authors": [
5 | "Linagora folks"
6 | ],
7 | "description": "A wrapper for linagora/clockpicker",
8 | "main": "dist/angular-clockpicker.min.js",
9 | "keywords": [
10 | "openpaas",
11 | "linagora",
12 | "angularjs",
13 | "timepicker",
14 | "clockpicker"
15 | ],
16 | "license": "MIT",
17 | "homepage": "https://github.com/linagora/angular-clockpicker.git",
18 | "ignore": [
19 | "**/.*",
20 | "node_modules",
21 | "bower_components",
22 | "test",
23 | "tests"
24 | ],
25 | "dependencies": {
26 | "lng-clockpicker": "~0.0.8",
27 | "angular": "^1.3.0",
28 | "angular-moment": "1.0.0-beta.3",
29 | "ng-device-detector": "~1.1.7"
30 | },
31 | "devDependencies": {
32 | "chai": "~3.0.0",
33 | "angular-mocks": "~1.3.0",
34 | "sinon-chai": "~2.8.0",
35 | "sinon-1.15.4": "http://sinonjs.org/releases/sinon-1.15.4.js",
36 | "lodash": "~2.4.1"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Linagora
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-clockpicker",
3 | "version": "1.2.0",
4 | "description": "A wrapper for linagora/clockpicker",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "grunt test"
8 | },
9 | "author": "Linagora Folks",
10 | "license": "MIT",
11 | "keywords": [
12 | "openpaas",
13 | "linagora",
14 | "angularjs",
15 | "timepicker",
16 | "clockpicker"
17 | ],
18 | "devDependencies": {
19 | "bower": "^1.5.3",
20 | "chai": "^3.3.0",
21 | "grunt": "^0.4.5",
22 | "grunt-cli": "^0.1.13",
23 | "grunt-contrib-clean": "^0.6.0",
24 | "grunt-contrib-jshint": "^0.11.3",
25 | "grunt-contrib-less": "^1.0.1",
26 | "grunt-contrib-uglify": "^0.9.2",
27 | "grunt-jscs": "2.1.0",
28 | "grunt-karma": "^0.11.2",
29 | "grunt-lint-pattern": "^0.1.4",
30 | "grunt-release": "^0.13.0",
31 | "karma": "0.12.28",
32 | "karma-mocha": "0.1.1",
33 | "karma-phantomjs-launcher": "^0.2.1",
34 | "karma-spec-reporter": "0.0.20",
35 | "less": "^2.5.3",
36 | "load-grunt-tasks": "^3.3.0",
37 | "mocha": "~1.18.2",
38 | "phantomjs": "^1.9.18",
39 | "phantomjs-polyfill": "0.0.1"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/test/config/karma.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = function(config) {
4 | config.set({
5 | basePath: '../../',
6 | singleRun: true,
7 | browsers: ['PhantomJS'],
8 | frameworks: ['mocha'],
9 | reporters: ['spec'],
10 | plugins: [
11 | 'karma-phantomjs-launcher',
12 | 'karma-mocha',
13 | 'karma-spec-reporter'
14 | ],
15 | color: true,
16 | autoWatch: true,
17 | files: [
18 | 'bower_components/chai/chai.js',
19 | 'node_modules/phantomjs-polyfill/bind-polyfill.js',
20 | 'bower_components/lodash/dist/lodash.min.js',
21 | 'bower_components/jquery/dist/jquery.min.js',
22 | 'bower_components/lng-clockpicker/dist/jquery-clockpicker.min.js',
23 | 'bower_components/moment/moment.js',
24 | 'bower_components/sinon-1.15.4/index.js',
25 | 'bower_components/sinon-chai/lib/sinon-chai.js',
26 | 'bower_components/angular/angular.js',
27 | 'bower_components/angular-moment/angular-moment.min.js',
28 | 'bower_components/re-tree/re-tree.min.js',
29 | 'bower_components/ng-device-detector/ng-device-detector.js',
30 | 'bower_components/angular-mocks/angular-mocks.js',
31 | 'lib/angular-clockpicker.js',
32 |
33 | // Tests
34 | 'test/*.js'
35 | ]
36 | });
37 | };
38 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | clockpicker wrapper
8 |
9 |
10 |
11 |
12 |
13 | Date : {{date.format('YYYY-MM-DD hh:mm A')}}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/linagora/angular-clockpicker)
2 |
3 | angular-clockpicker
4 | ===================
5 |
6 | This library use clockpicker and exposes a directive to use it.
7 | We do not use the clockpicker of [weareoutman](https://github.com/weareoutman/clockpicker) becauseit does not handle correctly twelvehour format and it is not supported.
8 | In order to correct this bug, we create a [fork](https://github.com/linagora/clockpicker) on linagora github.
9 |
10 | Usage
11 | =====
12 |
13 | You can take a look at the index.html and app.js in the example folder. But to use angular-clockpicker, you just need to add the attribute clockpicker-wrapper on a input field. This one will be made read-only on mobile devices in order to avoir the virtual keyboard to popup when a user touch the field.
14 | You can specify option of clock-picker documented [here](http://weareoutman.github.io/clockpicker/) by using the clockpicker-options attribute.
15 |
16 |
17 |
18 | Moreover if you set nativeOnMobile in clockpicker-options, on mobile devices the clockpicker will not be used and the system timepicker will be used instead.
19 |
20 | Changelog
21 | =========
22 |
23 | 1.1.1
24 |
25 | * Renaming clockpicker-wrapper directive into lng-clockpicker
26 |
27 | * Add nativeOnMobile options in order to delegate to system timepicker on mobile devices
28 |
29 | * Correct update of view on mutation of moment date
30 |
31 | * Change dependency angular-moment version to beta
32 |
33 | 1.2.0
34 |
35 | * Remove moment local convertion when bind to view
36 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = function(grunt) {
4 | var CI = grunt.option('ci');
5 |
6 | grunt.initConfig({
7 | pkg: grunt.file.readJSON('package.json'),
8 |
9 | project: {
10 | lib: 'lib',
11 | test: 'test',
12 | dist: 'dist',
13 | name: 'angular-clockpicker'
14 | },
15 |
16 | uglify: {
17 | dist: {
18 | files: [
19 | {
20 | dest: '<%= project.dist %>/<%= project.name %>.min.js',
21 | src: ['<%= project.lib %>/<%= project.name %>.js']
22 | }
23 | ]
24 | }
25 | },
26 |
27 | jshint: {
28 | options: {
29 | jshintrc: '.jshintrc',
30 | reporter: CI && 'checkstyle',
31 | reporterOutput: CI && 'jshint.xml'
32 | },
33 | all: {
34 | src: [
35 | 'Gruntfile.js',
36 | '<%= project.test %>/**/*.js',
37 | '<%= project.lib %>/**/*.js'
38 | ]
39 | }
40 | },
41 |
42 | jscs: {
43 | lint: {
44 | options: {
45 | config: '.jscsrc',
46 | esnext: true
47 | },
48 | src: ['<%= jshint.all.src %>']
49 | },
50 | fix: {
51 | options: {
52 | config: '.jscsrc',
53 | esnext: true,
54 | fix: true
55 | },
56 | src: ['<%= jshint.all.src %>']
57 | }
58 | },
59 |
60 | lint_pattern: {
61 | options: {
62 | rules: [
63 | { pattern: /(describe|it)\.only/, message: 'Must not use .only in tests' }
64 | ]
65 | },
66 | all: {
67 | src: ['<%= jshint.all.src %>']
68 | }
69 | },
70 |
71 | watch: {
72 | files: ['<%= jshint.all.src %>'],
73 | tasks: ['test']
74 | },
75 |
76 | // Empties folders to start fresh
77 | clean: {
78 | dist: {
79 | files: [{
80 | dot: true,
81 | src: [
82 | '<%= project.dist %>/*',
83 | '!<%= project.dist %>/.git*'
84 | ]
85 | }]
86 | }
87 | },
88 |
89 | karma: {
90 | unit: {
91 | configFile: '<%= project.test %>/config/karma.conf.js'
92 | }
93 | },
94 |
95 | less: {
96 | release: {
97 | files: {
98 | 'dist/<%= project.name %>.css': 'less/<%= project.name %>.less'
99 | }
100 | },
101 | 'release-compress': {
102 | files: {
103 | 'dist/<%= project.name %>.css': 'less/<%= project.name %>.min.less'
104 | },
105 | options: {
106 | compress: true,
107 | ieCompat: false
108 | }
109 | }
110 | },
111 |
112 | release: {
113 | options: {
114 | file: 'package.json',
115 | additionalFiles: ['bower.json'],
116 | commitMessage: 'Bumped version to <%= version %>',
117 | tagName: 'v<%= version %>',
118 | tagMessage: 'Version <%= version %>',
119 | afterBump: ['exec:gitcheckout_ReleaseBranch', 'test', 'apidoc'],
120 | beforeRelease: ['exec:gitadd_DistAndAPIDoc', 'exec:gitcommit_DistAndAPIDoc'],
121 | afterRelease: ['exec:gitcheckout_master']
122 | }
123 | }
124 | });
125 |
126 | require('load-grunt-tasks')(grunt);
127 | grunt.registerTask('compile', ['clean:dist', 'uglify', 'less:release', 'less:release-compress']);
128 | grunt.registerTask('dist', ['test']);
129 | grunt.registerTask('linters', 'Check code for lint', ['jshint:all', 'jscs:lint', 'lint_pattern:all']);
130 | grunt.registerTask('test', 'Lint, compile and launch test suite', ['linters', 'compile', 'karma']);
131 | grunt.registerTask('dev', 'Launch tests then for each changes relaunch it', ['test', 'watch']);
132 |
133 | grunt.registerTask('default', ['test']);
134 |
135 | };
136 |
--------------------------------------------------------------------------------
/lib/angular-clockpicker.js:
--------------------------------------------------------------------------------
1 | ;(function() {
2 | 'use strict';
3 |
4 | angular.module('angular-clockpicker', ['ng.deviceDetector', 'angularMoment'])
5 |
6 | .factory('clockpickerService', function() {
7 |
8 | function strictParse(twelvehour, string) {
9 | var match = string && string.trim().match(
10 | twelvehour ?
11 | /^(\d{1,2}):(\d{1,2})\s*(AM|PM)$/i :
12 | /^(\d{1,2}):(\d{1,2})$/
13 | );
14 |
15 | if (!match) {
16 | return;
17 | }
18 |
19 | var pm = match[3] && match[3].toUpperCase() === 'PM';
20 | var hour = parseInt(match[1], 10);
21 | var minute = parseInt(match[2], 10);
22 |
23 | if (minute > 59) {
24 | return;
25 | }
26 |
27 | if (twelvehour) {
28 | if (hour < 1 || hour > 12) {
29 | return;
30 | }
31 | hour = (hour % 12) + (pm ? 12 : 0);
32 | } else if (hour > 23) {
33 | return;
34 | }
35 |
36 | return {
37 | hour: hour,
38 | minute: minute
39 | };
40 | }
41 |
42 | function parseMobileTime(string) {
43 | if (!string) {
44 | return;
45 | } else if (string.match(/(AM|PM)\s*$/i)) {
46 | return strictParse(true, string);
47 | } else {
48 | var withoutPotentialMillisecond = (string.trim().match(/^\d{1,2}:\d{1,2}/) || [null])[0];
49 | return strictParse(false, withoutPotentialMillisecond);
50 | }
51 | }
52 |
53 | function parseTime(nativeMobile, twelvehour, string) {
54 | return nativeMobile ? parseMobileTime(string) : strictParse(twelvehour, string);
55 | }
56 |
57 | return {
58 | parseTime: parseTime
59 | };
60 | })
61 |
62 | .value('clockpickerDefaultOptions', {
63 | twelvehour: true,
64 | autoclose: false,
65 | donetext: 'ok'
66 | })
67 |
68 | .directive('lngClockpicker', ['clockpickerService', 'clockpickerDefaultOptions', 'moment', '$timeout', 'detectUtils', function(clockpickerService, clockpickerDefaultOptions, moment, $timeout, detectUtils) {
69 |
70 | function link(scope, element, attr, ngModel) {
71 |
72 | var options = angular.extend({}, clockpickerDefaultOptions, scope.$eval(attr.lngClockpickerOptions));
73 |
74 | var isMobile = detectUtils.isMobile();
75 |
76 | var formatTime = options.twelvehour && !(isMobile && options.nativeOnMobile) ? 'hh:mm A' : 'HH:mm';
77 |
78 | if (!isMobile || !options.nativeOnMobile) {
79 | element.clockpicker(options);
80 | }
81 |
82 | if (isMobile) {
83 | if (options.nativeOnMobile) {
84 | element.attr('type', 'time');
85 | } else if (!element.is('[readonly]')) {
86 | element.attr('readonly', 'readonly');
87 | element.addClass('ignore-readonly');
88 | }
89 | }
90 |
91 | function getModelValue() {
92 | return ngModel.$modelValue ? ngModel.$modelValue.clone() : moment();
93 | }
94 |
95 | var parseViewValue = clockpickerService.parseTime.bind(null, isMobile && options.nativeOnMobile, options.twelvehour);
96 |
97 | scope.$watch(function() {
98 | return ngModel.$modelValue && ngModel.$modelValue.unix && ngModel.$modelValue.unix();
99 | }, function() {
100 | ngModel.$viewValue = ngModel.$formatters.reduceRight(function(prev, formatter) {
101 | return formatter(prev);
102 | }, ngModel.$modelValue);
103 |
104 | ngModel.$render();
105 | });
106 |
107 | element.blur(function() {
108 | ngModel.$valid && element.val(getModelValue().format(formatTime));
109 | });
110 |
111 | ngModel.$render = function(val) {
112 | element.val(ngModel.$viewValue || '');
113 | };
114 |
115 | ngModel.$parsers.push(function(val) {
116 | var time = parseViewValue(val);
117 | ngModel.$setValidity('badFormat', !!time);
118 | if (!time) {
119 | return getModelValue();
120 | }
121 | var inUtc = getModelValue().isUTC();
122 | var newDate = moment(getModelValue());
123 | newDate = newDate;
124 | newDate.hour(time.hour);
125 | newDate.minute(time.minute);
126 | newDate.second(0);
127 | return inUtc ? newDate.utc() : newDate;
128 | });
129 |
130 | ngModel.$formatters.push(function(momentDate) {
131 | var val = parseViewValue(ngModel.$viewValue);
132 |
133 | if (!momentDate) {
134 | return '';
135 | }
136 |
137 | var localMomentDate = momentDate.clone();
138 | var isSameTime = !val ||
139 | (val.hour === localMomentDate.hour() && val.minute === localMomentDate.minute());
140 |
141 | return (element.is(':focus') && isSameTime) ?
142 | ngModel.$viewValue :
143 | localMomentDate.format(formatTime);
144 | });
145 | }
146 |
147 | return {
148 | restrict: 'A',
149 | require: 'ngModel',
150 | link: link
151 | };
152 | }]);
153 | })();
154 |
--------------------------------------------------------------------------------
/test/angular-clockpicker.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /* global chai: false */
4 | /* global sinon: false */
5 | /* global _: false */
6 |
7 | var expect = chai.expect;
8 |
9 | describe('The angular-clockpicker module', function() {
10 |
11 | var self;
12 |
13 | beforeEach(function() {
14 | angular.mock.module('angular-clockpicker');
15 | self = this;
16 | });
17 |
18 | describe('lng-clockpicker directive', function() {
19 |
20 | beforeEach(function() {
21 | this.directiveElement = {};
22 | self.isMobile = false;
23 |
24 | angular.mock.module(function($provide) {
25 | $provide.value('detectUtils', {
26 | isMobile: function() {
27 | return self.isMobile;
28 | }
29 | });
30 |
31 | $provide.decorator('lngClockpickerDirective', function($delegate) {
32 | var directive = $delegate[0];
33 | directive.compile = function() {
34 | return function() {
35 | self.directiveElement = arguments[1] = angular.extend(Object.create(arguments[1]), self.directiveElement, arguments[1]);
36 | return directive.link.apply(this, arguments);
37 | };
38 | };
39 |
40 | return $delegate;
41 | });
42 | });
43 | });
44 |
45 | beforeEach(angular.mock.inject(function($compile, $rootScope, moment, clockpickerDefaultOptions, clockpickerService) {
46 | this.$compile = $compile;
47 | this.$rootScope = $rootScope;
48 | this.$scope = this.$rootScope.$new();
49 | this.$scope.date = moment('1935-10-31 00:00');
50 | this.clockpickerDefaultOptions = clockpickerDefaultOptions;
51 | this.options = {};
52 | this.moment = moment;
53 | this.directiveElement.clockpicker = sinon.spy();
54 | this.clockpickerService = clockpickerService;
55 |
56 | this.initDirective = function() {
57 | var html = '';
58 | this.formElement = this.$compile(html)(this.$scope);
59 | this.$scope.$digest();
60 | this.dateNgModel = this.$scope.form.date;
61 | };
62 |
63 | this.initDirective();
64 | }));
65 |
66 | it('should properly watch change on the moment date', function() {
67 | this.$scope.date.hour('13');
68 | this.$scope.$digest();
69 | expect(this.directiveElement.val()).to.equal('01:00 PM');
70 | });
71 |
72 | it('should not make field read-only one non mobile device', function() {
73 | this.directiveElement.addClass = sinon.spy();
74 | this.directiveElement.attr = sinon.spy();
75 | this.initDirective();
76 | expect(this.directiveElement.addClass).to.not.have.been.called;
77 | expect(this.directiveElement.attr).to.not.have.been.called;
78 | });
79 |
80 | it('should make field read-only one mobile device if not nativeOnMobile', function() {
81 | this.isMobile = true;
82 | this.directiveElement.addClass = sinon.spy();
83 | this.directiveElement.attr = sinon.spy();
84 | this.initDirective();
85 | expect(this.directiveElement.addClass).to.have.been.calledWith('ignore-readonly');
86 | expect(this.directiveElement.attr).to.have.been.calledWith('readonly');
87 | });
88 |
89 | it('should not make field read-only one mobile device if nativeOnMobile', function() {
90 | this.directiveElement.addClass = sinon.spy();
91 | this.directiveElement.attr = sinon.spy();
92 | this.options.nativeOnMobile = true;
93 | this.isMobile = true;
94 | this.initDirective();
95 | expect(this.directiveElement.attr).to.not.have.been.calledWith('readonly');
96 | });
97 |
98 | it('should set type to time if mobile and nativeOnMobile', function() {
99 | this.directiveElement.attr = sinon.spy();
100 | this.isMobile = true;
101 | this.options.nativeOnMobile = true;
102 | this.initDirective();
103 | expect(this.directiveElement.attr).to.have.been.calledWith('type', 'time');
104 | });
105 |
106 | it('should not set type to time if not mobile or not nativeOnMobile', function() {
107 | [
108 | { nativeOnMobile: false, isMobile: false },
109 | { nativeOnMobile: true, isMobile: false },
110 | { nativeOnMobile: false, isMobile: true }
111 | ].forEach(function(o) {
112 | this.directiveElement.addClass = sinon.spy();
113 | this.directiveElement.attr = sinon.spy();
114 | this.options.nativeOnMobile = o.nativeOnMobile;
115 | this.isMobile = o.isMobile;
116 | this.initDirective();
117 | expect(this.directiveElement.attr).to.not.have.been.calledWith('type', 'time');
118 | }, this);
119 | });
120 |
121 | it('should not set css ignore-readonly one mobile phone if already readonly', function() {
122 | this.isMobile = true;
123 | this.directiveElement.addClass = sinon.spy();
124 | this.directiveElement.attr = sinon.spy();
125 | this.directiveElement.is = sinon.stub().returns(true);
126 | this.initDirective();
127 | expect(this.directiveElement.addClass).to.not.have.been.called;
128 | expect(this.directiveElement.attr).to.not.have.been.called;
129 | expect(this.directiveElement.is).to.have.been.calledWith('[readonly]');
130 | });
131 |
132 | it('should call clockpicker with default options if any are provided', function() {
133 | expect(this.directiveElement.clockpicker).to.have.been.calledWith(this.clockpickerDefaultOptions);
134 | });
135 |
136 | it('should merge given options with default options', function() {
137 | this.options = {
138 | twelvehour: false,
139 | autoclose: true
140 | };
141 |
142 | this.initDirective();
143 | expect(this.directiveElement.clockpicker).to.have.been.calledWith({
144 | twelvehour: false,
145 | autoclose: true,
146 | donetext: 'ok' });
147 | });
148 |
149 | it('should call clockpickerService.parseTime to set hour of model with correct arguments', function() {
150 | [true, false].forEach(function(isMobile) {
151 | [true, false].forEach(function(nativeOnMobile) {
152 | this.isMobile = isMobile;
153 | this.options = {
154 | twelvehour: 'true of false that is the question',
155 | nativeOnMobile: nativeOnMobile
156 | };
157 |
158 | this.clockpickerService.parseTime = sinon.stub().returns({ hour: 12, minute: 13 });
159 | this.initDirective();
160 | var input = 'a user input';
161 | this.dateNgModel.$setViewValue(input);
162 |
163 | expect(this.clockpickerService.parseTime).to.have.been.calledWith(isMobile && nativeOnMobile, this.options.twelvehour, input);
164 | expect(this.$scope.date.format('HH:mm')).to.equal('12:13');
165 | }, this);
166 | }, this);
167 | });
168 |
169 | it('should call clockpickerService.parseTime to ensure validity of input', function() {
170 | this.options = {
171 | twelvehour: 'true of false that is the question'
172 | };
173 |
174 | this.clockpickerService.parseTime = sinon.stub().returns({ hour: 12, minute: 13 });
175 |
176 | this.initDirective();
177 |
178 | var input = 'valid input';
179 | this.dateNgModel.$setValidity = sinon.spy();
180 | this.dateNgModel.$setViewValue(input);
181 |
182 | expect(this.clockpickerService.parseTime).to.have.been.calledWith(sinon.match.any, sinon.match.any, input);
183 | expect(this.dateNgModel.$setValidity).to.have.been.calledWith('badFormat', true);
184 |
185 | input = 'invalid input';
186 | this.clockpickerService.parseTime.returns(undefined);
187 | this.dateNgModel.$setViewValue(input);
188 | expect(this.clockpickerService.parseTime).to.have.been.calledWith(sinon.match.any, sinon.match.any, input);
189 | expect(this.dateNgModel.$setValidity).to.have.been.calledWith('badFormat', false);
190 | });
191 |
192 | it('should not change date of ng-model input', function() {
193 | ['bad input', '12:00 PM'].forEach(function(input) {
194 | this.dateNgModel.$setViewValue(input);
195 | expect(this.$scope.date.format('YYYY-MM-DD')).to.equal('1935-10-31');
196 | }, this);
197 | });
198 |
199 | describe('the formatting of time', function() {
200 | it('should format time correctly in twelvehour mode if not isMobile and nativeOnMobile', function() {
201 | [
202 | { isMobile: true, nativeOnMobile: false },
203 | { isMobile: false, nativeOnMobile: true },
204 | { isMobile: false, nativeOnMobile: false }
205 | ].forEach(function(o) {
206 | this.$scope = this.$rootScope.$new();
207 | this.isMobile = o.isMobile;
208 | this.options = {
209 | nativeOnMobile: o.nativeOnMobile
210 | };
211 | this.initDirective();
212 | var date = this.moment('1935-10-31 12:30');
213 | var formatedTime = 'time for british';
214 |
215 | var formatSpy = sinon.stub().returns(formatedTime);
216 |
217 | date.clone = _.wrap(date.clone, function(func) {
218 | var cloneDate = func.apply(date);
219 | cloneDate.local = _.wrap(cloneDate.local, function(func) {
220 | var formatDate = func.apply(cloneDate);
221 | formatDate.format = formatSpy;
222 | return formatDate;
223 | });
224 | return cloneDate;
225 | });
226 |
227 | this.$scope.$apply(function() {
228 | self.$scope.date = date;
229 | });
230 |
231 | expect(formatSpy).to.have.been.calledWith('hh:mm A');
232 | expect(this.dateNgModel.$viewValue).to.equal(formatedTime);
233 | this.$scope.$destroy();
234 | }, this);
235 | });
236 |
237 | it('should format time correctly in 24 hour mode', function() {
238 | this.options = {
239 | twelvehour: false
240 | };
241 |
242 | this.initDirective();
243 |
244 | var date = this.moment('1935-10-31 12:30');
245 | var formatedTime = 'time for frenchies';
246 | var formatSpy = sinon.stub().returns(formatedTime);
247 |
248 | date.clone = _.wrap(date.clone, function(func) {
249 | var cloneDate = func.apply(date);
250 | cloneDate.local = _.wrap(cloneDate.local, function(func) {
251 | var localDate = func.apply(cloneDate);
252 | localDate.format = formatSpy;
253 | return localDate;
254 | });
255 | return cloneDate;
256 | });
257 |
258 | this.$scope.$apply(function() {
259 | self.$scope.date = date;
260 | });
261 |
262 | expect(formatSpy).to.have.been.calledWith('HH:mm');
263 | expect(this.dateNgModel.$viewValue).to.equal(formatedTime);
264 | });
265 |
266 | it('should format time correctly in 24 hour mode if isMobile and nativeOnMobile no matter twelvehour mode', function() {
267 | this.options = {
268 | twelvehour: true,
269 | nativeOnMobile: true
270 | };
271 |
272 | this.isMobile = true;
273 |
274 | this.initDirective();
275 |
276 | var date = this.moment('1935-10-31 12:30');
277 | var formatedTime = 'time for mobile';
278 | var formatSpy = sinon.stub().returns(formatedTime);
279 |
280 | date.clone = _.wrap(date.clone, function(func) {
281 | var cloneDate = func.apply(date);
282 | cloneDate.local = _.wrap(cloneDate.local, function(func) {
283 | var localDate = func.apply(cloneDate);
284 | localDate.format = formatSpy;
285 | return localDate;
286 | });
287 | return cloneDate;
288 | });
289 |
290 | this.$scope.$apply(function() {
291 | self.$scope.date = date;
292 | });
293 |
294 | expect(formatSpy).to.have.been.calledWith('HH:mm');
295 | expect(this.dateNgModel.$viewValue).to.equal(formatedTime);
296 | });
297 |
298 | describe('formatters providden to ng-model', function() {
299 | it('should not reformat time while input is focused if time is the same', function() {
300 | var isSpy = this.directiveElement.is = sinon.stub().returns(true);
301 | var userTime = '1:1pm';
302 | this.dateNgModel.$setViewValue(userTime);
303 | expect(this.dateNgModel.$formatters[1](this.dateNgModel.$modelValue)).to.equal(userTime);
304 | expect(isSpy).to.have.been.calledWith(':focus');
305 | });
306 |
307 | it('should not reformat time while input is focused if value is not valid', function() {
308 | var isSpy = this.directiveElement.is = sinon.stub().returns(true);
309 | var userTime = 'invalid';
310 | this.dateNgModel.$setViewValue(userTime);
311 | expect(this.dateNgModel.$formatters[1](this.dateNgModel.$modelValue)).to.equal(userTime);
312 | expect(isSpy).to.have.been.calledWith(':focus');
313 | });
314 |
315 | it('should replace and format time while input is focused if time is not the same', function() {
316 | var isSpy = this.directiveElement.is = sinon.stub().returns(true);
317 | var userTime = '1:2pm';
318 | this.dateNgModel.$setViewValue(userTime);
319 | expect(this.dateNgModel.$formatters[1](this.moment('1935-10-31 13:01'))).to.equal('01:01 PM');
320 | expect(isSpy).to.have.been.calledWith(':focus');
321 | });
322 |
323 | it('should reformat time if input is not focused', function() {
324 | var isSpy = this.directiveElement.is = sinon.stub().returns(false);
325 | var userTime = '1:1pm';
326 | this.dateNgModel.$setViewValue(userTime);
327 | expect(this.dateNgModel.$formatters[1](this.dateNgModel.$modelValue)).to.equal('01:01 PM');
328 | expect(isSpy).to.have.been.calledWith(':focus');
329 | });
330 | });
331 |
332 | it('should reformat time properly on blur', function() {
333 | this.directiveElement.val('1:1pm');
334 | this.dateNgModel.$setViewValue('1:1pm');
335 |
336 | this.directiveElement.val = sinon.spy();
337 | this.directiveElement.blur();
338 | expect(this.directiveElement.val).to.have.been.calledWith('01:01 PM');
339 | });
340 | });
341 |
342 | it('should return a utc datetime if given initially a utc datetime', function() {
343 | this.$scope.date = this.moment.utc('2012-12-21 12:00');
344 | this.initDirective();
345 | var localDatePlusOne = this.$scope.date.clone().local();
346 | localDatePlusOne.hour(localDatePlusOne.hour() + 1);
347 | this.dateNgModel.$setViewValue(localDatePlusOne.format('hh:mm A'));
348 | expect(this.$scope.date.isUTC()).to.be.true;
349 | expect(this.$scope.date.format('HH:mm')).to.be.equal('13:00');
350 | });
351 |
352 | it('should return a local datetime if given initially a local datetime', function() {
353 | this.$scope.date = this.moment('2012-12-21 12:00');
354 | this.initDirective();
355 | this.dateNgModel.$setViewValue('4:00 PM');
356 | expect(this.$scope.date.isUTC()).to.be.false;
357 | });
358 |
359 | });
360 |
361 | describe('clockpickerService service', function() {
362 |
363 | beforeEach(angular.mock.inject(function(clockpickerService) {
364 | this.clockpickerService = clockpickerService;
365 | }));
366 |
367 | it('should correctly parse 12 hour format hours', function() {
368 | [{
369 | input: '12:00 AM',
370 | output: {
371 | minute: 0,
372 | hour: 0
373 | }
374 | }, {
375 | input: '12:42 PM',
376 | output: {
377 | minute: 42,
378 | hour: 12
379 | }
380 | }, {
381 | input: '1:42 AM',
382 | output: {
383 | minute: 42,
384 | hour: 1
385 | }
386 | }, {
387 | input: '10:05 PM',
388 | output: {
389 | hour: 22,
390 | minute: 5
391 | }
392 | }, {
393 | input: '1:4Am',
394 | output: {
395 | hour: 1,
396 | minute: 4
397 | }
398 | }, {
399 | input: '1:4pM',
400 | output: {
401 | hour: 13,
402 | minute: 4
403 | }
404 | }].forEach(function(obj) {
405 | expect(this.clockpickerService.parseTime(false, true, obj.input)).to.deep.equals(obj.output);
406 | }, this);
407 | });
408 |
409 | it('should not parse valid 24 hour format time when asking to parse twelve hour time', function() {
410 | ['00:00', '01:00', '23:00', '12:30'].forEach(function(date24) {
411 | expect(this.clockpickerService.parseTime(false, true, date24)).to.be.undefined;
412 | }, this);
413 | });
414 |
415 | it('should not parse invalid twelve hour time', function() {
416 | ['01:90 AM', '00:00 AM', '00:00 PM', '13:00 PM', '01:00 MM', '12:30', ':00', '00:', 'everybody as something to hide'].forEach(function(invalidDate) {
417 | expect(this.clockpickerService.parseTime(false, true, invalidDate)).to.be.undefined;
418 | }, this);
419 | });
420 |
421 | it('should not parse invalid 24 hour time', function() {
422 | ['00:00 AM', '25:00', '01:00 PM', '12:60', ':00', '00:', 'expect me and my monkey'].forEach(function(invalidDate) {
423 | expect(this.clockpickerService.parseTime(false, false, invalidDate)).to.be.undefined;
424 | }, this);
425 | });
426 |
427 | it('should correctly parse 24 hour format hours', function() {
428 | [{
429 | input: '00:00',
430 | output: {
431 | minute: 0,
432 | hour: 0
433 | }
434 | }, {
435 | input: '0:42',
436 | output: {
437 | minute: 42,
438 | hour: 0
439 | }
440 | }, {
441 | input: '22:05',
442 | output: {
443 | hour: 22,
444 | minute: 5
445 | }
446 | }, {
447 | input: '1:4',
448 | output: {
449 | hour: 1,
450 | minute: 4
451 | }
452 | }].forEach(function(obj) {
453 | expect(this.clockpickerService.parseTime(false, false, obj.input)).to.deep.equals(obj.output);
454 | }, this);
455 | });
456 |
457 | it('should handle opera date format if nativeMobile is one', function() {
458 | expect(this.clockpickerService.parseTime(true, false, '13:14:00')).to.deep.equals({
459 | hour: 13,
460 | minute: 14
461 | });
462 | });
463 |
464 | it('should parse twelvehour and 24 hour date if mobileNative is true', function() {
465 | [{
466 | input: ' 12:00 AM ',
467 | output: {
468 | minute: 0,
469 | hour: 0
470 | }
471 | }, {
472 | input: ' 00:00 ',
473 | output: {
474 | minute: 0,
475 | hour: 0
476 | }
477 | }, {
478 | input: '12:42 PM',
479 | output: {
480 | minute: 42,
481 | hour: 12
482 | }
483 | }, {
484 | input: '12:42',
485 | output: {
486 | minute: 42,
487 | hour: 12
488 | }
489 | }, {
490 | input: '1:42 AM',
491 | output: {
492 | minute: 42,
493 | hour: 1
494 | }
495 | }, {
496 | input: '1:42',
497 | output: {
498 | minute: 42,
499 | hour: 1
500 | }
501 | }, {
502 | input: '10:05 PM',
503 | output: {
504 | hour: 22,
505 | minute: 5
506 | }
507 | }, {
508 | input: '22:05',
509 | output: {
510 | hour: 22,
511 | minute: 5
512 | }
513 | }, {
514 | input: '1:4Am',
515 | output: {
516 | hour: 1,
517 | minute: 4
518 | }
519 | }, {
520 | input: '1:4pM',
521 | output: {
522 | hour: 13,
523 | minute: 4
524 | }
525 | }].forEach(function(obj) {
526 | expect(this.clockpickerService.parseTime(true, false, obj.input)).to.deep.equals(obj.output);
527 | }, this);
528 | });
529 | });
530 | });
531 |
--------------------------------------------------------------------------------