├── spec └── js │ ├── helpers │ ├── SpecHelper.js │ └── jasmine-jquery.js │ ├── fixtures │ └── timepicker.html │ ├── MouseEventsSpec.js │ ├── KeyboardEventsSpec.js │ └── TimepickerSpec.js ├── .bowerrc ├── .travis.yml ├── .gitignore ├── component.json ├── package.json ├── LICENSE ├── css ├── bootstrap-timepicker.min.css └── bootstrap-timepicker.css ├── README.md ├── less └── timepicker.less ├── grunt.js └── js ├── bootstrap-timepicker.min.js └── bootstrap-timepicker.js /spec/js/helpers/SpecHelper.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory" : "spec/js/libs/" 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.8" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | _site 3 | node_modules 4 | _SpecRunner.html 5 | spec/js/libs 6 | -------------------------------------------------------------------------------- /spec/js/fixtures/timepicker.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
6 | 7 | 8 |
9 |
10 | 11 |
12 | -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootstrap-timepicker", 3 | "version": "0.1.2", 4 | "description": "A timepicker component for Twitter Bootstrap 2.x", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/jdewit/bootstrap-timepicker" 8 | }, 9 | "main": ["js/bootstrap-timepicker.min.js", "css/bootstrap-timepicker.min.css"], 10 | "dependencies": { 11 | "bootstrap": "http://twitter.github.com/bootstrap/assets/bootstrap.zip", 12 | "jquery": "1.8.3", 13 | "autotype": "https://raw.github.com/mmonteleone/jquery.autotype/master/jquery.autotype.js" 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "bootstrap-timepicker", 3 | "description" : "Timepicker widget for Twitter Bootstrap 2.*", 4 | "version" : "0.1.1", 5 | "homepage" : "http://jdewit.github.com/bootstrap-timepicker", 6 | "author" : { 7 | "name" : "Joris de Wit", 8 | "email" : "joris.w.dewit@gmail.com", 9 | "url" : "http://jorisdewit.ca" 10 | }, 11 | "repository" : { 12 | "type" : "git", 13 | "url" : "git://github.com/jdewit/bootstrap-timepicker" 14 | }, 15 | "scripts" : { 16 | "test" : "bower install; grunt test; " 17 | }, 18 | "bugs" : { 19 | "url" : "https://github.com/jdewit/bootstrap-timepicker/issues" 20 | }, 21 | "devDependencies" : { 22 | "grunt-jasmine-runner" : "latest", 23 | "grunt-contrib-less": "latest", 24 | "grunt-exec": "latest", 25 | "grunt-reload": "latest", 26 | "grunt" : "~0.3.9", 27 | "bower" : "latest" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT license 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /css/bootstrap-timepicker.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Timepicker Component for Twitter Bootstrap 3 | * 4 | * Copyright 2013 Joris de Wit 5 | * 6 | * Contributors https://github.com/jdewit/bootstrap-timepicker/graphs/contributors 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */.bootstrap-timepicker{position:relative}.bootstrap-timepicker.pull-right .bootstrap-timepicker-widget.dropdown-menu{left:auto;right:0}.bootstrap-timepicker.pull-right .bootstrap-timepicker-widget.dropdown-menu:before{left:auto;right:12px}.bootstrap-timepicker.pull-right .bootstrap-timepicker-widget.dropdown-menu:after{left:auto;right:13px}.bootstrap-timepicker .add-on{cursor:pointer}.bootstrap-timepicker .add-on i{display:inline-block;width:16px;height:16px}.bootstrap-timepicker-widget.dropdown-menu{padding:2px 3px 2px 2px}.bootstrap-timepicker-widget.dropdown-menu.open{display:inline-block}.bootstrap-timepicker-widget.dropdown-menu:before{border-bottom:7px solid rgba(0,0,0,0.2);border-left:7px solid transparent;border-right:7px solid transparent;content:"";display:inline-block;left:9px;position:absolute;top:-7px}.bootstrap-timepicker-widget.dropdown-menu:after{border-bottom:6px solid #fff;border-left:6px solid transparent;border-right:6px solid transparent;content:"";display:inline-block;left:10px;position:absolute;top:-6px}.bootstrap-timepicker-widget a.btn,.bootstrap-timepicker-widget input{border-radius:4px}.bootstrap-timepicker-widget table{width:100%;margin:0}.bootstrap-timepicker-widget table td{text-align:center;height:30px;margin:0;padding:2px}.bootstrap-timepicker-widget table td:not(.separator){min-width:30px}.bootstrap-timepicker-widget table td span{width:100%}.bootstrap-timepicker-widget table td a{border:1px transparent solid;width:100%;display:inline-block;margin:0;padding:8px 0;outline:0;color:#333}.bootstrap-timepicker-widget table td a:hover{text-decoration:none;background-color:#eee;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;border-color:#ddd}.bootstrap-timepicker-widget table td a i{margin-top:2px}.bootstrap-timepicker-widget table td input{width:25px;margin:0;text-align:center}.bootstrap-timepicker-widget .modal-content{padding:4px}@media(min-width:767px){.bootstrap-timepicker-widget.modal{width:200px;margin-left:-100px}}@media(max-width:767px){.bootstrap-timepicker{width:100%}.bootstrap-timepicker .dropdown-menu{width:100%}} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Timepicker for Twitter Bootstrap 2.x [![Build Status](https://secure.travis-ci.org/jdewit/bootstrap-timepicker.png)](http://travis-ci.org/jdewit/bootstrap-timepicker) 2 | ------------------------------------ 3 | 4 | A simple timepicker component for Twitter Bootstrap. 5 | 6 | Demos & Documentation 7 | ===================== 8 | 9 | View demos & documentation. 10 | 11 | Support 12 | ======= 13 | 14 | If you make money using this timepicker, please consider 15 | supporting its development. 16 | 17 | Click here to support bootstrap-timepicker! 18 | 19 | Contributing 20 | ============ 21 | 22 | 1. Install NodeJS and Node Package Manager. 23 | 24 | ``` bash 25 | npm install 26 | ``` 27 | 28 | 2. Use Bower to get the dev dependencies. 29 | 30 | ``` bash 31 | $ bower install 32 | ``` 33 | 34 | 3. Use Grunt to run tests, compress assets, etc. 35 | 36 | ``` bash 37 | $ grunt jasmine // run the jasmine tests headless in the console 38 | $ grunt jasmine-server // run the tests and open in a browser 39 | $ grunt watch // run jsHint and Jasmine tests whenever the src file or spec file is changed 40 | $ grunt dump // minify the js and css files 41 | ``` 42 | 43 | - Please make it easy on me by covering any new features or issues 44 | with Jasmine tests. 45 | - If your changes need documentation, please take the time to update the docs 46 | and copy the minified assets (grunt copy) in the gh-pages branch. 47 | 48 | Acknowledgements 49 | ================ 50 | 51 | Thanks to everyone who have given feedback and submitted pull requests. A 52 | list of all the contributors can be found here. 53 | 54 | Special thanks to @eternicode and his Twitter Datepicker for inspiration. 55 | -------------------------------------------------------------------------------- /css/bootstrap-timepicker.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Timepicker Component for Twitter Bootstrap 3 | * 4 | * Copyright 2013 Joris de Wit 5 | * 6 | * Contributors https://github.com/jdewit/bootstrap-timepicker/graphs/contributors 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | .bootstrap-timepicker .add-on { 12 | cursor: pointer; 13 | } 14 | .bootstrap-timepicker .add-on i { 15 | display: inline-block; 16 | width: 16px; 17 | height: 16px; 18 | } 19 | .bootstrap-timepicker .dropdown-menu .dropdown-menu:before { 20 | border-bottom: 7px solid rgba(0, 0, 0, 0.2); 21 | border-left: 7px solid transparent; 22 | border-right: 7px solid transparent; 23 | content: ""; 24 | display: inline-block; 25 | left: 9px; 26 | position: absolute; 27 | top: -7px; 28 | } 29 | .bootstrap-timepicker .dropdown-menu .dropdown-menu:after { 30 | border-bottom: 6px solid #FFFFFF; 31 | border-left: 6px solid transparent; 32 | border-right: 6px solid transparent; 33 | content: ""; 34 | display: inline-block; 35 | left: 10px; 36 | position: absolute; 37 | top: -6px; 38 | } 39 | .bootstrap-timepicker .dropdown-menu .pull-right .dropdown-menu, 40 | .bootstrap-timepicker .dropdown-menu .dropdown-menu.pull-right { 41 | left: auto; 42 | right: 0; 43 | } 44 | .bootstrap-timepicker .dropdown-menu .pull-right .dropdown-menu:before, 45 | .bootstrap-timepicker .dropdown-menu .dropdown-menu.pull-right:before { 46 | left: auto; 47 | right: 12px; 48 | } 49 | .bootstrap-timepicker .dropdown-menu .pull-right .dropdown-menu:after, 50 | .bootstrap-timepicker .dropdown-menu .dropdown-menu.pull-right:after { 51 | left: auto; 52 | right: 13px; 53 | } 54 | .bootstrap-timepicker.modal { 55 | top: 30%; 56 | margin-top: 0; 57 | width: 200px; 58 | margin-left: -100px; 59 | } 60 | .bootstrap-timepicker.modal .modal-content { 61 | padding: 0; 62 | } 63 | .bootstrap-timepicker table { 64 | width: 100%; 65 | margin: 0; 66 | } 67 | .bootstrap-timepicker table td { 68 | text-align: center; 69 | height: 30px; 70 | margin: 0; 71 | padding: 2px; 72 | } 73 | .bootstrap-timepicker table td span { 74 | width: 100%; 75 | } 76 | .bootstrap-timepicker table td a { 77 | border: 1px transparent solid; 78 | width: 3em; 79 | display: inline-block; 80 | margin: 0; 81 | padding: 8px 0; 82 | outline: 0; 83 | color: #333; 84 | } 85 | .bootstrap-timepicker table td a:hover { 86 | text-decoration: none; 87 | background-color: #eee; 88 | -webkit-border-radius: 4px; 89 | -moz-border-radius: 4px; 90 | border-radius: 4px; 91 | border-color: #ddd; 92 | } 93 | .bootstrap-timepicker table td a i { 94 | margin-top: 2px; 95 | } 96 | .bootstrap-timepicker table td input { 97 | width: 25px; 98 | margin: 0; 99 | text-align: center; 100 | } 101 | -------------------------------------------------------------------------------- /less/timepicker.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Timepicker Component for Twitter Bootstrap 3 | * 4 | * Copyright 2013 Joris de Wit 5 | * 6 | * Contributors https://github.com/jdewit/bootstrap-timepicker/graphs/contributors 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | .bootstrap-timepicker { 12 | position: relative; 13 | 14 | &.pull-right { 15 | .bootstrap-timepicker-widget { 16 | &.dropdown-menu { 17 | left: auto; 18 | right: 0; 19 | 20 | &:before { 21 | left: auto; 22 | right: 12px; 23 | } 24 | &:after { 25 | left: auto; 26 | right: 13px; 27 | } 28 | } 29 | } 30 | } 31 | 32 | .add-on { 33 | cursor: pointer; 34 | i { 35 | display: inline-block; 36 | width: 16px; 37 | height: 16px; 38 | } 39 | } 40 | } 41 | .bootstrap-timepicker-widget { 42 | &.dropdown-menu { 43 | padding: 2px 3px 2px 2px; 44 | &.open { 45 | display: inline-block; 46 | } 47 | &:before { 48 | border-bottom: 7px solid rgba(0, 0, 0, 0.2); 49 | border-left: 7px solid transparent; 50 | border-right: 7px solid transparent; 51 | content: ""; 52 | display: inline-block; 53 | left: 9px; 54 | position: absolute; 55 | top: -7px; 56 | } 57 | &:after { 58 | border-bottom: 6px solid #FFFFFF; 59 | border-left: 6px solid transparent; 60 | border-right: 6px solid transparent; 61 | content: ""; 62 | display: inline-block; 63 | left: 10px; 64 | position: absolute; 65 | top: -6px; 66 | } 67 | } 68 | 69 | a.btn, input { 70 | border-radius: 4px; 71 | } 72 | 73 | table { 74 | width: 100%; 75 | margin: 0; 76 | 77 | td { 78 | text-align: center; 79 | height: 30px; 80 | margin: 0; 81 | padding: 2px; 82 | 83 | &:not(.separator) { 84 | min-width: 30px; 85 | } 86 | 87 | span { 88 | width: 100%; 89 | } 90 | a { 91 | border: 1px transparent solid; 92 | width: 100%; 93 | display: inline-block; 94 | margin: 0; 95 | padding: 8px 0; 96 | outline: 0; 97 | color: #333; 98 | 99 | &:hover { 100 | text-decoration: none; 101 | background-color: #eee; 102 | -webkit-border-radius: 4px; 103 | -moz-border-radius: 4px; 104 | border-radius: 4px; 105 | border-color: #ddd; 106 | } 107 | 108 | i { 109 | margin-top: 2px; 110 | } 111 | } 112 | input { 113 | width: 25px; 114 | margin: 0; 115 | text-align: center; 116 | } 117 | } 118 | } 119 | } 120 | 121 | .bootstrap-timepicker-widget .modal-content { 122 | padding: 4px; 123 | } 124 | 125 | @media (min-width: 767px) { 126 | .bootstrap-timepicker-widget.modal { 127 | width: 200px; 128 | margin-left: -100px; 129 | } 130 | } 131 | 132 | @media (max-width: 767px) { 133 | .bootstrap-timepicker { 134 | width: 100%; 135 | 136 | .dropdown-menu { 137 | width: 100%; 138 | } 139 | } 140 | } 141 | 142 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /grunt.js: -------------------------------------------------------------------------------- 1 | /*global module:false*/ 2 | module.exports = function(grunt) { 3 | 'use strict'; 4 | 5 | // Project configuration. 6 | grunt.loadNpmTasks('grunt-contrib-less'); 7 | grunt.loadNpmTasks('grunt-jasmine-runner'); 8 | grunt.loadNpmTasks('grunt-exec'); 9 | grunt.loadNpmTasks('grunt-reload'); 10 | 11 | grunt.initConfig({ 12 | meta: { 13 | project: 'Bootstrap-Timepicker', 14 | version: '0.1.0', 15 | banner: '/*! <%= meta.project %> v<%= meta.version %> \n' + 16 | '* http://jdewit.github.com/bootstrap-timepicker \n' + 17 | '* Copyright (c) <%= grunt.template.today("yyyy") %> Joris de Wit \n' + 18 | '* MIT License \n' + 19 | '*/' 20 | }, 21 | lint: { 22 | files: ['js/bootstrap-timepicker.js', 'grunt.js', 'package.json', 'spec/js/*Spec.js'] 23 | }, 24 | less: { 25 | development: { 26 | options: { 27 | paths: ['css'] 28 | }, 29 | files: { 30 | 'css/bootstrap-timepicker.css': 'less/*.less' 31 | } 32 | }, 33 | production: { 34 | options: { 35 | paths: ['css'], 36 | yuicompress: true 37 | }, 38 | files: { 39 | 'css/bootstrap-timepicker.min.css': ['less/*.less'] 40 | } 41 | } 42 | }, 43 | min: { 44 | dist: { 45 | src: ['','js/bootstrap-timepicker.js'], 46 | dest: 'js/bootstrap-timepicker.min.js' 47 | } 48 | }, 49 | jshint: { 50 | options: { 51 | browser: true, 52 | camelcase: true, 53 | curly: true, 54 | eqeqeq: true, 55 | eqnull: true, 56 | immed: true, 57 | indent: 2, 58 | latedef: true, 59 | newcap: true, 60 | noarg: true, 61 | quotmark: true, 62 | sub: true, 63 | strict: true, 64 | trailing: true, 65 | undef: true, 66 | unused: true, 67 | white: false 68 | }, 69 | globals: { 70 | jQuery: true, 71 | $: true, 72 | expect: true, 73 | it: true, 74 | beforeEach: true, 75 | afterEach: true, 76 | describe: true, 77 | loadFixtures: true, 78 | console: true 79 | } 80 | }, 81 | uglify: {}, 82 | watch: { 83 | master: { 84 | files: ['spec/js/*Spec.js', 'js/bootstrap-timepicker.js'], 85 | tasks: ['lint', 'jasmine'], 86 | options: { 87 | interrupt: true 88 | } 89 | }, 90 | ghPages: { 91 | files: ['index.html'], 92 | tasks: ['reload'], 93 | options: { 94 | interrupt: true 95 | } 96 | } 97 | }, 98 | jasmine: { 99 | src : ['spec/js/libs/jquery/jquery.min.js', 'spec/js/libs/bootstrap/js/bootstrap.min.js', 'spec/js/libs/autotype/index.js', 'js/bootstrap-timepicker.js'], 100 | specs : 'spec/js/*Spec.js', 101 | helpers : 'spec/js/helpers/*.js', 102 | timeout : 100, 103 | phantomjs : { 104 | 'ignore-ssl-errors' : true 105 | } 106 | }, 107 | reload: { 108 | port: 3000, 109 | proxy: { 110 | host: 'localhost' 111 | } 112 | }, 113 | exec: { 114 | dump: { 115 | command: 'grunt lint; grunt min; grunt exec:deleteAssets; grunt less:production;' 116 | }, 117 | copyAssets: { 118 | command: 'git checkout gh-pages -q; git checkout master css/bootstrap-timepicker.min.css; git checkout master js/bootstrap-timepicker.min.js;' 119 | }, 120 | deleteAssets: { 121 | command: 'rm -rf css/bootstrap-timepicker.css; rm -rf css/bootstrap-timepicker.min.css; rm -rf js/bootstrap-timepicker.min.js;' 122 | } 123 | } 124 | }); 125 | 126 | // Default task. 127 | grunt.registerTask('default', 'watch:master'); 128 | grunt.registerTask('test', 'jasmine lint'); 129 | grunt.registerTask('dump', 'min less:production'); 130 | grunt.registerTask('copy', 'exec:copyAssets'); 131 | 132 | }; 133 | -------------------------------------------------------------------------------- /spec/js/MouseEventsSpec.js: -------------------------------------------------------------------------------- 1 | describe('Mouse events feature', function() { 2 | 'use strict'; 3 | 4 | var $input1, 5 | $input2, 6 | $input3, 7 | $timepicker1, 8 | $timepicker2, 9 | $timepicker3, 10 | tp1, 11 | tp2, 12 | tp3; 13 | 14 | beforeEach(function () { 15 | loadFixtures('timepicker.html'); 16 | 17 | $input1 = $('#timepicker1'); 18 | $timepicker1 = $input1.timepicker(); 19 | tp1 = $timepicker1.data('timepicker'); 20 | 21 | $input2 = $('#timepicker2'); 22 | $timepicker2 = $input2.timepicker({ 23 | template: 'modal', 24 | showSeconds: true, 25 | minuteStep: 30, 26 | secondStep: 30, 27 | defaultTime: false 28 | }); 29 | tp2 = $timepicker2.data('timepicker'); 30 | 31 | $input3 = $('#timepicker3'); 32 | $timepicker3 = $input3.timepicker({ 33 | defaultTime: '23:15:20', 34 | showMeridian: false, 35 | showSeconds: true 36 | }); 37 | tp3 = $timepicker3.data('timepicker'); 38 | }); 39 | 40 | afterEach(function () { 41 | $input1.data('timepicker').remove(); 42 | $input2.data('timepicker').remove(); 43 | $input3.data('timepicker').remove(); 44 | $input1.remove(); 45 | $input2.remove(); 46 | $input3.remove(); 47 | }); 48 | 49 | it('should be shown and trigger show events on input click', function() { 50 | var showEvents = 0; 51 | 52 | $input1.on('show.timepicker', function() { 53 | showEvents++; 54 | }); 55 | 56 | $input1.parents('div').find('.add-on').trigger('click'); 57 | 58 | expect(tp1.isOpen).toBe(true); 59 | expect(showEvents).toBe(1); 60 | }); 61 | 62 | it('should be hidden and trigger hide events on click outside of widget', function() { 63 | var hideEvents = 0, 64 | time; 65 | $input1.val('11:30 AM'); 66 | 67 | $input1.on('hide.timepicker', function(e) { 68 | hideEvents++; 69 | 70 | time = e.time.value; 71 | }); 72 | 73 | $input1.parents('div').find('.add-on').trigger('click'); 74 | expect(tp1.isOpen).toBe(true); 75 | 76 | tp1.$widget.find('.bootstrap-timepicker-hour').trigger('mousedown'); 77 | $('body').trigger('mousedown'); 78 | 79 | expect(tp1.isOpen).toBe(false, 'widget is still open'); 80 | expect(hideEvents).toBe(1, 'hide event was not thrown once'); 81 | expect(time).toBe('11:30 AM'); 82 | 83 | }); 84 | 85 | it('should increment hour on button click', function() { 86 | tp1.setTime('11:30 AM'); 87 | tp1.update(); 88 | 89 | tp1.$widget.find('a[data-action="incrementHour"]').trigger('click'); 90 | 91 | expect(tp1.getTime()).toBe('12:30 PM'); 92 | 93 | tp2.$widget.find('a[data-action="incrementHour"]').trigger('click'); 94 | expect(tp2.getTime()).toBe('01:00:00 AM'); 95 | }); 96 | 97 | it('should decrement hour on button click', function() { 98 | tp1.setTime('12:30 PM'); 99 | tp1.update(); 100 | 101 | tp1.$widget.find('a[data-action="decrementHour"]').trigger('click'); 102 | 103 | expect(tp1.getTime()).toBe('11:30 AM', 'meridian isnt toggling'); 104 | 105 | tp2.$widget.find('a[data-action="incrementHour"]').trigger('click'); 106 | tp2.$widget.find('a[data-action="incrementHour"]').trigger('click'); 107 | tp2.$widget.find('a[data-action="decrementHour"]').trigger('click'); 108 | expect(tp2.getTime()).toBe('01:00:00 AM'); 109 | }); 110 | 111 | it('should increment minute on button click', function() { 112 | tp1.setTime('11:30 AM'); 113 | tp1.update(); 114 | 115 | tp1.$widget.find('a[data-action="incrementMinute"]').trigger('click'); 116 | 117 | expect(tp1.getTime()).toBe('11:45 AM'); 118 | 119 | tp2.$widget.find('a[data-action="incrementMinute"]').trigger('click'); 120 | expect(tp2.getTime()).toBe('00:30:00 AM'); 121 | }); 122 | 123 | it('should decrement minute on button click', function() { 124 | tp1.setTime('12:30 PM'); 125 | tp1.update(); 126 | 127 | tp1.$widget.find('a[data-action="decrementMinute"]').trigger('click'); 128 | 129 | expect(tp1.getTime()).toBe('12:15 PM'); 130 | }); 131 | 132 | it('should be 11:30:00 PM if minute is decremented on empty input', function() { 133 | tp2.$widget.find('a[data-action="decrementMinute"]').trigger('click'); 134 | expect(tp2.getTime()).toBe('11:30:00 PM'); 135 | }); 136 | 137 | it('should increment second on button click', function() { 138 | tp2.setTime('11:30:15 AM'); 139 | tp2.update(); 140 | 141 | tp2.$widget.find('a[data-action="incrementSecond"]').trigger('click'); 142 | 143 | expect(tp2.getTime()).toBe('11:30:30 AM'); 144 | }); 145 | 146 | it('should decrement second on button click', function() { 147 | tp2.setTime('12:30:15 PM'); 148 | tp2.update(); 149 | 150 | tp2.$widget.find('a[data-action="decrementSecond"]').trigger('click'); 151 | 152 | expect(tp2.getTime()).toBe('12:29:45 PM'); 153 | }); 154 | 155 | it('should toggle meridian on button click', function() { 156 | tp1.setTime('12:30 PM'); 157 | tp1.update(); 158 | 159 | tp1.$widget.find('a[data-action="toggleMeridian"]').first().trigger('click'); 160 | expect(tp1.getTime()).toBe('12:30 AM'); 161 | tp1.$widget.find('a[data-action="toggleMeridian"]').last().trigger('click'); 162 | expect(tp1.getTime()).toBe('12:30 PM'); 163 | }); 164 | 165 | 166 | it('should trigger changeTime event if time is changed', function() { 167 | var eventCount = 0, 168 | time; 169 | 170 | $input1.timepicker().on('changeTime.timepicker', function(e) { 171 | eventCount++; 172 | time = e.time.value; 173 | }); 174 | 175 | tp1.setTime('11:30 AM'); 176 | 177 | expect(eventCount).toBe(1); 178 | expect(time).toBe('11:30 AM'); 179 | 180 | tp1.$widget.find('a[data-action="incrementHour"]').trigger('click'); 181 | 182 | expect(eventCount).toBe(2); 183 | expect(tp1.getTime()).toBe('12:30 PM'); 184 | expect(time).toBe('12:30 PM'); 185 | 186 | tp1.$widget.find('a[data-action="incrementMinute"]').trigger('click'); 187 | 188 | expect(eventCount).toBe(3); 189 | expect(tp1.getTime()).toBe('12:45 PM'); 190 | }); 191 | 192 | it('should highlight widget inputs on click', function() { 193 | //TODO; 194 | //tp1.setTime('11:55 AM'); 195 | //tp1.update(); 196 | 197 | //$input1.parents('.bootstrap-timepicker').find('.add-on').trigger('click'); 198 | //expect(tp1.isOpen).toBe(true); 199 | //expect(tp1.$widget.find('.bootstrap-timepicker-hour').val()).toBe('11'); 200 | //tp1.$widget.find('.bootstrap-timepicker-hour').trigger('click'); 201 | //var hour1 = window.getSelection().toString(); 202 | ////var range = window.getSelection().getRangeAt(0); 203 | ////var hour1 = range.extractContents(); 204 | 205 | //expect(hour1).toBe('11', 'hour input not being highlighted'); 206 | 207 | //tp1.$widget.find('.bootstrap-timepicker-minute').trigger('click'); 208 | //var minute1 = window.getSelection().toString(); 209 | //expect(minute1).toBe('55', 'minute input not being highlighted'); 210 | 211 | //tp1.$widget.find('.bootstrap-timepicker-meridian').trigger('click'); 212 | //var meridian1 = window.getSelection().toString(); 213 | //expect(meridian1).toBe('AM', 'meridian input not being highlighted'); 214 | }); 215 | }); 216 | -------------------------------------------------------------------------------- /spec/js/KeyboardEventsSpec.js: -------------------------------------------------------------------------------- 1 | describe('Keyboard events feature', function() { 2 | 'use strict'; 3 | 4 | var $input1, 5 | $input2, 6 | $input3, 7 | $timepicker1, 8 | $timepicker2, 9 | $timepicker3, 10 | tp1, 11 | tp2, 12 | tp3; 13 | 14 | beforeEach(function () { 15 | loadFixtures('timepicker.html'); 16 | 17 | $input1 = $('#timepicker1'); 18 | $timepicker1 = $input1.timepicker(); 19 | tp1 = $timepicker1.data('timepicker'); 20 | 21 | $input2 = $('#timepicker2'); 22 | $timepicker2 = $input2.timepicker({ 23 | template: 'modal', 24 | showSeconds: true, 25 | minuteStep: 30, 26 | secondStep: 30, 27 | defaultTime: false 28 | }); 29 | tp2 = $timepicker2.data('timepicker'); 30 | 31 | $input3 = $('#timepicker3'); 32 | $timepicker3 = $input3.timepicker({ 33 | defaultTime: '23:15:20', 34 | showMeridian: false, 35 | showSeconds: true, 36 | template: false 37 | }); 38 | tp3 = $timepicker3.data('timepicker'); 39 | }); 40 | 41 | afterEach(function () { 42 | $input1.data('timepicker').remove(); 43 | $input2.data('timepicker').remove(); 44 | $input3.data('timepicker').remove(); 45 | $input1.remove(); 46 | $input2.remove(); 47 | $input3.remove(); 48 | }); 49 | 50 | it('should be able to control element by the arrow keys', function() { 51 | tp1.setTime('11:30 AM'); 52 | tp1.update(); 53 | 54 | $input1.trigger('focus'); 55 | 56 | if (tp1.highlightedUnit !== 'hour') { 57 | tp1.highlightHour(); 58 | } 59 | 60 | expect(tp1.highlightedUnit).toBe('hour', 'hour should be highlighted by default'); 61 | // hours 62 | $input1.trigger({ 63 | 'type': 'keydown', 64 | 'keyCode': 38 //up 65 | }); 66 | expect(tp1.getTime()).toBe('12:30 PM', '1'); 67 | $input1.trigger({ 68 | 'type': 'keydown', 69 | 'keyCode': 40 //down 70 | }); 71 | expect(tp1.getTime()).toBe('11:30 AM', '2'); 72 | expect(tp1.highlightedUnit).toBe('hour', 'hour should be highlighted'); 73 | 74 | $input1.trigger({ 75 | 'type': 'keydown', 76 | 'keyCode': 39 //right 77 | }); 78 | expect(tp1.highlightedUnit).toBe('minute', 'minute should be highlighted'); 79 | 80 | //minutes 81 | $input1.trigger({ 82 | 'type': 'keydown', 83 | 'keyCode': 38 //up 84 | }); 85 | expect(tp1.getTime()).toBe('11:45 AM', '3'); 86 | expect(tp1.highlightedUnit).toBe('minute', 'minute should be highlighted 1'); 87 | 88 | $input1.trigger({ 89 | 'type': 'keydown', 90 | 'keyCode': 40 //down 91 | }); 92 | expect(tp1.getTime()).toBe('11:30 AM', '4'); 93 | expect(tp1.highlightedUnit).toBe('minute', 'minute should be highlighted 2'); 94 | 95 | $input1.trigger({ 96 | 'type': 'keydown', 97 | 'keyCode': 39 //right 98 | }); 99 | expect(tp1.highlightedUnit).toBe('meridian', 'meridian should be highlighted'); 100 | 101 | //meridian 102 | $input1.trigger({ 103 | 'type': 'keydown', 104 | 'keyCode': 38 //up 105 | }); 106 | expect(tp1.getTime()).toBe('11:30 PM', '5'); 107 | expect(tp1.highlightedUnit).toBe('meridian', 'meridian should be highlighted'); 108 | 109 | $input1.trigger({ 110 | 'type': 'keydown', 111 | 'keyCode': 40 //down 112 | }); 113 | expect(tp1.getTime()).toBe('11:30 AM', '6'); 114 | expect(tp1.highlightedUnit).toBe('meridian', 'meridian should be highlighted'); 115 | 116 | $input1.trigger({ 117 | 'type': 'keydown', 118 | 'keyCode': 37 //left 119 | }); 120 | expect(tp1.highlightedUnit).toBe('minute', 'minutes should be highlighted'); 121 | 122 | // minutes 123 | $input1.trigger({ 124 | 'type': 'keydown', 125 | 'keyCode': 40 //down 126 | }); 127 | expect(tp1.getTime()).toBe('11:15 AM', '7'); 128 | 129 | $input1.trigger({ 130 | 'type': 'keydown', 131 | 'keyCode': 37 //left 132 | }); 133 | expect(tp1.highlightedUnit).toBe('hour', 'hours should be highlighted'); 134 | 135 | // hours 136 | $input1.trigger({ 137 | 'type': 'keydown', 138 | 'keyCode': 40 //down 139 | }); 140 | expect(tp1.getTime()).toBe('10:15 AM', '8'); 141 | 142 | $input1.trigger({ 143 | 'type': 'keydown', 144 | 'keyCode': 37 //left 145 | }); 146 | expect(tp1.highlightedUnit).toBe('meridian', 'meridian should be highlighted'); 147 | 148 | // meridian 149 | $input1.trigger({ 150 | 'type': 'keydown', 151 | 'keyCode': 40 //down 152 | }); 153 | expect(tp1.getTime()).toBe('10:15 PM', '9'); 154 | }); 155 | 156 | it('should be able to change time via widget inputs in a dropdown', function() { 157 | var $hourInput = tp1.$widget.find('input.bootstrap-timepicker-hour'), 158 | $minuteInput = tp1.$widget.find('input.bootstrap-timepicker-minute'), 159 | $meridianInput = tp1.$widget.find('input.bootstrap-timepicker-meridian'), 160 | eventCount = 0, 161 | time; 162 | 163 | 164 | tp1.setTime('9:30 AM'); 165 | tp1.update(); 166 | $input1.parents('div').find('.add-on').click(); 167 | 168 | $input1.timepicker().on('changeTime.timepicker', function(e) { 169 | eventCount++; 170 | time = e.time.value; 171 | }); 172 | 173 | expect(tp1.isOpen).toBe(true); 174 | 175 | $hourInput.trigger('focus'); 176 | $hourInput.autotype('{{back}}{{back}}11{{tab}}'); 177 | 178 | expect(tp1.hour).toBe(11); 179 | expect(eventCount).toBe(1, 'incorrect update events thrown'); 180 | expect(time).toBe('11:30 AM'); 181 | 182 | $minuteInput.autotype('{{back}}{{back}}45{{tab}}'); 183 | 184 | expect(tp1.minute).toBe(45); 185 | expect(eventCount).toBe(2, 'incorrect update events thrown'); 186 | expect(time).toBe('11:45 AM'); 187 | 188 | $meridianInput.autotype('{{back}}{{back}}pm{{tab}}'); 189 | 190 | expect(tp1.meridian).toBe('PM'); 191 | expect(eventCount).toBe(3, 'incorrect update events thrown'); 192 | expect(time).toBe('11:45 PM'); 193 | }); 194 | 195 | it('should allow time to be changed via widget inputs in a modal', function() { 196 | //tp2.setTime('9:30 AM'); 197 | //tp2.update(); 198 | //$input2.parents('div').find('.add-on').click(); 199 | 200 | //var $hourInput = $('body').find('input.bootstrap-timepicker-hour'), 201 | //$minuteInput = $('body').find('input.bootstrap-timepicker-minute'), 202 | //$secondInput = $('body').find('input.bootstrap-timepicker-second'), 203 | //$meridianInput = $('body').find('input.bootstrap-timepicker-meridian'); 204 | 205 | //$hourInput.autotype('{{back}}{{back}}2'); 206 | //$hourInput.trigger({ 207 | //'type': 'keydown', 208 | //'keyCode': 9 //tab 209 | //}); 210 | 211 | //expect(tp2.getTime()).toBe('02:30:00 AM'); 212 | 213 | 214 | //$minuteInput.autotype('{{back}}{{back}}0'); 215 | //$minuteInput.trigger({ 216 | //'type': 'keydown', 217 | //'keyCode': 9 //tab 218 | //}); 219 | 220 | //expect(tp2.getTime()).toBe('02:00:00 AM'); 221 | 222 | //$secondInput.autotype('{{back}}{{back}}30'); 223 | //$secondInput.trigger({ 224 | //'type': 'keydown', 225 | //'keyCode': 9 //tab 226 | //}); 227 | 228 | //expect(tp2.getTime()).toBe('02:00:30 AM'); 229 | 230 | //$meridianInput.autotype('{{back}}{{back}}p'); 231 | //$meridianInput.trigger({ 232 | //'type': 'keydown', 233 | //'keyCode': 9 //tab 234 | //}); 235 | 236 | //expect(tp2.getTime()).toBe('02:00:30 PM'); 237 | }); 238 | 239 | it('should be 12:00 AM if 00:00 AM is entered', function() { 240 | //$input1.autotype('{{back}}{{back}}{{back}}{{back}}{{back}}{{back}}{{back}}{{back}}0:0 AM'); 241 | //$input1.trigger({ 242 | //'type': 'keydown', 243 | //'keyCode': 9 //tab 244 | //}); 245 | 246 | //expect(tp1.getTime()).toBe('12:00 AM'); 247 | }); 248 | 249 | it('should validate input', function() { 250 | //var $hourInput = tp1.$widget.find('input.bootstrap-timepicker-hour'), 251 | //$minuteInput = tp1.$widget.find('input.bootstrap-timepicker-minute'), 252 | //$meridianInput = tp1.$widget.find('input.bootstrap-timepicker-meridian'), 253 | //$input3 = tp3.$element; 254 | 255 | //tp1.setTime('11:30 AM'); 256 | //tp1.update(); 257 | 258 | //$hourInput.autotype('{{back}}{{back}}13'); 259 | //tp1.updateFromWidgetInputs(); 260 | //expect(tp1.getTime()).toBe('12:30 AM'); 261 | 262 | //$minuteInput.autotype('{{back}}{{back}}60'); 263 | //tp1.updateFromWidgetInputs(); 264 | //expect(tp1.getTime()).toBe('12:59 AM'); 265 | 266 | //$meridianInput.autotype('{{back}}{{back}}dk'); 267 | //tp1.updateFromWidgetInputs(); 268 | //expect(tp1.getTime()).toBe('12:59 AM'); 269 | 270 | //$meridianInput.autotype('{{back}}{{back}}p'); 271 | //tp1.updateFromWidgetInputs(); 272 | //expect(tp1.getTime()).toBe('12:59 PM'); 273 | 274 | //$input3.autotype('{{back}}{{back}}{{back}}{{back}}{{back}}{{back}}{{back}}{{back}}25:60:60'); 275 | //tp3.updateFromElementVal(); 276 | //expect(tp3.getTime()).toBe('23:59:59'); 277 | }); 278 | }); 279 | -------------------------------------------------------------------------------- /spec/js/TimepickerSpec.js: -------------------------------------------------------------------------------- 1 | describe('Timepicker feature', function() { 2 | 'use strict'; 3 | 4 | var $input1, 5 | $input2, 6 | $input3, 7 | $timepicker1, 8 | $timepicker2, 9 | $timepicker3, 10 | tp1, 11 | tp2, 12 | tp3; 13 | 14 | beforeEach(function () { 15 | loadFixtures('timepicker.html'); 16 | 17 | $input1 = $('#timepicker1'); 18 | $timepicker1 = $input1.timepicker(); 19 | tp1 = $timepicker1.data('timepicker'); 20 | 21 | $input2 = $('#timepicker2'); 22 | $timepicker2 = $input2.timepicker({ 23 | template: 'modal', 24 | showSeconds: true, 25 | minuteStep: 30, 26 | secondStep: 30, 27 | defaultTime: false 28 | }); 29 | tp2 = $timepicker2.data('timepicker'); 30 | 31 | $input3 = $('#timepicker3'); 32 | $timepicker3 = $input3.timepicker({ 33 | showMeridian: false, 34 | showSeconds: true, 35 | defaultTime: '13:25:15' 36 | }); 37 | tp3 = $timepicker3.data('timepicker'); 38 | }); 39 | 40 | afterEach(function () { 41 | if ($input1.data('timepicker') !== undefined) { 42 | $input1.data('timepicker').remove(); 43 | } 44 | if ($input2.data('timepicker') !== undefined) { 45 | $input2.data('timepicker').remove(); 46 | } 47 | if ($input3.data('timepicker') !== undefined) { 48 | $input3.data('timepicker').remove(); 49 | } 50 | $input1.remove(); 51 | $input2.remove(); 52 | $input3.remove(); 53 | }); 54 | 55 | it('should be available on the jquery object', function() { 56 | expect($.fn.timepicker).toBeDefined(); 57 | }); 58 | 59 | it('should be chainable', function() { 60 | expect($timepicker1).toBe($input1); 61 | }); 62 | 63 | it('should have sensible defaults', function() { 64 | expect(tp1.defaultTime).toBeTruthy(); 65 | expect(tp1.minuteStep).toBe(15); 66 | expect(tp1.secondStep).toBe(15); 67 | expect(tp1.disableFocus).toBe(false); 68 | expect(tp1.showSeconds).toBe(false); 69 | expect(tp1.showInputs).toBe(true); 70 | expect(tp1.showMeridian).toBe(true); 71 | expect(tp1.template).toBe('dropdown'); 72 | expect(tp1.modalBackdrop).toBe(false); 73 | expect(tp1.modalBackdrop).toBe(false); 74 | expect(tp1.isOpen).toBe(false); 75 | }); 76 | 77 | it('should allow user to configure defaults', function() { 78 | expect(tp2.template).toBe('modal'); 79 | expect(tp2.minuteStep).toBe(30); 80 | }); 81 | 82 | it('should be configurable with data attributes', function() { 83 | $('body').append('
12) { 103 | hour = hour - 12; 104 | } 105 | 106 | expect(tp1.hour).toBe(hour); 107 | expect(tp1.minute).toBe(minute); 108 | }); 109 | 110 | it('should not override time with current time if value is already set', function() { 111 | $('body').append('
'); 112 | var $input4 = $('#timepicker4Input').timepicker(), 113 | tp4 = $input4.data('timepicker'); 114 | 115 | expect($input4.val()).toBe('12:15 AM'); 116 | 117 | tp4.remove(); 118 | $('#timepicker4').remove(); 119 | }); 120 | 121 | it('should have no value if defaultTime is set to false', function() { 122 | expect($input2.val()).toBe(''); 123 | }); 124 | 125 | it('should be able to set default time with config option', function() { 126 | expect(tp3.getTime()).toBe('13:25:15'); 127 | }); 128 | 129 | it('should update the element and widget with the setTime method', function() { 130 | tp2.setTime('09:15:20 AM'); 131 | 132 | expect(tp2.hour).toBe(9); 133 | expect(tp2.minute).toBe(15); 134 | expect(tp2.second).toBe(20); 135 | expect(tp2.meridian).toBe('AM'); 136 | expect($input2.val()).toBe('09:15:20 AM'); 137 | expect(tp2.$widget.find('.bootstrap-timepicker-hour').val()).toBe('09'); 138 | expect(tp2.$widget.find('.bootstrap-timepicker-minute').val()).toBe('15'); 139 | expect(tp2.$widget.find('.bootstrap-timepicker-second').val()).toBe('20'); 140 | expect(tp2.$widget.find('.bootstrap-timepicker-meridian').val()).toBe('AM'); 141 | }); 142 | 143 | it('should be able to format time values into a string', function() { 144 | expect(tp2.formatTime(3, 15, 45, 'PM')).toBe('03:15:45 PM'); 145 | }); 146 | 147 | it('should be able get & set the pickers time', function() { 148 | tp3.setTime('23:15:20'); 149 | expect(tp3.getTime()).toBe('23:15:20'); 150 | }); 151 | 152 | it('should update picker on blur', function() { 153 | $input1.val('10:25 AM'); 154 | expect(tp1.getTime()).not.toBe('10:25 AM'); 155 | $input1.trigger('blur'); 156 | expect(tp1.getTime()).toBe('10:25 AM'); 157 | }); 158 | 159 | it('should update element with updateElement method', function() { 160 | tp1.hour = 10; 161 | tp1.minute = 30; 162 | tp1.meridian = 'PM'; 163 | expect($input1.val()).not.toBe('10:30 PM'); 164 | tp1.updateElement(); 165 | expect($input1.val()).toBe('10:30 PM'); 166 | }); 167 | 168 | it('should update widget with updateWidget method', function() { 169 | tp2.hour = 10; 170 | tp2.minute = 30; 171 | tp2.second = 15; 172 | 173 | expect(tp2.$widget.find('.bootstrap-timepicker-hour').val()).not.toBe('10'); 174 | expect(tp2.$widget.find('.bootstrap-timepicker-minute').val()).not.toBe('30'); 175 | expect(tp2.$widget.find('.bootstrap-timepicker-second').val()).not.toBe('15'); 176 | 177 | tp2.updateWidget(); 178 | 179 | expect(tp2.$widget.find('.bootstrap-timepicker-hour').val()).toBe('10'); 180 | expect(tp2.$widget.find('.bootstrap-timepicker-minute').val()).toBe('30'); 181 | expect(tp2.$widget.find('.bootstrap-timepicker-second').val()).toBe('15'); 182 | }); 183 | 184 | it('should update picker with updateFromElementVal method', function() { 185 | tp1.hour = 12; 186 | tp1.minute = 12; 187 | tp1.meridian = 'PM'; 188 | tp1.update(); 189 | 190 | $input1.val('10:30 AM'); 191 | 192 | expect(tp1.$widget.find('.bootstrap-timepicker-hour').val()).not.toBe('10'); 193 | expect(tp1.$widget.find('.bootstrap-timepicker-minute').val()).not.toBe('30'); 194 | expect(tp1.$widget.find('.bootstrap-timepicker-meridian').val()).not.toBe('AM'); 195 | expect(tp1.hour).not.toBe(10); 196 | expect(tp1.minute).not.toBe(30); 197 | expect(tp1.meridian).not.toBe('AM'); 198 | 199 | tp1.updateFromElementVal(); 200 | 201 | expect(tp1.$widget.find('.bootstrap-timepicker-hour').val()).toBe('10'); 202 | expect(tp1.$widget.find('.bootstrap-timepicker-minute').val()).toBe('30'); 203 | expect(tp1.$widget.find('.bootstrap-timepicker-meridian').val()).toBe('AM'); 204 | expect(tp1.hour).toBe(10); 205 | expect(tp1.minute).toBe(30); 206 | expect(tp1.meridian).toBe('AM'); 207 | }); 208 | 209 | it('should update picker with updateFromWidgetInputs method', function() { 210 | tp1.hour = 12; 211 | tp1.minute = 12; 212 | tp1.meridian = 'PM'; 213 | tp1.update(); 214 | 215 | tp1.$widget.find('.bootstrap-timepicker-hour').val(10); 216 | tp1.$widget.find('.bootstrap-timepicker-minute').val(30); 217 | tp1.$widget.find('.bootstrap-timepicker-meridian').val('AM'); 218 | 219 | expect(tp1.hour).not.toBe(10); 220 | expect(tp1.minute).not.toBe(30); 221 | expect(tp1.meridian).not.toBe('AM'); 222 | expect($input1.val()).not.toBe('10:30 AM'); 223 | 224 | tp1.updateFromWidgetInputs(); 225 | 226 | expect(tp1.hour).toBe(10); 227 | expect(tp1.minute).toBe(30); 228 | expect(tp1.meridian).toBe('AM'); 229 | expect($input1.val()).toBe('10:30 AM'); 230 | }); 231 | 232 | it('should increment hours with incrementHour method', function() { 233 | tp1.hour = 9; 234 | tp1.incrementHour(); 235 | expect(tp1.hour).toBe(10); 236 | }); 237 | 238 | it('should decrement hours with decrementHour method', function() { 239 | tp1.hour = 9; 240 | tp1.decrementHour(); 241 | expect(tp1.hour).toBe(8); 242 | }); 243 | 244 | it('should toggle meridian if hour goes past 12', function() { 245 | $input1.val('11:00 AM'); 246 | tp1.updateFromElementVal(); 247 | tp1.incrementHour(); 248 | 249 | expect(tp1.hour).toBe(12); 250 | expect(tp1.minute).toBe(0); 251 | expect(tp1.meridian).toBe('PM'); 252 | }); 253 | 254 | it('should toggle meridian if hour goes below 1', function() { 255 | $input1.val('11:00 AM'); 256 | tp1.updateFromElementVal(); 257 | tp1.incrementHour(); 258 | 259 | expect(tp1.hour).toBe(12); 260 | expect(tp1.minute).toBe(0); 261 | expect(tp1.meridian).toBe('PM'); 262 | }); 263 | 264 | it('should set hour to 1 if hour increments on 12 for 12h clock', function() { 265 | $input1.val('11:15 PM'); 266 | tp1.updateFromElementVal(); 267 | tp1.incrementHour(); 268 | tp1.incrementHour(); 269 | 270 | expect(tp1.getTime()).toBe('01:15 AM'); 271 | }); 272 | 273 | it('should set hour to 0 if hour increments on 23 for 24h clock', function() { 274 | $input3.val('22:15:30'); 275 | tp3.updateFromElementVal(); 276 | tp3.incrementHour(); 277 | tp3.incrementHour(); 278 | 279 | expect(tp3.hour).toBe(0); 280 | expect(tp3.minute).toBe(15); 281 | expect(tp3.second).toBe(30); 282 | }); 283 | 284 | it('should increment minutes with incrementMinute method', function() { 285 | tp1.minute = 10; 286 | tp1.incrementMinute(); 287 | 288 | expect(tp1.minute).toBe(15); 289 | 290 | tp2.minute = 0; 291 | tp2.incrementMinute(); 292 | 293 | expect(tp2.minute).toBe(30); 294 | }); 295 | 296 | it('should decrement minutes with decrementMinute method', function() { 297 | tp1.hour = 11; 298 | tp1.minute = 0; 299 | tp1.decrementMinute(); 300 | 301 | expect(tp1.hour).toBe(10); 302 | expect(tp1.minute).toBe(45); 303 | 304 | tp2.hour = 11; 305 | tp2.minute = 0; 306 | tp2.decrementMinute(); 307 | 308 | expect(tp2.hour).toBe(10); 309 | expect(tp2.minute).toBe(30); 310 | }); 311 | 312 | 313 | it('should increment hour if minutes increment past 59', function() { 314 | $input1.val('11:55 AM'); 315 | tp1.updateFromElementVal(); 316 | tp1.incrementMinute(); 317 | tp1.update(); 318 | 319 | expect(tp1.getTime()).toBe('12:00 PM'); 320 | }); 321 | 322 | it('should toggle meridian with toggleMeridian method', function() { 323 | tp1.meridian = 'PM'; 324 | tp1.toggleMeridian(); 325 | 326 | expect(tp1.meridian).toBe('AM'); 327 | }); 328 | 329 | it('should increment seconds with incrementSecond method', function() { 330 | tp1.second = 0; 331 | tp1.incrementSecond(); 332 | 333 | expect(tp1.second).toBe(15); 334 | 335 | tp2.second = 0; 336 | tp2.incrementSecond(); 337 | 338 | expect(tp2.second).toBe(30); 339 | }); 340 | 341 | it('should decrement seconds with decrementSecond method', function() { 342 | tp2.hour = 11; 343 | tp2.minute = 0; 344 | tp2.second = 0; 345 | tp2.decrementSecond(); 346 | 347 | expect(tp2.minute).toBe(59); 348 | expect(tp2.second).toBe(30); 349 | }); 350 | 351 | 352 | it('should increment minute by 1 if seconds increment past 59', function() { 353 | $input2.val('11:55:30 AM'); 354 | tp2.updateFromElementVal(); 355 | tp2.incrementSecond(); 356 | tp2.update(); 357 | 358 | expect(tp2.getTime()).toBe('11:56:00 AM'); 359 | }); 360 | 361 | it('should not have any remaining events if remove is called', function() { 362 | var hideEvents = 0; 363 | 364 | $input1.on('hide.timepicker', function() { 365 | hideEvents++; 366 | }); 367 | 368 | $input1.parents('div').find('.add-on').trigger('click'); 369 | $('body').trigger('mousedown'); 370 | 371 | expect(hideEvents).toBe(1); 372 | 373 | tp1.remove(); 374 | tp2.remove(); 375 | tp3.remove(); 376 | 377 | $('body').trigger('click'); 378 | expect(hideEvents).toBe(1); 379 | }); 380 | 381 | it('should not have the widget in the DOM if remove method is called', function() { 382 | expect($('body')).toContain('.bootstrap-timepicker-widget'); 383 | tp1.remove(); 384 | tp2.remove(); 385 | tp3.remove(); 386 | expect($('body')).not.toContain('.bootstrap-timepicker-widget'); 387 | }); 388 | 389 | it('should be able to set time from a script', function() { 390 | $input1.timepicker('setTime', '12:35 PM'); 391 | tp1.update(); 392 | expect(tp1.getTime()).toBe('12:35 PM'); 393 | }); 394 | 395 | it('should be able to opened from script', function() { 396 | expect(tp1.isOpen).toBe(false); 397 | $input1.timepicker('showWidget'); 398 | expect(tp1.isOpen).toBe(true); 399 | }); 400 | 401 | }); 402 | -------------------------------------------------------------------------------- /js/bootstrap-timepicker.min.js: -------------------------------------------------------------------------------- 1 | /*! Bootstrap-Timepicker v0.1.0 2 | * http://jdewit.github.com/bootstrap-timepicker 3 | * Copyright (c) 2013 Joris de Wit 4 | * MIT License 5 | */ 6 | (function(e,t,n,r){"use strict";var i=function(t,n){this.widget="",this.$element=e(t),this.defaultTime=n.defaultTime,this.disableFocus=n.disableFocus,this.isOpen=n.isOpen,this.minuteStep=n.minuteStep,this.modalBackdrop=n.modalBackdrop,this.secondStep=n.secondStep,this.showInputs=n.showInputs,this.showMeridian=n.showMeridian,this.showSeconds=n.showSeconds,this.template=n.template,this._init()};i.prototype={constructor:i,_init:function(){var t=this;this.$element.parent().hasClass("input-append")?(this.$element.parent(".input-append").find(".add-on").on({"click.timepicker":e.proxy(this.showWidget,this)}),this.$element.on({"focus.timepicker":e.proxy(this.highlightUnit,this),"click.timepicker":e.proxy(this.highlightUnit,this),"keydown.timepicker":e.proxy(this.elementKeydown,this),"blur.timepicker":e.proxy(this.blurElement,this)})):this.template?this.$element.on({"focus.timepicker":e.proxy(this.showWidget,this),"click.timepicker":e.proxy(this.showWidget,this),"blur.timepicker":e.proxy(this.blurElement,this)}):this.$element.on({"focus.timepicker":e.proxy(this.highlightUnit,this),"click.timepicker":e.proxy(this.highlightUnit,this),"keydown.timepicker":e.proxy(this.elementKeydown,this),"blur.timepicker":e.proxy(this.blurElement,this)}),this.template!==!1?this.$widget=e(this.getTemplate()).appendTo(this.$element.parents(".bootstrap-timepicker")).on("click",e.proxy(this.widgetClick,this)):this.$widget=!1,this.showInputs&&this.$widget!==!1&&this.$widget.find("input").each(function(){e(this).on({"click.timepicker":function(){e(this).select()},"keydown.timepicker":e.proxy(t.widgetKeydown,t)})}),this.setDefaultTime(this.defaultTime)},blurElement:function(){this.highlightedUnit=r,this.updateFromElementVal()},decrementHour:function(){if(this.showMeridian)if(this.hour===1)this.hour=12;else{if(this.hour===12)return this.hour--,this.toggleMeridian();if(this.hour===0)return this.hour=11,this.toggleMeridian();this.hour--}else this.hour===0?this.hour=23:this.hour--;this.update()},decrementMinute:function(e){var t;e?t=this.minute-e:t=this.minute-this.minuteStep,t<0?(this.decrementHour(),this.minute=t+60):this.minute=t,this.update()},decrementSecond:function(){var e=this.second-this.secondStep;e<0?(this.decrementMinute(!0),this.second=e+60):this.second=e,this.update()},elementKeydown:function(e){switch(e.keyCode){case 9:this.updateFromElementVal();switch(this.highlightedUnit){case"hour":e.preventDefault(),this.highlightNextUnit();break;case"minute":if(this.showMeridian||this.showSeconds)e.preventDefault(),this.highlightNextUnit();break;case"second":this.showMeridian&&(e.preventDefault(),this.highlightNextUnit())}break;case 27:this.updateFromElementVal();break;case 37:e.preventDefault(),this.highlightPrevUnit(),this.updateFromElementVal();break;case 38:e.preventDefault();switch(this.highlightedUnit){case"hour":this.incrementHour(),this.highlightHour();break;case"minute":this.incrementMinute(),this.highlightMinute();break;case"second":this.incrementSecond(),this.highlightSecond();break;case"meridian":this.toggleMeridian(),this.highlightMeridian()}break;case 39:e.preventDefault(),this.updateFromElementVal(),this.highlightNextUnit();break;case 40:e.preventDefault();switch(this.highlightedUnit){case"hour":this.decrementHour(),this.highlightHour();break;case"minute":this.decrementMinute(),this.highlightMinute();break;case"second":this.decrementSecond(),this.highlightSecond();break;case"meridian":this.toggleMeridian(),this.highlightMeridian()}}},formatTime:function(e,t,n,r){return e=e<10?"0"+e:e,t=t<10?"0"+t:t,n=n<10?"0"+n:n,e+":"+t+(this.showSeconds?":"+n:"")+(this.showMeridian?" "+r:"")},getCursorPosition:function(){var e=this.$element.get(0);if("selectionStart"in e)return e.selectionStart;if(n.selection){e.focus();var t=n.selection.createRange(),r=n.selection.createRange().text.length;return t.moveStart("character",-e.value.length),t.text.length-r}},getTemplate:function(){var e,t,n,r,i,s;this.showInputs?(t='',n='',r='',i=''):(t='',n='',r='',i=''),s=''+(this.showSeconds?'':"")+(this.showMeridian?'':"")+""+""+" "+''+" "+(this.showSeconds?'":"")+(this.showMeridian?'":"")+""+""+''+''+''+(this.showSeconds?'':"")+(this.showMeridian?'':"")+""+"
   
"+t+":"+n+":'+r+" '+i+"
  
";switch(this.template){case"modal":e='";break;case"dropdown":e='"}return e},getTime:function(){return this.formatTime(this.hour,this.minute,this.second,this.meridian)},hideWidget:function(){if(this.isOpen===!1)return;this.updateFromWidgetInputs(),this.$element.trigger({type:"hide.timepicker",time:{value:this.getTime(),hours:this.hour,minutes:this.minute,seconds:this.second,meridian:this.meridian}}),this.template==="modal"?this.$widget.modal("hide"):this.$widget.removeClass("open"),e(n).off("mousedown.timepicker"),this.isOpen=!1},highlightUnit:function(){this.position=this.getCursorPosition(),this.position>=0&&this.position<=2?this.highlightHour():this.position>=3&&this.position<=5?this.highlightMinute():this.position>=6&&this.position<=8?this.showSeconds?this.highlightSecond():this.highlightMeridian():this.position>=9&&this.position<=11&&this.highlightMeridian()},highlightNextUnit:function(){switch(this.highlightedUnit){case"hour":this.highlightMinute();break;case"minute":this.showSeconds?this.highlightSecond():this.showMeridian?this.highlightMeridian():this.highlightHour();break;case"second":this.showMeridian?this.highlightMeridian():this.highlightHour();break;case"meridian":this.highlightHour()}},highlightPrevUnit:function(){switch(this.highlightedUnit){case"hour":this.highlightMeridian();break;case"minute":this.highlightHour();break;case"second":this.highlightMinute();break;case"meridian":this.showSeconds?this.highlightSecond():this.highlightMinute()}},highlightHour:function(){var e=this.$element;this.highlightedUnit="hour",setTimeout(function(){e.get(0).setSelectionRange(0,2)},0)},highlightMinute:function(){var e=this.$element;this.highlightedUnit="minute",setTimeout(function(){e.get(0).setSelectionRange(3,5)},0)},highlightSecond:function(){var e=this.$element;this.highlightedUnit="second",setTimeout(function(){e.get(0).setSelectionRange(6,8)},0)},highlightMeridian:function(){var e=this.$element;this.highlightedUnit="meridian",this.showSeconds?setTimeout(function(){e.get(0).setSelectionRange(9,11)},0):setTimeout(function(){e.get(0).setSelectionRange(6,8)},0)},incrementHour:function(){if(this.showMeridian){if(this.hour===11)return this.hour++,this.toggleMeridian();if(this.hour===12)return this.hour=1}if(this.hour===23)return this.hour=0;this.hour++,this.update()},incrementMinute:function(e){var t;e?t=this.minute+e:t=this.minute+this.minuteStep-this.minute%this.minuteStep,t>59?(this.incrementHour(),this.minute=t-60):this.minute=t,this.update()},incrementSecond:function(){var e=this.second+this.secondStep-this.second%this.secondStep;e>59?(this.incrementMinute(!0),this.second=e-60):this.second=e,this.update()},remove:function(){e("document").off(".timepicker"),this.$widget&&this.$widget.remove(),delete this.$element.data().timepicker},setDefaultTime:function(e){if(!this.$element.val())if(e==="current"){var t=new Date,n=t.getHours(),r=Math.floor(t.getMinutes()/this.minuteStep)*this.minuteStep,i=Math.floor(t.getSeconds()/this.secondStep)*this.secondStep,s="AM";this.showMeridian&&(n===0?n=12:n>=12?(n>12&&(n-=12),s="PM"):s="AM"),this.hour=n,this.minute=r,this.second=i,this.meridian=s,this.update()}else e===!1?(this.hour=0,this.minute=0,this.second=0,this.meridian="AM"):this.setTime(e);else this.updateFromElementVal()},setTime:function(e){var t,n;this.showMeridian?(t=e.split(" "),n=t[0].split(":"),this.meridian=t[1]):n=e.split(":"),this.hour=parseInt(n[0],10),this.minute=parseInt(n[1],10),this.second=parseInt(n[2],10),isNaN(this.hour)&&(this.hour=0),isNaN(this.minute)&&(this.minute=0);if(this.showMeridian){this.hour>12?this.hour=12:this.hour<1&&(this.hour=12);if(this.meridian==="am"||this.meridian==="a")this.meridian="AM";else if(this.meridian==="pm"||this.meridian==="p")this.meridian="PM";this.meridian!=="AM"&&this.meridian!=="PM"&&(this.meridian="AM")}else this.hour>=24?this.hour=23:this.hour<0&&(this.hour=0);this.minute<0?this.minute=0:this.minute>=60&&(this.minute=59),this.showSeconds&&(isNaN(this.second)?this.second=0:this.second<0?this.second=0:this.second>=60&&(this.second=59)),this.update()},showWidget:function(){if(this.isOpen)return;var t=this;e(n).on("mousedown.timepicker",function(n){e(n.target).closest(".bootstrap-timepicker-widget").length===0&&t.hideWidget()}),this.$element.trigger({type:"show.timepicker",time:{value:this.getTime(),hours:this.hour,minutes:this.minute,seconds:this.second,meridian:this.meridian}}),this.disableFocus&&this.$element.blur(),this.updateFromElementVal(),this.template==="modal"?this.$widget.modal("show").on("hidden",e.proxy(this.hideWidget,this)):this.isOpen===!1&&this.$widget.addClass("open"),this.isOpen=!0},toggleMeridian:function(){this.meridian=this.meridian==="AM"?"PM":"AM",this.update()},update:function(){this.$element.trigger({type:"changeTime.timepicker",time:{value:this.getTime(),hours:this.hour,minutes:this.minute,seconds:this.second,meridian:this.meridian}}),this.updateElement(),this.updateWidget()},updateElement:function(){this.$element.val(this.getTime())},updateFromElementVal:function(){this.setTime(this.$element.val())},updateWidget:function(){if(this.$widget===!1)return;var e=this.hour<10?"0"+this.hour:this.hour,t=this.minute<10?"0"+this.minute:this.minute,n=this.second<10?"0"+this.second:this.second;this.showInputs?(this.$widget.find("input.bootstrap-timepicker-hour").val(e),this.$widget.find("input.bootstrap-timepicker-minute").val(t),this.showSeconds&&this.$widget.find("input.bootstrap-timepicker-second").val(n),this.showMeridian&&this.$widget.find("input.bootstrap-timepicker-meridian").val(this.meridian)):(this.$widget.find("span.bootstrap-timepicker-hour").text(e),this.$widget.find("span.bootstrap-timepicker-minute").text(t),this.showSeconds&&this.$widget.find("span.bootstrap-timepicker-second").text(n),this.showMeridian&&this.$widget.find("span.bootstrap-timepicker-meridian").text(this.meridian))},updateFromWidgetInputs:function(){if(this.$widget===!1)return;var t=e("input.bootstrap-timepicker-hour",this.$widget).val()+":"+e("input.bootstrap-timepicker-minute",this.$widget).val()+(this.showSeconds?":"+e("input.bootstrap-timepicker-second",this.$widget).val():"")+(this.showMeridian?" "+e("input.bootstrap-timepicker-meridian",this.$widget).val():"");this.setTime(t)},widgetClick:function(t){t.stopPropagation(),t.preventDefault();var n=e(t.target).closest("a").data("action");n&&this[n]()},widgetKeydown:function(t){var n=e(t.target).closest("input"),r=n.attr("name");switch(t.keyCode){case 9:if(this.showMeridian){if(r==="meridian")return this.hideWidget()}else if(this.showSeconds){if(r==="second")return this.hideWidget()}else if(r==="minute")return this.hideWidget();this.updateFromWidgetInputs();break;case 27:this.hideWidget();break;case 38:t.preventDefault();switch(r){case"hour":this.incrementHour();break;case"minute":this.incrementMinute();break;case"second":this.incrementSecond();break;case"meridian":this.toggleMeridian()}break;case 40:t.preventDefault();switch(r){case"hour":this.decrementHour();break;case"minute":this.decrementMinute();break;case"second":this.decrementSecond();break;case"meridian":this.toggleMeridian()}}}},e.fn.timepicker=function(t){var n=Array.apply(null,arguments);return n.shift(),this.each(function(){var r=e(this),s=r.data("timepicker"),o=typeof t=="object"&&t;s||r.data("timepicker",s=new i(this,e.extend({},e.fn.timepicker.defaults,o,e(this).data()))),typeof t=="string"&&s[t].apply(s,n)})},e.fn.timepicker.defaults={defaultTime:"current",disableFocus:!1,isOpen:!1,minuteStep:15,modalBackdrop:!1,secondStep:15,showSeconds:!1,showInputs:!0,showMeridian:!0,template:"dropdown"},e.fn.timepicker.Constructor=i})(jQuery,window,document); -------------------------------------------------------------------------------- /spec/js/helpers/jasmine-jquery.js: -------------------------------------------------------------------------------- 1 | var readFixtures = function() { 2 | return jasmine.getFixtures().proxyCallTo_('read', arguments) 3 | } 4 | 5 | var preloadFixtures = function() { 6 | jasmine.getFixtures().proxyCallTo_('preload', arguments) 7 | } 8 | 9 | var loadFixtures = function() { 10 | jasmine.getFixtures().proxyCallTo_('load', arguments) 11 | } 12 | 13 | var appendLoadFixtures = function() { 14 | jasmine.getFixtures().proxyCallTo_('appendLoad', arguments) 15 | } 16 | 17 | var setFixtures = function(html) { 18 | jasmine.getFixtures().proxyCallTo_('set', arguments) 19 | } 20 | 21 | var appendSetFixtures = function() { 22 | jasmine.getFixtures().proxyCallTo_('appendSet', arguments) 23 | } 24 | 25 | var sandbox = function(attributes) { 26 | return jasmine.getFixtures().sandbox(attributes) 27 | } 28 | 29 | var spyOnEvent = function(selector, eventName) { 30 | return jasmine.JQuery.events.spyOn(selector, eventName) 31 | } 32 | 33 | var preloadStyleFixtures = function() { 34 | jasmine.getStyleFixtures().proxyCallTo_('preload', arguments) 35 | } 36 | 37 | var loadStyleFixtures = function() { 38 | jasmine.getStyleFixtures().proxyCallTo_('load', arguments) 39 | } 40 | 41 | var appendLoadStyleFixtures = function() { 42 | jasmine.getStyleFixtures().proxyCallTo_('appendLoad', arguments) 43 | } 44 | 45 | var setStyleFixtures = function(html) { 46 | jasmine.getStyleFixtures().proxyCallTo_('set', arguments) 47 | } 48 | 49 | var appendSetStyleFixtures = function(html) { 50 | jasmine.getStyleFixtures().proxyCallTo_('appendSet', arguments) 51 | } 52 | 53 | var loadJSONFixtures = function() { 54 | return jasmine.getJSONFixtures().proxyCallTo_('load', arguments) 55 | } 56 | 57 | var getJSONFixture = function(url) { 58 | return jasmine.getJSONFixtures().proxyCallTo_('read', arguments)[url] 59 | } 60 | 61 | jasmine.spiedEventsKey = function (selector, eventName) { 62 | return [$(selector).selector, eventName].toString() 63 | } 64 | 65 | jasmine.getFixtures = function() { 66 | return jasmine.currentFixtures_ = jasmine.currentFixtures_ || new jasmine.Fixtures() 67 | } 68 | 69 | jasmine.getStyleFixtures = function() { 70 | return jasmine.currentStyleFixtures_ = jasmine.currentStyleFixtures_ || new jasmine.StyleFixtures() 71 | } 72 | 73 | jasmine.Fixtures = function() { 74 | this.containerId = 'jasmine-fixtures' 75 | this.fixturesCache_ = {} 76 | this.fixturesPath = 'spec/js/fixtures' 77 | } 78 | 79 | jasmine.Fixtures.prototype.set = function(html) { 80 | this.cleanUp() 81 | this.createContainer_(html) 82 | } 83 | 84 | jasmine.Fixtures.prototype.appendSet= function(html) { 85 | this.addToContainer_(html) 86 | } 87 | 88 | jasmine.Fixtures.prototype.preload = function() { 89 | this.read.apply(this, arguments) 90 | } 91 | 92 | jasmine.Fixtures.prototype.load = function() { 93 | this.cleanUp() 94 | this.createContainer_(this.read.apply(this, arguments)) 95 | } 96 | 97 | jasmine.Fixtures.prototype.appendLoad = function() { 98 | this.addToContainer_(this.read.apply(this, arguments)) 99 | } 100 | 101 | jasmine.Fixtures.prototype.read = function() { 102 | var htmlChunks = [] 103 | 104 | var fixtureUrls = arguments 105 | for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) { 106 | htmlChunks.push(this.getFixtureHtml_(fixtureUrls[urlIndex])) 107 | } 108 | 109 | return htmlChunks.join('') 110 | } 111 | 112 | jasmine.Fixtures.prototype.clearCache = function() { 113 | this.fixturesCache_ = {} 114 | } 115 | 116 | jasmine.Fixtures.prototype.cleanUp = function() { 117 | $('#' + this.containerId).remove() 118 | } 119 | 120 | jasmine.Fixtures.prototype.sandbox = function(attributes) { 121 | var attributesToSet = attributes || {} 122 | return $('
').attr(attributesToSet) 123 | } 124 | 125 | jasmine.Fixtures.prototype.createContainer_ = function(html) { 126 | var container 127 | if(html instanceof $) { 128 | container = $('
') 129 | container.html(html) 130 | } else { 131 | container = '
' + html + '
' 132 | } 133 | $(document.body).append(container) 134 | } 135 | 136 | jasmine.Fixtures.prototype.addToContainer_ = function(html){ 137 | var container = $(document.body).find('#'+this.containerId).append(html) 138 | if(!container.length){ 139 | this.createContainer_(html) 140 | } 141 | } 142 | 143 | jasmine.Fixtures.prototype.getFixtureHtml_ = function(url) { 144 | if (typeof this.fixturesCache_[url] === 'undefined') { 145 | this.loadFixtureIntoCache_(url) 146 | } 147 | return this.fixturesCache_[url] 148 | } 149 | 150 | jasmine.Fixtures.prototype.loadFixtureIntoCache_ = function(relativeUrl) { 151 | var url = this.makeFixtureUrl_(relativeUrl) 152 | var request = $.ajax({ 153 | type: "GET", 154 | url: url + "?" + new Date().getTime(), 155 | async: false 156 | }) 157 | this.fixturesCache_[relativeUrl] = request.responseText 158 | } 159 | 160 | jasmine.Fixtures.prototype.makeFixtureUrl_ = function(relativeUrl){ 161 | return this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl 162 | } 163 | 164 | jasmine.Fixtures.prototype.proxyCallTo_ = function(methodName, passedArguments) { 165 | return this[methodName].apply(this, passedArguments) 166 | } 167 | 168 | 169 | jasmine.StyleFixtures = function() { 170 | this.fixturesCache_ = {} 171 | this.fixturesNodes_ = [] 172 | this.fixturesPath = 'spec/javascripts/fixtures' 173 | } 174 | 175 | jasmine.StyleFixtures.prototype.set = function(css) { 176 | this.cleanUp() 177 | this.createStyle_(css) 178 | } 179 | 180 | jasmine.StyleFixtures.prototype.appendSet = function(css) { 181 | this.createStyle_(css) 182 | } 183 | 184 | jasmine.StyleFixtures.prototype.preload = function() { 185 | this.read_.apply(this, arguments) 186 | } 187 | 188 | jasmine.StyleFixtures.prototype.load = function() { 189 | this.cleanUp() 190 | this.createStyle_(this.read_.apply(this, arguments)) 191 | } 192 | 193 | jasmine.StyleFixtures.prototype.appendLoad = function() { 194 | this.createStyle_(this.read_.apply(this, arguments)) 195 | } 196 | 197 | jasmine.StyleFixtures.prototype.cleanUp = function() { 198 | while(this.fixturesNodes_.length) { 199 | this.fixturesNodes_.pop().remove() 200 | } 201 | } 202 | 203 | jasmine.StyleFixtures.prototype.createStyle_ = function(html) { 204 | var styleText = $('
').html(html).text(), 205 | style = $('') 206 | 207 | this.fixturesNodes_.push(style) 208 | 209 | $('head').append(style) 210 | } 211 | 212 | jasmine.StyleFixtures.prototype.clearCache = jasmine.Fixtures.prototype.clearCache 213 | 214 | jasmine.StyleFixtures.prototype.read_ = jasmine.Fixtures.prototype.read 215 | 216 | jasmine.StyleFixtures.prototype.getFixtureHtml_ = jasmine.Fixtures.prototype.getFixtureHtml_ 217 | 218 | jasmine.StyleFixtures.prototype.loadFixtureIntoCache_ = jasmine.Fixtures.prototype.loadFixtureIntoCache_ 219 | 220 | jasmine.StyleFixtures.prototype.makeFixtureUrl_ = jasmine.Fixtures.prototype.makeFixtureUrl_ 221 | 222 | jasmine.StyleFixtures.prototype.proxyCallTo_ = jasmine.Fixtures.prototype.proxyCallTo_ 223 | 224 | jasmine.getJSONFixtures = function() { 225 | return jasmine.currentJSONFixtures_ = jasmine.currentJSONFixtures_ || new jasmine.JSONFixtures() 226 | } 227 | 228 | jasmine.JSONFixtures = function() { 229 | this.fixturesCache_ = {} 230 | this.fixturesPath = 'spec/javascripts/fixtures/json' 231 | } 232 | 233 | jasmine.JSONFixtures.prototype.load = function() { 234 | this.read.apply(this, arguments) 235 | return this.fixturesCache_ 236 | } 237 | 238 | jasmine.JSONFixtures.prototype.read = function() { 239 | var fixtureUrls = arguments 240 | for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) { 241 | this.getFixtureData_(fixtureUrls[urlIndex]) 242 | } 243 | return this.fixturesCache_ 244 | } 245 | 246 | jasmine.JSONFixtures.prototype.clearCache = function() { 247 | this.fixturesCache_ = {} 248 | } 249 | 250 | jasmine.JSONFixtures.prototype.getFixtureData_ = function(url) { 251 | this.loadFixtureIntoCache_(url) 252 | return this.fixturesCache_[url] 253 | } 254 | 255 | jasmine.JSONFixtures.prototype.loadFixtureIntoCache_ = function(relativeUrl) { 256 | var self = this 257 | var url = this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl 258 | $.ajax({ 259 | async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded 260 | cache: false, 261 | dataType: 'json', 262 | url: url, 263 | success: function(data) { 264 | self.fixturesCache_[relativeUrl] = data 265 | }, 266 | fail: function(jqXHR, status, errorThrown) { 267 | throw Error('JSONFixture could not be loaded: ' + url + ' (status: ' + status + ', message: ' + errorThrown.message + ')') 268 | } 269 | }) 270 | } 271 | 272 | jasmine.JSONFixtures.prototype.proxyCallTo_ = function(methodName, passedArguments) { 273 | return this[methodName].apply(this, passedArguments) 274 | } 275 | 276 | jasmine.JQuery = function() {} 277 | 278 | jasmine.JQuery.browserTagCaseIndependentHtml = function(html) { 279 | return $('
').append(html).html() 280 | } 281 | 282 | jasmine.JQuery.elementToString = function(element) { 283 | var domEl = $(element).get(0) 284 | if (domEl == undefined || domEl.cloneNode) 285 | return $('
').append($(element).clone()).html() 286 | else 287 | return element.toString() 288 | } 289 | 290 | jasmine.JQuery.matchersClass = {} 291 | 292 | !function(namespace) { 293 | var data = { 294 | spiedEvents: {}, 295 | handlers: [] 296 | } 297 | 298 | namespace.events = { 299 | spyOn: function(selector, eventName) { 300 | var handler = function(e) { 301 | data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] = e 302 | } 303 | $(selector).bind(eventName, handler) 304 | data.handlers.push(handler) 305 | return { 306 | selector: selector, 307 | eventName: eventName, 308 | handler: handler, 309 | reset: function(){ 310 | delete data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] 311 | } 312 | } 313 | }, 314 | 315 | wasTriggered: function(selector, eventName) { 316 | return !!(data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]) 317 | }, 318 | 319 | wasPrevented: function(selector, eventName) { 320 | var e; 321 | return (e = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]) && e.isDefaultPrevented() 322 | }, 323 | 324 | cleanUp: function() { 325 | data.spiedEvents = {} 326 | data.handlers = [] 327 | } 328 | } 329 | }(jasmine.JQuery) 330 | 331 | !function(){ 332 | var jQueryMatchers = { 333 | toHaveClass: function(className) { 334 | return this.actual.hasClass(className) 335 | }, 336 | 337 | toHaveCss: function(css){ 338 | for (var prop in css){ 339 | if (this.actual.css(prop) !== css[prop]) return false 340 | } 341 | return true 342 | }, 343 | 344 | toBeVisible: function() { 345 | return this.actual.is(':visible') 346 | }, 347 | 348 | toBeHidden: function() { 349 | return this.actual.is(':hidden') 350 | }, 351 | 352 | toBeSelected: function() { 353 | return this.actual.is(':selected') 354 | }, 355 | 356 | toBeChecked: function() { 357 | return this.actual.is(':checked') 358 | }, 359 | 360 | toBeEmpty: function() { 361 | return this.actual.is(':empty') 362 | }, 363 | 364 | toExist: function() { 365 | return $(document).find(this.actual).length 366 | }, 367 | 368 | toHaveLength: function(length) { 369 | return this.actual.length === length 370 | }, 371 | 372 | toHaveAttr: function(attributeName, expectedAttributeValue) { 373 | return hasProperty(this.actual.attr(attributeName), expectedAttributeValue) 374 | }, 375 | 376 | toHaveProp: function(propertyName, expectedPropertyValue) { 377 | return hasProperty(this.actual.prop(propertyName), expectedPropertyValue) 378 | }, 379 | 380 | toHaveId: function(id) { 381 | return this.actual.attr('id') == id 382 | }, 383 | 384 | toHaveHtml: function(html) { 385 | return this.actual.html() == jasmine.JQuery.browserTagCaseIndependentHtml(html) 386 | }, 387 | 388 | toContainHtml: function(html){ 389 | var actualHtml = this.actual.html() 390 | var expectedHtml = jasmine.JQuery.browserTagCaseIndependentHtml(html) 391 | return (actualHtml.indexOf(expectedHtml) >= 0) 392 | }, 393 | 394 | toHaveText: function(text) { 395 | var trimmedText = $.trim(this.actual.text()) 396 | if (text && $.isFunction(text.test)) { 397 | return text.test(trimmedText) 398 | } else { 399 | return trimmedText == text 400 | } 401 | }, 402 | 403 | toHaveValue: function(value) { 404 | return this.actual.val() == value 405 | }, 406 | 407 | toHaveData: function(key, expectedValue) { 408 | return hasProperty(this.actual.data(key), expectedValue) 409 | }, 410 | 411 | toBe: function(selector) { 412 | return this.actual.is(selector) 413 | }, 414 | 415 | toContain: function(selector) { 416 | return this.actual.find(selector).length 417 | }, 418 | 419 | toBeDisabled: function(selector){ 420 | return this.actual.is(':disabled') 421 | }, 422 | 423 | toBeFocused: function(selector) { 424 | return this.actual[0] === this.actual[0].ownerDocument.activeElement 425 | }, 426 | 427 | toHandle: function(event) { 428 | 429 | var events = $._data(this.actual.get(0), "events") 430 | 431 | if(!events || !event || typeof event !== "string") { 432 | return false 433 | } 434 | 435 | var namespaces = event.split(".") 436 | var eventType = namespaces.shift() 437 | var sortedNamespaces = namespaces.slice(0).sort() 438 | var namespaceRegExp = new RegExp("(^|\\.)" + sortedNamespaces.join("\\.(?:.*\\.)?") + "(\\.|$)") 439 | 440 | if(events[eventType] && namespaces.length) { 441 | for(var i = 0; i < events[eventType].length; i++) { 442 | var namespace = events[eventType][i].namespace 443 | if(namespaceRegExp.test(namespace)) { 444 | return true 445 | } 446 | } 447 | } else { 448 | return events[eventType] && events[eventType].length > 0 449 | } 450 | }, 451 | 452 | // tests the existence of a specific event binding + handler 453 | toHandleWith: function(eventName, eventHandler) { 454 | var stack = $._data(this.actual.get(0), "events")[eventName] 455 | for (var i = 0; i < stack.length; i++) { 456 | if (stack[i].handler == eventHandler) return true 457 | } 458 | return false 459 | } 460 | } 461 | 462 | var hasProperty = function(actualValue, expectedValue) { 463 | if (expectedValue === undefined) return actualValue !== undefined 464 | return actualValue == expectedValue 465 | } 466 | 467 | var bindMatcher = function(methodName) { 468 | var builtInMatcher = jasmine.Matchers.prototype[methodName] 469 | 470 | jasmine.JQuery.matchersClass[methodName] = function() { 471 | if (this.actual 472 | && (this.actual instanceof $ 473 | || jasmine.isDomNode(this.actual))) { 474 | this.actual = $(this.actual) 475 | var result = jQueryMatchers[methodName].apply(this, arguments) 476 | var element 477 | if (this.actual.get && (element = this.actual.get()[0]) && !$.isWindow(element) && element.tagName !== "HTML") 478 | this.actual = jasmine.JQuery.elementToString(this.actual) 479 | return result 480 | } 481 | 482 | if (builtInMatcher) { 483 | return builtInMatcher.apply(this, arguments) 484 | } 485 | 486 | return false 487 | } 488 | } 489 | 490 | for(var methodName in jQueryMatchers) { 491 | bindMatcher(methodName) 492 | } 493 | }() 494 | 495 | beforeEach(function() { 496 | this.addMatchers(jasmine.JQuery.matchersClass) 497 | this.addMatchers({ 498 | toHaveBeenTriggeredOn: function(selector) { 499 | this.message = function() { 500 | return [ 501 | "Expected event " + this.actual + " to have been triggered on " + selector, 502 | "Expected event " + this.actual + " not to have been triggered on " + selector 503 | ] 504 | } 505 | return jasmine.JQuery.events.wasTriggered(selector, this.actual) 506 | } 507 | }) 508 | this.addMatchers({ 509 | toHaveBeenTriggered: function(){ 510 | var eventName = this.actual.eventName, 511 | selector = this.actual.selector 512 | this.message = function() { 513 | return [ 514 | "Expected event " + eventName + " to have been triggered on " + selector, 515 | "Expected event " + eventName + " not to have been triggered on " + selector 516 | ] 517 | } 518 | return jasmine.JQuery.events.wasTriggered(selector, eventName) 519 | } 520 | }) 521 | this.addMatchers({ 522 | toHaveBeenPreventedOn: function(selector) { 523 | this.message = function() { 524 | return [ 525 | "Expected event " + this.actual + " to have been prevented on " + selector, 526 | "Expected event " + this.actual + " not to have been prevented on " + selector 527 | ] 528 | } 529 | return jasmine.JQuery.events.wasPrevented(selector, this.actual) 530 | } 531 | }) 532 | this.addMatchers({ 533 | toHaveBeenPrevented: function() { 534 | var eventName = this.actual.eventName, 535 | selector = this.actual.selector 536 | this.message = function() { 537 | return [ 538 | "Expected event " + eventName + " to have been prevented on " + selector, 539 | "Expected event " + eventName + " not to have been prevented on " + selector 540 | ] 541 | } 542 | return jasmine.JQuery.events.wasPrevented(selector, eventName) 543 | } 544 | }) 545 | }) 546 | 547 | afterEach(function() { 548 | jasmine.getFixtures().cleanUp() 549 | jasmine.getStyleFixtures().cleanUp() 550 | jasmine.JQuery.events.cleanUp() 551 | }) 552 | 553 | -------------------------------------------------------------------------------- /js/bootstrap-timepicker.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Timepicker Component for Twitter Bootstrap 3 | * 4 | * Copyright 2013 Joris de Wit 5 | * 6 | * Contributors https://github.com/jdewit/bootstrap-timepicker/graphs/contributors 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | ;(function($, window, document, undefined) { 12 | 13 | 'use strict'; // jshint ;_; 14 | 15 | // TIMEPICKER PUBLIC CLASS DEFINITION 16 | var Timepicker = function(element, options) { 17 | this.widget = ''; 18 | this.$element = $(element); 19 | this.defaultTime = options.defaultTime; 20 | this.disableFocus = options.disableFocus; 21 | this.isOpen = options.isOpen; 22 | this.minuteStep = options.minuteStep; 23 | this.modalBackdrop = options.modalBackdrop; 24 | this.secondStep = options.secondStep; 25 | this.showInputs = options.showInputs; 26 | this.showMeridian = options.showMeridian; 27 | this.showSeconds = options.showSeconds; 28 | this.template = options.template; 29 | 30 | this._init(); 31 | }; 32 | 33 | Timepicker.prototype = { 34 | 35 | constructor: Timepicker, 36 | 37 | _init: function() { 38 | var self = this; 39 | 40 | if (this.$element.parent().hasClass('input-append')) { 41 | this.$element.parent('.input-append').find('.add-on').on({ 42 | 'click.timepicker': $.proxy(this.showWidget, this) 43 | }); 44 | this.$element.on({ 45 | 'focus.timepicker': $.proxy(this.highlightUnit, this), 46 | 'click.timepicker': $.proxy(this.highlightUnit, this), 47 | 'keydown.timepicker': $.proxy(this.elementKeydown, this), 48 | 'blur.timepicker': $.proxy(this.blurElement, this) 49 | }); 50 | } else { 51 | if (this.template) { 52 | this.$element.on({ 53 | 'focus.timepicker': $.proxy(this.showWidget, this), 54 | 'click.timepicker': $.proxy(this.showWidget, this), 55 | 'blur.timepicker': $.proxy(this.blurElement, this) 56 | }); 57 | } else { 58 | this.$element.on({ 59 | 'focus.timepicker': $.proxy(this.highlightUnit, this), 60 | 'click.timepicker': $.proxy(this.highlightUnit, this), 61 | 'keydown.timepicker': $.proxy(this.elementKeydown, this), 62 | 'blur.timepicker': $.proxy(this.blurElement, this) 63 | }); 64 | } 65 | } 66 | 67 | if (this.template !== false) { 68 | this.$widget = $(this.getTemplate()).appendTo(this.$element.parents('.bootstrap-timepicker')).on('click', $.proxy(this.widgetClick, this)); 69 | } else { 70 | this.$widget = false; 71 | } 72 | 73 | if (this.showInputs && this.$widget !== false) { 74 | this.$widget.find('input').each(function() { 75 | $(this).on({ 76 | 'click.timepicker': function() { $(this).select(); }, 77 | 'keydown.timepicker': $.proxy(self.widgetKeydown, self) 78 | }); 79 | }); 80 | } 81 | 82 | this.setDefaultTime(this.defaultTime); 83 | }, 84 | 85 | blurElement: function() { 86 | this.highlightedUnit = undefined; 87 | this.updateFromElementVal(); 88 | }, 89 | 90 | decrementHour: function() { 91 | if (this.showMeridian) { 92 | if (this.hour === 1) { 93 | this.hour = 12; 94 | } else if (this.hour === 12) { 95 | this.hour--; 96 | 97 | return this.toggleMeridian(); 98 | } else if (this.hour === 0) { 99 | this.hour = 11; 100 | 101 | return this.toggleMeridian(); 102 | } else { 103 | this.hour--; 104 | } 105 | } else { 106 | if (this.hour === 0) { 107 | this.hour = 23; 108 | } else { 109 | this.hour--; 110 | } 111 | } 112 | this.update(); 113 | }, 114 | 115 | decrementMinute: function(step) { 116 | var newVal; 117 | 118 | if (step) { 119 | newVal = this.minute - step; 120 | } else { 121 | newVal = this.minute - this.minuteStep; 122 | } 123 | 124 | if (newVal < 0) { 125 | this.decrementHour(); 126 | this.minute = newVal + 60; 127 | } else { 128 | this.minute = newVal; 129 | } 130 | this.update(); 131 | }, 132 | 133 | decrementSecond: function() { 134 | var newVal = this.second - this.secondStep; 135 | 136 | if (newVal < 0) { 137 | this.decrementMinute(true); 138 | this.second = newVal + 60; 139 | } else { 140 | this.second = newVal; 141 | } 142 | this.update(); 143 | }, 144 | 145 | elementKeydown: function(e) { 146 | switch (e.keyCode) { 147 | case 9: //tab 148 | this.updateFromElementVal(); 149 | 150 | switch (this.highlightedUnit) { 151 | case 'hour': 152 | e.preventDefault(); 153 | this.highlightNextUnit(); 154 | break; 155 | case 'minute': 156 | if (this.showMeridian || this.showSeconds) { 157 | e.preventDefault(); 158 | this.highlightNextUnit(); 159 | } 160 | break; 161 | case 'second': 162 | if (this.showMeridian) { 163 | e.preventDefault(); 164 | this.highlightNextUnit(); 165 | } 166 | break; 167 | } 168 | break; 169 | case 27: // escape 170 | this.updateFromElementVal(); 171 | break; 172 | case 37: // left arrow 173 | e.preventDefault(); 174 | this.highlightPrevUnit(); 175 | this.updateFromElementVal(); 176 | break; 177 | case 38: // up arrow 178 | e.preventDefault(); 179 | switch (this.highlightedUnit) { 180 | case 'hour': 181 | this.incrementHour(); 182 | this.highlightHour(); 183 | break; 184 | case 'minute': 185 | this.incrementMinute(); 186 | this.highlightMinute(); 187 | break; 188 | case 'second': 189 | this.incrementSecond(); 190 | this.highlightSecond(); 191 | break; 192 | case 'meridian': 193 | this.toggleMeridian(); 194 | this.highlightMeridian(); 195 | break; 196 | } 197 | break; 198 | case 39: // right arrow 199 | e.preventDefault(); 200 | this.updateFromElementVal(); 201 | this.highlightNextUnit(); 202 | break; 203 | case 40: // down arrow 204 | e.preventDefault(); 205 | switch (this.highlightedUnit) { 206 | case 'hour': 207 | this.decrementHour(); 208 | this.highlightHour(); 209 | break; 210 | case 'minute': 211 | this.decrementMinute(); 212 | this.highlightMinute(); 213 | break; 214 | case 'second': 215 | this.decrementSecond(); 216 | this.highlightSecond(); 217 | break; 218 | case 'meridian': 219 | this.toggleMeridian(); 220 | this.highlightMeridian(); 221 | break; 222 | } 223 | break; 224 | } 225 | }, 226 | 227 | formatTime: function(hour, minute, second, meridian) { 228 | hour = hour < 10 ? '0' + hour : hour; 229 | minute = minute < 10 ? '0' + minute : minute; 230 | second = second < 10 ? '0' + second : second; 231 | 232 | return hour + ':' + minute + (this.showSeconds ? ':' + second : '') + (this.showMeridian ? ' ' + meridian : ''); 233 | }, 234 | 235 | getCursorPosition: function() { 236 | var input = this.$element.get(0); 237 | 238 | if ('selectionStart' in input) {// Standard-compliant browsers 239 | 240 | return input.selectionStart; 241 | } else if (document.selection) {// IE fix 242 | input.focus(); 243 | var sel = document.selection.createRange(), 244 | selLen = document.selection.createRange().text.length; 245 | 246 | sel.moveStart('character', - input.value.length); 247 | 248 | return sel.text.length - selLen; 249 | } 250 | }, 251 | 252 | getTemplate: function() { 253 | var template, 254 | hourTemplate, 255 | minuteTemplate, 256 | secondTemplate, 257 | meridianTemplate, 258 | templateContent; 259 | 260 | if (this.showInputs) { 261 | hourTemplate = ''; 262 | minuteTemplate = ''; 263 | secondTemplate = ''; 264 | meridianTemplate = ''; 265 | } else { 266 | hourTemplate = ''; 267 | minuteTemplate = ''; 268 | secondTemplate = ''; 269 | meridianTemplate = ''; 270 | } 271 | 272 | templateContent = ''+ 273 | ''+ 274 | ''+ 275 | ''+ 276 | ''+ 277 | (this.showSeconds ? 278 | ''+ 279 | '' 280 | : '') + 281 | (this.showMeridian ? 282 | ''+ 283 | '' 284 | : '') + 285 | ''+ 286 | ''+ 287 | ' '+ 288 | ''+ 289 | ' '+ 290 | (this.showSeconds ? 291 | ''+ 292 | '' 293 | : '') + 294 | (this.showMeridian ? 295 | ''+ 296 | '' 297 | : '') + 298 | ''+ 299 | ''+ 300 | ''+ 301 | ''+ 302 | ''+ 303 | (this.showSeconds ? 304 | ''+ 305 | '' 306 | : '') + 307 | (this.showMeridian ? 308 | ''+ 309 | '' 310 | : '') + 311 | ''+ 312 | '
   
'+ hourTemplate +':'+ minuteTemplate +':'+ secondTemplate +' '+ meridianTemplate +'
  
'; 313 | 314 | switch(this.template) { 315 | case 'modal': 316 | template = ''; 328 | break; 329 | case 'dropdown': 330 | template = ''; 331 | break; 332 | } 333 | 334 | return template; 335 | }, 336 | 337 | getTime: function() { 338 | return this.formatTime(this.hour, this.minute, this.second, this.meridian); 339 | }, 340 | 341 | hideWidget: function() { 342 | if (this.isOpen === false) { 343 | return; 344 | } 345 | 346 | this.updateFromWidgetInputs(); 347 | 348 | this.$element.trigger({ 349 | 'type': 'hide.timepicker', 350 | 'time': { 351 | 'value': this.getTime(), 352 | 'hours': this.hour, 353 | 'minutes': this.minute, 354 | 'seconds': this.second, 355 | 'meridian': this.meridian 356 | } 357 | }); 358 | 359 | if (this.template === 'modal') { 360 | this.$widget.modal('hide'); 361 | } else { 362 | this.$widget.removeClass('open'); 363 | } 364 | 365 | $(document).off('mousedown.timepicker'); 366 | 367 | this.isOpen = false; 368 | }, 369 | 370 | highlightUnit: function() { 371 | this.position = this.getCursorPosition(); 372 | if (this.position >= 0 && this.position <= 2) { 373 | this.highlightHour(); 374 | } else if (this.position >= 3 && this.position <= 5) { 375 | this.highlightMinute(); 376 | } else if (this.position >= 6 && this.position <= 8) { 377 | if (this.showSeconds) { 378 | this.highlightSecond(); 379 | } else { 380 | this.highlightMeridian(); 381 | } 382 | } else if (this.position >= 9 && this.position <= 11) { 383 | this.highlightMeridian(); 384 | } 385 | }, 386 | 387 | highlightNextUnit: function() { 388 | switch (this.highlightedUnit) { 389 | case 'hour': 390 | this.highlightMinute(); 391 | break; 392 | case 'minute': 393 | if (this.showSeconds) { 394 | this.highlightSecond(); 395 | } else if (this.showMeridian){ 396 | this.highlightMeridian(); 397 | } else { 398 | this.highlightHour(); 399 | } 400 | break; 401 | case 'second': 402 | if (this.showMeridian) { 403 | this.highlightMeridian(); 404 | } else { 405 | this.highlightHour(); 406 | } 407 | break; 408 | case 'meridian': 409 | this.highlightHour(); 410 | break; 411 | } 412 | }, 413 | 414 | highlightPrevUnit: function() { 415 | switch (this.highlightedUnit) { 416 | case 'hour': 417 | this.highlightMeridian(); 418 | break; 419 | case 'minute': 420 | this.highlightHour(); 421 | break; 422 | case 'second': 423 | this.highlightMinute(); 424 | break; 425 | case 'meridian': 426 | if (this.showSeconds) { 427 | this.highlightSecond(); 428 | } else { 429 | this.highlightMinute(); 430 | } 431 | break; 432 | } 433 | }, 434 | 435 | highlightHour: function() { 436 | var $element = this.$element; 437 | 438 | this.highlightedUnit = 'hour'; 439 | 440 | setTimeout(function() { 441 | $element.get(0).setSelectionRange(0,2); 442 | }, 0); 443 | }, 444 | 445 | highlightMinute: function() { 446 | var $element = this.$element; 447 | 448 | this.highlightedUnit = 'minute'; 449 | 450 | setTimeout(function() { 451 | $element.get(0).setSelectionRange(3,5); 452 | }, 0); 453 | }, 454 | 455 | highlightSecond: function() { 456 | var $element = this.$element; 457 | 458 | this.highlightedUnit = 'second'; 459 | 460 | setTimeout(function() { 461 | $element.get(0).setSelectionRange(6,8); 462 | }, 0); 463 | }, 464 | 465 | highlightMeridian: function() { 466 | var $element = this.$element; 467 | 468 | this.highlightedUnit = 'meridian'; 469 | 470 | if (this.showSeconds) { 471 | setTimeout(function() { 472 | $element.get(0).setSelectionRange(9,11); 473 | }, 0); 474 | } else { 475 | setTimeout(function() { 476 | $element.get(0).setSelectionRange(6,8); 477 | }, 0); 478 | } 479 | }, 480 | 481 | incrementHour: function() { 482 | if (this.showMeridian) { 483 | if (this.hour === 11) { 484 | this.hour++; 485 | return this.toggleMeridian(); 486 | } else if (this.hour === 12) { 487 | return this.hour = 1; 488 | } 489 | } 490 | if (this.hour === 23) { 491 | return this.hour = 0; 492 | } 493 | this.hour++; 494 | this.update(); 495 | }, 496 | 497 | incrementMinute: function(step) { 498 | var newVal; 499 | 500 | if (step) { 501 | newVal = this.minute + step; 502 | } else { 503 | newVal = this.minute + this.minuteStep - (this.minute % this.minuteStep); 504 | } 505 | 506 | if (newVal > 59) { 507 | this.incrementHour(); 508 | this.minute = newVal - 60; 509 | } else { 510 | this.minute = newVal; 511 | } 512 | this.update(); 513 | }, 514 | 515 | incrementSecond: function() { 516 | var newVal = this.second + this.secondStep - (this.second % this.secondStep); 517 | 518 | if (newVal > 59) { 519 | this.incrementMinute(true); 520 | this.second = newVal - 60; 521 | } else { 522 | this.second = newVal; 523 | } 524 | this.update(); 525 | }, 526 | 527 | remove: function() { 528 | $('document').off('.timepicker'); 529 | if (this.$widget) { 530 | this.$widget.remove(); 531 | } 532 | delete this.$element.data().timepicker; 533 | }, 534 | 535 | setDefaultTime: function(defaultTime){ 536 | if (!this.$element.val()) { 537 | if (defaultTime === 'current') { 538 | var dTime = new Date(), 539 | hours = dTime.getHours(), 540 | minutes = Math.floor(dTime.getMinutes() / this.minuteStep) * this.minuteStep, 541 | seconds = Math.floor(dTime.getSeconds() / this.secondStep) * this.secondStep, 542 | meridian = 'AM'; 543 | 544 | if (this.showMeridian) { 545 | if (hours === 0) { 546 | hours = 12; 547 | } else if (hours >= 12) { 548 | if (hours > 12) { 549 | hours = hours - 12; 550 | } 551 | meridian = 'PM'; 552 | } else { 553 | meridian = 'AM'; 554 | } 555 | } 556 | 557 | this.hour = hours; 558 | this.minute = minutes; 559 | this.second = seconds; 560 | this.meridian = meridian; 561 | 562 | this.update(); 563 | 564 | } else if (defaultTime === false) { 565 | this.hour = 0; 566 | this.minute = 0; 567 | this.second = 0; 568 | this.meridian = 'AM'; 569 | } else { 570 | this.setTime(defaultTime); 571 | } 572 | } else { 573 | this.updateFromElementVal(); 574 | } 575 | }, 576 | 577 | setTime: function(time) { 578 | var arr, 579 | timeArray; 580 | 581 | if (this.showMeridian) { 582 | arr = time.split(' '); 583 | timeArray = arr[0].split(':'); 584 | this.meridian = arr[1]; 585 | } else { 586 | timeArray = time.split(':'); 587 | } 588 | 589 | this.hour = parseInt(timeArray[0], 10); 590 | this.minute = parseInt(timeArray[1], 10); 591 | this.second = parseInt(timeArray[2], 10); 592 | 593 | if (isNaN(this.hour)) { 594 | this.hour = 0; 595 | } 596 | if (isNaN(this.minute)) { 597 | this.minute = 0; 598 | } 599 | 600 | if (this.showMeridian) { 601 | if (this.hour > 12) { 602 | this.hour = 12; 603 | } else if (this.hour < 1) { 604 | this.hour = 12; 605 | } 606 | 607 | if (this.meridian === 'am' || this.meridian === 'a') { 608 | this.meridian = 'AM'; 609 | } else if (this.meridian === 'pm' || this.meridian === 'p') { 610 | this.meridian = 'PM'; 611 | } 612 | 613 | if (this.meridian !== 'AM' && this.meridian !== 'PM') { 614 | this.meridian = 'AM'; 615 | } 616 | } else { 617 | if (this.hour >= 24) { 618 | this.hour = 23; 619 | } else if (this.hour < 0) { 620 | this.hour = 0; 621 | } 622 | } 623 | 624 | if (this.minute < 0) { 625 | this.minute = 0; 626 | } else if (this.minute >= 60) { 627 | this.minute = 59; 628 | } 629 | 630 | if (this.showSeconds) { 631 | if (isNaN(this.second)) { 632 | this.second = 0; 633 | } else if (this.second < 0) { 634 | this.second = 0; 635 | } else if (this.second >= 60) { 636 | this.second = 59; 637 | } 638 | } 639 | 640 | this.update(); 641 | }, 642 | 643 | showWidget: function() { 644 | if (this.isOpen) { 645 | return; 646 | } 647 | 648 | var self = this; 649 | $(document).on('mousedown.timepicker', function (e) { 650 | // Clicked outside the timepicker, hide it 651 | if ($(e.target).closest('.bootstrap-timepicker-widget').length === 0) { 652 | self.hideWidget(); 653 | } 654 | }); 655 | 656 | this.$element.trigger({ 657 | 'type': 'show.timepicker', 658 | 'time': { 659 | 'value': this.getTime(), 660 | 'hours': this.hour, 661 | 'minutes': this.minute, 662 | 'seconds': this.second, 663 | 'meridian': this.meridian 664 | } 665 | }); 666 | 667 | if (this.disableFocus) { 668 | this.$element.blur(); 669 | } 670 | 671 | this.updateFromElementVal(); 672 | 673 | if (this.template === 'modal') { 674 | this.$widget.modal('show').on('hidden', $.proxy(this.hideWidget, this)); 675 | } else { 676 | if (this.isOpen === false) { 677 | this.$widget.addClass('open'); 678 | } 679 | } 680 | 681 | this.isOpen = true; 682 | }, 683 | 684 | toggleMeridian: function() { 685 | this.meridian = this.meridian === 'AM' ? 'PM' : 'AM'; 686 | this.update(); 687 | }, 688 | 689 | update: function() { 690 | this.$element.trigger({ 691 | 'type': 'changeTime.timepicker', 692 | 'time': { 693 | 'value': this.getTime(), 694 | 'hours': this.hour, 695 | 'minutes': this.minute, 696 | 'seconds': this.second, 697 | 'meridian': this.meridian 698 | } 699 | }); 700 | 701 | this.updateElement(); 702 | this.updateWidget(); 703 | }, 704 | 705 | updateElement: function() { 706 | this.$element.val(this.getTime()); 707 | }, 708 | 709 | updateFromElementVal: function() { 710 | this.setTime(this.$element.val()); 711 | }, 712 | 713 | updateWidget: function() { 714 | if (this.$widget === false) { 715 | return; 716 | } 717 | 718 | var hour = this.hour < 10 ? '0' + this.hour : this.hour, 719 | minute = this.minute < 10 ? '0' + this.minute : this.minute, 720 | second = this.second < 10 ? '0' + this.second : this.second; 721 | 722 | if (this.showInputs) { 723 | this.$widget.find('input.bootstrap-timepicker-hour').val(hour); 724 | this.$widget.find('input.bootstrap-timepicker-minute').val(minute); 725 | 726 | if (this.showSeconds) { 727 | this.$widget.find('input.bootstrap-timepicker-second').val(second); 728 | } 729 | if (this.showMeridian) { 730 | this.$widget.find('input.bootstrap-timepicker-meridian').val(this.meridian); 731 | } 732 | } else { 733 | this.$widget.find('span.bootstrap-timepicker-hour').text(hour); 734 | this.$widget.find('span.bootstrap-timepicker-minute').text(minute); 735 | 736 | if (this.showSeconds) { 737 | this.$widget.find('span.bootstrap-timepicker-second').text(second); 738 | } 739 | if (this.showMeridian) { 740 | this.$widget.find('span.bootstrap-timepicker-meridian').text(this.meridian); 741 | } 742 | } 743 | }, 744 | 745 | updateFromWidgetInputs: function() { 746 | if (this.$widget === false) { 747 | return; 748 | } 749 | var time = $('input.bootstrap-timepicker-hour', this.$widget).val() + ':' + 750 | $('input.bootstrap-timepicker-minute', this.$widget).val() + 751 | (this.showSeconds ? ':' + $('input.bootstrap-timepicker-second', this.$widget).val() : '') + 752 | (this.showMeridian ? ' ' + $('input.bootstrap-timepicker-meridian', this.$widget).val() : ''); 753 | 754 | this.setTime(time); 755 | }, 756 | 757 | widgetClick: function(e) { 758 | e.stopPropagation(); 759 | e.preventDefault(); 760 | 761 | var action = $(e.target).closest('a').data('action'); 762 | if (action) { 763 | this[action](); 764 | } 765 | }, 766 | 767 | widgetKeydown: function(e) { 768 | var $input = $(e.target).closest('input'), 769 | name = $input.attr('name'); 770 | 771 | switch (e.keyCode) { 772 | case 9: //tab 773 | if (this.showMeridian) { 774 | if (name === 'meridian') { 775 | return this.hideWidget(); 776 | } 777 | } else { 778 | if (this.showSeconds) { 779 | if (name === 'second') { 780 | return this.hideWidget(); 781 | } 782 | } else { 783 | if (name === 'minute') { 784 | return this.hideWidget(); 785 | } 786 | } 787 | } 788 | 789 | this.updateFromWidgetInputs(); 790 | break; 791 | case 27: // escape 792 | this.hideWidget(); 793 | break; 794 | case 38: // up arrow 795 | e.preventDefault(); 796 | switch (name) { 797 | case 'hour': 798 | this.incrementHour(); 799 | break; 800 | case 'minute': 801 | this.incrementMinute(); 802 | break; 803 | case 'second': 804 | this.incrementSecond(); 805 | break; 806 | case 'meridian': 807 | this.toggleMeridian(); 808 | break; 809 | } 810 | break; 811 | case 40: // down arrow 812 | e.preventDefault(); 813 | switch (name) { 814 | case 'hour': 815 | this.decrementHour(); 816 | break; 817 | case 'minute': 818 | this.decrementMinute(); 819 | break; 820 | case 'second': 821 | this.decrementSecond(); 822 | break; 823 | case 'meridian': 824 | this.toggleMeridian(); 825 | break; 826 | } 827 | break; 828 | } 829 | } 830 | }; 831 | 832 | 833 | //TIMEPICKER PLUGIN DEFINITION 834 | $.fn.timepicker = function(option) { 835 | var args = Array.apply(null, arguments); 836 | args.shift(); 837 | return this.each(function() { 838 | var $this = $(this), 839 | data = $this.data('timepicker'), 840 | options = typeof option === 'object' && option; 841 | 842 | if (!data) { 843 | $this.data('timepicker', (data = new Timepicker(this, $.extend({}, $.fn.timepicker.defaults, options, $(this).data())))); 844 | } 845 | 846 | if (typeof option === 'string') { 847 | data[option].apply(data, args); 848 | } 849 | }); 850 | }; 851 | 852 | $.fn.timepicker.defaults = { 853 | defaultTime: 'current', 854 | disableFocus: false, 855 | isOpen: false, 856 | minuteStep: 15, 857 | modalBackdrop: false, 858 | secondStep: 15, 859 | showSeconds: false, 860 | showInputs: true, 861 | showMeridian: true, 862 | template: 'dropdown' 863 | }; 864 | 865 | $.fn.timepicker.Constructor = Timepicker; 866 | 867 | })(jQuery, window, document); 868 | --------------------------------------------------------------------------------