├── .bowerrc ├── .gitignore ├── LICENSE.txt ├── README.md ├── bower.json ├── demo └── index.html ├── dist ├── angular-date-picker-polyfill-basic.css ├── angular-date-picker-polyfill-basic.min.css ├── angular-date-picker-polyfill.js └── angular-date-picker-polyfill.min.js ├── gulpfile.coffee ├── gulpfile.js ├── karma.conf.coffee ├── package.json └── src ├── css ├── _calendar.scss ├── _input.scss ├── _popup.scss ├── _timepicker.scss ├── _variables.scss └── basic.scss └── js ├── app.coffee ├── calendar.directive.coffee ├── calendar.directive.spec.coffee ├── date.util.coffee ├── date.util.spec.coffee ├── input.directive.coffee ├── month.util.coffee ├── month.util.spec.coffee ├── time.util.coffee ├── time.util.spec.coffee ├── timepicker.directive.coffee └── timepicker.directive.spec.coffee /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "/vendor/bower" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules 3 | /generated 4 | .sass-cache 5 | /vendor/bower 6 | demo/scripts.js 7 | demo/qdate* 8 | *.swp 9 | npm-debug.log 10 | .tmp 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2015 Adam Albrecht and other contributors 2 | http://adamalbrecht.com 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without 7 | restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Date Picker Polyfill 2 | 3 | This is an [HTML5 date input](http://diveintohtml5.info/forms.html#type-date) polyfill for Angular.js. For browsers such as Chrome and Safari for iOS, the native datepicker will be used and this directive will have no effect, but for others it will add a datepicker popup to inputs of type `date` and `datetime-local`. Or if you want your UX to be completely consistent across browsers, you can add the directive to any other type of element. 4 | 5 | ## Dependencies 6 | 7 | * Angular 1.3+ 8 | * Modernizr 9 | 10 | ## Usage 11 | 12 | First, you'll need to include the javascript and css file and include 'angular-date-picker-polyfill' as a dependency in your angular module. 13 | 14 | ```javascript 15 | angular.module('myApp', ['angular-date-picker-polyfill']); 16 | ``` 17 | 18 | Next, it will simply work for any date inputs in your app: 19 | 20 | ```html 21 | 22 | ``` 23 | 24 | Angular 1.3 has built-in support for date inputs and takes care of setting validity on the field. But note that your model must be a javascript date object. It will not parse dates for you. 25 | 26 | If you want to apply it to a normal text field, or if you don't want to use native datepickers, you can just add the `aa-date-input` attribute like so: 27 | 28 | ```html 29 | 30 | ``` 31 | 32 | And if you want to use it on a non-input element, you can do something like the following: 33 | 34 | ```html 35 | 39 | ``` 40 | 41 | Note the `tabindex` attribute. This will allow the button (or any other element) to gain focus like a normal form field. 42 | 43 | It works similarly for Date/Time. It will automatically work for inputs of type `datetime-local`. 44 | 45 | ```html 46 | 47 | ``` 48 | 49 | And if you want to apply it to other elements, just add the `aa-date-time-input` attribute. 50 | 51 | ```html 52 | 56 | ``` 57 | 58 | You can also use the Calendar and Timepicker inline and outside the context of the popup: 59 | 60 | ```html 61 |
62 | ``` 63 | 64 | ```html 65 |
66 | ``` 67 | 68 | ## Theming 69 | 70 | There is a very basic theme provided. Others may be added later and I would love for others to contribute additional ones. 71 | 72 | ## Dev Status 73 | 74 | This is still alpha software and has not yet been well tested. 75 | 76 | ## TODO: 77 | 78 | - [ ] Better test coverage 79 | - [ ] Setup automatic testing using a CI service 80 | - [ ] Additional Themes 81 | 82 | ## Development 83 | 84 | ### Requirements: 85 | 86 | * [NPM](https://www.npmjs.com) 87 | * [Gulp](http://gulpjs.com) 88 | * [PhantomJS](http://phantomjs.org) 89 | 90 | After cloning the repository, run the following to install the dependencies: 91 | 92 | ```sh 93 | npm install && bower install 94 | ``` 95 | 96 | Then to watch and compile assets: 97 | ```sh 98 | gulp dev 99 | ``` 100 | 101 | And to run the tests: 102 | ``` 103 | npm run spec 104 | ``` 105 | 106 | ## Contributing 107 | 108 | Contributions are welcome. Non-CSS changes that don't include test coverage will not be merged. 109 | 110 | 1. Fork it 111 | 2. Create your feature branch (`git checkout -b my-new-feature`) 112 | 3. Commit your changes (`git commit -am 'Add some feature'`) 113 | 4. Push to the branch (`git push origin my-new-feature`) 114 | 5. Create new Pull Request 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-date-picker-polyfill", 3 | "version": "0.0.2", 4 | "authors": ["Adam Albrecht"], 5 | "description": "A date and datetime-local picker polyfill for Angular.js", 6 | "keywords": [ 7 | "angular", 8 | "angularjs", 9 | "datepicker", 10 | "datetimepicker" 11 | ], 12 | "license": "MIT", 13 | "main": "dist/angular-date-picker-polyfill.js", 14 | "ignore": [ 15 | "src", 16 | "demo", 17 | "README.md", 18 | "gulpfile.*", 19 | "karma.conf.coffee", 20 | "package.json", 21 | "vendor", 22 | "node_modules", 23 | ".bowerrc", 24 | ".gitignore" 25 | 26 | ], 27 | "dependencies": { 28 | "angular": "~1.3.0", 29 | "modernizr": "~2.8.3" 30 | }, 31 | "devDependencies": { 32 | "angular-mocks": "~1.3.0", 33 | "jquery": "~2.1.3", 34 | "lodash": "~3.1.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Date Picker Polyfill 6 | 7 | 16 | 17 | 18 |
19 |
20 |

21 | 22 |

23 |

24 | 25 |

26 |

27 |

28 | 29 | 30 |

31 |

32 | 33 | 34 |

35 |

36 | 37 | 41 |

42 |

43 | 44 | 48 |

49 |
50 |
51 | Values: 52 | 60 |
61 | 62 | 63 | 64 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /dist/angular-date-picker-polyfill-basic.css: -------------------------------------------------------------------------------- 1 | .aa-date-input { 2 | display: inline-block; 3 | position: relative; } 4 | 5 | .aa-date-input-cover { 6 | cursor: pointer; 7 | position: absolute; 8 | top: 0; 9 | left: 0; 10 | display: inline-block; 11 | z-index: 1; 12 | width: 100%; 13 | background-color: #fff; 14 | border: solid 1px #ccc; 15 | padding: 2px 5px; } 16 | 17 | .aa-cal { 18 | padding: 0; 19 | border: none; 20 | display: inline-block; 21 | font-family: inherit; 22 | font-size: auto; 23 | position: relative; } 24 | .aa-cal-controls { 25 | background-color: #fff; 26 | color: #333; 27 | min-width: 250px; 28 | padding: 0 0 5px; 29 | text-align: center; 30 | vertical-align: middle; } 31 | .aa-cal-btn { 32 | background-color: inherit; 33 | border: none; 34 | cursor: pointer; 35 | display: inline-block; 36 | font-size: 1.1em; 37 | padding: 3px 7.5px; } 38 | .aa-cal-btn:hover { 39 | background-color: #eee; 40 | border: none; } 41 | .aa-cal-month-name { 42 | display: inline-block; 43 | font-size: auto; 44 | padding: 5px 10px 2px; 45 | text-align: center; } 46 | .aa-cal-prev-month, .aa-cal-prev-year { 47 | float: left; } 48 | .aa-cal-next-month, .aa-cal-next-year { 49 | float: right; } 50 | .aa-cal-next-month:before { 51 | content: "\203A"; } 52 | .aa-cal-prev-month:before { 53 | content: "\2039"; } 54 | .aa-cal-next-year:before { 55 | content: "\00bb"; } 56 | .aa-cal-prev-year:before { 57 | content: "\00ab"; } 58 | .aa-cal-set-to-today:before { 59 | content: "\29be"; } 60 | .aa-cal-set-to-today { 61 | display: none; } 62 | 63 | table.aa-cal-month { 64 | border-collapse: collapse; 65 | border-spacing: 0; 66 | table-layout: fixed; 67 | min-width: 250px; } 68 | table.aa-cal-month td, table.aa-cal-month th { 69 | border: 1px solid #e6e6e6; 70 | margin: 0; 71 | padding: 5px; 72 | text-align: center; 73 | vertical-align: middle; } 74 | table.aa-cal-month th { 75 | background-color: #fff; 76 | color: #333; 77 | font-weight: bold; 78 | padding: 5px; } 79 | table.aa-cal-month td { 80 | background-color: #fff; 81 | color: #333; 82 | cursor: pointer; } 83 | table.aa-cal-month td span { 84 | border: 1px solid #fff; } 85 | table.aa-cal-month td.aa-cal-other-month { 86 | color: #999; 87 | background-color: #eee; 88 | font-weight: normal; } 89 | table.aa-cal-month td.aa-cal-other-month span { 90 | border: 1px solid #eee; } 91 | table.aa-cal-month td.aa-cal-other-month:hover { 92 | background-color: #d4d4d4; } 93 | table.aa-cal-month td.aa-cal-other-month:hover span { 94 | border-color: #d4d4d4; } 95 | table.aa-cal-month td.aa-cal-selected { 96 | color: #333; 97 | background-color: #fff; 98 | font-weight: bold; } 99 | table.aa-cal-month td.aa-cal-selected span { 100 | border: 1px solid #fff; } 101 | table.aa-cal-month td.aa-cal-today { 102 | color: #333; 103 | background-color: #fff; 104 | font-weight: bold; } 105 | table.aa-cal-month td.aa-cal-today span { 106 | border: 1px solid #fff; } 107 | table.aa-cal-month td.aa-cal-today:hover { 108 | background-color: #e6e6e6; } 109 | table.aa-cal-month td.aa-cal-today:hover span { 110 | border-color: #e6e6e6; } 111 | table.aa-cal-month td.aa-cal-today.q-calendar-selected { 112 | color: #000; 113 | background-color: #fff; 114 | font-weight: normal; } 115 | table.aa-cal-month td.aa-cal-today.q-calendar-selected span { 116 | border: 1px solid #fff; } 117 | table.aa-cal-month td.aa-cal-today.q-calendar-selected:hover { 118 | background-color: #e6e6e6; } 119 | table.aa-cal-month td.aa-cal-today.q-calendar-selected:hover span { 120 | border-color: #e6e6e6; } 121 | table.aa-cal-month td.aa-cal-disabled { 122 | cursor: default; 123 | color: #999; 124 | background-color: #eee; 125 | font-weight: normal; } 126 | table.aa-cal-month td.aa-cal-disabled span { 127 | border: 1px solid #eee; } 128 | table.aa-cal-month td.aa-cal-disabled:hover { 129 | background-color: #eee; } 130 | table.aa-cal-month td.aa-cal-disabled:hover span { 131 | border-color: #eee; } 132 | table.aa-cal-month td:hover { 133 | background-color: #e6e6e6; } 134 | table.aa-cal-month td:hover span { 135 | border-color: #e6e6e6; } 136 | 137 | .aa-datepicker-popup { 138 | border: solid 1px #e6e6e6; 139 | padding: 20px 10px 10px; 140 | position: absolute; 141 | background-color: #fff; 142 | box-shadow: 3px 3px 10px 0 rgba(0, 0, 0, 0.4); 143 | top: 100%; 144 | left: 0px; 145 | z-index: 10000; } 146 | .aa-datepicker-popup-close { 147 | width: 10px; 148 | height: 10px; 149 | position: absolute; 150 | top: 4px; 151 | right: 4px; 152 | font-size: 18px; 153 | cursor: pointer; 154 | line-height: 1; } 155 | .aa-datepicker-popup-close:hover { 156 | color: #e6e6e6; } 157 | .aa-datepicker-popup-close:before { 158 | content: "\d7"; } 159 | .aa-datepicker-popup .aa-timepicker { 160 | padding: 5px 0; 161 | text-align: center; } 162 | 163 | .aa-timepicker select.aa-timepicker-hour, .aa-timepicker select.aa-timepicker-minute, .aa-timepicker select.aa-timepicker-ampm { 164 | box-shadow: none; 165 | display: inline; 166 | height: auto; 167 | margin: auto; 168 | padding: 0; 169 | width: auto; } 170 | -------------------------------------------------------------------------------- /dist/angular-date-picker-polyfill-basic.min.css: -------------------------------------------------------------------------------- 1 | .aa-date-input{display:inline-block;position:relative}.aa-date-input-cover{cursor:pointer;position:absolute;top:0;left:0;display:inline-block;z-index:1;width:100%;background-color:#fff;border:1px solid #ccc;padding:2px 5px}.aa-cal{padding:0;border:none;display:inline-block;font-family:inherit;font-size:auto;position:relative}.aa-cal-controls{background-color:#fff;color:#333;min-width:250px;padding:0 0 5px;text-align:center;vertical-align:middle}.aa-cal-btn{background-color:inherit;border:none;cursor:pointer;display:inline-block;font-size:1.1em;padding:3px 7.5px}.aa-cal-btn:hover{background-color:#eee;border:none}.aa-cal-month-name{display:inline-block;font-size:auto;padding:5px 10px 2px;text-align:center}.aa-cal-prev-month,.aa-cal-prev-year{float:left}.aa-cal-next-month,.aa-cal-next-year{float:right}.aa-cal-next-month:before{content:"\203A"}.aa-cal-prev-month:before{content:"\2039"}.aa-cal-next-year:before{content:"\00bb"}.aa-cal-prev-year:before{content:"\00ab"}.aa-cal-set-to-today:before{content:"\29be"}.aa-cal-set-to-today{display:none}table.aa-cal-month{border-collapse:collapse;border-spacing:0;table-layout:fixed;min-width:250px}table.aa-cal-month td,table.aa-cal-month th{border:1px solid #e6e6e6;margin:0;padding:5px;text-align:center;vertical-align:middle}table.aa-cal-month th{background-color:#fff;color:#333;font-weight:700;padding:5px}table.aa-cal-month td{background-color:#fff;color:#333;cursor:pointer}table.aa-cal-month td span{border:1px solid #fff}table.aa-cal-month td.aa-cal-other-month{color:#999;background-color:#eee;font-weight:400}table.aa-cal-month td.aa-cal-other-month span{border:1px solid #eee}table.aa-cal-month td.aa-cal-other-month:hover{background-color:#d4d4d4}table.aa-cal-month td.aa-cal-other-month:hover span{border-color:#d4d4d4}table.aa-cal-month td.aa-cal-selected{color:#333;background-color:#fff;font-weight:700}table.aa-cal-month td.aa-cal-selected span{border:1px solid #fff}table.aa-cal-month td.aa-cal-today{color:#333;background-color:#fff;font-weight:700}table.aa-cal-month td.aa-cal-today span{border:1px solid #fff}table.aa-cal-month td.aa-cal-today:hover{background-color:#e6e6e6}table.aa-cal-month td.aa-cal-today:hover span{border-color:#e6e6e6}table.aa-cal-month td.aa-cal-today.q-calendar-selected{color:#000;background-color:#fff;font-weight:400}table.aa-cal-month td.aa-cal-today.q-calendar-selected span{border:1px solid #fff}table.aa-cal-month td.aa-cal-today.q-calendar-selected:hover{background-color:#e6e6e6}table.aa-cal-month td.aa-cal-today.q-calendar-selected:hover span{border-color:#e6e6e6}table.aa-cal-month td.aa-cal-disabled{cursor:default;color:#999;background-color:#eee;font-weight:400}table.aa-cal-month td.aa-cal-disabled span{border:1px solid #eee}table.aa-cal-month td.aa-cal-disabled:hover{background-color:#eee}table.aa-cal-month td.aa-cal-disabled:hover span{border-color:#eee}table.aa-cal-month td:hover{background-color:#e6e6e6}table.aa-cal-month td:hover span{border-color:#e6e6e6}.aa-datepicker-popup{border:1px solid #e6e6e6;padding:20px 10px 10px;position:absolute;background-color:#fff;box-shadow:3px 3px 10px 0 rgba(0,0,0,.4);top:100%;left:0;z-index:10000}.aa-datepicker-popup-close{width:10px;height:10px;position:absolute;top:4px;right:4px;font-size:18px;cursor:pointer;line-height:1}.aa-datepicker-popup-close:hover{color:#e6e6e6}.aa-datepicker-popup-close:before{content:"\d7"}.aa-datepicker-popup .aa-timepicker{padding:5px 0;text-align:center}.aa-timepicker select.aa-timepicker-ampm,.aa-timepicker select.aa-timepicker-hour,.aa-timepicker select.aa-timepicker-minute{box-shadow:none;display:inline;height:auto;margin:auto;padding:0;width:auto} -------------------------------------------------------------------------------- /dist/angular-date-picker-polyfill.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | angular.module('angular-date-picker-polyfill', []); 3 | 4 | }).call(this); 5 | 6 | (function() { 7 | angular.module('angular-date-picker-polyfill').directive('aaCalendar', ["aaMonthUtil", "aaDateUtil", "$filter", function(aaMonthUtil, aaDateUtil, $filter) { 8 | return { 9 | restrict: 'A', 10 | replace: true, 11 | require: 'ngModel', 12 | scope: {}, 13 | link: function(scope, elem, attrs, ngModelCtrl) { 14 | var pullMonthDateFromModel, refreshView; 15 | scope.dayAbbreviations = ['Su', 'M', 'T', 'W', 'R', 'F', 'S']; 16 | scope.monthArray = [[]]; 17 | scope.monthDate = null; 18 | scope.selected = null; 19 | ngModelCtrl.$render = function() { 20 | scope.selected = ngModelCtrl.$viewValue; 21 | pullMonthDateFromModel(); 22 | return refreshView(); 23 | }; 24 | pullMonthDateFromModel = function() { 25 | var d; 26 | if (angular.isDate(ngModelCtrl.$viewValue)) { 27 | d = angular.copy(ngModelCtrl.$viewValue); 28 | } else { 29 | d = new Date(); 30 | } 31 | d.setDate(1); 32 | return scope.monthDate = d; 33 | }; 34 | refreshView = function() { 35 | return scope.monthArray = aaMonthUtil.generateMonthArray(scope.monthDate.getFullYear(), scope.monthDate.getMonth(), ngModelCtrl.$viewValue); 36 | }; 37 | scope.setDate = function(d) { 38 | var c; 39 | c = angular.isDate(ngModelCtrl.$viewValue) ? angular.copy(ngModelCtrl.$viewValue) : aaDateUtil.todayStart(); 40 | c.setYear(d.getFullYear()); 41 | c.setMonth(d.getMonth()); 42 | c.setDate(d.getDate()); 43 | ngModelCtrl.$setViewValue(c); 44 | if (!aaDateUtil.dateObjectsAreEqualToMonth(d, scope.monthDate)) { 45 | pullMonthDateFromModel(); 46 | } 47 | refreshView(); 48 | return scope.$emit('aa:calendar:set-date'); 49 | }; 50 | scope.setToToday = function() { 51 | return scope.setDate(aaDateUtil.todayStart()); 52 | }; 53 | return scope.incrementMonths = function(num) { 54 | scope.monthDate.setMonth(scope.monthDate.getMonth() + num); 55 | return refreshView(); 56 | }; 57 | }, 58 | template: "
\n
\n \n \n \n \n \n \n
\n \n \n \n \n \n \n \n \n \n \n \n
\n
" 59 | }; 60 | }]); 61 | 62 | }).call(this); 63 | 64 | (function() { 65 | angular.module('angular-date-picker-polyfill').factory('aaDateUtil', function() { 66 | return { 67 | dateObjectsAreEqualToDay: function(d1, d2) { 68 | if (!(angular.isDate(d1) && angular.isDate(d2))) { 69 | return false; 70 | } 71 | return (d1.getFullYear() === d2.getFullYear()) && (d1.getMonth() === d2.getMonth()) && (d1.getDate() === d2.getDate()); 72 | }, 73 | dateObjectsAreEqualToMonth: function(d1, d2) { 74 | if (!(angular.isDate(d1) && angular.isDate(d2))) { 75 | return false; 76 | } 77 | return (d1.getFullYear() === d2.getFullYear()) && (d1.getMonth() === d2.getMonth()); 78 | }, 79 | convertToDate: function(val) { 80 | var d; 81 | if (angular.isDate(val)) { 82 | return val; 83 | } else { 84 | d = Date.parse(val); 85 | if (angular.isDate(d)) { 86 | return d; 87 | } else { 88 | return null; 89 | } 90 | } 91 | }, 92 | todayStart: function() { 93 | var d; 94 | d = new Date(); 95 | d.setHours(0); 96 | d.setMinutes(0); 97 | d.setSeconds(0); 98 | d.setMilliseconds(0); 99 | return d; 100 | } 101 | }; 102 | }); 103 | 104 | }).call(this); 105 | 106 | (function() { 107 | var linker; 108 | 109 | linker = function(scope, elem, attrs, ngModelCtrl, $compile, aaDateUtil, includeTimepicker) { 110 | var compileTemplate, init, setupNonInputEvents, setupNonInputValidatorAndFormatter, setupPopupTogglingEvents, setupViewActionMethods; 111 | if (includeTimepicker == null) { 112 | includeTimepicker = false; 113 | } 114 | init = function() { 115 | compileTemplate(); 116 | setupViewActionMethods(); 117 | setupPopupTogglingEvents(); 118 | if (elem.prop('tagName') !== 'INPUT' || (attrs.type !== 'date' && attrs.type !== 'datetime-local')) { 119 | setupNonInputEvents(); 120 | return setupNonInputValidatorAndFormatter(); 121 | } 122 | }; 123 | setupNonInputValidatorAndFormatter = function() { 124 | ngModelCtrl.$formatters.unshift(aaDateUtil.convertToDate); 125 | if (includeTimepicker) { 126 | return ngModelCtrl.$validators['datetime-local'] = function(modelValue, viewValue) { 127 | return !viewValue || angular.isDate(viewValue); 128 | }; 129 | } else { 130 | return ngModelCtrl.$validators.date = function(modelValue, viewValue) { 131 | return !viewValue || angular.isDate(viewValue); 132 | }; 133 | } 134 | }; 135 | compileTemplate = function() { 136 | var $popup, popupDiv, tmpl, useAmPm; 137 | elem.wrap("
"); 138 | tmpl = "
\n
\n
"; 139 | if (includeTimepicker) { 140 | useAmPm = attrs.useAmPm != null ? attrs.useAmPm === true || attrs.useAmPm === 'true' : true; 141 | tmpl += "
"; 142 | } 143 | tmpl += "
"; 144 | popupDiv = angular.element(tmpl); 145 | $popup = $compile(popupDiv)(scope); 146 | return elem.after($popup); 147 | }; 148 | setupPopupTogglingEvents = function() { 149 | var $wrapper, onDocumentClick, wrapperClicked; 150 | scope.$on('aa:calendar:set-date', function() { 151 | if (!includeTimepicker) { 152 | return scope.closePopup(); 153 | } 154 | }); 155 | wrapperClicked = false; 156 | elem.on('focus', function(e) { 157 | if (!scope.isOpen) { 158 | return scope.$apply(function() { 159 | return scope.openPopup(); 160 | }); 161 | } 162 | }); 163 | $wrapper = elem.parent(); 164 | $wrapper.on('mousedown', function(e) { 165 | wrapperClicked = true; 166 | return setTimeout(function() { 167 | return wrapperClicked = false; 168 | }, 100); 169 | }); 170 | elem.on('blur', function(e) { 171 | if (scope.isOpen && !wrapperClicked) { 172 | return scope.$apply(function() { 173 | return scope.closePopup(); 174 | }); 175 | } 176 | }); 177 | onDocumentClick = function(e) { 178 | if (scope.isOpen && !wrapperClicked) { 179 | return scope.$apply(function() { 180 | return scope.closePopup(); 181 | }); 182 | } 183 | }; 184 | angular.element(window.document).on('mousedown', onDocumentClick); 185 | return scope.$on('$destroy', function() { 186 | elem.off('focus'); 187 | elem.off('blur'); 188 | $wrapper.off('mousedown'); 189 | return angular.element(window.document).off('mousedown', onDocumentClick); 190 | }); 191 | }; 192 | setupNonInputEvents = function() { 193 | elem.on('click', function(e) { 194 | if (!scope.isOpen) { 195 | return scope.$apply(function() { 196 | return scope.openPopup(); 197 | }); 198 | } 199 | }); 200 | return scope.$on('$destroy', function() { 201 | return elem.off('click'); 202 | }); 203 | }; 204 | setupViewActionMethods = function() { 205 | scope.openPopup = function() { 206 | return scope.isOpen = true; 207 | }; 208 | return scope.closePopup = function() { 209 | return scope.isOpen = false; 210 | }; 211 | }; 212 | return init(); 213 | }; 214 | 215 | angular.module('angular-date-picker-polyfill').directive('aaDateInput', ["$compile", "aaDateUtil", function($compile, aaDateUtil) { 216 | return { 217 | restrict: 'A', 218 | require: 'ngModel', 219 | scope: { 220 | ngModel: '=' 221 | }, 222 | link: function(scope, elem, attrs, ngModelCtrl) { 223 | return linker(scope, elem, attrs, ngModelCtrl, $compile, aaDateUtil, false); 224 | } 225 | }; 226 | }]); 227 | 228 | angular.module('angular-date-picker-polyfill').directive('aaDateTimeInput', ["$compile", "aaDateUtil", function($compile, aaDateUtil) { 229 | return { 230 | restrict: 'A', 231 | require: 'ngModel', 232 | scope: { 233 | ngModel: '=' 234 | }, 235 | link: function(scope, elem, attrs, ngModelCtrl) { 236 | return linker(scope, elem, attrs, ngModelCtrl, $compile, aaDateUtil, true); 237 | } 238 | }; 239 | }]); 240 | 241 | if (!Modernizr.inputtypes.date) { 242 | angular.module('angular-date-picker-polyfill').directive('input', ["$compile", "aaDateUtil", function($compile, aaDateUtil) { 243 | return { 244 | restrict: 'E', 245 | require: '?ngModel', 246 | scope: { 247 | ngModel: '=' 248 | }, 249 | compile: function(elem, attrs) { 250 | if (!((attrs.ngModel != null) && (attrs.type === 'date' || attrs.type === 'datetime-local'))) { 251 | return; 252 | } 253 | return function(scope, elem, attrs, ngModelCtrl) { 254 | return linker(scope, elem, attrs, ngModelCtrl, $compile, aaDateUtil, attrs.type === 'datetime-local'); 255 | }; 256 | } 257 | }; 258 | }]); 259 | } 260 | 261 | }).call(this); 262 | 263 | (function() { 264 | angular.module('angular-date-picker-polyfill').factory('aaMonthUtil', ["aaDateUtil", function(aaDateUtil) { 265 | return { 266 | numberOfDaysInMonth: function(year, month) { 267 | return [31, ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0 ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]; 268 | }, 269 | generateMonthArray: function(year, month, selected) { 270 | var arr, d, dayIndex, endDate, obj, offset, today, weekNum, _i; 271 | if (selected == null) { 272 | selected = null; 273 | } 274 | d = new Date(year, month, 1); 275 | today = new Date(); 276 | endDate = new Date(year, month, this.numberOfDaysInMonth(year, month)); 277 | offset = d.getDay(); 278 | d.setDate(d.getDate() + (offset * -1)); 279 | arr = []; 280 | weekNum = 0; 281 | while (d <= endDate) { 282 | arr.push([]); 283 | for (dayIndex = _i = 0; _i <= 6; dayIndex = ++_i) { 284 | obj = { 285 | date: angular.copy(d), 286 | isToday: aaDateUtil.dateObjectsAreEqualToDay(d, today), 287 | isSelected: selected && aaDateUtil.dateObjectsAreEqualToDay(d, selected) ? true : false, 288 | isOtherMonth: d.getMonth() !== month 289 | }; 290 | arr[weekNum].push(obj); 291 | d.setDate(d.getDate() + 1); 292 | } 293 | weekNum += 1; 294 | } 295 | return arr; 296 | } 297 | }; 298 | }]); 299 | 300 | }).call(this); 301 | 302 | (function() { 303 | angular.module('angular-date-picker-polyfill').factory('aaTimeUtil', function() { 304 | return { 305 | getMinuteAndHourFromDate: function(d, useAmPmHours) { 306 | var amPm, h, m; 307 | if (useAmPmHours == null) { 308 | useAmPmHours = true; 309 | } 310 | if (!angular.isDate(d)) { 311 | return null; 312 | } 313 | h = d.getHours(); 314 | amPm = null; 315 | if (useAmPmHours) { 316 | switch (false) { 317 | case h !== 0: 318 | h = 12; 319 | amPm = 'AM'; 320 | break; 321 | case h !== 12: 322 | amPm = 'PM'; 323 | break; 324 | case !(h > 12): 325 | h = h - 12; 326 | amPm = 'PM'; 327 | } 328 | } 329 | m = d.getMinutes(); 330 | return [h, m, amPm]; 331 | }, 332 | applyTimeValuesToDateObject: function(timeValues, d) { 333 | var amPm, hour, minute; 334 | hour = timeValues[0], minute = timeValues[1], amPm = timeValues[2]; 335 | d.setMinutes(minute); 336 | if (amPm === 'AM') { 337 | d.setHours(hour === 12 ? 0 : hour); 338 | } else if (amPm === 'PM' && hour === 12) { 339 | d.setHours(12); 340 | } else if (amPm === 'PM' && hour !== 12) { 341 | d.setHours(hour + 12); 342 | } else { 343 | d.setHours(hour); 344 | } 345 | return d; 346 | } 347 | }; 348 | }); 349 | 350 | }).call(this); 351 | 352 | (function() { 353 | angular.module('angular-date-picker-polyfill').directive('aaTimepicker', ["aaTimeUtil", "aaDateUtil", function(aaTimeUtil, aaDateUtil) { 354 | return { 355 | restrict: 'A', 356 | replace: true, 357 | require: 'ngModel', 358 | scope: {}, 359 | link: function(scope, elem, attrs, ngModelCtrl) { 360 | var init, pullTimeFromModel, resetToNull, setupSelectOptions; 361 | init = function() { 362 | setupSelectOptions(); 363 | return resetToNull(); 364 | }; 365 | setupSelectOptions = function() { 366 | var _i, _j, _results, _results1; 367 | scope.useAmPm = attrs.useAmPm != null ? attrs.useAmPm === true || attrs.useAmPm === 'true' : true; 368 | scope.hourOptions = scope.useAmPm ? [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] : (function() { 369 | _results = []; 370 | for (_i = 0; _i <= 23; _i++){ _results.push(_i); } 371 | return _results; 372 | }).apply(this); 373 | scope.minuteOptions = (function() { 374 | _results1 = []; 375 | for (_j = 0; _j <= 59; _j++){ _results1.push(_j); } 376 | return _results1; 377 | }).apply(this); 378 | return scope.amPmOptions = ['AM', 'PM']; 379 | }; 380 | resetToNull = function() { 381 | scope.hour = null; 382 | scope.minute = null; 383 | return scope.amPm = null; 384 | }; 385 | ngModelCtrl.$render = function() { 386 | return pullTimeFromModel(); 387 | }; 388 | pullTimeFromModel = function() { 389 | var d, _ref; 390 | if (angular.isDate(ngModelCtrl.$viewValue)) { 391 | d = angular.copy(ngModelCtrl.$viewValue); 392 | return _ref = aaTimeUtil.getMinuteAndHourFromDate(d, scope.useAmPm), scope.hour = _ref[0], scope.minute = _ref[1], scope.amPm = _ref[2], _ref; 393 | } else { 394 | return resetToNull(); 395 | } 396 | }; 397 | scope.setTimeFromFields = function() { 398 | var d; 399 | if ((scope.hour != null) && (scope.minute == null)) { 400 | scope.minute = 0; 401 | } 402 | if ((scope.hour != null) && scope.useAmPm && (scope.amPm == null)) { 403 | scope.amPm = 'AM'; 404 | } 405 | if (!((scope.hour != null) && (scope.minute != null) && (!scope.useAmPm || (scope.amPm != null)))) { 406 | return; 407 | } 408 | if ((ngModelCtrl.$viewValue != null) && angular.isDate(ngModelCtrl.$viewValue)) { 409 | d = new Date(ngModelCtrl.$viewValue); 410 | } else { 411 | d = aaDateUtil.todayStart(); 412 | } 413 | aaTimeUtil.applyTimeValuesToDateObject([scope.hour, parseInt(scope.minute), scope.amPm], d); 414 | return ngModelCtrl.$setViewValue(d); 415 | }; 416 | return init(); 417 | }, 418 | template: "
\n \n \n \n \n \n \n
" 419 | }; 420 | }]); 421 | 422 | }).call(this); 423 | -------------------------------------------------------------------------------- /dist/angular-date-picker-polyfill.min.js: -------------------------------------------------------------------------------- 1 | (function(){angular.module("angular-date-picker-polyfill",[])}).call(this),function(){angular.module("angular-date-picker-polyfill").directive("aaCalendar",["aaMonthUtil","aaDateUtil","$filter",function(e,n){return{restrict:"A",replace:!0,require:"ngModel",scope:{},link:function(t,a,r,u){var i,l;return t.dayAbbreviations=["Su","M","T","W","R","F","S"],t.monthArray=[[]],t.monthDate=null,t.selected=null,u.$render=function(){return t.selected=u.$viewValue,i(),l()},i=function(){var e;return e=angular.isDate(u.$viewValue)?angular.copy(u.$viewValue):new Date,e.setDate(1),t.monthDate=e},l=function(){return t.monthArray=e.generateMonthArray(t.monthDate.getFullYear(),t.monthDate.getMonth(),u.$viewValue)},t.setDate=function(e){var a;return a=angular.isDate(u.$viewValue)?angular.copy(u.$viewValue):n.todayStart(),a.setYear(e.getFullYear()),a.setMonth(e.getMonth()),a.setDate(e.getDate()),u.$setViewValue(a),n.dateObjectsAreEqualToMonth(e,t.monthDate)||i(),l(),t.$emit("aa:calendar:set-date")},t.setToToday=function(){return t.setDate(n.todayStart())},t.incrementMonths=function(e){return t.monthDate.setMonth(t.monthDate.getMonth()+e),l()}},template:"
\n
\n \n \n \n \n \n \n
\n \n \n \n \n \n \n \n \n \n \n \n
\n
"}}])}.call(this),function(){angular.module("angular-date-picker-polyfill").factory("aaDateUtil",function(){return{dateObjectsAreEqualToDay:function(e,n){return angular.isDate(e)&&angular.isDate(n)?e.getFullYear()===n.getFullYear()&&e.getMonth()===n.getMonth()&&e.getDate()===n.getDate():!1},dateObjectsAreEqualToMonth:function(e,n){return angular.isDate(e)&&angular.isDate(n)?e.getFullYear()===n.getFullYear()&&e.getMonth()===n.getMonth():!1},convertToDate:function(e){var n;return angular.isDate(e)?e:(n=Date.parse(e),angular.isDate(n)?n:null)},todayStart:function(){var e;return e=new Date,e.setHours(0),e.setMinutes(0),e.setSeconds(0),e.setMilliseconds(0),e}}})}.call(this),function(){var e;e=function(e,n,t,a,r,u,i){var l,o,c,s,d,m;return null==i&&(i=!1),o=function(){return l(),m(),d(),"INPUT"!==n.prop("tagName")||"date"!==t.type&&"datetime-local"!==t.type?(c(),s()):void 0},s=function(){return a.$formatters.unshift(u.convertToDate),i?a.$validators["datetime-local"]=function(e,n){return!n||angular.isDate(n)}:a.$validators.date=function(e,n){return!n||angular.isDate(n)}},l=function(){var a,u,l,o;return n.wrap("
"),l="
\n
\n
",i&&(o=null!=t.useAmPm?t.useAmPm===!0||"true"===t.useAmPm:!0,l+="
"),l+="
",u=angular.element(l),a=r(u)(e),n.after(a)},d=function(){var t,a,r;return e.$on("aa:calendar:set-date",function(){return i?void 0:e.closePopup()}),r=!1,n.on("focus",function(){return e.isOpen?void 0:e.$apply(function(){return e.openPopup()})}),t=n.parent(),t.on("mousedown",function(){return r=!0,setTimeout(function(){return r=!1},100)}),n.on("blur",function(){return e.isOpen&&!r?e.$apply(function(){return e.closePopup()}):void 0}),a=function(){return e.isOpen&&!r?e.$apply(function(){return e.closePopup()}):void 0},angular.element(window.document).on("mousedown",a),e.$on("$destroy",function(){return n.off("focus"),n.off("blur"),t.off("mousedown"),angular.element(window.document).off("mousedown",a)})},c=function(){return n.on("click",function(){return e.isOpen?void 0:e.$apply(function(){return e.openPopup()})}),e.$on("$destroy",function(){return n.off("click")})},m=function(){return e.openPopup=function(){return e.isOpen=!0},e.closePopup=function(){return e.isOpen=!1}},o()},angular.module("angular-date-picker-polyfill").directive("aaDateInput",["$compile","aaDateUtil",function(n,t){return{restrict:"A",require:"ngModel",scope:{ngModel:"="},link:function(a,r,u,i){return e(a,r,u,i,n,t,!1)}}}]),angular.module("angular-date-picker-polyfill").directive("aaDateTimeInput",["$compile","aaDateUtil",function(n,t){return{restrict:"A",require:"ngModel",scope:{ngModel:"="},link:function(a,r,u,i){return e(a,r,u,i,n,t,!0)}}}]),Modernizr.inputtypes.date||angular.module("angular-date-picker-polyfill").directive("input",["$compile","aaDateUtil",function(n,t){return{restrict:"E",require:"?ngModel",scope:{ngModel:"="},compile:function(a,r){return null==r.ngModel||"date"!==r.type&&"datetime-local"!==r.type?void 0:function(a,r,u,i){return e(a,r,u,i,n,t,"datetime-local"===u.type)}}}}])}.call(this),function(){angular.module("angular-date-picker-polyfill").factory("aaMonthUtil",["aaDateUtil",function(e){return{numberOfDaysInMonth:function(e,n){return[31,e%4===0&&e%100!==0||e%400===0?29:28,31,30,31,30,31,31,30,31,30,31][n]},generateMonthArray:function(n,t,a){var r,u,i,l,o,c,s,d,m;for(null==a&&(a=null),u=new Date(n,t,1),s=new Date,l=new Date(n,t,this.numberOfDaysInMonth(n,t)),c=u.getDay(),u.setDate(u.getDate()+-1*c),r=[],d=0;l>=u;){for(r.push([]),i=m=0;6>=m;i=++m)o={date:angular.copy(u),isToday:e.dateObjectsAreEqualToDay(u,s),isSelected:a&&e.dateObjectsAreEqualToDay(u,a)?!0:!1,isOtherMonth:u.getMonth()!==t},r[d].push(o),u.setDate(u.getDate()+1);d+=1}return r}}}])}.call(this),function(){angular.module("angular-date-picker-polyfill").factory("aaTimeUtil",function(){return{getMinuteAndHourFromDate:function(e,n){var t,a,r;if(null==n&&(n=!0),!angular.isDate(e))return null;if(a=e.getHours(),t=null,n)switch(!1){case 0!==a:a=12,t="AM";break;case 12!==a:t="PM";break;case!(a>12):a-=12,t="PM"}return r=e.getMinutes(),[a,r,t]},applyTimeValuesToDateObject:function(e,n){var t,a,r;return a=e[0],r=e[1],t=e[2],n.setMinutes(r),n.setHours("AM"===t?12===a?0:a:"PM"===t&&12===a?12:"PM"===t&&12!==a?a+12:a),n}}})}.call(this),function(){angular.module("angular-date-picker-polyfill").directive("aaTimepicker",["aaTimeUtil","aaDateUtil",function(e,n){return{restrict:"A",replace:!0,require:"ngModel",scope:{},link:function(t,a,r,u){var i,l,o,c;return i=function(){return c(),o()},c=function(){var e,n,a,u;return t.useAmPm=null!=r.useAmPm?r.useAmPm===!0||"true"===r.useAmPm:!0,t.hourOptions=t.useAmPm?[1,2,3,4,5,6,7,8,9,10,11,12]:function(){for(a=[],e=0;23>=e;e++)a.push(e);return a}.apply(this),t.minuteOptions=function(){for(u=[],n=0;59>=n;n++)u.push(n);return u}.apply(this),t.amPmOptions=["AM","PM"]},o=function(){return t.hour=null,t.minute=null,t.amPm=null},u.$render=function(){return l()},l=function(){var n,a;return angular.isDate(u.$viewValue)?(n=angular.copy(u.$viewValue),a=e.getMinuteAndHourFromDate(n,t.useAmPm),t.hour=a[0],t.minute=a[1],t.amPm=a[2],a):o()},t.setTimeFromFields=function(){var a;return null!=t.hour&&null==t.minute&&(t.minute=0),null!=t.hour&&t.useAmPm&&null==t.amPm&&(t.amPm="AM"),null==t.hour||null==t.minute||t.useAmPm&&null==t.amPm?void 0:(a=null!=u.$viewValue&&angular.isDate(u.$viewValue)?new Date(u.$viewValue):n.todayStart(),e.applyTimeValuesToDateObject([t.hour,parseInt(t.minute),t.amPm],a),u.$setViewValue(a))},i()},template:"
\n \n \n \n \n \n \n
"}}])}.call(this); -------------------------------------------------------------------------------- /gulpfile.coffee: -------------------------------------------------------------------------------- 1 | gulp = require('gulp') 2 | $ = require('gulp-load-plugins')() 3 | 4 | packageName = 'angular-date-picker-polyfill' 5 | 6 | gulp.task 'scripts', -> 7 | gulp.src(['src/js/**/*.coffee', '!src/js/**/*.spec.coffee']) 8 | .pipe($.plumber({errorHandler: $.util.log})) 9 | .pipe($.coffee()) 10 | .pipe($.ngAnnotate()) 11 | .pipe($.concat("#{packageName}.js")) 12 | .pipe(gulp.dest('dist')) 13 | .pipe($.uglify()) 14 | .pipe($.rename({suffix: '.min'})) 15 | .pipe(gulp.dest('dist')) 16 | 17 | gulp.task 'stylesheets', -> 18 | gulp.src(['src/css/**/*.scss']) 19 | .pipe($.plumber({errorHandler: $.util.log})) 20 | .pipe($.sass({ 21 | outputStyle: 'nested', 22 | errLogToConsole: true 23 | })) 24 | .pipe($.rename({prefix: packageName + "-"})) 25 | .pipe(gulp.dest('dist')) 26 | .pipe($.minifyCss()) 27 | .pipe($.rename({suffix: '.min'})) 28 | .pipe(gulp.dest('dist')) 29 | 30 | gulp.task 'watch', -> 31 | gulp.watch('src/js/**/*.coffee', ['scripts']) 32 | gulp.watch('src/css/**/*.scss', ['stylesheets']) 33 | 34 | gulp.task 'clean', -> 35 | return gulp.src(["dist"], {read: false}) 36 | .pipe($.rimraf({force: true})) 37 | 38 | gulp.task 'compile', ['clean'], -> 39 | gulp.start('scripts', 'stylesheets') 40 | 41 | gulp.task 'dev', ['compile'], -> 42 | gulp.start('watch') 43 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | require('coffee-script/register'); 2 | require('./gulpfile.coffee'); 3 | -------------------------------------------------------------------------------- /karma.conf.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (config) -> 2 | config.set 3 | autoWatch: true 4 | frameworks: ['jasmine'] 5 | browsers: ['PhantomJS'] 6 | preprocessors: { 7 | '**/*.coffee': ['coffee'], 8 | }, 9 | coffeePreprocessor: { 10 | options: { 11 | sourceMap: false 12 | } 13 | transformPath: (path) -> path.replace(/\.js$/, '.coffee') 14 | } 15 | reporters: ['progress', 'osx'], 16 | files: [ 17 | "vendor/bower/modernizr/modernizr.js", 18 | "vendor/bower/jquery/dist/jquery.min.js", 19 | "vendor/bower/lodash/lodash.js", 20 | "vendor/bower/angular/angular.js", 21 | "vendor/bower/angular-mocks/angular-mocks.js", 22 | "src/js/**/*.coffee" 23 | ] 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-datepicker-polyfill", 3 | "version": "0.0.2", 4 | "author": "Adam Albrecht", 5 | "devDependencies": { 6 | "coffee-script": "^1.9.0", 7 | "gulp": "^3.8.10", 8 | "gulp-coffee": "^2.3.1", 9 | "gulp-concat": "^2.4.3", 10 | "gulp-load-plugins": "^0.8.0", 11 | "gulp-minify-css": "^0.4.4", 12 | "gulp-ng-annotate": "^0.5.2", 13 | "gulp-plumber": "^0.6.6", 14 | "gulp-rename": "^1.2.0", 15 | "gulp-rimraf": "^0.1.1", 16 | "gulp-sass": "^1.3.2", 17 | "gulp-uglify": "^1.1.0", 18 | "gulp-util": "^3.0.3", 19 | "jasmine-core": "^2.1.3", 20 | "karma": "^0.12.31", 21 | "karma-coffee-preprocessor": "^0.2.1", 22 | "karma-jasmine": "^0.3.5", 23 | "karma-osx-reporter": "^0.2.0", 24 | "karma-phantomjs-launcher": "^0.1.4" 25 | }, 26 | "scripts": { 27 | "spec": "node_modules/karma/bin/karma start karma.conf.coffee" 28 | }, 29 | "dependencies": {} 30 | } 31 | -------------------------------------------------------------------------------- /src/css/_calendar.scss: -------------------------------------------------------------------------------- 1 | // Calendar Wrapper 2 | .aa-cal { 3 | padding: $calendar-wrapper-padding; 4 | border: $calendar-border; 5 | display: inline-block; 6 | font-family: $font-family; 7 | font-size: $font-size; 8 | position: relative; 9 | 10 | // Contains the month name and buttons 11 | &-controls { 12 | background-color: $controls-bg-color; 13 | color: $controls-text-color; 14 | min-width: $cal-min-width; 15 | padding: $controls-padding; 16 | text-align: center; 17 | vertical-align: middle; 18 | } 19 | 20 | // Buttons shown at the top 21 | &-btn { 22 | background-color: $arrow-bg-color; 23 | border: $arrow-border; 24 | cursor: pointer; 25 | display: inline-block; 26 | font-size: $arrow-font-size; 27 | padding: $arrow-padding; 28 | &:hover { 29 | background-color: $arrow-hover-bg-color; 30 | border: $arrow-hover-border; 31 | } 32 | } 33 | // Name of the month 34 | &-month-name { 35 | display: inline-block; 36 | font-size: $month-name-font-size; 37 | padding: $month-name-padding; 38 | text-align: center; 39 | } 40 | &-prev-month, &-prev-year { float: left; } 41 | &-next-month, &-next-year { float: right; } 42 | &-next-month:before { content: "\203A"; } 43 | &-prev-month:before { content: "\2039"; } 44 | &-next-year:before { content: "\00bb"; } 45 | &-prev-year:before { content: "\00ab"; } 46 | &-set-to-today:before { content: "\29be" } 47 | &-set-to-today { display: none; } 48 | } 49 | 50 | // Calendar Table 51 | table.aa-cal-month { 52 | border-collapse: collapse; 53 | border-spacing: 0; 54 | table-layout: fixed; 55 | min-width: $cal-min-width; 56 | 57 | 58 | // Calenadr Table Cells 59 | td, 60 | th { 61 | border: 1px solid $cell-border-color; 62 | margin: 0; 63 | padding: $cell-padding; 64 | text-align: center; 65 | vertical-align: middle; 66 | } 67 | th { 68 | background-color: $header-row-bg-color; 69 | color: $header-row-color; 70 | font-weight: $header-row-font-weight; 71 | padding: $cell-padding; 72 | } 73 | 74 | td { 75 | background-color: $cell-bg-color; 76 | color: $cell-color; 77 | cursor: pointer; 78 | // Dates from the previous or next month 79 | // that show up in the first and last week 80 | span { 81 | border: 1px solid $cell-bg-color; 82 | } 83 | &.aa-cal-other-month { 84 | color: $other-month-cell-color; 85 | background-color: $other-month-cell-bg-color; 86 | font-weight: $other-month-cell-font-weight; 87 | span { 88 | border: 1px solid $other-month-cell-bg-color; 89 | } 90 | &:hover { 91 | background-color: $other-month-cell-hover-bg-color; 92 | span { border-color: $other-month-cell-hover-bg-color; } 93 | } 94 | } 95 | // The selected date 96 | &.aa-cal-selected { 97 | color: $today-cell-color; 98 | background-color: $today-cell-bg-color; 99 | font-weight: $today-cell-font-weight; 100 | span { 101 | border: 1px solid $selected-cell-inner-border-color; 102 | } 103 | } 104 | // Todays date 105 | &.aa-cal-today { 106 | color: $today-cell-color; 107 | background-color: $today-cell-bg-color; 108 | font-weight: $today-cell-font-weight; 109 | 110 | span { 111 | border: 1px solid $today-cell-bg-color; 112 | } 113 | 114 | &:hover { 115 | background-color: $today-cell-hover-bg-color; 116 | span { border-color: $today-cell-hover-bg-color; } 117 | } 118 | // Todays date AND selected 119 | &.q-calendar-selected { 120 | color: $selected-cell-color; 121 | background-color: $cell-bg-color; 122 | font-weight: $selected-cell-font-weight; 123 | span { 124 | border: 1px solid $selected-cell-inner-border-color; 125 | } 126 | &:hover { 127 | background-color: $selected-cell-hover-bg-color; 128 | span { border-color: $selected-cell-hover-inner-border-color; } 129 | } 130 | } 131 | } 132 | // Dates that have been disabled 133 | &.aa-cal-disabled { 134 | cursor: default; 135 | color: $disabled-cell-color; 136 | background-color: $disabled-cell-bg-color; 137 | font-weight: $disabled-cell-font-weight; 138 | span { 139 | border: 1px solid $disabled-cell-bg-color; 140 | } 141 | &:hover { 142 | background-color: $disabled-cell-hover-bg-color; 143 | span { border-color: $disabled-cell-hover-bg-color; } 144 | } 145 | } 146 | &:hover { 147 | background-color: $cell-hover-bg-color; 148 | span { border-color: $cell-hover-bg-color; } 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/css/_input.scss: -------------------------------------------------------------------------------- 1 | .aa-date-input { 2 | display: inline-block; 3 | position: relative; 4 | } 5 | 6 | .aa-date-input-cover { 7 | cursor: pointer; 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | display: inline-block; 12 | z-index: 1; 13 | width: 100%; 14 | background-color: #fff; 15 | border: solid 1px $medium-gray; 16 | padding: 2px 5px; 17 | } 18 | -------------------------------------------------------------------------------- /src/css/_popup.scss: -------------------------------------------------------------------------------- 1 | .aa-datepicker-popup { 2 | border: $popup-border; 3 | padding: $popup-padding; 4 | position: absolute; 5 | background-color: $popup-bg-color; 6 | box-shadow: 3px 3px 10px 0 rgba(#000, 0.4); 7 | top: 100%; 8 | left: 0px; 9 | z-index: 10000; 10 | 11 | &-close { 12 | width: 10px; 13 | height: 10px; 14 | position: absolute; 15 | top: 4px; 16 | right: 4px; 17 | font-size: 18px; 18 | cursor: pointer; 19 | line-height: 1.0; 20 | &:hover { 21 | // background-color: $light-gray; 22 | color: $light-gray; 23 | } 24 | &:before { 25 | content: "\d7"; 26 | } 27 | } 28 | 29 | .aa-timepicker { 30 | padding: 5px 0; 31 | text-align: center; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/css/_timepicker.scss: -------------------------------------------------------------------------------- 1 | .aa-timepicker { 2 | select { 3 | // Extra specific without having to resort to !important 4 | &.aa-timepicker-hour, &.aa-timepicker-minute, &.aa-timepicker-ampm { 5 | // Try to reset it to look as normal as possible 6 | // border: 0; 7 | // border-radius: 0; 8 | box-shadow: none; 9 | display: inline; 10 | height: auto; 11 | margin: auto; 12 | padding: 0; 13 | width: auto; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/css/_variables.scss: -------------------------------------------------------------------------------- 1 | // Colors 2 | $medium-gray: #ccc !default; 3 | $light-gray: lighten($medium-gray, 10%) !default; 4 | $primary-text-color: #333 !default; 5 | $selected-text-color: #000 !default; 6 | $disabled-text-color: #999 !default; 7 | $disabled-bg-color: #eee !default; 8 | 9 | // Fonts 10 | $font-family: inherit !default; 11 | $font-size: auto !default; 12 | 13 | // Border Colors 14 | $light-border-color: $light-gray !default; 15 | $medium-border-color: $medium-gray !default; 16 | 17 | // Calendar Title Bar 18 | $cal-min-width: 250px !default; 19 | $controls-bg-color: #fff !default; 20 | $controls-text-color: $primary-text-color !default; 21 | $controls-padding: 0 0 5px !default; 22 | 23 | // Calendar Title 24 | $month-name-font-size: $font-size !default; 25 | $month-name-padding: 5px 10px 2px !default; 26 | 27 | // Next, Prev Arrow Buttons 28 | $arrow-font-size: 1.1em !default; 29 | $arrow-padding: 3px 7.5px !default; 30 | $arrow-color: $primary-text-color !default; 31 | $arrow-border: none !default; 32 | $arrow-bg-color: inherit !default; 33 | $arrow-hover-bg-color: $disabled-bg-color !default; 34 | $arrow-hover-border: none !default; 35 | 36 | // Calendar wrapper 37 | $calendar-wrapper-padding: 0; 38 | $calendar-border: none !default; 39 | $calendar-bg: #fff !default; 40 | 41 | // Header Row (days of the week) 42 | $header-row-color: $primary-text-color !default; 43 | $header-row-bg-color: #fff !default; 44 | $header-row-font-weight: bold !default; 45 | 46 | // Basic Date Cells 47 | $cell-color: $primary-text-color !default; 48 | $cell-bg-color: #fff !default; 49 | $cell-padding: 5px !default; 50 | $cell-border-color: $light-border-color !default; 51 | $cell-hover-bg-color: darken($cell-bg-color, 10%) !default; 52 | 53 | // Selected Date Cell 54 | $selected-cell-color: $selected-text-color !default; 55 | $selected-cell-bg-color: $cell-bg-color !default; 56 | $selected-cell-font-weight: normal !default; 57 | $selected-cell-inner-border-color: $cell-bg-color !default; 58 | $selected-cell-hover-bg-color: darken($selected-cell-bg-color, 10%) !default; 59 | $selected-cell-hover-inner-border-color: darken($selected-cell-inner-border-color, 10%) !default; 60 | 61 | // Date Cell for next/prev month 62 | $other-month-cell-color: $disabled-text-color !default; 63 | $other-month-cell-bg-color: $disabled-bg-color !default; 64 | $other-month-cell-font-weight: normal !default; 65 | $other-month-cell-hover-bg-color: darken($other-month-cell-bg-color, 10%) !default; 66 | 67 | // Disabled date cell 68 | $disabled-cell-color: $disabled-text-color !default; 69 | $disabled-cell-bg-color: $disabled-bg-color !default; 70 | $disabled-cell-font-weight: normal !default; 71 | $disabled-cell-hover-bg-color: $disabled-cell-bg-color !default; 72 | 73 | // Today date cell 74 | $today-cell-color: $cell-color !default; 75 | $today-cell-bg-color: $cell-bg-color !default; 76 | $today-cell-font-weight: bold !default; 77 | $today-cell-hover-bg-color: darken($today-cell-bg-color, 10%); 78 | 79 | // Popup 80 | $popup-bg-color: #fff !default; 81 | $popup-padding: 20px 10px 10px !default; 82 | $popup-border: solid 1px $light-gray !default; 83 | -------------------------------------------------------------------------------- /src/css/basic.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | @import 'input'; 3 | @import 'calendar'; 4 | @import 'popup'; 5 | @import 'timepicker'; 6 | -------------------------------------------------------------------------------- /src/js/app.coffee: -------------------------------------------------------------------------------- 1 | angular.module('angular-date-picker-polyfill', []) 2 | -------------------------------------------------------------------------------- /src/js/calendar.directive.coffee: -------------------------------------------------------------------------------- 1 | #
2 | angular.module('angular-date-picker-polyfill') 3 | .directive 'aaCalendar', (aaMonthUtil, aaDateUtil, $filter) -> 4 | { 5 | restrict: 'A', 6 | replace: true, 7 | require: 'ngModel', 8 | scope: {}, 9 | link: (scope, elem, attrs, ngModelCtrl) -> 10 | scope.dayAbbreviations = ['Su', 'M', 'T', 'W', 'R', 'F', 'S'] 11 | # Nested array of the dates in the month 12 | scope.monthArray = [[]] 13 | # Date representing the calendar month shown 14 | scope.monthDate = null 15 | scope.selected = null 16 | 17 | # ngModelController Communication 18 | # ============================================ 19 | ngModelCtrl.$render = -> 20 | scope.selected = ngModelCtrl.$viewValue 21 | pullMonthDateFromModel() 22 | refreshView() 23 | 24 | # View / Scope Helpers 25 | # ============================================ 26 | pullMonthDateFromModel = -> 27 | if angular.isDate(ngModelCtrl.$viewValue) 28 | d = angular.copy(ngModelCtrl.$viewValue) 29 | else 30 | d = new Date() 31 | d.setDate(1) 32 | scope.monthDate = d 33 | 34 | 35 | refreshView = -> 36 | scope.monthArray = aaMonthUtil.generateMonthArray( 37 | scope.monthDate.getFullYear(), 38 | scope.monthDate.getMonth(), 39 | ngModelCtrl.$viewValue 40 | ) 41 | 42 | # View Actions 43 | # ============================================ 44 | scope.setDate = (d) -> 45 | c = if angular.isDate(ngModelCtrl.$viewValue) then angular.copy(ngModelCtrl.$viewValue) else aaDateUtil.todayStart() 46 | c.setYear(d.getFullYear()) 47 | c.setMonth(d.getMonth()) 48 | c.setDate(d.getDate()) 49 | ngModelCtrl.$setViewValue(c) 50 | unless aaDateUtil.dateObjectsAreEqualToMonth(d, scope.monthDate) 51 | pullMonthDateFromModel() 52 | refreshView() 53 | scope.$emit('aa:calendar:set-date') 54 | 55 | scope.setToToday = -> 56 | scope.setDate(aaDateUtil.todayStart()) 57 | 58 | scope.incrementMonths = (num) -> 59 | scope.monthDate.setMonth(scope.monthDate.getMonth() + num) 60 | refreshView() 61 | 62 | template: """ 63 |
64 |
65 | 66 | 67 | 68 | 69 | 70 | 71 |
72 | 73 | 74 | 75 | 76 | 77 | 78 | 84 | 85 | 86 |
82 | 83 |
87 |
88 | """ 89 | } 90 | -------------------------------------------------------------------------------- /src/js/calendar.directive.spec.coffee: -------------------------------------------------------------------------------- 1 | monthNames = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ] 2 | 3 | curMonthName = -> 4 | monthNames[(new Date()).getMonth()] 5 | 6 | describe 'aaCalendar', -> 7 | element = null 8 | scope = null 9 | $compile = null 10 | cal = null 11 | 12 | beforeEach(angular.mock.module('angular-date-picker-polyfill')) 13 | beforeEach(inject((_$compile_, $rootScope) -> 14 | scope = $rootScope.$new() 15 | $compile = _$compile_ 16 | return 17 | )) 18 | 19 | buildCalendar = (model) -> 20 | scope.myDate = model 21 | element = $compile("
")(scope) 22 | scope.$digest() 23 | new CalInterface(element) 24 | 25 | describe 'a basic calendar set to null', -> 26 | beforeEach -> cal = buildCalendar(null) 27 | 28 | it 'defaults to the current month', -> 29 | expect(cal.getMonthName()).toEqual("#{curMonthName()} #{(new Date()).getFullYear()}") 30 | 31 | it "adds a special class to today's date", -> 32 | expect($(element).find("table td.aa-cal-today").length).toEqual(1) 33 | 34 | it 'does not add the selected class to any date', -> 35 | expect($(element).find('.aa-cal-selected').length).toEqual(0) 36 | 37 | it 'has a header row of day abbreviations', -> 38 | expect($(element).find("table thead tr th:nth-child(1)").text()).toEqual("Su") 39 | expect($(element).find("table thead tr th:nth-child(2)").text()).toEqual("M") 40 | 41 | describe 'a calendar set to February 1st, 2015', -> 42 | beforeEach -> cal = buildCalendar(new Date(2015, 1, 1)) 43 | it 'has the first Sunday as Feb 1st', -> 44 | cell = cal.getDateCell(0, 0) 45 | expect($(cell).text().trim()).toEqual('1') 46 | 47 | describe 'a calendar set to April 9, 2015 at 14:30', -> 48 | beforeEach -> cal = buildCalendar(new Date(2015, 3, 9, 14, 30)) 49 | it 'shows the month name and year', -> 50 | expect(cal.getMonthName()).toEqual 'April 2015' 51 | 52 | it 'has the first Sunday as March 29th', -> 53 | cell = cal.getDateCell(0, 0) 54 | expect($(cell).text().trim()).toEqual('29') 55 | 56 | it "Adds 'other' classes to the first 3 days since they are part of March", -> 57 | $sun = cal.getDateCell(0, 0) 58 | $mon = cal.getDateCell(0, 1) 59 | $tue = cal.getDateCell(0, 2) 60 | $wed = cal.getDateCell(0, 3) 61 | expect($sun.text().trim()).toEqual('29') 62 | cls = "aa-cal-other-month" 63 | for day in [$sun, $mon, $tue] 64 | expect(day.hasClass(cls)).toBeTruthy() 65 | expect($wed.hasClass(cls)).toBeFalsy() 66 | 67 | it "Adds 'other' classes to the last 2 days since they are part of May", -> 68 | $thu = cal.getDateCell("last", 4) 69 | $fri = cal.getDateCell("last", 5) 70 | $sat = cal.getDateCell("last", 6) 71 | expect($thu.text().trim()).toEqual('30') 72 | cls = "aa-cal-other-month" 73 | expect($thu.hasClass(cls)).toBeFalsy() 74 | for day in [$fri, $sat] 75 | expect(day.hasClass(cls)).toBeTruthy() 76 | 77 | it 'applies the selected class to the selected date model cell', -> 78 | expect(cal.getDateCell(1, 4).hasClass('aa-cal-selected')).toBeTruthy() 79 | 80 | describe 'And I click the Next month button', -> 81 | beforeEach -> 82 | cal.clickNextMonth() 83 | 84 | it 'updates the month name to May', -> 85 | expect(cal.getMonthName()).toEqual("May 2015") 86 | 87 | describe 'And I click the Prev month button', -> 88 | beforeEach -> 89 | cal.clickPrevMonth() 90 | 91 | it 'updates the month name to March', -> 92 | expect(cal.getMonthName()).toEqual("March 2015") 93 | 94 | describe 'And I click the next year button', -> 95 | beforeEach -> 96 | cal.clickNextYear() 97 | 98 | it 'updates the year in the title to 2015', -> 99 | expect(cal.getMonthName()).toEqual("April 2016") 100 | 101 | describe 'And I click the prev year button', -> 102 | beforeEach -> 103 | cal.clickPrevYear() 104 | 105 | it 'updates the year in the title to 2014', -> 106 | expect(cal.getMonthName()).toEqual("April 2014") 107 | 108 | describe 'And I click on April 15', -> 109 | beforeEach -> 110 | cal.clickCalendarCell(2, 3) 111 | 112 | it 'updates the selected date model to April 15', -> 113 | expect(scope.myDate.getMonth()).toEqual(3) 114 | expect(scope.myDate.getDate()).toEqual(15) 115 | 116 | it 'does not update the time', -> 117 | expect(scope.myDate.getHours()).toEqual(14) 118 | expect(scope.myDate.getMinutes()).toEqual(30) 119 | 120 | it 'applies the selected class to the cell and no other cells', -> 121 | expect(cal.getDateCell(2, 3).hasClass('aa-cal-selected')).toBeTruthy() 122 | expect($(element).find('.aa-cal-selected').length).toEqual(1) 123 | 124 | describe 'And I click on March 29', -> 125 | beforeEach -> 126 | cal.clickCalendarCell(0, 0) 127 | 128 | it 'updates the selected date model to March 29', -> 129 | expect(scope.myDate.getMonth()).toEqual(2) 130 | expect(scope.myDate.getDate()).toEqual(29) 131 | 132 | it 'switches the month to March', -> 133 | expect(cal.getMonthName()).toEqual("March 2015") 134 | 135 | it 'updates the date model on the scope', -> 136 | expect(scope.myDate.getMonth()).toEqual(2) # 0-indexed months 137 | 138 | describe 'And the selected date is changed to July 1 outside the directive', -> 139 | beforeEach -> 140 | scope.myDate = new Date(2015, 6, 1) 141 | scope.$apply() 142 | 143 | it 'switches the calendar to July', -> 144 | expect(cal.getMonthName()).toEqual("July 2015") 145 | 146 | describe 'And the selected date is changed to null outside the directive', -> 147 | beforeEach -> 148 | scope.myDate = null 149 | scope.$apply() 150 | 151 | it 'removes the selected class from the cell', -> 152 | expect($(element).find('.aa-cal-selected').length).toEqual(0) 153 | 154 | it 'resets the calendar to the current month', -> 155 | expect(cal.getMonthName()).toEqual("#{curMonthName()} #{(new Date()).getFullYear()}") 156 | 157 | describe 'a calendar whose model is set to May 15, 2015', -> 158 | beforeEach -> cal = buildCalendar(new Date(2015, 4, 15)) 159 | 160 | it 'shows the 1st on the first friday', -> 161 | expect(cal.getDateCell(0, 5).text().trim()).toEqual("1") 162 | 163 | 164 | 165 | 166 | 167 | 168 | class CalInterface 169 | constructor: (element) -> 170 | @element = $(element) 171 | @scope = element.scope() 172 | 173 | 174 | getMonthName: => 175 | $(@element).find('.aa-cal-month-name').text() 176 | 177 | getTodayCell: => 178 | $(@element).find('table tbody .aa-cal-today') 179 | 180 | getDateCell: (w, d) => 181 | weekSelector = if typeof w == "string" then w else "nth-child(#{w + 1})" 182 | daySelector = if typeof d == "string" then d else "nth-child(#{d + 1})" 183 | $(@element).find("table tbody tr:#{weekSelector} td:#{daySelector}") 184 | 185 | clickNextMonth: => 186 | $(@element).find(".aa-cal-next-month").click() 187 | 188 | clickPrevMonth: => 189 | $(@element).find(".aa-cal-prev-month").click() 190 | 191 | clickNextYear: => 192 | $(@element).find(".aa-cal-next-year").click() 193 | 194 | clickPrevYear: => 195 | $(@element).find(".aa-cal-prev-year").click() 196 | 197 | clickCalendarCell: (w, d) => 198 | $td = @getDateCell(w, d) 199 | $td.click() 200 | -------------------------------------------------------------------------------- /src/js/date.util.coffee: -------------------------------------------------------------------------------- 1 | angular.module('angular-date-picker-polyfill') 2 | .factory 'aaDateUtil', -> 3 | dateObjectsAreEqualToDay: (d1, d2) -> 4 | return false unless angular.isDate(d1) && angular.isDate(d2) 5 | (d1.getFullYear() == d2.getFullYear()) && 6 | (d1.getMonth() == d2.getMonth()) && 7 | (d1.getDate() == d2.getDate()) 8 | 9 | dateObjectsAreEqualToMonth: (d1, d2) -> 10 | return false unless angular.isDate(d1) && angular.isDate(d2) 11 | (d1.getFullYear() == d2.getFullYear()) && 12 | (d1.getMonth() == d2.getMonth()) 13 | 14 | convertToDate: (val) -> 15 | if angular.isDate(val) 16 | val 17 | else 18 | d = Date.parse(val) 19 | if angular.isDate(d) then d else null 20 | 21 | todayStart: -> 22 | d = new Date() 23 | d.setHours(0) 24 | d.setMinutes(0) 25 | d.setSeconds(0) 26 | d.setMilliseconds(0) 27 | d 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/js/date.util.spec.coffee: -------------------------------------------------------------------------------- 1 | describe 'aaDateUtil', -> 2 | util = null 3 | beforeEach(angular.mock.module('angular-date-picker-polyfill')) 4 | 5 | beforeEach(inject((_aaDateUtil_) -> 6 | util = _aaDateUtil_ 7 | return 8 | )) 9 | 10 | describe 'dateObjectsAreEqualToDay', -> 11 | it 'is true for 2 exact copies of the same date', -> 12 | d1 = new Date() 13 | d2 = angular.copy(d1) 14 | expect(util.dateObjectsAreEqualToDay(d1, d2)).toEqual(true) 15 | it 'is not true for 2 completely different dates', -> 16 | d1 = new Date() 17 | d2 = new Date(2010, 1, 1) 18 | expect(util.dateObjectsAreEqualToDay(d1, d2)).toEqual(false) 19 | 20 | 21 | describe 'dateObjectsAreEqualToMonth', -> 22 | it 'is true for 2 exact copies of the same date', -> 23 | d1 = new Date() 24 | d2 = angular.copy(d1) 25 | expect(util.dateObjectsAreEqualToMonth(d1, d2)).toEqual(true) 26 | it 'is true for Jan 1 and Jan 31', -> 27 | d1 = new Date(2015, 0, 1) 28 | d2 = new Date(2015, 0, 31) 29 | expect(util.dateObjectsAreEqualToMonth(d1, d2)).toEqual(true) 30 | it 'is false for Jan 1 of different years', -> 31 | d1 = new Date(2015, 0, 1) 32 | d2 = new Date(2014, 0, 1) 33 | expect(util.dateObjectsAreEqualToMonth(d1, d2)).toEqual(false) 34 | it 'is false for Jan 1 and Feb 1', -> 35 | d1 = new Date(2015, 0, 1) 36 | d2 = new Date(2015, 1, 1) 37 | expect(util.dateObjectsAreEqualToMonth(d1, d2)).toEqual(false) 38 | -------------------------------------------------------------------------------- /src/js/input.directive.coffee: -------------------------------------------------------------------------------- 1 | linker = (scope, elem, attrs, ngModelCtrl, $compile, aaDateUtil, includeTimepicker=false) -> 2 | # Main Function. Calls all functions below 3 | init = -> 4 | compileTemplate() 5 | setupViewActionMethods() 6 | setupPopupTogglingEvents() 7 | 8 | if elem.prop('tagName') != 'INPUT' || (attrs.type != 'date' && attrs.type != 'datetime-local') 9 | setupNonInputEvents() 10 | setupNonInputValidatorAndFormatter() 11 | 12 | # For elments that are not date inputs, do some light formatting 13 | # and validation 14 | setupNonInputValidatorAndFormatter = -> 15 | ngModelCtrl.$formatters.unshift(aaDateUtil.convertToDate) 16 | 17 | if includeTimepicker 18 | ngModelCtrl.$validators['datetime-local'] = (modelValue, viewValue) -> 19 | !viewValue || angular.isDate(viewValue) 20 | else 21 | ngModelCtrl.$validators.date = (modelValue, viewValue) -> 22 | !viewValue || angular.isDate(viewValue) 23 | 24 | 25 | # Wrap the element in a div, then add the popup div after 26 | compileTemplate = -> 27 | elem.wrap("
") 28 | tmpl = """ 29 |
30 |
31 |
32 | """ 33 | 34 | if includeTimepicker 35 | useAmPm = if attrs.useAmPm? then (attrs.useAmPm == true || attrs.useAmPm == 'true') else true 36 | tmpl += "
" 37 | 38 | tmpl += "
" 39 | 40 | popupDiv = angular.element(tmpl) 41 | $popup = $compile(popupDiv)(scope) 42 | elem.after($popup) 43 | 44 | # Various events need to be created related to opening and closing 45 | # the popup. They also need to be disabled when the directive 46 | # is destroyed 47 | setupPopupTogglingEvents = -> 48 | # Upon setting the date from the calendar, close the popup 49 | scope.$on 'aa:calendar:set-date', -> 50 | scope.closePopup() unless includeTimepicker 51 | wrapperClicked = false 52 | 53 | # Open on focus 54 | elem.on 'focus', (e) -> 55 | unless scope.isOpen 56 | scope.$apply -> scope.openPopup() 57 | 58 | # Upon click, set a variable so that other 59 | # events know that the click was intentional 60 | $wrapper = elem.parent() 61 | $wrapper.on 'mousedown', (e) -> 62 | wrapperClicked = true 63 | setTimeout( 64 | -> 65 | wrapperClicked = false 66 | 100 67 | ) 68 | 69 | # On blur (onfocus), close the popup unless 70 | # there was an intentional click 71 | elem.on 'blur', (e) -> 72 | if scope.isOpen && !wrapperClicked 73 | scope.$apply -> scope.closePopup() 74 | 75 | # If there was a click anywhere else on the 76 | # page, close the popup 77 | onDocumentClick = (e) -> 78 | if scope.isOpen && !wrapperClicked 79 | scope.$apply -> scope.closePopup() 80 | angular.element(window.document).on 'mousedown', onDocumentClick 81 | 82 | # Disable events when directive is destroyed 83 | scope.$on '$destroy', -> 84 | elem.off 'focus' 85 | elem.off 'blur' 86 | $wrapper.off 'mousedown' 87 | angular.element(window.document).off 'mousedown', onDocumentClick 88 | 89 | # Since not all events respond to focus events, add a click event 90 | setupNonInputEvents = -> 91 | elem.on 'click', (e) -> 92 | unless scope.isOpen 93 | scope.$apply -> scope.openPopup() 94 | scope.$on '$destroy', -> elem.off 'click' 95 | 96 | 97 | # Open and Close methods on the scope 98 | setupViewActionMethods = -> 99 | scope.openPopup = -> 100 | scope.isOpen = true 101 | 102 | scope.closePopup = -> 103 | scope.isOpen = false 104 | 105 | 106 | init() 107 | 108 | 109 | angular.module('angular-date-picker-polyfill') 110 | .directive 'aaDateInput', ($compile, aaDateUtil) -> 111 | { 112 | restrict: 'A', 113 | require: 'ngModel', 114 | scope: { 115 | ngModel: '=' 116 | }, 117 | link: (scope, elem, attrs, ngModelCtrl) -> 118 | linker(scope, elem, attrs, ngModelCtrl, $compile, aaDateUtil, false) 119 | } 120 | 121 | angular.module('angular-date-picker-polyfill') 122 | .directive 'aaDateTimeInput', ($compile, aaDateUtil) -> 123 | { 124 | restrict: 'A', 125 | require: 'ngModel', 126 | scope: { 127 | ngModel: '=' 128 | }, 129 | link: (scope, elem, attrs, ngModelCtrl) -> 130 | linker(scope, elem, attrs, ngModelCtrl, $compile, aaDateUtil, true) 131 | } 132 | 133 | 134 | 135 | unless Modernizr.inputtypes.date 136 | angular.module('angular-date-picker-polyfill') 137 | .directive 'input', ($compile, aaDateUtil) -> 138 | { 139 | restrict: 'E', 140 | require: '?ngModel', 141 | scope: { 142 | ngModel: '=' 143 | }, 144 | compile: (elem, attrs) -> 145 | return unless attrs.ngModel? && (attrs.type == 'date' || attrs.type == 'datetime-local') 146 | (scope, elem, attrs, ngModelCtrl) -> 147 | linker(scope, elem, attrs, ngModelCtrl, $compile, aaDateUtil, (attrs.type == 'datetime-local')) 148 | } 149 | -------------------------------------------------------------------------------- /src/js/month.util.coffee: -------------------------------------------------------------------------------- 1 | angular.module('angular-date-picker-polyfill') 2 | .factory 'aaMonthUtil', (aaDateUtil) -> 3 | # zero indexed months 4 | numberOfDaysInMonth: (year, month) -> 5 | [31, (if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) then 29 else 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month] 6 | 7 | generateMonthArray: (year, month, selected=null) -> 8 | d = new Date(year, month, 1) 9 | today = new Date() 10 | endDate = new Date(year, month, @numberOfDaysInMonth(year, month)) 11 | offset = d.getDay() 12 | d.setDate(d.getDate() + (offset * -1)) 13 | arr = [] 14 | weekNum = 0 15 | while d <= endDate 16 | arr.push([]) 17 | for dayIndex in [0..6] 18 | obj = { 19 | date: angular.copy(d), 20 | isToday: aaDateUtil.dateObjectsAreEqualToDay(d, today), 21 | isSelected: if (selected && aaDateUtil.dateObjectsAreEqualToDay(d, selected)) then true else false, 22 | isOtherMonth: d.getMonth() != month 23 | } 24 | arr[weekNum].push(obj) 25 | d.setDate(d.getDate() + 1) 26 | weekNum += 1 27 | arr 28 | -------------------------------------------------------------------------------- /src/js/month.util.spec.coffee: -------------------------------------------------------------------------------- 1 | describe 'aaMonthUtil', -> 2 | util = null 3 | beforeEach(angular.mock.module('angular-date-picker-polyfill')) 4 | 5 | beforeEach(inject((_aaMonthUtil_) -> 6 | util = _aaMonthUtil_ 7 | return 8 | )) 9 | 10 | describe 'numberOfDaysInMonth', -> 11 | it 'is correct for a bunch of examples', -> 12 | # Remember, the months are zero indexed 13 | expect(util.numberOfDaysInMonth(2015, 0)).toEqual(31) 14 | expect(util.numberOfDaysInMonth(2015, 1)).toEqual(28) 15 | 16 | describe 'generateMonthArray', -> 17 | monthArr = null 18 | today = null 19 | describe 'for February 2015', -> 20 | beforeEach -> 21 | monthArr = util.generateMonthArray(2015, 1) 22 | it 'returns an array of 4 nested arrays, each with 7 days', -> 23 | expect(monthArr.length).toEqual(4) 24 | for week in monthArr 25 | expect(week.length).toEqual(7) 26 | 27 | it 'has the 1st at [0][0] and the 28th at [3][6]', -> 28 | expect(monthArr[0][0].date.getDate()).toEqual(1) 29 | expect(monthArr[3][6].date.getDate()).toEqual(28) 30 | 31 | describe 'for May 2015', -> 32 | beforeEach -> 33 | monthArr = util.generateMonthArray(2015, 4) 34 | 35 | it 'returns an array of 6 nested arrays, each with 7 days', -> 36 | expect(monthArr.length).toEqual(6) 37 | for week in monthArr 38 | expect(week.length).toEqual(7) 39 | 40 | it 'has the first 5 days of April 2015 in week 1', -> 41 | expect(monthArr[0][0].date.toDateString()).toEqual('Sun Apr 26 2015') 42 | expect(monthArr[0][4].date.toDateString()).toEqual('Thu Apr 30 2015') 43 | expect(monthArr[0][5].date.toDateString()).toEqual('Fri May 01 2015') 44 | 45 | it 'has the first 6 days of June 2015 in week 6', -> 46 | expect(monthArr[5][0].date.toDateString()).toEqual('Sun May 31 2015') 47 | expect(monthArr[5][1].date.toDateString()).toEqual('Mon Jun 01 2015') 48 | expect(monthArr[5][6].date.toDateString()).toEqual('Sat Jun 06 2015') 49 | 50 | it 'flags dates that are not in May 2015', -> 51 | expect(monthArr[0][0].isOtherMonth).toEqual(true) 52 | expect(monthArr[0][4].isOtherMonth).toEqual(true) 53 | expect(monthArr[0][5].isOtherMonth).toEqual(false) 54 | expect(monthArr[5][0].isOtherMonth).toEqual(false) 55 | expect(monthArr[5][1].isOtherMonth).toEqual(true) 56 | expect(monthArr[5][6].isOtherMonth).toEqual(true) 57 | 58 | 59 | describe 'with a selected date', -> 60 | it 'flags the selected date', -> 61 | expect(hasSelected(util.generateMonthArray(2015, 4, (new Date(2015, 4, 10))))).toEqual(true) 62 | expect(hasSelected(util.generateMonthArray(2015, 3, (new Date(2015, 4, 10))))).toEqual(false) 63 | expect(hasSelected(util.generateMonthArray(2015, 4, (new Date(2014, 4, 10))))).toEqual(false) 64 | 65 | describe 'for the current month', -> 66 | beforeEach -> 67 | today = new Date() 68 | 69 | it 'flags todays date', -> 70 | thisMonth = util.generateMonthArray(today.getFullYear(), today.getMonth()) 71 | nextMonth = util.generateMonthArray(today.getFullYear(), today.getMonth() + 1) 72 | nextYear = util.generateMonthArray(today.getFullYear() + 1, today.getMonth() ) 73 | expect(hasToday(thisMonth)).toEqual(true) 74 | expect(hasToday(nextMonth)).toEqual(false) 75 | expect(hasToday(nextYear)).toEqual(false) 76 | 77 | 78 | hasToday = (arr) -> 79 | todayCount = 0 80 | for week in arr 81 | for day in week 82 | if day.isToday 83 | todayCount += 1 84 | todayCount == 1 85 | 86 | hasSelected = (arr) -> 87 | sCount = 0 88 | for week in arr 89 | for day in week 90 | if day.isSelected 91 | sCount += 1 92 | sCount == 1 93 | -------------------------------------------------------------------------------- /src/js/time.util.coffee: -------------------------------------------------------------------------------- 1 | angular.module('angular-date-picker-polyfill') 2 | .factory 'aaTimeUtil', -> 3 | getMinuteAndHourFromDate: (d, useAmPmHours=true) -> 4 | return null unless angular.isDate(d) 5 | h = d.getHours() 6 | amPm = null 7 | if useAmPmHours 8 | switch 9 | when h == 0 10 | h = 12 11 | amPm = 'AM' 12 | when h == 12 13 | amPm = 'PM' 14 | when h > 12 15 | h = h - 12 16 | amPm = 'PM' 17 | m = d.getMinutes() 18 | [h, m, amPm] 19 | 20 | applyTimeValuesToDateObject: (timeValues, d) -> 21 | [hour, minute, amPm] = timeValues 22 | d.setMinutes(minute) 23 | if amPm == 'AM' 24 | d.setHours(if hour == 12 then 0 else hour) 25 | else if amPm == 'PM' && hour == 12 26 | d.setHours(12) 27 | else if amPm == 'PM' && hour != 12 28 | d.setHours(hour + 12) 29 | else 30 | d.setHours(hour) 31 | d 32 | -------------------------------------------------------------------------------- /src/js/time.util.spec.coffee: -------------------------------------------------------------------------------- 1 | describe 'aaTimeUtil', -> 2 | util = null 3 | beforeEach(angular.mock.module('angular-date-picker-polyfill')) 4 | 5 | beforeEach(inject((_aaTimeUtil_) -> 6 | util = _aaTimeUtil_ 7 | return 8 | )) 9 | 10 | describe 'getMinuteAndHourFromDate', -> 11 | d = null 12 | amPm = null 13 | getResult = -> util.getMinuteAndHourFromDate(d, amPm) 14 | describe 'on a date set to 12:30am', -> 15 | beforeEach -> d = new Date(2015, 1, 5, 0, 30) 16 | describe 'when using AM/PM', -> 17 | beforeEach -> amPm = true 18 | it 'returns [12, 30, am]', -> 19 | expect(getResult()).toEqual([12, 30, 'AM']) 20 | describe 'when not using AM/PM', -> 21 | beforeEach -> amPm = false 22 | it 'returns [0, 30]', -> 23 | expect(getResult()).toEqual([0, 30, null]) 24 | describe 'on a date set to 12:30pm', -> 25 | beforeEach -> d = new Date(2015, 1, 5, 12, 30) 26 | describe 'when using AM/PM', -> 27 | beforeEach -> amPm = true 28 | it 'returns [12, 30, pm]', -> 29 | expect(getResult()).toEqual([12, 30, 'PM']) 30 | describe 'when not using AM/PM', -> 31 | beforeEach -> amPm = false 32 | it 'returns [0, 30]', -> 33 | expect(getResult()).toEqual([12, 30, null]) 34 | 35 | 36 | describe 'applyTimeValuesToDateObject', -> 37 | d = null 38 | describe 'given a date object set to 0:00:00', -> 39 | beforeEach -> d = new Date(2015, 1, 5, 0, 0, 0) 40 | it 'can be set to 5:05 pm', -> 41 | util.applyTimeValuesToDateObject([5, 5, 'PM'], d) 42 | expect(d.getHours()).toEqual(17) 43 | expect(d.getMinutes()).toEqual(5) 44 | 45 | it 'can be set to 12pm', -> 46 | util.applyTimeValuesToDateObject([12, 0, 'PM'], d) 47 | expect(d.getHours()).toEqual(12) 48 | expect(d.getMinutes()).toEqual(0) 49 | 50 | it 'can be set to 8am', -> 51 | util.applyTimeValuesToDateObject([8, 0, 'AM'], d) 52 | expect(d.getHours()).toEqual(8) 53 | expect(d.getMinutes()).toEqual(0) 54 | 55 | it 'can be set to 8pm', -> 56 | util.applyTimeValuesToDateObject([8, 0, 'PM'], d) 57 | expect(d.getHours()).toEqual(20) 58 | expect(d.getMinutes()).toEqual(0) 59 | 60 | it 'can be set to 8am via 24-hour time', -> 61 | util.applyTimeValuesToDateObject([8, 0], d) 62 | expect(d.getHours()).toEqual(8) 63 | expect(d.getMinutes()).toEqual(0) 64 | 65 | it 'can be set to 8pm via 24-hour time', -> 66 | util.applyTimeValuesToDateObject([20, 0], d) 67 | expect(d.getHours()).toEqual(20) 68 | expect(d.getMinutes()).toEqual(0) 69 | -------------------------------------------------------------------------------- /src/js/timepicker.directive.coffee: -------------------------------------------------------------------------------- 1 | #
2 | angular.module('angular-date-picker-polyfill') 3 | .directive 'aaTimepicker', (aaTimeUtil, aaDateUtil) -> 4 | { 5 | restrict: 'A', 6 | replace: true, 7 | require: 'ngModel', 8 | scope: {}, 9 | link: (scope, elem, attrs, ngModelCtrl) -> 10 | 11 | init = -> 12 | setupSelectOptions() 13 | resetToNull() 14 | 15 | setupSelectOptions = -> 16 | scope.useAmPm = if attrs.useAmPm? then (attrs.useAmPm == true || attrs.useAmPm == 'true') else true 17 | scope.hourOptions = if scope.useAmPm then [1..12] else [0..23] 18 | scope.minuteOptions = [0..59] 19 | scope.amPmOptions = ['AM', 'PM'] 20 | 21 | resetToNull = -> 22 | scope.hour = null 23 | scope.minute = null 24 | scope.amPm = null 25 | 26 | ngModelCtrl.$render = -> 27 | pullTimeFromModel() 28 | 29 | pullTimeFromModel = -> 30 | if angular.isDate(ngModelCtrl.$viewValue) 31 | d = angular.copy(ngModelCtrl.$viewValue) 32 | [scope.hour, scope.minute, scope.amPm] = aaTimeUtil.getMinuteAndHourFromDate(d, scope.useAmPm) 33 | else 34 | resetToNull() 35 | 36 | scope.setTimeFromFields = -> 37 | if scope.hour? && !scope.minute? 38 | scope.minute = 0 39 | if scope.hour? && scope.useAmPm && !scope.amPm? 40 | scope.amPm = 'AM' 41 | return unless scope.hour? && scope.minute? && (!scope.useAmPm || scope.amPm?) 42 | if ngModelCtrl.$viewValue? && angular.isDate(ngModelCtrl.$viewValue) 43 | d = new Date(ngModelCtrl.$viewValue) 44 | else 45 | d = aaDateUtil.todayStart() 46 | aaTimeUtil.applyTimeValuesToDateObject([scope.hour, parseInt(scope.minute), scope.amPm], d) 47 | ngModelCtrl.$setViewValue(d) 48 | 49 | init() 50 | 51 | 52 | template: """ 53 |
54 | 61 | 68 | 76 |
77 | """ 78 | } 79 | -------------------------------------------------------------------------------- /src/js/timepicker.directive.spec.coffee: -------------------------------------------------------------------------------- 1 | areTimesEqual = (d1, d2) -> 2 | angular.isDate(d1) && 3 | angular.isDate(d2) && 4 | d1.getHours() == d2.getHours() && 5 | d1.getMinutes() == d2.getMinutes() 6 | 7 | describe 'aaTimepicker', -> 8 | element = null 9 | scope = null 10 | $compile = null 11 | picker = null 12 | 13 | beforeEach(angular.mock.module('angular-date-picker-polyfill')) 14 | beforeEach(inject((_$compile_, $rootScope) -> 15 | scope = $rootScope.$new() 16 | $compile = _$compile_ 17 | return 18 | )) 19 | 20 | buildTimepicker = (model, amPm=null) -> 21 | scope.myDate = model 22 | if amPm == null 23 | element = $compile("
")(scope) 24 | else 25 | element = $compile("
")(scope) 26 | scope.$digest() 27 | new TimepickerInterface(element) 28 | 29 | describe 'a timepicker with no am-pm value set', -> 30 | beforeEach -> picker = buildTimepicker(null, null) 31 | 32 | it 'defaults to showing the AM/PM select', -> 33 | expect($(element).find(".aa-timepicker-ampm").length).toEqual(1) 34 | 35 | 36 | describe 'a basic timepicker with am-pm set to true', -> 37 | beforeEach -> picker = buildTimepicker(null, true) 38 | 39 | it 'defaults to showing the AM/PM select', -> 40 | expect($(element).find(".aa-timepicker-ampm").length).toEqual(1) 41 | 42 | describe 'with the model set to null', -> 43 | it 'defaults to blank values', -> 44 | expect(picker.getTimeShown()).toEqual("") 45 | 46 | describe 'and the hour is set to 5', -> 47 | beforeEach -> picker.setHourSelect(5) 48 | it 'sets the time to 5:00 AM', -> 49 | expect(picker.getTimeShown()).toEqual("5:00 AM") 50 | 51 | describe 'and the minute is set to 28', -> 52 | beforeEach -> picker.setMinuteSelect(28) 53 | it 'sets the model to 5:28 AM', -> 54 | expect(areTimesEqual(scope.myDate, new Date(2015, 1, 1, 5, 28))).toEqual(true) 55 | 56 | describe 'and it is set to PM', -> 57 | beforeEach -> picker.setAmPmSelect('PM') 58 | it 'sets the model to 5:28 PM', -> 59 | dt = new Date(2015, 1, 1, 17, 28) 60 | expect(areTimesEqual(scope.myDate, dt)).toEqual(true) 61 | 62 | describe 'with the model set to a date at 12:05 AM', -> 63 | beforeEach -> 64 | scope.myDate = new Date(2014, 5, 5, 0, 5) 65 | scope.$apply() 66 | 67 | it 'shows the proper time in 12-hour format', -> 68 | expect(picker.getTimeShown()).toEqual("12:05 AM") 69 | scope.myDate = new Date(2014, 5, 5, 23, 59) 70 | scope.$apply() 71 | expect(picker.getTimeShown()).toEqual("11:59 PM") 72 | scope.myDate = new Date(2014, 5, 5, 0, 0) 73 | scope.$apply() 74 | expect(picker.getTimeShown()).toEqual("12:00 AM") 75 | 76 | describe 'and the date object is incremented by 5 minutes from the outside', -> 77 | beforeEach -> 78 | scope.myDate = new Date(scope.myDate.setMinutes(10)) 79 | scope.$digest() 80 | 81 | it 'is reflected in the select boxes', -> 82 | expect(picker.getTimeShown()).toEqual("12:10 AM") 83 | 84 | describe 'and the hour select is set to 9', -> 85 | beforeEach -> picker.setHourSelect(9) 86 | it 'changes the displayed time to 9:05', -> 87 | expect(picker.getTimeShown()).toEqual('9:05 AM') 88 | it 'changes the model time to 9:05', -> 89 | scope.$apply() 90 | expect(scope.myDate.getHours()).toEqual(9) 91 | expect(scope.myDate.getMinutes()).toEqual(5) 92 | 93 | describe 'a timepicker with am-pm set to false', -> 94 | beforeEach -> picker = buildTimepicker(new Date(2014, 5, 5, 0, 5), false) 95 | 96 | it 'does not show the am-pm select', -> 97 | expect($(element).find(".aa-timepicker-ampm").attr('class')).toMatch(/ng-hide/) 98 | 99 | it 'shows the proper time in 24-hour format', -> 100 | expect(picker.getTimeShown()).toEqual("0:05") 101 | scope.myDate = new Date(2014, 5, 5, 23, 59) 102 | scope.$apply() 103 | expect(picker.getTimeShown()).toEqual("23:59") 104 | scope.myDate = new Date(2014, 5, 5, 0, 0) 105 | scope.$apply() 106 | expect(picker.getTimeShown()).toEqual("0:00") 107 | 108 | 109 | class TimepickerInterface 110 | constructor: (element) -> 111 | @element = $(element) 112 | @scope = element.scope() 113 | 114 | hourSelect: => $(@element).find(".aa-timepicker-hour") 115 | minuteSelect: => $(@element).find(".aa-timepicker-minute") 116 | amPmSelect: => $(@element).find(".aa-timepicker-ampm") 117 | 118 | getHour: => 119 | $(@element).find(".aa-timepicker-hour option:selected").text() 120 | 121 | 122 | getMinute: => 123 | $(@element).find(".aa-timepicker-minute option:selected").text() 124 | 125 | getAmPm: => 126 | if $(@element).find(".aa-timepicker-ampm").length 127 | $(@element).find(".aa-timepicker-ampm option:selected").text() 128 | else 129 | null 130 | 131 | setHourSelect: (hour) => 132 | $(@element).find(".aa-timepicker-hour option").each((i, opt) => 133 | if $(opt).text() == "#{hour}" 134 | @hourSelect().val($(opt).val()) 135 | @hourSelect().trigger('change') 136 | return false 137 | ) 138 | 139 | setMinuteSelect: (min) => 140 | $(@element).find(".aa-timepicker-minute option").each((i, opt) => 141 | if $(opt).text() == "#{min}" 142 | @minuteSelect().val($(opt).val()) 143 | @minuteSelect().trigger('change') 144 | return false 145 | ) 146 | 147 | setAmPmSelect: (val) => 148 | amPmVals = { 149 | 'AM': '0', 150 | 'PM': '1' 151 | } 152 | @amPmSelect().val(amPmVals[val]) 153 | @amPmSelect().trigger('change') 154 | return 155 | 156 | getTimeShown: => 157 | t = "#{@getHour()}:#{@getMinute()}" 158 | ampm = @getAmPm() 159 | t = if ampm then "#{t} #{ampm}" else t 160 | if t? 161 | t = t.trim() 162 | if t == ":" 163 | t = "" 164 | t 165 | --------------------------------------------------------------------------------