├── .gitignore ├── LICENSE.md ├── README.md ├── bower.json ├── demoApp ├── app.css ├── demoApp.js └── index.html ├── dist ├── md-time-picker.css └── md-time-picker.js ├── img ├── modal-hour-12.PNG ├── modal-hour-24.PNG ├── modal-minute.PNG ├── time-picker-12.PNG └── time-picker-24.PNG ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # hidden files 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Matthew Bajorek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/angular-material-time-picker.svg)](https://badge.fury.io/js/angular-material-time-picker) 2 | 3 | # Looking for maintainers 4 | There are people still using angular-material-time-picker in their projects and we have a few issues open + pull requests. 5 | This projects is open to new lead maintainers. Please contact the current maintainer. 6 | 7 | # Angular Material Time Picker (md-time-picker) 8 | 9 | Angular Material has a date picker, but no time picker. This module is an effort to implement a time picker in [Angular Material](https://material.angularjs.org/latest/). 10 | 11 | The modal popup is based off [mdPickers](https://github.com/alenaksu/mdPickers), but with modifications for 24 hour time, removal of dependency on momentjs, and improvements in usability. 12 | 13 | The "time input field" is made up of three individual text inputs for hour, minute, and meridiem. This allows better cross browser support, especially for older browsers that do not support time input. 14 | 15 | * [Screenshots](#screenshots) 16 | * [Demo](#demo) 17 | * [Requirements](#requirements) 18 | * [Installation](#installation) 19 | * [Usage](#usage) 20 | * [License](#license) 21 | 22 | ## Screenshots 23 | 24 | #### On page 25 | 26 | 12 hour | 24 hour 27 | --------|-------- 28 | ![12 Hour](/img/time-picker-12.PNG?raw=true) | ![24 hour](/img/time-picker-24.PNG?raw=true) 29 | 30 | #### Modal 31 | 32 | 12 Hour | 24 Hour | Minute 33 | --------|---------|-------- 34 | ![12Hour](/img/modal-hour-12.PNG?raw=true) | ![24 hour](/img/modal-hour-24.PNG?raw=true) | ![Minute](/img/modal-minute.PNG?raw=true) 35 | 36 | ## Demo 37 | 38 | A live [Codepen demo](http://codepen.io/mattbajorek/pen/OpGVyv). The same html/css file is also included in the `demoApp` folder. 39 | 40 | ## Requirements 41 | 42 | * 1.5.0 < AngularJS < 2 (angular-messages, angular-animate, angular-aria) 43 | * Angular Material > 1 44 | * Material Icons 45 | 46 | ## Installation 47 | 48 | #### Using npm 49 | 50 | ``` 51 | npm install angular-material-time-picker --save 52 | ``` 53 | 54 | You may use Webpack to inject this module into your application. 55 | 56 | #### ES5 57 | ```javascript 58 | require('angular-material-time-picker/dist/md-time-picker.css'); 59 | var ngTimePicker = require('angular-material-time-picker'); 60 | angular.module('myApp', [ngTimePicker]); 61 | ``` 62 | 63 | #### ES6 64 | ```javascript 65 | import 'angular-material-time-picker/dist/md-time-picker.css'; 66 | import ngTimePicker from 'angular-material-time-picker'; 67 | angular.module('myApp', [ngTimePicker]); 68 | ``` 69 | 70 | ## Usage 71 | 72 | **Example Controller** 73 | 74 | ```javascript 75 | angular.module('demoApp') 76 | 77 | .controller('demoAppController', ['$scope','$mdpTimePicker', function($scope) { 78 | 79 | // Model bound to input fields and modal 80 | $scope.time = new Date(); 81 | 82 | // Optional message to display below each input field 83 | $scope.message = { 84 | hour: 'Hour is required', 85 | minute: 'Minute is required', 86 | meridiem: 'Meridiem is required' 87 | } 88 | 89 | $scope.readonly = false; 90 | 91 | $scope.required = true; 92 | 93 | }]); 94 | ``` 95 | 96 | **Example Template** 97 | 98 | ```html 99 | 100 | ``` 101 | **Optional Attributes** 102 | * **message** (takes an object with keys: hour, minute, and meridiem) 103 | * **no-meridiem** (changes time picker to 24 hour time, 12 hour time is default) 104 | * **no-auto-switch** (stops modal from switching to minutes automatically after an hour is pressed) 105 | * **read-only** (set read only on input. Accepts true or false) 106 | * **mandatory** (input will be validated as required if true. Accepts true or false). 107 | 108 | ## License 109 | 110 | This software is provided free of charge and without restriction under the [MIT License](LICENSE.md) 111 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-material-time-picker", 3 | "description": "Material design time picker for 12 hour and 24 hour time", 4 | "main": [ 5 | "dist/md-time-picker.js", 6 | "dist/md-time-picker.css" 7 | ], 8 | "authors": [ 9 | "Matt Bajorek" 10 | ], 11 | "license": "MIT", 12 | "keywords": [ 13 | "angular", 14 | "material", 15 | "time", 16 | "picker", 17 | "material", 18 | "design" 19 | ], 20 | "homepage": "https://github.com/classlinkinc/angular-material-time-picker", 21 | "private": true, 22 | "ignore": [ 23 | "**/.*", 24 | "node_modules", 25 | "bower_components", 26 | "test", 27 | "tests" 28 | ] 29 | } -------------------------------------------------------------------------------- /demoApp/app.css: -------------------------------------------------------------------------------- 1 | body, 2 | md-content { 3 | background-color: #f5f5f5 !important; 4 | } 5 | 6 | body > md-toolbar { 7 | z-index: 3; 8 | } 9 | 10 | md-toolbar.md-table-toolbar.alternate { 11 | color: #1e88e5; 12 | background-color: #e3f2fd; 13 | } 14 | 15 | md-toolbar.md-table-toolbar.alternate .md-toolbar-tools { 16 | font-size: 16px; 17 | } 18 | 19 | md-card { 20 | padding: 8px 8px 8px 24px; 21 | } 22 | -------------------------------------------------------------------------------- /demoApp/demoApp.js: -------------------------------------------------------------------------------- 1 | angular.module('demoApp', ['ngMaterial','md.time.picker']) 2 | 3 | .config(['$mdThemingProvider', function ($mdThemingProvider) { 4 | 'use strict'; 5 | 6 | $mdThemingProvider.theme('default') 7 | .primaryPalette('blue'); 8 | }]) 9 | 10 | .controller('demoAppController', ['$scope','$mdpTimePicker', function($scope) { 11 | 12 | $scope.time = { 13 | twelve: new Date(), 14 | twentyfour: new Date() 15 | }; 16 | 17 | $scope.message = { 18 | hour: 'Hour is required', 19 | minute: 'Minute is required', 20 | meridiem: 'Meridiem is required' 21 | } 22 | 23 | }]); 24 | -------------------------------------------------------------------------------- /demoApp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Material Design Time Picker 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
Material Design Time Picker
25 |
26 |
27 | 28 | 29 | 30 | 31 | 12 Hour Time 32 | 33 | {{'Hours: ' + time.twelve.getHours()}}
34 | {{'Minutes: ' + time.twelve.getMinutes()}} 35 |
36 | 37 | 38 | 24 Hour Time 39 | 40 | {{'Hours: ' + time.twentyfour.getHours()}}
41 | {{'Minutes: ' + time.twentyfour.getMinutes()}} 42 |
43 | 44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /dist/md-time-picker.css: -------------------------------------------------------------------------------- 1 | ng-form[name=timeForm] { 2 | display: block; 3 | position: relative; 4 | vertical-align: middle; 5 | } 6 | ng-form[name=timeForm]>* { 7 | display: inline-block; 8 | } 9 | ng-form[name=timeForm] input, 10 | ng-form[name=timeForm] md-input-container { 11 | text-align: center; 12 | padding: 0; 13 | } 14 | ng-form[name=timeForm] md-input-container .md-errors-spacer { 15 | min-height: 0; 16 | } 17 | ng-form[name=timeForm] div.time-error-messages { 18 | position: absolute; 19 | top: 30px; 20 | } 21 | md-hours-minutes { 22 | width: 60px; 23 | } 24 | md-meridiem { 25 | max-width: 100px; 26 | } 27 | md-meridiem .md-select-value { 28 | padding: 2px; 29 | min-height: 25px; 30 | } 31 | md-meridiem .md-select-value.md-select-placeholder { 32 | padding-left: 5px; 33 | } 34 | md-meridiem .md-select-value .md-select-icon { 35 | width: 15px; 36 | } 37 | .time-colon { 38 | position: absolute; 39 | top: 23px; 40 | padding-left: 4px; 41 | } 42 | .md-up-arrow, .md-down-arrow { 43 | position: absolute; 44 | height: 0; 45 | top: 7px; 46 | right: 2px; 47 | color: rgba(0,0,0,0.38); 48 | -webkit-user-select: none; /* Chrome all / Safari all */ 49 | -moz-user-select: none; /* Firefox all */ 50 | -ms-user-select: none; /* IE 10+ */ 51 | user-select: none; /* Likely future */ 52 | } 53 | .md-up-arrow:focus,.md-down-arrow:focus { 54 | outline: none; 55 | } 56 | .md-up-arrow:hover,.md-down-arrow:hover { 57 | color: black; 58 | cursor: pointer; 59 | } 60 | .md-up-arrow:after, .md-down-arrow:after{ 61 | display: block; 62 | position: relative; 63 | speak: none; 64 | font-size: 13px; 65 | transform: scaleY(.5) scaleX(1); 66 | } 67 | .md-up-arrow:after{ 68 | content: '\25B2'; 69 | top: -6px; 70 | } 71 | .md-down-arrow:after{ 72 | content: '\25BC'; 73 | top: 3px; 74 | } 75 | 76 | 77 | .mdp-animate-next { 78 | opacity: 0; 79 | -webkit-transform: translate3d(50%, 0, 1px); 80 | transform: translate3d(50%, 0, 1px); 81 | } 82 | .mdp-animate-next-remove { 83 | -webkit-transition: all 0.5s cubic-bezier(0.35, 0, 0.25, 1); 84 | transition: all 0.5s cubic-bezier(0.35, 0, 0.25, 1); 85 | opacity: 0; 86 | -webkit-transform: translate3d(50%, 0, 1px); 87 | transform: translate3d(50%, 0, 1px); 88 | } 89 | .mdp-animate-next-remove-active { 90 | opacity: 1; 91 | -webkit-transform: translate3d(0, 0, 1px); 92 | transform: translate3d(0, 0, 1px); 93 | } 94 | .mdp-animate-prev { 95 | opacity: 0; 96 | -webkit-transform: translate3d(-50%, 0, 1px); 97 | transform: translate3d(-50%, 0, 1px); 98 | } 99 | .mdp-animate-prev-remove { 100 | -webkit-transition: all 0.3s cubic-bezier(0.35, 0, 0.25, 1); 101 | transition: all 0.3s cubic-bezier(0.35, 0, 0.25, 1); 102 | opacity: 0; 103 | -webkit-transform: translate3d(-50%, 0, 1px); 104 | transform: translate3d(-50%, 0, 1px); 105 | } 106 | .mdp-animate-prev-remove-active { 107 | opacity: 1; 108 | -webkit-transform: translate3d(0, 0, 1px); 109 | transform: translate3d(0, 0, 1px); 110 | } 111 | @-webkit-keyframes mdp-animation-bounce { 112 | from { 113 | opacity: 0; 114 | -webkit-transform: scale(0.95); 115 | transform: scale(0.95); 116 | } 117 | 70% { 118 | opacity: 1; 119 | -webkit-transform: scale(1.05); 120 | transform: scale(1.05); 121 | } 122 | to { 123 | -webkit-transform: scale(1); 124 | transform: scale(1); 125 | } 126 | } 127 | @keyframes mdp-animation-bounce { 128 | from { 129 | opacity: 0; 130 | -webkit-transform: scale(0.95); 131 | transform: scale(0.95); 132 | } 133 | 70% { 134 | opacity: 1; 135 | -webkit-transform: scale(1.05); 136 | transform: scale(1.05); 137 | } 138 | to { 139 | -webkit-transform: scale(1); 140 | transform: scale(1); 141 | } 142 | } 143 | .mdp-animation-zoom.ng-enter { 144 | -webkit-transition: all 0.3s cubic-bezier(0.35, 0, 0.25, 1); 145 | transition: all 0.3s cubic-bezier(0.35, 0, 0.25, 1); 146 | -webkit-animation-duration: 0.3s; 147 | animation-duration: 0.3s; 148 | -webkit-animation-name: mdp-animation-bounce; 149 | animation-name: mdp-animation-bounce; 150 | } 151 | .mdp-datepicker { 152 | max-height: initial; 153 | min-width: 234px; 154 | } 155 | .mdp-datepicker * { 156 | outline: 0; 157 | } 158 | .mdp-datepicker .md-actions { 159 | width: 100%; 160 | padding: 0px 5px; 161 | } 162 | .mdp-calendar-week-days { 163 | font-size: 0.75rem; 164 | opacity: 0.6; 165 | } 166 | .mdp-calendar-week-days > * { 167 | width: 32px; 168 | height: 32px; 169 | line-height: 32px; 170 | margin: 0 5px; 171 | padding: 0; 172 | min-width: 0px; 173 | min-height: 0px; 174 | box-shadow: none !important; 175 | background-color: transparent; 176 | } 177 | .mdp-calendar-days { 178 | font-size: 1rem; 179 | max-width: 100%; 180 | } 181 | .mdp-calendar-days .md-button, 182 | .mdp-calendar-days .mdp-day-placeholder { 183 | width: 32px; 184 | height: 32px; 185 | margin: 0 5px; 186 | } 187 | .mdp-calendar-days .md-button { 188 | cursor: pointer; 189 | border-radius: 50%; 190 | font-size: 12px; 191 | width: 32px; 192 | height: 32px; 193 | line-height: 32px; 194 | padding: 0; 195 | min-width: 0px; 196 | min-height: 0px; 197 | box-shadow: none !important; 198 | background-color: transparent; 199 | } 200 | .mdp-calendar-days .md-button[disabled]:not(.md-accent) { 201 | background-color: transparent !important; 202 | } 203 | .mdp-calendar-monthyear { 204 | font-size: 0.8125rem; 205 | font-weight: bold; 206 | line-height: 32px; 207 | min-height: 32px; 208 | } 209 | .mdp-datepicker-date, 210 | .mdp-datepicker-day, 211 | .mdp-datepicker-dow, 212 | .mdp-datepicker-month { 213 | font-size: 1.8rem; 214 | opacity: 0.6; 215 | } 216 | .mdp-datepicker-date:not(.active), 217 | .mdp-datepicker-day:not(.active), 218 | .mdp-datepicker-dow:not(.active), 219 | .mdp-datepicker-month:not(.active) { 220 | cursor: pointer; 221 | } 222 | .mdp-datepicker-date.active, 223 | .mdp-datepicker-day.active, 224 | .mdp-datepicker-dow.active, 225 | .mdp-datepicker-month.active, 226 | .mdp-datepicker-date:hover, 227 | .mdp-datepicker-day:hover, 228 | .mdp-datepicker-dow:hover, 229 | .mdp-datepicker-month:hover { 230 | opacity: 1; 231 | } 232 | .mdp-datepicker-year { 233 | font-size: 0.9rem; 234 | opacity: 0.6; 235 | padding: 0; 236 | margin: 0; 237 | } 238 | .mdp-datepicker-year:not(.active) { 239 | cursor: pointer; 240 | } 241 | .mdp-datepicker-year.active, 242 | .mdp-datepicker-year:hover { 243 | opacity: 1; 244 | } 245 | .mdp-datepicker-select-year { 246 | height: 232px; 247 | } 248 | .mdp-datepicker-select-year .repeated-year { 249 | text-align: center; 250 | } 251 | .mdp-datepicker-select-year .repeated-year .md-button { 252 | width: 100%; 253 | margin: 0; 254 | border-radius: 0; 255 | padding: 0; 256 | font-size: 1.0rem; 257 | line-height: 42px; 258 | } 259 | .mdp-datepicker-select-year .repeated-year .md-button.current { 260 | font-size: 1.8rem; 261 | line-height: 42px; 262 | } 263 | .mdp-datepicker-select-year .md-virtual-repeat-container, 264 | .mdp-datepicker-select-year .md-virtual-repeat-offsetter, 265 | .mdp-datepicker-select-year .md-virtual-repeat-scroller { 266 | height: 100%; 267 | width: 100%; 268 | } 269 | mdp-date-picker > md-input-container.md-has-icon { 270 | padding-left: 40px; 271 | } 272 | mdp-date-picker .md-button.md-icon-button { 273 | margin: 18px 0 0 0; 274 | } 275 | .mdp-datepicker:not(.portrait) .mdp-datepicker-select-year { 276 | width: 309px; 277 | } 278 | .mdp-datepicker:not(.portrait) .mdp-calendar { 279 | margin-right: 5px; 280 | width: 294px; 281 | margin-left: 10px; 282 | } 283 | .mdp-datepicker:not(.portrait) .mdp-datepicker-date-wrapper { 284 | width: 150px; 285 | } 286 | .mdp-datepicker:not(.portrait) .mdp-datepicker-dow { 287 | width: 100%; 288 | display: block; 289 | } 290 | .mdp-datepicker:not(.portrait) .mdp-calendar-week-days > *, 291 | .mdp-datepicker:not(.portrait) .mdp-calendar-days > * { 292 | width: 42px; 293 | } 294 | .mdp-datepicker .mdp-datepicker-date-wrapper { 295 | padding: 16px 35px 16px 16px; 296 | } 297 | .mdp-datepicker md-dialog-content { 298 | overflow: hidden; 299 | padding: 0px; 300 | } 301 | .mdp-datepicker md-dialog-content .mdp-calendar { 302 | width: 294px; 303 | overflow-x: hidden; 304 | } 305 | .mdp-datepicker.portrait { 306 | max-width: 234px; 307 | } 308 | .mdp-datepicker.portrait .mdp-calendar { 309 | text-align: center; 310 | width: 100%; 311 | } 312 | .mdp-datepicker.portrait .mdp-datepicker-select-year { 313 | height: 252px; 314 | } 315 | .mdp-datepicker.portrait md-dialog-content > * { 316 | width: 100%; 317 | } 318 | .mdp-datepicker.portrait .mdp-calendar-week-days, 319 | .mdp-datepicker.portrait .mdp-calendar-days, 320 | .mdp-datepicker.portrait .md-actions { 321 | padding: 0 5px; 322 | } 323 | .mdp-datepicker.portrait .md-actions { 324 | margin-top: 20px; 325 | } 326 | .mdp-datepicker.portrait .mdp-calendar-week-days > *, 327 | .mdp-datepicker.portrait .mdp-calendar-days > * { 328 | width: 32px; 329 | } 330 | mdp-calendar { 331 | display: block; 332 | } 333 | .mdp-calendar-week-days { 334 | width: 100%; 335 | } 336 | .mdp-calendar-week-days > * { 337 | margin: 0; 338 | } 339 | .mdp-calendar-days .md-button, 340 | .mdp-calendar-days .mdp-day-placeholder { 341 | margin: 0; 342 | } 343 | .mdp-clock { 344 | width: 200px; 345 | height: 200px; 346 | border-radius: 50%; 347 | cursor: pointer; 348 | padding: 24px; 349 | background: #ededed; 350 | } 351 | .mdp-clock .md-button { 352 | box-shadow: none !important; 353 | background-color: transparent; 354 | display: block; 355 | position: absolute; 356 | min-height: 32px; 357 | width: 32px; 358 | height: 32px; 359 | font-size: 12px; 360 | line-height: 32px; 361 | margin: 0; 362 | padding: 0; 363 | -webkit-transform: translate(-50%, -50%); 364 | transform: translate(-50%, -50%); 365 | } 366 | .mdp-clock .mdp-clock-container { 367 | width: 100%; 368 | height: 100%; 369 | position: relative; 370 | display: block; 371 | } 372 | .mdp-clock .mdp-pointer { 373 | min-height: 0px; 374 | width: 1px; 375 | height: 50%; 376 | position: absolute; 377 | left: 0; 378 | right: 0; 379 | bottom: 0; 380 | margin: 0 auto; 381 | -webkit-transform-origin: top center; 382 | transform-origin: top center; 383 | z-index: 0; 384 | pointer-events: none; 385 | } 386 | .mdp-clock .mdp-clock-center { 387 | min-height: 0px; 388 | height: 6px; 389 | width: 6px; 390 | position: absolute; 391 | left: 0; 392 | right: 0; 393 | top: 0; 394 | bottom: 0; 395 | margin: auto; 396 | border-radius: 50%; 397 | } 398 | .mdp-clock .md-button.mdp-clock-selected { 399 | position: absolute; 400 | border-radius: 50%; 401 | width: 8px; 402 | height: 8px; 403 | bottom: -8px; 404 | left: 0px; 405 | min-width: 0; 406 | min-height: 0; 407 | pointer-events: none; 408 | } 409 | .mdp-timepicker .mdp-clock-switch-container { 410 | padding: 20px; 411 | width: 309px; 412 | } 413 | .mdp-timepicker .mdp-timepicker-time { 414 | padding: 15px; 415 | } 416 | .mdp-timepicker .mdp-timepicker-selected-time { 417 | font-size: 3rem; 418 | } 419 | .mdp-timepicker:not(.portrait) .mdp-timepicker-time { 420 | width: 160px; 421 | } 422 | .mdp-timepicker.portrait .mdp-timepicker-selected-time { 423 | font-size: 4rem; 424 | margin-right: 1.5rem; 425 | } 426 | mdp-time-picker md-input-container.md-has-icon { 427 | padding-left: 40px; 428 | } 429 | mdp-time-picker .md-button.md-icon-button { 430 | margin: 18px 0 0 0; 431 | } 432 | .mdp-timepicker-selected-time > span, 433 | .mdp-timepicker-selected-ampm > span { 434 | outline: 0; 435 | opacity: 0.6; 436 | } 437 | .mdp-timepicker-selected-time > span:not(.active), 438 | .mdp-timepicker-selected-ampm > span:not(.active) { 439 | cursor: pointer; 440 | } 441 | .mdp-timepicker-selected-time > span.active, 442 | .mdp-timepicker-selected-ampm > span.active { 443 | opacity: 1; 444 | } 445 | .mdp-clock-deg0 { 446 | top: 0%; 447 | left: 50%; 448 | } 449 | .mdp-clock-deg15 { 450 | top: 1.70370869%; 451 | left: 62.94095226%; 452 | } 453 | .mdp-clock-deg30 { 454 | top: 6.69872981%; 455 | left: 75%; 456 | } 457 | .mdp-clock-deg45 { 458 | top: 14.64466094%; 459 | left: 85.35533905%; 460 | } 461 | .mdp-clock-deg60 { 462 | top: 25%; 463 | left: 93.30127019%; 464 | } 465 | .mdp-clock-deg75 { 466 | top: 37.05904774%; 467 | left: 98.29629131%; 468 | } 469 | .mdp-clock-deg90 { 470 | top: 50%; 471 | left: 100%; 472 | } 473 | .mdp-clock-deg105 { 474 | top: 62.94095226%; 475 | left: 98.29629131%; 476 | } 477 | .mdp-clock-deg120 { 478 | top: 75%; 479 | left: 93.30127019%; 480 | } 481 | .mdp-clock-deg135 { 482 | top: 85.35533906%; 483 | left: 85.35533906%; 484 | } 485 | .mdp-clock-deg150 { 486 | top: 93.30127019%; 487 | left: 75%; 488 | } 489 | .mdp-clock-deg165 { 490 | top: 98.29629131%; 491 | left: 62.94095226%; 492 | } 493 | .mdp-clock-deg180 { 494 | top: 100%; 495 | left: 50%; 496 | } 497 | .mdp-clock-deg195 { 498 | top: 98.29629131%; 499 | left: 37.05904774%; 500 | } 501 | .mdp-clock-deg210 { 502 | top: 93.30127019%; 503 | left: 25%; 504 | } 505 | .mdp-clock-deg225 { 506 | top: 85.35533906%; 507 | left: 14.64466094%; 508 | } 509 | .mdp-clock-deg240 { 510 | top: 75%; 511 | left: 6.69872981%; 512 | } 513 | .mdp-clock-deg255 { 514 | top: 62.94095226%; 515 | left: 1.703708686%; 516 | } 517 | .mdp-clock-deg270 { 518 | top: 50%; 519 | left: 0%; 520 | } 521 | .mdp-clock-deg285 { 522 | top: 37.05904774%; 523 | left: 1.703708686%; 524 | } 525 | .mdp-clock-deg300 { 526 | top: 25%; 527 | left: 6.69872981%; 528 | } 529 | .mdp-clock-deg315 { 530 | top: 14.64466094%; 531 | left: 14.64466094%; 532 | } 533 | .mdp-clock-deg330 { 534 | top: 6.69872981%; 535 | left: 25%; 536 | } 537 | .mdp-clock-deg345 { 538 | top: 1.703708686%; 539 | left: 37.05904774%; 540 | } 541 | .mdp-clock-deg360 { 542 | top: 0%; 543 | left: 50%; 544 | } 545 | -------------------------------------------------------------------------------- /dist/md-time-picker.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Angular Material Time Picker 3 | * https://github.com/classlinkinc/angular-material-time-picker 4 | * @license MIT 5 | * v1.0.1 6 | */ 7 | (function(window, angular, undefined) { 8 | 'use strict'; 9 | 10 | function increase(value, min, max, type) { 11 | var num = parseInt(value); 12 | if (isNaN(num) || num === max) 13 | num = min; 14 | else 15 | num++; 16 | if (type === 'MM') 17 | return format(num); 18 | return String(num); 19 | } 20 | 21 | function decrease(value, min, max, type) { 22 | var num = parseInt(value); 23 | if (isNaN(num) || num === min) 24 | num = max; 25 | else 26 | num--; 27 | if (type === 'MM') 28 | return format(num); 29 | return String(num); 30 | } 31 | 32 | function format(num) { 33 | if (num < 10) 34 | return '0' + String(num); 35 | return String(num); 36 | } 37 | 38 | function handleInput(value, max, blur, type) { 39 | var num = parseInt(value); 40 | if (type === 'HH' && num === 0) { 41 | if (num === 0) { 42 | return String(num); 43 | } 44 | return; 45 | } 46 | if (num > max) 47 | return String(num)[0]; 48 | else if (!isNaN(num)) { 49 | if (value.length === 2 || (blur && type === 'MM')) 50 | return format(num); 51 | return String(num); 52 | } 53 | } 54 | 55 | angular.module('md.time.picker', ['ngMessages']) 56 | 57 | .directive('mdHoursMinutes', function() { 58 | 59 | return { 60 | 61 | restrict: 'E', 62 | scope: { 63 | type: '@', 64 | message: '@', 65 | ngModel: '=', 66 | readOnly: '<', // true or false 67 | mandatory: '<' // true or false 68 | }, 69 | template: '' + 70 | '' + 80 | '' + 81 | '' + 82 | '' + 85 | '', 86 | controller: ["$scope", "$rootScope", function($scope, $rootScope) { 87 | 88 | if ($scope.type === "HH") { 89 | if ($scope.$parent.noMeridiem) { 90 | $scope.min = 0; 91 | $scope.max = 23; 92 | } else { 93 | $scope.min = 1; 94 | $scope.max = 12; 95 | } 96 | } else { 97 | $scope.min = 0; 98 | $scope.max = 59; 99 | } 100 | 101 | function setTime() { 102 | if ($scope.type === "HH") { 103 | var hours = ''; 104 | try { 105 | hours = $scope.$parent.ngModel.getHours(); 106 | } catch (e) { 107 | // leave hours empty to allow empty values 108 | } 109 | 110 | if (!$scope.$parent.noMeridiem) { 111 | if (hours > 12) 112 | hours -= 12; 113 | else if (hours === 0) 114 | hours += 12; 115 | } 116 | $scope.time.HH = String(hours); 117 | } else 118 | if ($scope.$parent.ngModel) { 119 | $scope.time.MM = format($scope.$parent.ngModel.getMinutes()); 120 | } else { 121 | // leave MM empty to allow empty values 122 | $scope.time.MM = ''; 123 | } 124 | } 125 | 126 | $scope.time = {}; 127 | 128 | // make sure we update our variables if new values 129 | $scope.$watch("ngModel", function() { 130 | setTime(); 131 | }); 132 | 133 | var removeListener = $scope.$on('mdpTimePickerModalUpdated', setTime); 134 | $scope.$on('$destroy', removeListener); 135 | 136 | function updateTime(next) { 137 | // prevent NaN value in input field 138 | if (isNaN(next)) 139 | return; 140 | 141 | // if $scope.ngModel is undefined, create new date object. else leave as is, which means user has specified date object 142 | // Set hours, minutes, seconds and milliseconds to 0 in order for the user to be able to set own values 143 | if (angular.isDate($scope.ngModel)) { 144 | if (isNaN($scope.ngModel.getTime())) { 145 | $scope.ngModel = new Date(2017, 0, 0, 0, 0, 0, 0); 146 | } else { 147 | // continue 148 | } 149 | } else { 150 | $scope.ngModel = new Date(2017, 0, 0, 0, 0, 0, 0); 151 | } 152 | if ($scope.type === 'MM') { 153 | $scope.ngModel.setMinutes(next); 154 | return; 155 | } else if (!$scope.$parent.noMeridiem) { 156 | var hours = $scope.ngModel.getHours(); 157 | if (hours >= 12 && next != 12) 158 | next += 12; 159 | else if (hours < 12 && next == 12) 160 | next = 0; 161 | } 162 | $scope.ngModel.setHours(next); 163 | } 164 | 165 | $scope.increase = function() { 166 | var next = increase($scope.time[$scope.type], $scope.min, $scope.max, $scope.type) 167 | $scope.time[$scope.type] = next; 168 | updateTime(parseInt(next)); 169 | $rootScope.$emit('mdpTimePickerUpdated'); 170 | } 171 | 172 | $scope.decrease = function() { 173 | var next = decrease($scope.time[$scope.type], $scope.min, $scope.max, $scope.type); 174 | $scope.time[$scope.type] = next; 175 | updateTime(parseInt(next)); 176 | $rootScope.$emit('mdpTimePickerUpdated'); 177 | } 178 | 179 | $scope.handleInput = function(blur) { 180 | var next = handleInput($scope.time[$scope.type], $scope.max, blur, $scope.type); 181 | $scope.time[$scope.type] = next; 182 | updateTime(parseInt(next)); 183 | $rootScope.$emit('mdpTimePickerUpdated'); 184 | } 185 | 186 | $scope.handleKeypress = function(ev) { 187 | if (ev.keyCode === 38) $scope.increase(); 188 | else if (ev.keyCode === 40) $scope.decrease(); 189 | } 190 | 191 | }] 192 | } 193 | 194 | }) 195 | 196 | .directive('mdMeridiem', function() { 197 | 198 | return { 199 | 200 | restrict: 'E', 201 | scope: { 202 | message: '@', 203 | readOnly: '<', // true or false 204 | ngModel: '=', 205 | mandatory: '<' // true or false 206 | }, 207 | template: '' + 208 | '' + 216 | 'AM' + 217 | 'PM' + 218 | '' + 219 | '' + 222 | '', 223 | controller: ["$scope", "$rootScope", function($scope, $rootScope) { 224 | 225 | function setMeridiem() { 226 | var hours = ''; 227 | try { 228 | hours = $scope.$parent.$parent.ngModel.getHours(); 229 | } catch (e) { 230 | // leave hours empty 231 | } 232 | $scope.meridiem = hours >= 0 && hours < 12 ? 'AM' : 'PM'; 233 | } 234 | 235 | // update meridiem on load of view and when model is changing 236 | $scope.$watch("ngModel", function() { 237 | setMeridiem(); 238 | }); 239 | 240 | $scope.updateTime = function() { 241 | var hours = $scope.$parent.$parent.ngModel.getHours(); 242 | if ($scope.meridiem === 'AM') $scope.$parent.$parent.ngModel.setHours(hours-12); 243 | else $scope.$parent.$parent.ngModel.setHours(hours+12); 244 | $rootScope.$emit('mdpTimePickerUpdated'); 245 | } 246 | 247 | var removeListener = $scope.$on('mdpTimePickerModalUpdated', setMeridiem); 248 | $scope.$on('$destroy', removeListener); 249 | 250 | }] 251 | 252 | } 253 | 254 | }) 255 | 256 | .directive('mdTimePicker', function() { 257 | 258 | return { 259 | 260 | restrict: 'E', 261 | scope: { 262 | message: '<', 263 | ngModel: '=', 264 | readOnly: '<', // true or false 265 | mandatory: '<' // true or false 266 | }, 267 | template: '' + 268 | '' + 274 | '' + 275 | ':' + 276 | '' + 277 | '' + 278 | '', 279 | controller: ["$scope", "$rootScope", "$mdpTimePicker", "$attrs", function($scope, $rootScope, $mdpTimePicker, $attrs) { 280 | 281 | $scope.showPicker = function(ev) { 282 | 283 | $mdpTimePicker($scope.ngModel, { 284 | targetEvent: ev, 285 | noMeridiem: $scope.noMeridiem, 286 | autoSwitch: !$scope.noAutoSwitch 287 | }).then(function(time) { 288 | // if $scope.ngModel is not a valid date, create new date object. 289 | // Set hours, minutes, seconds and milliseconds to 0 in order for the user to be able to set own values 290 | if (angular.isDate($scope.ngModel)) { 291 | if (isNaN($scope.ngModel.getTime())) { 292 | $scope.ngModel = new Date(2017, 0, 0, 0, 0, 0, 0); 293 | } 294 | } else { 295 | $scope.ngModel = new Date(2017, 0, 0, 0, 0, 0, 0); 296 | } 297 | $scope.ngModel.setHours(time.getHours()); 298 | $scope.ngModel.setMinutes(time.getMinutes()); 299 | $scope.$broadcast('mdpTimePickerModalUpdated'); 300 | $rootScope.$emit('mdpTimePickerUpdated'); 301 | }); 302 | 303 | } 304 | 305 | }], 306 | compile: function(tElement, tAttrs) { 307 | return { 308 | pre: function preLink(scope) { 309 | scope.noMeridiem = tAttrs.noMeridiem === "" ? true : false; 310 | scope.noAutoSwitch = tAttrs.noAutoSwitch === "" ? true : false; 311 | } 312 | } 313 | } 314 | 315 | } 316 | 317 | }) 318 | 319 | .provider("$mdpTimePicker", function() { 320 | var LABEL_OK = "OK", 321 | LABEL_CANCEL = "Cancel"; 322 | 323 | this.setOKButtonLabel = function(label) { 324 | LABEL_OK = label; 325 | }; 326 | 327 | this.setCancelButtonLabel = function(label) { 328 | LABEL_CANCEL = label; 329 | }; 330 | 331 | this.$get = ["$mdDialog", function($mdDialog) { 332 | var timePicker = function(time, options) { 333 | 334 | return $mdDialog.show({ 335 | controller: ['$scope', '$mdDialog', '$mdMedia', function ($scope, $mdDialog, $mdMedia) { 336 | var self = this; 337 | 338 | // check if time is valid date. Create new date object if not. 339 | if (angular.isDate(time)) { 340 | if (isNaN(time.getTime())) { 341 | time = new Date(2017, 0, 0, 0, 0, 0, 0); 342 | } else { 343 | // continue 344 | } 345 | } else { 346 | time = new Date(2017, 0, 0, 0, 0, 0, 0); 347 | } 348 | 349 | this.time = new Date(time.getTime()); 350 | this.noMeridiem = options.noMeridiem; 351 | if (!self.noMeridiem) 352 | this.meridiem = time.getHours() < 12 ? 'AM' : 'PM'; 353 | 354 | this.VIEW_HOURS = 1; 355 | this.VIEW_MINUTES = 2; 356 | this.currentView = this.VIEW_HOURS; 357 | this.autoSwitch = !!options.autoSwitch; 358 | 359 | $scope.$mdMedia = $mdMedia; 360 | 361 | this.switchView = function() { 362 | self.currentView = self.currentView == self.VIEW_HOURS ? self.VIEW_MINUTES : self.VIEW_HOURS; 363 | }; 364 | 365 | this.hours = function() { 366 | var hours = self.time.getHours(); 367 | if (self.noMeridiem) return hours; 368 | if (hours > 12) return hours-12; 369 | else if (hours === 0) return 12; 370 | return hours; 371 | } 372 | 373 | this.minutes = function() { 374 | return format(self.time.getMinutes()); 375 | } 376 | 377 | this.setAM = function() { 378 | var hours = self.time.getHours(); 379 | if (hours >= 12) { 380 | self.time.setHours(hours - 12); 381 | self.meridiem = 'AM'; 382 | } 383 | }; 384 | 385 | this.setPM = function() { 386 | var hours = self.time.getHours(); 387 | if (hours < 12) { 388 | self.time.setHours(hours + 12); 389 | self.meridiem = 'PM'; 390 | } 391 | }; 392 | 393 | this.cancel = function() { 394 | $mdDialog.cancel(); 395 | }; 396 | 397 | this.confirm = function() { 398 | $mdDialog.hide(this.time); 399 | }; 400 | }], 401 | controllerAs: 'timepicker', 402 | clickOutsideToClose: true, 403 | template: '' + 404 | '' + 405 | '' + 406 | '
' + 407 | '{{ timepicker.hours() }}:' + 408 | '{{ timepicker.minutes() }}' + 409 | '
' + 410 | '
' + 411 | 'AM' + 412 | 'PM' + 413 | '
' + 414 | '
' + 415 | '
' + 416 | '
' + 417 | '' + 418 | '' + 419 | '
' + 420 | 421 | '' + 422 | '' + 423 | '' + LABEL_CANCEL + '' + 424 | '' + LABEL_OK + '' + 425 | '' + 426 | '
' + 427 | '
' + 428 | '
', 429 | targetEvent: options.targetEvent, 430 | locals: { 431 | time: time, 432 | noMeridiem: options.noMeridiem, 433 | autoSwitch: options.autoSwitch 434 | }, 435 | skipHide: true, 436 | multiple: true 437 | }); 438 | }; 439 | 440 | return timePicker; 441 | }]; 442 | }) 443 | 444 | .directive("mdpClock", ["$animate", "$timeout", function($animate, $timeout) { 445 | return { 446 | restrict: 'E', 447 | bindToController: { 448 | 'type': '@?', 449 | 'time': '=', 450 | 'autoSwitch': '=?' 451 | }, 452 | replace: true, 453 | template: '
' + 454 | '
' + 455 | '' + 456 | '' + 457 | '' + 458 | '' + 459 | '{{ step }}' + 460 | '{{ step }}' + 461 | '
' + 462 | '
', 463 | controller: ["$scope", function ($scope) { 464 | var TYPE_HOURS = "hours"; 465 | var TYPE_MINUTES = "minutes"; 466 | var self = this; 467 | 468 | this.noMeridiem = $scope.$parent.timepicker.noMeridiem; 469 | 470 | this.STEP_DEG = this.noMeridiem ? 360/24 : 360/12; 471 | this.STEP_DEG_MINUTES = 360/12; 472 | this.steps = []; 473 | 474 | this.CLOCK_TYPES = { 475 | "hours": { 476 | range: this.noMeridiem ? 24 : 12, 477 | }, 478 | "minutes": { 479 | range: 60, 480 | } 481 | } 482 | 483 | this.getPointerStyle = function() { 484 | var divider = 1; 485 | switch (self.type) { 486 | case TYPE_HOURS: 487 | divider = self.noMeridiem ? 24 : 12; 488 | break; 489 | case TYPE_MINUTES: 490 | divider = 60; 491 | break; 492 | } 493 | var degrees = Math.round(self.selected * (360 / divider)) - 180; 494 | return { 495 | "-webkit-transform": "rotate(" + degrees + "deg)", 496 | "-ms-transform": "rotate(" + degrees + "deg)", 497 | "transform": "rotate(" + degrees + "deg)" 498 | } 499 | }; 500 | 501 | this.setTimeByDeg = function(deg) { 502 | 503 | var divider = 0; 504 | switch (self.type) { 505 | case TYPE_HOURS: 506 | divider = self.noMeridiem ? 24 : 12; 507 | break; 508 | case TYPE_MINUTES: 509 | divider = 60; 510 | break; 511 | } 512 | 513 | var time = Math.round(divider / 360 * deg); 514 | if (!self.noMeridiem && self.type === "hours" && time === 0) 515 | time = 12; 516 | else if (self.type === "minutes" && time === 60) 517 | time = 0; 518 | self.setTime(time); 519 | }; 520 | 521 | this.setTime = function(time) { 522 | 523 | this.selected = time; 524 | 525 | switch (self.type) { 526 | case TYPE_HOURS: 527 | if (!self.noMeridiem) { 528 | var PM = this.time.getHours() >= 12 ? true : false; 529 | if (PM && time != 12) 530 | time += 12; 531 | else if (!PM && time === 12) 532 | time = 0; 533 | } 534 | this.time.setHours(time); 535 | break; 536 | case TYPE_MINUTES: 537 | this.time.setMinutes(time); 538 | break; 539 | } 540 | 541 | }; 542 | 543 | this.$onInit = function() { 544 | 545 | self.type = self.type || "hours"; 546 | 547 | switch (self.type) { 548 | case TYPE_HOURS: 549 | if (self.noMeridiem) { 550 | for (var i = 1; i <= 23; i++) 551 | self.steps.push(i); 552 | self.steps.push(0); 553 | self.selected = self.time.getHours() || 0; 554 | } 555 | else { 556 | for (var i = 1; i <= 12; i++) 557 | self.steps.push(i); 558 | self.selected = self.time.getHours() || 0; 559 | if (self.selected > 12) self.selected -= 12; 560 | } 561 | 562 | break; 563 | case TYPE_MINUTES: 564 | for (var i = 5; i <= 55; i += 5) 565 | self.steps.push(i); 566 | self.steps.push(0); 567 | 568 | self.selected = self.time.getMinutes() || 0; 569 | 570 | break; 571 | } 572 | }; 573 | // Prior to v1.5, we need to call `$onInit()` manually. 574 | // (Bindings will always be pre-assigned in these versions.) 575 | if (angular.version.major === 1 && angular.version.minor < 5) { 576 | this.$onInit(); 577 | } 578 | }], 579 | controllerAs: "clock", 580 | link: function(scope, element, attrs, ctrl) { 581 | var pointer = angular.element(element[0].querySelector(".mdp-pointer")), 582 | timepickerCtrl = scope.$parent.timepicker; 583 | 584 | var onEvent = function(event) { 585 | var containerCoords = event.currentTarget.getClientRects()[0]; 586 | var x = ((event.currentTarget.offsetWidth / 2) - (event.pageX - containerCoords.left)), 587 | y = ((event.pageY - containerCoords.top) - (event.currentTarget.offsetHeight / 2)); 588 | 589 | var deg = Math.round((Math.atan2(x, y) * (180 / Math.PI))); 590 | $timeout(function() { 591 | ctrl.setTimeByDeg(deg + 180); 592 | if (ctrl.type === 'hours' && ctrl.autoSwitch) timepickerCtrl.switchView(); 593 | }); 594 | }; 595 | 596 | element.on("click", onEvent); 597 | scope.$on("$destroy", function() { 598 | element.off("click", onEvent); 599 | }); 600 | 601 | } 602 | } 603 | }]); 604 | 605 | })(window, angular); 606 | -------------------------------------------------------------------------------- /img/modal-hour-12.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/classlinkinc/angular-material-time-picker/f539e7cca6127505aba29865a72523a606ba743a/img/modal-hour-12.PNG -------------------------------------------------------------------------------- /img/modal-hour-24.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/classlinkinc/angular-material-time-picker/f539e7cca6127505aba29865a72523a606ba743a/img/modal-hour-24.PNG -------------------------------------------------------------------------------- /img/modal-minute.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/classlinkinc/angular-material-time-picker/f539e7cca6127505aba29865a72523a606ba743a/img/modal-minute.PNG -------------------------------------------------------------------------------- /img/time-picker-12.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/classlinkinc/angular-material-time-picker/f539e7cca6127505aba29865a72523a606ba743a/img/time-picker-12.PNG -------------------------------------------------------------------------------- /img/time-picker-24.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/classlinkinc/angular-material-time-picker/f539e7cca6127505aba29865a72523a606ba743a/img/time-picker-24.PNG -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('angular-material'); 2 | require('./dist/md-time-picker'); 3 | 4 | module.exports = 'md.time.picker'; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-material-time-picker", 3 | "version": "1.0.8", 4 | "description": "Material design time picker for 12 hour and 24 hour time", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/classlinkinc/angular-material-time-picker.git" 9 | }, 10 | "keywords": [ 11 | "angular", 12 | "material", 13 | "time", 14 | "picker", 15 | "material", 16 | "design" 17 | ], 18 | "author": "Matt Bajorek", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/classlinkinc/angular-material-time-picker/issues" 22 | }, 23 | "homepage": "https://github.com/classlinkinc/angular-material-time-picker#readme", 24 | "peerDependencies": { 25 | "angular": "^1.5.0", 26 | "angular-material": "^1.0.0" 27 | } 28 | } 29 | --------------------------------------------------------------------------------