├── 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 | [![Build Status](https://travis-ci.org/linagora/angular-clockpicker.svg?branch=master)](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 | --------------------------------------------------------------------------------