├── 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 [](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 |
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?' | | ':"")+"
"+""+"| "+t+" | "+': | '+""+n+" | "+(this.showSeconds?': | '+r+" | ":"")+(this.showMeridian?' | '+i+" | ":"")+"
"+""+' | '+' | '+' | '+(this.showSeconds?' | | ':"")+(this.showMeridian?' | | ':"")+"
"+"
";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 | '| '+ hourTemplate +' | '+
288 | ': | '+
289 | ''+ minuteTemplate +' | '+
290 | (this.showSeconds ?
291 | ': | '+
292 | ''+ secondTemplate +' | '
293 | : '') +
294 | (this.showMeridian ?
295 | ' | '+
296 | ''+ meridianTemplate +' | '
297 | : '') +
298 | '
'+
299 | ''+
300 | ' | '+
301 | ' | '+
302 | ' | '+
303 | (this.showSeconds ?
304 | ' | '+
305 | ' | '
306 | : '') +
307 | (this.showMeridian ?
308 | ' | '+
309 | ' | '
310 | : '') +
311 | '
'+
312 | '
';
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 |
--------------------------------------------------------------------------------