├── .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 |
36 | {{ myDate | date:'shortDate'}}
37 | Not Set
38 |
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 |
53 | {{ myDate | date:'short'}}
54 | Not Set
55 |
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 |
50 |
51 |
Values:
52 |
53 | Calendar Field: {{myCalendarDate | date:'shortDate'}}
54 | Time Field: {{myTimepickerDate | date:'shortTime'}}
55 | Date Field: {{myDate | date:'shortDate'}}
56 | Date/Time Field: {{myDateTime | date: 'short'}}
57 | Button Date Field: {{myButtonDate | date: 'shortDate'}}
58 | Button Date/Time Field: {{myButtonDateTime | date: 'short'}}
59 |
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 = "";
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="",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 |
82 |
83 |
84 |
85 |
86 |
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 | "
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 |
60 |
61 |
67 |
68 |
75 |
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 |
--------------------------------------------------------------------------------