├── .bowerrc ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintrc ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── README.md ├── app ├── index.html ├── scripts │ ├── datePicker.js │ ├── datePickerUtils.js │ ├── dateRange.js │ └── input.js ├── styles │ ├── bootstrap.css │ ├── mixins.less │ ├── style.less │ └── variables.less └── templates │ └── datepicker.html ├── bower.json ├── circle.yml ├── dist ├── angular-datepicker.css ├── angular-datepicker.js ├── angular-datepicker.min.css └── angular-datepicker.min.js ├── index.js ├── karma-e2e.conf.js ├── karma.conf.js ├── package.json └── test ├── runner.html └── spec ├── datePickerTest.js ├── datePickerUtilsTest.js ├── inputTest.js └── querySelectorFind.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/components" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .tmp 3 | .sass-cache 4 | app/components 5 | .idea 6 | *.iml 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": false, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": false, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "angular": false, 23 | "define": false, 24 | "moment": false 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | before_install: 5 | - npm install -g grunt-cli bower 6 | install: 7 | - npm install 8 | - bower install 9 | script: 10 | - grunt test 11 | cache: 12 | directories: 13 | - node_modules 14 | - bower_components 15 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var lrSnippet = require('grunt-contrib-livereload/lib/utils').livereloadSnippet; 3 | var mountFolder = function (connect, dir) { 4 | return connect.static(require('path').resolve(dir)); 5 | }; 6 | 7 | module.exports = function (grunt) { 8 | // load all grunt tasks 9 | require('load-grunt-tasks')(grunt); 10 | 11 | // configurable paths 12 | var yeomanConfig = { 13 | app: 'app', 14 | dist: 'dist', 15 | tmp: '.tmp' 16 | }; 17 | 18 | try { 19 | yeomanConfig.app = require('./component.json').appPath || yeomanConfig.app; 20 | } catch (e) { 21 | } 22 | 23 | grunt.initConfig({ 24 | yeoman: yeomanConfig, 25 | watch: { 26 | less: { 27 | files: ['<%= yeoman.app %>/styles/{,*/}*.less'], 28 | tasks: ['less', 'copy:styles'], 29 | options: { 30 | nospawn: true 31 | } 32 | }, 33 | styles: { 34 | files: ['<%= yeoman.app %>/styles/{,*/}*.css'], 35 | tasks: ['copy:styles'] 36 | }, 37 | livereload: { 38 | files: [ 39 | '<%= yeoman.app %>/{,*/}*.html', 40 | '<%= yeoman.tmp %>/styles/{,*/}*.css', 41 | '{<%= yeoman.tmp %>,<%= yeoman.app %>}/scripts/{,*/}*.js', 42 | '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' 43 | ] 44 | } 45 | }, 46 | less: { 47 | options: { 48 | compile: true 49 | }, 50 | dist: { 51 | files: [ 52 | { 53 | expand: true, 54 | cwd: '<%= yeoman.app %>/styles', 55 | src: 'style.less', 56 | dest: '<%= yeoman.tmp %>/styles/', 57 | ext: '.css' 58 | } 59 | ] 60 | } 61 | }, 62 | connect: { 63 | options: { 64 | port: 9000, 65 | // Change this to '0.0.0.0' to access the server from outside. 66 | hostname: 'localhost' 67 | }, 68 | livereload: { 69 | options: { 70 | middleware: function (connect) { 71 | return [ 72 | lrSnippet, 73 | mountFolder(connect, yeomanConfig.tmp), 74 | mountFolder(connect, yeomanConfig.app) 75 | ]; 76 | } 77 | } 78 | }, 79 | test: { 80 | options: { 81 | middleware: function (connect) { 82 | return [ 83 | mountFolder(connect, yeomanConfig.tmp), 84 | mountFolder(connect, 'test') 85 | ]; 86 | } 87 | } 88 | } 89 | }, 90 | open: { 91 | server: { 92 | url: 'http://localhost:<%= connect.options.port %>' 93 | } 94 | }, 95 | clean: { 96 | dist: { 97 | files: [ 98 | { 99 | dot: false, 100 | src: [ 101 | '<%= yeoman.tmp %>', 102 | '<%= yeoman.dist %>' 103 | ] 104 | } 105 | ] 106 | }, 107 | server: '<%= yeoman.tmp %>' 108 | }, 109 | jshint: { 110 | options: { 111 | jshintrc: '.jshintrc' 112 | }, 113 | all: [ 114 | 'Gruntfile.js', 115 | '<%= yeoman.app %>/scripts/{,*/}*.js' 116 | ] 117 | }, 118 | karma: { 119 | unit: { 120 | configFile: 'karma.conf.js', 121 | singleRun: true 122 | } 123 | }, 124 | cssmin: { 125 | dist: { 126 | expand: true, 127 | cwd: '<%= yeoman.dist %>', 128 | src: ['*.css', '!*.min.css'], 129 | dest: '<%= yeoman.dist %>', 130 | ext: '.min.css' 131 | } 132 | }, 133 | ngmin: { 134 | dist: { 135 | expand: true, 136 | cwd: '<%= yeoman.dist %>', 137 | src: ['*.js', '!*.min.js'], 138 | dest: '<%= yeoman.dist %>', 139 | ext: '.min.js' 140 | } 141 | }, 142 | uglify: { 143 | dist: { 144 | expand: true, 145 | cwd: '<%= yeoman.dist %>', 146 | src: ['*.min.js'], 147 | dest: '<%= yeoman.dist %>', 148 | ext: '.min.js' 149 | } 150 | }, 151 | copy: { 152 | styles: { 153 | expand: true, 154 | cwd: '<%= yeoman.app %>/styles', 155 | dest: '<%= yeoman.tmp %>/styles/', 156 | src: '{,*/}*.css' 157 | } 158 | }, 159 | ngtemplates: { 160 | dist: { 161 | options: { 162 | base: '<%= yeoman.app %>', 163 | module: 'datePicker', 164 | url: function(templateUrl) { 165 | // on production it should be the same path as the one defined on datePicker.js 166 | return templateUrl.replace('app/', ''); 167 | } 168 | }, 169 | src: '<%= yeoman.app %>/templates/*.html', 170 | dest: '<%= yeoman.tmp %>/templates.js' 171 | 172 | } 173 | }, 174 | concurrent: { 175 | server: [ 176 | 'less', 177 | 'copy:styles' 178 | ], 179 | test: [ 180 | 'copy:styles' 181 | ], 182 | dist: [ 183 | 'copy:styles' 184 | ] 185 | }, 186 | concat: { 187 | options: { 188 | separator: '\n' 189 | }, 190 | js: { 191 | src: ['<%= yeoman.app %>/scripts/{datePicker,input,dateRange,datePickerUtils}.js', '<%= yeoman.tmp %>/templates.js'], 192 | dest: '<%= yeoman.dist %>/angular-datepicker.js', 193 | options: { 194 | banner:'\(function (global, factory) {\'use strict\';var fnc;fnc = (typeof exports === \'object\' && typeof module !== \'undefined\') ? module.exports = factory(require(\'angular\'), require(\'moment\')) :(typeof define === \'function\' && define.amd) ? define([\'angular\', \'moment\'], factory) :factory(global.angular, global.moment);}(this, function (angular, moment) {\n', 195 | footer:'}));', 196 | // Replace all 'use strict' statements in the code with a single one at the top 197 | process: function(src) { 198 | return src.replace(/(^|\n)[ \t]*('use strict'|"use strict");?\s*/g, '$1'); 199 | } 200 | } 201 | }, 202 | css: { 203 | src: ['<%= yeoman.tmp %>/{,*/}*.css'], 204 | dest: '<%= yeoman.dist %>/angular-datepicker.css' 205 | } 206 | }, 207 | 208 | bump : { 209 | options : { 210 | files : [ 'package.json', 'bower.json' ], 211 | commitFiles : [ 'package.json', 'bower.json' ], 212 | pushTo : 'origin' 213 | } 214 | } 215 | }); 216 | 217 | grunt.registerTask('server', [ 218 | 'clean:server', 219 | 'less', 220 | 'concurrent:server', 221 | 'connect:livereload', 222 | 'open', 223 | 'watch' 224 | ]); 225 | 226 | grunt.registerTask('test', [ 227 | 'clean:server', 228 | 'connect:test', 229 | 'karma' 230 | ]); 231 | 232 | grunt.registerTask('build', [ 233 | 'jshint', 234 | 'clean:dist', 235 | 'less', 236 | 'ngtemplates', 237 | 'concat', 238 | 'cssmin', 239 | 'ngmin', 240 | 'uglify' 241 | ]); 242 | 243 | grunt.registerTask('default', ['build']); 244 | }; 245 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013-2014 Piotrek Majewski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AngularJS datepicker directives 2 | 3 | #### Requirements 4 | 5 | - Angular v1.2+ 6 | - MomentJS 7 | - Moment Timezone (If timezones are being used) 8 | 9 | ## Installation 10 | 11 | ### via bower 12 | 13 | ``` 14 | bower install angular-datepicker --save 15 | ``` 16 | 17 | 18 | ### via npm 19 | 20 | ``` 21 | npm install angular-datepicker --save 22 | ``` 23 | 24 | After the install add the js, css and the moment files to your page. 25 | 26 | Add the following module to your page : `datePicker` 27 | 28 | 29 | ## Usage Example 30 | 31 | [Live demo](https://rawgithub.com/g00fy-/angular-datepicker/master/app/index.html) 32 | 33 | ## New features 34 | 35 | This fork of angular-datepicker contains several features. 36 | 37 | ### Timezone Support 38 | 39 | * The directive will work with or without a specified timezone. 40 | * If the timezone is known, it can be assigned to the datepicker via the `timezone` attribute. 41 | * If no timezone is provided, then the local time will be used. 42 | 43 | ##### No timezone information 44 | 45 | ```html 46 |
47 | ``` 48 | 49 | ##### Specific timezone (London, UK) 50 | 51 | ```html 52 |
53 | ``` 54 | 55 | 56 | ##### Specific timezone (Hong Kong, CN) 57 | 58 | ```html 59 |
60 | ``` 61 | 62 | 63 | ### Maximum / minimum dates: 64 | 65 | * These attributes restrict the dates that can be selected. 66 | * These work differently from the original `min-date` and `max-date` attributes, which they replace. 67 | * The original attributes allow selecting any dates and just mark the input as invalid. 68 | * With these attributes, if a date in the picker is outside of the valid range, then it will not be selectable. 69 | 70 | ##### Minimum date: 71 | 72 | ```html 73 | 74 | ``` 75 | 76 | ##### Maximum date: 77 | 78 | ```html 79 | 80 | ``` 81 | 82 | ##### Minimum and maximum date: 83 | 84 | ```html 85 | 86 | ``` 87 | 88 | ### Date format (for input fields): 89 | 90 | * A custom format for a date can be assigned via the `format` atribute. 91 | * This format will be used to display the date on an input field. 92 | * If not provided, a default format will be used. 93 | * See: [format options](http://momentjs.com/docs/#/displaying/format/) 94 | 95 | ```html 96 | 97 | ``` 98 | 99 | 100 | ### Callback on date change 101 | 102 | * Adding a `date-change` attribute containing a function name will cause this function to be called when the date changes in the picker. 103 | 104 | ```html 105 | 106 | ``` 107 | 108 | ### Update events 109 | 110 | * An event can be broadcast from the parent scope which will update specific pickers with new settings. The settings which can be changed are: 111 | * `minDate`: Earliest selectable date for this picker. Disabled if this value is falsy. 112 | * `maxDate`: Latest selectable date for this picker. Disabled if this value is falsy. 113 | * `minView`: Minimum zoom level for date/time selection. Disabled if this value is falsy. 114 | * `maxView`: Maximum zoom level for date/time selection. Disabled if this value is falsy. 115 | * `view`: Default zoom level for date/time selection. Set to default value if this value is falsy. 116 | * `format`: Format string used to display dates on the input field. Set to default value if this value is falsy. 117 | * See: [format options](http://momentjs.com/docs/#/displaying/format/) 118 | * This option cannot be used on the `date-picker` directive directly, it must be used on a `date-time` input field. 119 | * The possible for the `view`, `minView` and `maxView` fields are: 120 | * `year`, `month`, `date`, `hours`, `minutes`. 121 | * The event is targeted at specific pickers using their `ID` attributes. 122 | * If a picker exists with the same `ID` then the information in this picker will be updated. 123 | * A single `ID` can be used, or an array of `ID`s 124 | 125 | #### Create picker with ID 126 | 127 | ```html 128 | 129 | ``` 130 | 131 | #### Update one picker. 132 | 133 | ```javascript 134 | $scope.$broadcast('pickerUpdate', 'pickerToUpdate', { 135 | format: 'D MMM YYYY HH:mm', 136 | maxDate: maxSelectableDate, //A moment object, date object, or date/time string parsable by momentjs 137 | minView: 'hours', 138 | view: false //Use default 139 | }); 140 | ``` 141 | 142 | #### Update multiple pickers. 143 | 144 | ```javascript 145 | $scope.$broadcast('pickerUpdate', ['pickerToUpdate', 'secondPickerToUpdate'], { 146 | format: 'lll' 147 | }); 148 | ``` 149 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |

Timezones

19 | 20 |
21 | 22 |

{{timezone[0]}}

23 | 24 |
25 | 26 |
27 | 28 |
29 | 30 |

No timezone (Uses local)

31 | 32 |
33 | 34 |
35 |
36 | 37 |

Custom formats

38 |
39 |
{{format}}
40 | 41 |
42 | 43 |
44 |
45 |

Minimum / Maximum dates

46 | 47 |
48 |

Always open div (date-picker directive)

49 | 50 |
51 |
Minimum
52 |
53 |
54 | 55 |
56 |
Maximum
57 |
58 |
59 | 60 |
61 |
Minimum + maximum
62 |
63 |
64 |
65 | 66 |
67 |

Input with popup (date-time directive)

68 | 69 |
70 |
71 |
Min ({{demoForm.pickerMinDate.$error.min ? 'Min: invalid' : 'Min: valid'}})
72 | 73 |
74 | 75 |
76 |
Max ({{demoForm.pickerMaxDate.$error.max ? 'Max: invalid' : 'Max: valid'}})
77 | 78 |
79 | 80 |
81 |
Min + max ({{demoForm.pickerBothDates.$error.min ? 'Min: invalid, ' : 'Min: valid, '}} {{demoForm.pickerBothDates.$error.max ? 'max: invalid' : 'max: valid'}})
82 | 83 |
84 |
85 |
86 | 87 |
88 |

Range

89 | 90 |
91 | 92 |
93 | 94 |
95 |
96 |

Update events

97 |
98 |

Minimum date

99 | 100 |
101 |
102 |

Maximum date

103 | 104 |
105 | 106 |
107 |

Default view

108 | 109 | 112 |
113 | 114 |
115 |

Min view

116 | 117 | 122 |
123 | 124 |
125 |

Max view

126 | 127 | 132 |
133 | 134 |
135 |

Format

136 | 137 | 142 |
143 | 144 |

Callback

145 |
146 |

Select a date from either picker

147 |
148 | 149 |
150 | 151 |
152 |
153 |
154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 252 | 253 | 254 | -------------------------------------------------------------------------------- /app/scripts/datePicker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Module = angular.module('datePicker', []); 3 | 4 | Module.constant('datePickerConfig', { 5 | template: 'templates/datepicker.html', 6 | view: 'month', 7 | views: ['year', 'month', 'date', 'hours', 'minutes'], 8 | momentNames: { 9 | year: 'year', 10 | month: 'month', 11 | date: 'day', 12 | hours: 'hours', 13 | minutes: 'minutes', 14 | }, 15 | viewConfig: { 16 | year: ['years', 'isSameYear'], 17 | month: ['months', 'isSameMonth'], 18 | hours: ['hours', 'isSameHour'], 19 | minutes: ['minutes', 'isSameMinutes'], 20 | }, 21 | step: 5 22 | }); 23 | 24 | //Moment format filter. 25 | Module.filter('mFormat', function () { 26 | return function (m, format, tz) { 27 | if (!(moment.isMoment(m))) { 28 | return moment(m).format(format); 29 | } 30 | return tz ? moment.tz(m, tz).format(format) : m.format(format); 31 | }; 32 | }); 33 | 34 | Module.directive('datePicker', ['datePickerConfig', 'datePickerUtils', function datePickerDirective(datePickerConfig, datePickerUtils) { 35 | 36 | //noinspection JSUnusedLocalSymbols 37 | return { 38 | // this is a bug ? 39 | require: '?ngModel', 40 | template: '
', 41 | scope: { 42 | model: '=datePicker', 43 | after: '=?', 44 | before: '=?' 45 | }, 46 | link: function (scope, element, attrs, ngModel) { 47 | function prepareViews() { 48 | scope.views = datePickerConfig.views.concat(); 49 | scope.view = attrs.view || datePickerConfig.view; 50 | 51 | scope.views = scope.views.slice( 52 | scope.views.indexOf(attrs.maxView || 'year'), 53 | scope.views.indexOf(attrs.minView || 'minutes') + 1 54 | ); 55 | 56 | if (scope.views.length === 1 || scope.views.indexOf(scope.view) === -1) { 57 | scope.view = scope.views[0]; 58 | } 59 | } 60 | 61 | function getDate(name) { 62 | return datePickerUtils.getDate(scope, attrs, name); 63 | } 64 | 65 | var arrowClick = false, 66 | tz = scope.tz = attrs.timezone, 67 | createMoment = datePickerUtils.createMoment, 68 | eventIsForPicker = datePickerUtils.eventIsForPicker, 69 | step = parseInt(attrs.step || datePickerConfig.step, 10), 70 | partial = !!attrs.partial, 71 | minDate = getDate('minDate'), 72 | maxDate = getDate('maxDate'), 73 | pickerID = element[0].id, 74 | now = scope.now = createMoment(), 75 | selected = scope.date = createMoment(scope.model || now), 76 | autoclose = attrs.autoClose === 'true', 77 | // Either gets the 1st day from the attributes, or asks moment.js to give it to us as it is localized. 78 | firstDay = attrs.firstDay && attrs.firstDay >= 0 && attrs.firstDay <= 6 ? parseInt(attrs.firstDay, 10) : moment().weekday(0).day(), 79 | setDate, 80 | prepareViewData, 81 | isSame, 82 | clipDate, 83 | isNow, 84 | inValidRange; 85 | 86 | datePickerUtils.setParams(tz, firstDay); 87 | 88 | if (!scope.model) { 89 | selected.minute(Math.ceil(selected.minute() / step) * step).second(0); 90 | } 91 | 92 | scope.template = attrs.template || datePickerConfig.template; 93 | 94 | scope.watchDirectChanges = attrs.watchDirectChanges !== undefined; 95 | scope.callbackOnSetDate = attrs.dateChange ? datePickerUtils.findFunction(scope, attrs.dateChange) : undefined; 96 | 97 | prepareViews(); 98 | 99 | scope.setView = function (nextView) { 100 | if (scope.views.indexOf(nextView) !== -1) { 101 | scope.view = nextView; 102 | } 103 | }; 104 | 105 | scope.selectDate = function (date) { 106 | if (attrs.disabled) { 107 | return false; 108 | } 109 | if (isSame(scope.date, date)) { 110 | date = scope.date; 111 | } 112 | date = clipDate(date); 113 | if (!date) { 114 | return false; 115 | } 116 | scope.date = date; 117 | 118 | var nextView = scope.views[scope.views.indexOf(scope.view) + 1]; 119 | if ((!nextView || partial) || scope.model) { 120 | setDate(date); 121 | } 122 | 123 | if (nextView) { 124 | scope.setView(nextView); 125 | } else if (autoclose) { 126 | element.addClass('hidden'); 127 | scope.$emit('hidePicker'); 128 | } else { 129 | prepareViewData(); 130 | } 131 | }; 132 | 133 | setDate = function (date) { 134 | if (date) { 135 | scope.model = date; 136 | if (ngModel) { 137 | ngModel.$setViewValue(date); 138 | } 139 | } 140 | scope.$emit('setDate', scope.model, scope.view); 141 | 142 | //This is duplicated in the new functionality. 143 | if (scope.callbackOnSetDate) { 144 | scope.callbackOnSetDate(attrs.datePicker, scope.date); 145 | } 146 | }; 147 | 148 | function update() { 149 | var view = scope.view; 150 | datePickerUtils.setParams(tz, firstDay); 151 | 152 | if (scope.model && !arrowClick) { 153 | scope.date = createMoment(scope.model); 154 | arrowClick = false; 155 | } 156 | 157 | var date = scope.date; 158 | 159 | switch (view) { 160 | case 'year': 161 | scope.years = datePickerUtils.getVisibleYears(date); 162 | break; 163 | case 'month': 164 | scope.months = datePickerUtils.getVisibleMonths(date); 165 | break; 166 | case 'date': 167 | scope.weekdays = scope.weekdays || datePickerUtils.getDaysOfWeek(); 168 | scope.weeks = datePickerUtils.getVisibleWeeks(date); 169 | break; 170 | case 'hours': 171 | scope.hours = datePickerUtils.getVisibleHours(date); 172 | break; 173 | case 'minutes': 174 | scope.minutes = datePickerUtils.getVisibleMinutes(date, step); 175 | break; 176 | } 177 | 178 | prepareViewData(); 179 | } 180 | 181 | function watch() { 182 | if (scope.view !== 'date') { 183 | return scope.view; 184 | } 185 | return scope.date ? scope.date.month() : null; 186 | } 187 | 188 | scope.$watch(watch, update); 189 | 190 | if (scope.watchDirectChanges) { 191 | scope.$watch('model', function () { 192 | arrowClick = false; 193 | update(); 194 | }); 195 | } 196 | 197 | prepareViewData = function () { 198 | var view = scope.view, 199 | date = scope.date, 200 | classes = [], classList = '', 201 | i, j; 202 | 203 | datePickerUtils.setParams(tz, firstDay); 204 | 205 | if (view === 'date') { 206 | var weeks = scope.weeks, week; 207 | for (i = 0; i < weeks.length; i++) { 208 | week = weeks[i]; 209 | classes.push([]); 210 | for (j = 0; j < week.length; j++) { 211 | classList = ''; 212 | if (datePickerUtils.isSameDay(date, week[j])) { 213 | classList += 'active'; 214 | } 215 | if (isNow(week[j], view)) { 216 | classList += ' now'; 217 | } 218 | //if (week[j].month() !== date.month()) classList += ' disabled'; 219 | if (week[j].month() !== date.month() || !inValidRange(week[j])) { 220 | classList += ' disabled'; 221 | } 222 | classes[i].push(classList); 223 | } 224 | } 225 | } else { 226 | var params = datePickerConfig.viewConfig[view], 227 | dates = scope[params[0]], 228 | compareFunc = params[1]; 229 | 230 | for (i = 0; i < dates.length; i++) { 231 | classList = ''; 232 | if (datePickerUtils[compareFunc](date, dates[i])) { 233 | classList += 'active'; 234 | } 235 | if (isNow(dates[i], view)) { 236 | classList += ' now'; 237 | } 238 | if (!inValidRange(dates[i])) { 239 | classList += ' disabled'; 240 | } 241 | classes.push(classList); 242 | } 243 | } 244 | scope.classes = classes; 245 | }; 246 | 247 | scope.next = function (delta) { 248 | var date = moment(scope.date); 249 | delta = delta || 1; 250 | switch (scope.view) { 251 | case 'year': 252 | /*falls through*/ 253 | case 'month': 254 | date.year(date.year() + delta); 255 | break; 256 | case 'date': 257 | date.month(date.month() + delta); 258 | break; 259 | case 'hours': 260 | /*falls through*/ 261 | case 'minutes': 262 | date.hours(date.hours() + delta); 263 | break; 264 | } 265 | date = clipDate(date); 266 | if (date) { 267 | scope.date = date; 268 | arrowClick = true; 269 | update(); 270 | } 271 | }; 272 | 273 | inValidRange = function (date) { 274 | var valid = true; 275 | if (minDate && minDate.isAfter(date)) { 276 | valid = isSame(minDate, date); 277 | } 278 | if (maxDate && maxDate.isBefore(date)) { 279 | valid &= isSame(maxDate, date); 280 | } 281 | return valid; 282 | }; 283 | 284 | isSame = function (date1, date2) { 285 | return date1.isSame(date2, datePickerConfig.momentNames[scope.view]) ? true : false; 286 | }; 287 | 288 | clipDate = function (date) { 289 | if (minDate && minDate.isAfter(date)) { 290 | return minDate; 291 | } else if (maxDate && maxDate.isBefore(date)) { 292 | return maxDate; 293 | } else { 294 | return date; 295 | } 296 | }; 297 | 298 | isNow = function (date, view) { 299 | var is = true; 300 | 301 | switch (view) { 302 | case 'minutes': 303 | is &= ~~(now.minutes() / step) === ~~(date.minutes() / step); 304 | /* falls through */ 305 | case 'hours': 306 | is &= now.hours() === date.hours(); 307 | /* falls through */ 308 | case 'date': 309 | is &= now.date() === date.date(); 310 | /* falls through */ 311 | case 'month': 312 | is &= now.month() === date.month(); 313 | /* falls through */ 314 | case 'year': 315 | is &= now.year() === date.year(); 316 | } 317 | return is; 318 | }; 319 | 320 | scope.prev = function (delta) { 321 | return scope.next(-delta || -1); 322 | }; 323 | 324 | if (pickerID) { 325 | scope.$on('pickerUpdate', function (event, pickerIDs, data) { 326 | if (eventIsForPicker(pickerIDs, pickerID)) { 327 | var updateViews = false, updateViewData = false; 328 | 329 | if (angular.isDefined(data.minDate)) { 330 | minDate = data.minDate ? data.minDate : false; 331 | updateViewData = true; 332 | } 333 | if (angular.isDefined(data.maxDate)) { 334 | maxDate = data.maxDate ? data.maxDate : false; 335 | updateViewData = true; 336 | } 337 | 338 | if (angular.isDefined(data.minView)) { 339 | attrs.minView = data.minView; 340 | updateViews = true; 341 | } 342 | if (angular.isDefined(data.maxView)) { 343 | attrs.maxView = data.maxView; 344 | updateViews = true; 345 | } 346 | attrs.view = data.view || attrs.view; 347 | 348 | if (updateViews) { 349 | prepareViews(); 350 | } 351 | 352 | if (updateViewData) { 353 | update(); 354 | } 355 | } 356 | }); 357 | } 358 | } 359 | }; 360 | }]); 361 | -------------------------------------------------------------------------------- /app/scripts/datePickerUtils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | angular.module('datePicker').factory('datePickerUtils', function () { 3 | var tz, firstDay; 4 | var createNewDate = function (year, month, day, hour, minute) { 5 | var utc = Date.UTC(year | 0, month | 0, day | 0, hour | 0, minute | 0); 6 | return tz ? moment.tz(utc, tz) : moment(utc); 7 | }; 8 | 9 | return { 10 | getVisibleMinutes: function (m, step) { 11 | var year = m.year(), 12 | month = m.month(), 13 | day = m.date(), 14 | hour = m.hours(), pushedDate, 15 | offset = m.utcOffset() / 60, 16 | minutes = [], minute; 17 | 18 | for (minute = 0; minute < 60; minute += step) { 19 | pushedDate = createNewDate(year, month, day, hour - offset, minute); 20 | minutes.push(pushedDate); 21 | } 22 | return minutes; 23 | }, 24 | getVisibleWeeks: function (m) { 25 | m = moment(m); 26 | var startYear = m.year(), 27 | startMonth = m.month(); 28 | 29 | //Set date to the first day of the month 30 | m.date(1); 31 | 32 | //Grab day of the week 33 | var day = m.day(); 34 | 35 | //Go back the required number of days to arrive at the previous week start 36 | m.date(firstDay - (day + (firstDay >= day ? 6 : -1))); 37 | 38 | var weeks = []; 39 | 40 | while (weeks.length < 6) { 41 | if (m.year() === startYear && m.month() > startMonth) { 42 | break; 43 | } 44 | weeks.push(this.getDaysOfWeek(m)); 45 | m.add(7, 'd'); 46 | } 47 | return weeks; 48 | }, 49 | getVisibleYears: function (d) { 50 | var m = moment(d), 51 | year = m.year(); 52 | 53 | m.year(year - (year % 10)); 54 | year = m.year(); 55 | 56 | var offset = m.utcOffset() / 60, 57 | years = [], 58 | pushedDate, 59 | actualOffset; 60 | 61 | for (var i = 0; i < 12; i++) { 62 | pushedDate = createNewDate(year, 0, 1, 0 - offset); 63 | actualOffset = pushedDate.utcOffset() / 60; 64 | if (actualOffset !== offset) { 65 | pushedDate = createNewDate(year, 0, 1, 0 - actualOffset); 66 | offset = actualOffset; 67 | } 68 | years.push(pushedDate); 69 | year++; 70 | } 71 | return years; 72 | }, 73 | getDaysOfWeek: function (m) { 74 | m = m ? m : (tz ? moment.tz(tz).day(firstDay) : moment().day(firstDay)); 75 | 76 | var year = m.year(), 77 | month = m.month(), 78 | day = m.date(), 79 | days = [], 80 | pushedDate, 81 | offset = m.utcOffset() / 60, 82 | actualOffset; 83 | 84 | for (var i = 0; i < 7; i++) { 85 | pushedDate = createNewDate(year, month, day, 0 - offset, 0, false); 86 | actualOffset = pushedDate.utcOffset() / 60; 87 | if (actualOffset !== offset) { 88 | pushedDate = createNewDate(year, month, day, 0 - actualOffset, 0, false); 89 | } 90 | days.push(pushedDate); 91 | day++; 92 | } 93 | return days; 94 | }, 95 | getVisibleMonths: function (m) { 96 | var year = m.year(), 97 | offset = m.utcOffset() / 60, 98 | months = [], 99 | pushedDate, 100 | actualOffset; 101 | 102 | for (var month = 0; month < 12; month++) { 103 | pushedDate = createNewDate(year, month, 1, 0 - offset, 0, false); 104 | actualOffset = pushedDate.utcOffset() / 60; 105 | if (actualOffset !== offset) { 106 | pushedDate = createNewDate(year, month, 1, 0 - actualOffset, 0, false); 107 | } 108 | months.push(pushedDate); 109 | } 110 | return months; 111 | }, 112 | getVisibleHours: function (m) { 113 | var year = m.year(), 114 | month = m.month(), 115 | day = m.date(), 116 | hours = [], 117 | hour, pushedDate, actualOffset, 118 | offset = m.utcOffset() / 60; 119 | 120 | for (hour = 0; hour < 24; hour++) { 121 | pushedDate = createNewDate(year, month, day, hour - offset, 0, false); 122 | actualOffset = pushedDate.utcOffset() / 60; 123 | if (actualOffset !== offset) { 124 | pushedDate = createNewDate(year, month, day, hour - actualOffset, 0, false); 125 | } 126 | hours.push(pushedDate); 127 | } 128 | 129 | return hours; 130 | }, 131 | isAfter: function (model, date) { 132 | return model && model.unix() >= date.unix(); 133 | }, 134 | isBefore: function (model, date) { 135 | return model.unix() <= date.unix(); 136 | }, 137 | isSameYear: function (model, date) { 138 | return model && model.year() === date.year(); 139 | }, 140 | isSameMonth: function (model, date) { 141 | return this.isSameYear(model, date) && model.month() === date.month(); 142 | }, 143 | isSameDay: function (model, date) { 144 | return this.isSameMonth(model, date) && model.date() === date.date(); 145 | }, 146 | isSameHour: function (model, date) { 147 | return this.isSameDay(model, date) && model.hours() === date.hours(); 148 | }, 149 | isSameMinutes: function (model, date) { 150 | return this.isSameHour(model, date) && model.minutes() === date.minutes(); 151 | }, 152 | setParams: function (zone, fd) { 153 | tz = zone; 154 | firstDay = fd; 155 | }, 156 | scopeSearch: function (scope, name, comparisonFn) { 157 | var parentScope = scope, 158 | nameArray = name.split('.'), 159 | target, i, j = nameArray.length; 160 | 161 | do { 162 | target = parentScope = parentScope.$parent; 163 | 164 | //Loop through provided names. 165 | for (i = 0; i < j; i++) { 166 | target = target[nameArray[i]]; 167 | if (!target) { 168 | continue; 169 | } 170 | } 171 | 172 | //If we reached the end of the list for this scope, 173 | //and something was found, trigger the comparison 174 | //function. If the comparison function is happy, return 175 | //found result. Otherwise, continue to the next parent scope 176 | if (target && comparisonFn(target)) { 177 | return target; 178 | } 179 | 180 | } while (parentScope.$parent); 181 | 182 | return false; 183 | }, 184 | findFunction: function (scope, name) { 185 | //Search scope ancestors for a matching function. 186 | return this.scopeSearch(scope, name, function (target) { 187 | //Property must also be a function 188 | return angular.isFunction(target); 189 | }); 190 | }, 191 | findParam: function (scope, name) { 192 | //Search scope ancestors for a matching parameter. 193 | return this.scopeSearch(scope, name, function () { 194 | //As long as the property exists, we're good 195 | return true; 196 | }); 197 | }, 198 | createMoment: function (m) { 199 | if (tz) { 200 | return moment.tz(m, tz); 201 | } else { 202 | //If input is a moment, and we have no TZ info, we need to remove TZ 203 | //info from the moment, otherwise the newly created moment will take 204 | //the timezone of the input moment. The easiest way to do that is to 205 | //take the unix timestamp, and use that to create a new moment. 206 | //The new moment will use the local timezone of the user machine. 207 | return moment.isMoment(m) ? moment.unix(m.unix()) : moment(m); 208 | } 209 | }, 210 | getDate: function (scope, attrs, name) { 211 | var result = false; 212 | if (attrs[name]) { 213 | result = this.createMoment(attrs[name]); 214 | if (!result.isValid()) { 215 | result = this.findParam(scope, attrs[name]); 216 | if (result) { 217 | result = this.createMoment(result); 218 | } 219 | } 220 | } 221 | 222 | return result; 223 | }, 224 | //Checks if an event targeted at a specific picker, via either a string name, or an array of strings. 225 | eventIsForPicker: function (targetIDs, pickerID) { 226 | function matches(id) { 227 | if (id instanceof RegExp) { 228 | return id.test(pickerID); 229 | } 230 | return id === pickerID; 231 | } 232 | 233 | if (angular.isArray(targetIDs)) { 234 | return targetIDs.some(matches); 235 | } 236 | return matches(targetIDs); 237 | } 238 | }; 239 | }); 240 | -------------------------------------------------------------------------------- /app/scripts/dateRange.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Module = angular.module('datePicker'); 3 | 4 | Module.directive('dateRange', ['$compile', 'datePickerUtils', 'dateTimeConfig', function ($compile, datePickerUtils, dateTimeConfig) { 5 | function getTemplate(attrs, id, model, min, max) { 6 | return dateTimeConfig.template(angular.extend(attrs, { 7 | ngModel: model, 8 | minDate: min && moment.isMoment(min) ? min.format() : false, 9 | maxDate: max && moment.isMoment(max) ? max.format() : false 10 | }), id); 11 | } 12 | 13 | function randomName() { 14 | return 'picker' + Math.random().toString().substr(2); 15 | } 16 | 17 | return { 18 | scope: { 19 | start: '=', 20 | end: '=' 21 | }, 22 | link: function (scope, element, attrs) { 23 | var dateChange = null, 24 | pickerRangeID = element[0].id, 25 | pickerIDs = [randomName(), randomName()], 26 | createMoment = datePickerUtils.createMoment, 27 | eventIsForPicker = datePickerUtils.eventIsForPicker; 28 | 29 | scope.dateChange = function (modelName, newDate) { 30 | //Notify user if callback exists. 31 | if (dateChange) { 32 | dateChange(modelName, newDate); 33 | } 34 | }; 35 | 36 | function setMax(date) { 37 | scope.$broadcast('pickerUpdate', pickerIDs[0], { 38 | maxDate: date 39 | }); 40 | } 41 | 42 | function setMin(date) { 43 | scope.$broadcast('pickerUpdate', pickerIDs[1], { 44 | minDate: date 45 | }); 46 | } 47 | 48 | if (pickerRangeID) { 49 | scope.$on('pickerUpdate', function (event, targetIDs, data) { 50 | if (eventIsForPicker(targetIDs, pickerRangeID)) { 51 | //If we received an update event, dispatch it to the inner pickers using their IDs. 52 | scope.$broadcast('pickerUpdate', pickerIDs, data); 53 | } 54 | }); 55 | } 56 | 57 | datePickerUtils.setParams(attrs.timezone); 58 | 59 | scope.start = createMoment(scope.start); 60 | scope.end = createMoment(scope.end); 61 | 62 | scope.$watchGroup(['start', 'end'], function (dates) { 63 | //Scope data changed, update picker min/max 64 | setMin(dates[0]); 65 | setMax(dates[1]); 66 | }); 67 | 68 | if (angular.isDefined(attrs.dateChange)) { 69 | dateChange = datePickerUtils.findFunction(scope, attrs.dateChange); 70 | } 71 | 72 | attrs.onSetDate = 'dateChange'; 73 | 74 | var template = '
' + 75 | getTemplate(attrs, pickerIDs[0], 'start', false, scope.end) + 76 | '' + 77 | getTemplate(attrs, pickerIDs[1], 'end', scope.start, false) + 78 | '
'; 79 | 80 | var picker = $compile(template)(scope); 81 | element.append(picker); 82 | } 83 | }; 84 | }]); 85 | -------------------------------------------------------------------------------- /app/scripts/input.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var PRISTINE_CLASS = 'ng-pristine', 3 | DIRTY_CLASS = 'ng-dirty'; 4 | 5 | var Module = angular.module('datePicker'); 6 | 7 | Module.constant('dateTimeConfig', { 8 | template: function (attrs, id) { 9 | return '' + 10 | '
'; 27 | }, 28 | format: 'YYYY-MM-DD HH:mm', 29 | views: ['date', 'year', 'month', 'hours', 'minutes'], 30 | autoClose: false, 31 | position: 'relative' 32 | }); 33 | 34 | Module.directive('dateTimeAppend', function () { 35 | return { 36 | link: function (scope, element) { 37 | element.bind('click', function () { 38 | element.find('input')[0].focus(); 39 | }); 40 | } 41 | }; 42 | }); 43 | 44 | Module.directive('dateTime', ['$compile', '$document', '$filter', 'dateTimeConfig', '$parse', 'datePickerUtils', function ($compile, $document, $filter, dateTimeConfig, $parse, datePickerUtils) { 45 | var body = $document.find('body'); 46 | var dateFilter = $filter('mFormat'); 47 | 48 | return { 49 | require: 'ngModel', 50 | scope: true, 51 | link: function (scope, element, attrs, ngModel) { 52 | var format = attrs.format || dateTimeConfig.format, 53 | parentForm = element.inheritedData('$formController'), 54 | views = $parse(attrs.views)(scope) || dateTimeConfig.views.concat(), 55 | view = attrs.view || views[0], 56 | index = views.indexOf(view), 57 | dismiss = attrs.autoClose ? $parse(attrs.autoClose)(scope) : dateTimeConfig.autoClose, 58 | picker = null, 59 | pickerID = element[0].id, 60 | position = attrs.position || dateTimeConfig.position, 61 | container = null, 62 | minDate = null, 63 | minValid = null, 64 | maxDate = null, 65 | maxValid = null, 66 | timezone = attrs.timezone || false, 67 | eventIsForPicker = datePickerUtils.eventIsForPicker, 68 | dateChange = null, 69 | shownOnce = false, 70 | template; 71 | 72 | if (index === -1) { 73 | views.splice(index, 1); 74 | } 75 | 76 | views.unshift(view); 77 | 78 | function formatter(value) { 79 | if (value) { 80 | return dateFilter(value, format, timezone); 81 | } 82 | } 83 | 84 | function parser(viewValue) { 85 | if (!viewValue) { 86 | return ''; 87 | } 88 | var parsed = moment(viewValue, format); 89 | if (parsed.isValid()) { 90 | return parsed; 91 | } 92 | } 93 | 94 | function setMin(date) { 95 | minDate = date; 96 | attrs.minDate = date ? date.format() : date; 97 | minValid = moment.isMoment(date); 98 | } 99 | 100 | function setMax(date) { 101 | maxDate = date; 102 | attrs.maxDate = date ? date.format() : date; 103 | maxValid = moment.isMoment(date); 104 | } 105 | 106 | ngModel.$formatters.push(formatter); 107 | ngModel.$parsers.unshift(parser); 108 | 109 | if (angular.isDefined(attrs.minDate)) { 110 | setMin(datePickerUtils.findParam(scope, attrs.minDate)); 111 | 112 | ngModel.$validators.min = function (value) { 113 | //If we don't have a min / max value, then any value is valid. 114 | return minValid ? moment.isMoment(value) && (minDate.isSame(value) || minDate.isBefore(value)) : true; 115 | }; 116 | } 117 | 118 | if (angular.isDefined(attrs.maxDate)) { 119 | setMax(datePickerUtils.findParam(scope, attrs.maxDate)); 120 | 121 | ngModel.$validators.max = function (value) { 122 | return maxValid ? moment.isMoment(value) && (maxDate.isSame(value) || maxDate.isAfter(value)) : true; 123 | }; 124 | } 125 | 126 | if (angular.isDefined(attrs.dateChange)) { 127 | dateChange = datePickerUtils.findFunction(scope, attrs.dateChange); 128 | } 129 | 130 | function getTemplate() { 131 | template = dateTimeConfig.template(attrs); 132 | } 133 | 134 | 135 | function updateInput(event) { 136 | event.stopPropagation(); 137 | if (ngModel.$pristine) { 138 | ngModel.$dirty = true; 139 | ngModel.$pristine = false; 140 | element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS); 141 | if (parentForm) { 142 | parentForm.$setDirty(); 143 | } 144 | ngModel.$render(); 145 | } 146 | } 147 | 148 | function clear() { 149 | if (picker) { 150 | picker.remove(); 151 | picker = null; 152 | } 153 | if (container) { 154 | container.remove(); 155 | container = null; 156 | } 157 | } 158 | 159 | if (pickerID) { 160 | scope.$on('pickerUpdate', function (event, pickerIDs, data) { 161 | if (eventIsForPicker(pickerIDs, pickerID)) { 162 | if (picker) { 163 | //Need to handle situation where the data changed but the picker is currently open. 164 | //To handle this, we can create the inner picker with a random ID, then forward 165 | //any events received to it. 166 | } else { 167 | var validateRequired = false; 168 | if (angular.isDefined(data.minDate)) { 169 | setMin(data.minDate); 170 | validateRequired = true; 171 | } 172 | if (angular.isDefined(data.maxDate)) { 173 | setMax(data.maxDate); 174 | validateRequired = true; 175 | } 176 | 177 | if (angular.isDefined(data.minView)) { 178 | attrs.minView = data.minView; 179 | } 180 | if (angular.isDefined(data.maxView)) { 181 | attrs.maxView = data.maxView; 182 | } 183 | attrs.view = data.view || attrs.view; 184 | 185 | if (validateRequired) { 186 | ngModel.$validate(); 187 | } 188 | if (angular.isDefined(data.format)) { 189 | format = attrs.format = data.format || dateTimeConfig.format; 190 | ngModel.$modelValue = -1; //Triggers formatters. This value will be discarded. 191 | } 192 | getTemplate(); 193 | } 194 | } 195 | }); 196 | } 197 | 198 | function showPicker() { 199 | if (picker) { 200 | return; 201 | } 202 | // create picker element 203 | picker = $compile(template)(scope); 204 | scope.$digest(); 205 | 206 | //If the picker has already been shown before then we shouldn't be binding to events, as these events are already bound to in this scope. 207 | if (!shownOnce) { 208 | scope.$on('setDate', function (event, date, view) { 209 | updateInput(event); 210 | if (dateChange) { 211 | dateChange(attrs.ngModel, date); 212 | } 213 | if (dismiss && views[views.length - 1] === view) { 214 | clear(); 215 | } 216 | }); 217 | 218 | scope.$on('hidePicker', function () { 219 | element[0].blur(); 220 | }); 221 | 222 | scope.$on('$destroy', clear); 223 | 224 | shownOnce = true; 225 | } 226 | 227 | 228 | // move picker below input element 229 | 230 | if (position === 'absolute') { 231 | var pos = element[0].getBoundingClientRect(); 232 | // Support IE8 233 | var height = pos.height || element[0].offsetHeight; 234 | picker.css({top: (pos.top + height) + 'px', left: pos.left + 'px', display: 'block', position: position}); 235 | body.append(picker); 236 | } else { 237 | // relative 238 | container = angular.element('
'); 239 | element[0].parentElement.insertBefore(container[0], element[0]); 240 | container.append(picker); 241 | // this approach doesn't work 242 | // element.before(picker); 243 | picker.css({top: element[0].offsetHeight + 'px', display: 'block'}); 244 | } 245 | picker.bind('mousedown', function (evt) { 246 | evt.preventDefault(); 247 | }); 248 | } 249 | 250 | element.bind('focus', showPicker); 251 | element.bind('blur', clear); 252 | getTemplate(); 253 | } 254 | }; 255 | }]); 256 | -------------------------------------------------------------------------------- /app/styles/mixins.less: -------------------------------------------------------------------------------- 1 | // 2 | // Mixins 3 | // -------------------------------------------------- 4 | 5 | 6 | // UTILITY MIXINS 7 | // -------------------------------------------------- 8 | 9 | // Clearfix 10 | // -------- 11 | // For clearing floats like a boss h5bp.com/q 12 | .clearfix { 13 | *zoom: 1; 14 | &:before, 15 | &:after { 16 | display: table; 17 | content: ""; 18 | // Fixes Opera/contenteditable bug: 19 | // http://nicolasgallagher.com/micro-clearfix-hack/#comment-36952 20 | line-height: 0; 21 | } 22 | &:after { 23 | clear: both; 24 | } 25 | } 26 | 27 | // Webkit-style focus 28 | // ------------------ 29 | .tab-focus() { 30 | // Default 31 | outline: thin dotted #333; 32 | // Webkit 33 | outline: 5px auto -webkit-focus-ring-color; 34 | outline-offset: -2px; 35 | } 36 | 37 | // Center-align a block level element 38 | // ---------------------------------- 39 | .center-block() { 40 | display: block; 41 | margin-left: auto; 42 | margin-right: auto; 43 | } 44 | 45 | // IE7 inline-block 46 | // ---------------- 47 | .ie7-inline-block() { 48 | *display: inline; /* IE7 inline-block hack */ 49 | *zoom: 1; 50 | } 51 | 52 | // IE7 likes to collapse whitespace on either side of the inline-block elements. 53 | // Ems because we're attempting to match the width of a space character. Left 54 | // version is for form buttons, which typically come after other elements, and 55 | // right version is for icons, which come before. Applying both is ok, but it will 56 | // mean that space between those elements will be .6em (~2 space characters) in IE7, 57 | // instead of the 1 space in other browsers. 58 | .ie7-restore-left-whitespace() { 59 | *margin-left: .3em; 60 | 61 | &:first-child { 62 | *margin-left: 0; 63 | } 64 | } 65 | 66 | .ie7-restore-right-whitespace() { 67 | *margin-right: .3em; 68 | } 69 | 70 | // Sizing shortcuts 71 | // ------------------------- 72 | .size(@height, @width) { 73 | width: @width; 74 | height: @height; 75 | } 76 | .square(@size) { 77 | .size(@size, @size); 78 | } 79 | 80 | // Placeholder text 81 | // ------------------------- 82 | .placeholder(@color: @placeholderText) { 83 | &:-moz-placeholder { 84 | color: @color; 85 | } 86 | &:-ms-input-placeholder { 87 | color: @color; 88 | } 89 | &::-webkit-input-placeholder { 90 | color: @color; 91 | } 92 | } 93 | 94 | // Text overflow 95 | // ------------------------- 96 | // Requires inline-block or block for proper styling 97 | .text-overflow() { 98 | overflow: hidden; 99 | text-overflow: ellipsis; 100 | white-space: nowrap; 101 | } 102 | 103 | // CSS image replacement 104 | // ------------------------- 105 | // Source: https://github.com/h5bp/html5-boilerplate/commit/aa0396eae757 106 | .hide-text { 107 | font: 0/0 a; 108 | color: transparent; 109 | text-shadow: none; 110 | background-color: transparent; 111 | border: 0; 112 | } 113 | 114 | 115 | // FONTS 116 | // -------------------------------------------------- 117 | 118 | #font { 119 | #family { 120 | .serif() { 121 | font-family: @serifFontFamily; 122 | } 123 | .sans-serif() { 124 | font-family: @sansFontFamily; 125 | } 126 | .monospace() { 127 | font-family: @monoFontFamily; 128 | } 129 | } 130 | .shorthand(@size: @baseFontSize, @weight: normal, @lineHeight: @baseLineHeight) { 131 | font-size: @size; 132 | font-weight: @weight; 133 | line-height: @lineHeight; 134 | } 135 | .serif(@size: @baseFontSize, @weight: normal, @lineHeight: @baseLineHeight) { 136 | #font > #family > .serif; 137 | #font > .shorthand(@size, @weight, @lineHeight); 138 | } 139 | .sans-serif(@size: @baseFontSize, @weight: normal, @lineHeight: @baseLineHeight) { 140 | #font > #family > .sans-serif; 141 | #font > .shorthand(@size, @weight, @lineHeight); 142 | } 143 | .monospace(@size: @baseFontSize, @weight: normal, @lineHeight: @baseLineHeight) { 144 | #font > #family > .monospace; 145 | #font > .shorthand(@size, @weight, @lineHeight); 146 | } 147 | } 148 | 149 | 150 | // FORMS 151 | // -------------------------------------------------- 152 | 153 | // Block level inputs 154 | .input-block-level { 155 | display: block; 156 | width: 100%; 157 | min-height: @inputHeight; // Make inputs at least the height of their button counterpart (base line-height + padding + border) 158 | .box-sizing(border-box); // Makes inputs behave like true block-level elements 159 | } 160 | 161 | 162 | 163 | // Mixin for form field states 164 | .formFieldState(@textColor: #555, @borderColor: #ccc, @backgroundColor: #f5f5f5) { 165 | // Set the text color 166 | .control-label, 167 | .help-block, 168 | .help-inline { 169 | color: @textColor; 170 | } 171 | // Style inputs accordingly 172 | .checkbox, 173 | .radio, 174 | input, 175 | select, 176 | textarea { 177 | color: @textColor; 178 | } 179 | input, 180 | select, 181 | textarea { 182 | border-color: @borderColor; 183 | .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work 184 | &:focus { 185 | border-color: darken(@borderColor, 10%); 186 | @shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten(@borderColor, 20%); 187 | .box-shadow(@shadow); 188 | } 189 | } 190 | // Give a small background color for input-prepend/-append 191 | .input-prepend .add-on, 192 | .input-append .add-on { 193 | color: @textColor; 194 | background-color: @backgroundColor; 195 | border-color: @textColor; 196 | } 197 | } 198 | 199 | 200 | 201 | // CSS3 PROPERTIES 202 | // -------------------------------------------------- 203 | 204 | // Border Radius 205 | .border-radius(@radius) { 206 | -webkit-border-radius: @radius; 207 | -moz-border-radius: @radius; 208 | border-radius: @radius; 209 | } 210 | 211 | // Single Corner Border Radius 212 | .border-top-left-radius(@radius) { 213 | -webkit-border-top-left-radius: @radius; 214 | -moz-border-radius-topleft: @radius; 215 | border-top-left-radius: @radius; 216 | } 217 | .border-top-right-radius(@radius) { 218 | -webkit-border-top-right-radius: @radius; 219 | -moz-border-radius-topright: @radius; 220 | border-top-right-radius: @radius; 221 | } 222 | .border-bottom-right-radius(@radius) { 223 | -webkit-border-bottom-right-radius: @radius; 224 | -moz-border-radius-bottomright: @radius; 225 | border-bottom-right-radius: @radius; 226 | } 227 | .border-bottom-left-radius(@radius) { 228 | -webkit-border-bottom-left-radius: @radius; 229 | -moz-border-radius-bottomleft: @radius; 230 | border-bottom-left-radius: @radius; 231 | } 232 | 233 | // Single Side Border Radius 234 | .border-top-radius(@radius) { 235 | .border-top-right-radius(@radius); 236 | .border-top-left-radius(@radius); 237 | } 238 | .border-right-radius(@radius) { 239 | .border-top-right-radius(@radius); 240 | .border-bottom-right-radius(@radius); 241 | } 242 | .border-bottom-radius(@radius) { 243 | .border-bottom-right-radius(@radius); 244 | .border-bottom-left-radius(@radius); 245 | } 246 | .border-left-radius(@radius) { 247 | .border-top-left-radius(@radius); 248 | .border-bottom-left-radius(@radius); 249 | } 250 | 251 | // Drop shadows 252 | .box-shadow(@shadow) { 253 | -webkit-box-shadow: @shadow; 254 | -moz-box-shadow: @shadow; 255 | box-shadow: @shadow; 256 | } 257 | 258 | // Transitions 259 | .transition(@transition) { 260 | -webkit-transition: @transition; 261 | -moz-transition: @transition; 262 | -o-transition: @transition; 263 | transition: @transition; 264 | } 265 | .transition-delay(@transition-delay) { 266 | -webkit-transition-delay: @transition-delay; 267 | -moz-transition-delay: @transition-delay; 268 | -o-transition-delay: @transition-delay; 269 | transition-delay: @transition-delay; 270 | } 271 | 272 | // Transformations 273 | .rotate(@degrees) { 274 | -webkit-transform: rotate(@degrees); 275 | -moz-transform: rotate(@degrees); 276 | -ms-transform: rotate(@degrees); 277 | -o-transform: rotate(@degrees); 278 | transform: rotate(@degrees); 279 | } 280 | .scale(@ratio) { 281 | -webkit-transform: scale(@ratio); 282 | -moz-transform: scale(@ratio); 283 | -ms-transform: scale(@ratio); 284 | -o-transform: scale(@ratio); 285 | transform: scale(@ratio); 286 | } 287 | .translate(@x, @y) { 288 | -webkit-transform: translate(@x, @y); 289 | -moz-transform: translate(@x, @y); 290 | -ms-transform: translate(@x, @y); 291 | -o-transform: translate(@x, @y); 292 | transform: translate(@x, @y); 293 | } 294 | .skew(@x, @y) { 295 | -webkit-transform: skew(@x, @y); 296 | -moz-transform: skew(@x, @y); 297 | -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twitter/bootstrap/issues/4885 298 | -o-transform: skew(@x, @y); 299 | transform: skew(@x, @y); 300 | -webkit-backface-visibility: hidden; // See https://github.com/twitter/bootstrap/issues/5319 301 | } 302 | .translate3d(@x, @y, @z) { 303 | -webkit-transform: translate3d(@x, @y, @z); 304 | -moz-transform: translate3d(@x, @y, @z); 305 | -o-transform: translate3d(@x, @y, @z); 306 | transform: translate3d(@x, @y, @z); 307 | } 308 | 309 | // Backface visibility 310 | // Prevent browsers from flickering when using CSS 3D transforms. 311 | // Default value is `visible`, but can be changed to `hidden 312 | // See git pull https://github.com/dannykeane/bootstrap.git backface-visibility for examples 313 | .backface-visibility(@visibility){ 314 | -webkit-backface-visibility: @visibility; 315 | -moz-backface-visibility: @visibility; 316 | backface-visibility: @visibility; 317 | } 318 | 319 | // Background clipping 320 | // Heads up: FF 3.6 and under need "padding" instead of "padding-box" 321 | .background-clip(@clip) { 322 | -webkit-background-clip: @clip; 323 | -moz-background-clip: @clip; 324 | background-clip: @clip; 325 | } 326 | 327 | // Background sizing 328 | .background-size(@size) { 329 | -webkit-background-size: @size; 330 | -moz-background-size: @size; 331 | -o-background-size: @size; 332 | background-size: @size; 333 | } 334 | 335 | 336 | // Box sizing 337 | .box-sizing(@boxmodel) { 338 | -webkit-box-sizing: @boxmodel; 339 | -moz-box-sizing: @boxmodel; 340 | box-sizing: @boxmodel; 341 | } 342 | 343 | // User select 344 | // For selecting text on the page 345 | .user-select(@select) { 346 | -webkit-user-select: @select; 347 | -moz-user-select: @select; 348 | -ms-user-select: @select; 349 | -o-user-select: @select; 350 | user-select: @select; 351 | } 352 | 353 | // Resize anything 354 | .resizable(@direction) { 355 | resize: @direction; // Options: horizontal, vertical, both 356 | overflow: auto; // Safari fix 357 | } 358 | 359 | // CSS3 Content Columns 360 | .content-columns(@columnCount, @columnGap: @gridGutterWidth) { 361 | -webkit-column-count: @columnCount; 362 | -moz-column-count: @columnCount; 363 | column-count: @columnCount; 364 | -webkit-column-gap: @columnGap; 365 | -moz-column-gap: @columnGap; 366 | column-gap: @columnGap; 367 | } 368 | 369 | // Optional hyphenation 370 | .hyphens(@mode: auto) { 371 | word-wrap: break-word; 372 | -webkit-hyphens: @mode; 373 | -moz-hyphens: @mode; 374 | -ms-hyphens: @mode; 375 | -o-hyphens: @mode; 376 | hyphens: @mode; 377 | } 378 | 379 | // Opacity 380 | .opacity(@opacity) { 381 | opacity: @opacity / 100; 382 | filter: ~"alpha(opacity=@{opacity})"; 383 | } 384 | 385 | 386 | 387 | // BACKGROUNDS 388 | // -------------------------------------------------- 389 | 390 | // Add an alphatransparency value to any background or border color (via Elyse Holladay) 391 | #translucent { 392 | .background(@color: @white, @alpha: 1) { 393 | background-color: hsla(hue(@color), saturation(@color), lightness(@color), @alpha); 394 | } 395 | .border(@color: @white, @alpha: 1) { 396 | border-color: hsla(hue(@color), saturation(@color), lightness(@color), @alpha); 397 | .background-clip(padding-box); 398 | } 399 | } 400 | 401 | // Gradient Bar Colors for buttons and alerts 402 | .gradientBar(@primaryColor, @secondaryColor, @textColor: #fff, @textShadow: 0 -1px 0 rgba(0,0,0,.25)) { 403 | color: @textColor; 404 | text-shadow: @textShadow; 405 | #gradient > .vertical(@primaryColor, @secondaryColor); 406 | border-color: @secondaryColor @secondaryColor darken(@secondaryColor, 15%); 407 | border-color: rgba(0,0,0,.1) rgba(0,0,0,.1) fadein(rgba(0,0,0,.1), 15%); 408 | } 409 | 410 | // Gradients 411 | #gradient { 412 | .horizontal(@startColor: #555, @endColor: #333) { 413 | background-color: @endColor; 414 | background-image: -moz-linear-gradient(left, @startColor, @endColor); // FF 3.6+ 415 | background-image: -webkit-gradient(linear, 0 0, 100% 0, from(@startColor), to(@endColor)); // Safari 4+, Chrome 2+ 416 | background-image: -webkit-linear-gradient(left, @startColor, @endColor); // Safari 5.1+, Chrome 10+ 417 | background-image: -o-linear-gradient(left, @startColor, @endColor); // Opera 11.10 418 | background-image: linear-gradient(to right, @startColor, @endColor); // Standard, IE10 419 | background-repeat: repeat-x; 420 | filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)",argb(@startColor),argb(@endColor))); // IE9 and down 421 | } 422 | .vertical(@startColor: #555, @endColor: #333) { 423 | background-color: mix(@startColor, @endColor, 60%); 424 | background-image: -moz-linear-gradient(top, @startColor, @endColor); // FF 3.6+ 425 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(@startColor), to(@endColor)); // Safari 4+, Chrome 2+ 426 | background-image: -webkit-linear-gradient(top, @startColor, @endColor); // Safari 5.1+, Chrome 10+ 427 | background-image: -o-linear-gradient(top, @startColor, @endColor); // Opera 11.10 428 | background-image: linear-gradient(to bottom, @startColor, @endColor); // Standard, IE10 429 | background-repeat: repeat-x; 430 | filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@startColor),argb(@endColor))); // IE9 and down 431 | } 432 | .directional(@startColor: #555, @endColor: #333, @deg: 45deg) { 433 | background-color: @endColor; 434 | background-repeat: repeat-x; 435 | background-image: -moz-linear-gradient(@deg, @startColor, @endColor); // FF 3.6+ 436 | background-image: -webkit-linear-gradient(@deg, @startColor, @endColor); // Safari 5.1+, Chrome 10+ 437 | background-image: -o-linear-gradient(@deg, @startColor, @endColor); // Opera 11.10 438 | background-image: linear-gradient(@deg, @startColor, @endColor); // Standard, IE10 439 | } 440 | .vertical-three-colors(@startColor: #00b3ee, @midColor: #7a43b6, @colorStop: 50%, @endColor: #c3325f) { 441 | background-color: mix(@midColor, @endColor, 80%); 442 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(@startColor), color-stop(@colorStop, @midColor), to(@endColor)); 443 | background-image: -webkit-linear-gradient(@startColor, @midColor @colorStop, @endColor); 444 | background-image: -moz-linear-gradient(top, @startColor, @midColor @colorStop, @endColor); 445 | background-image: -o-linear-gradient(@startColor, @midColor @colorStop, @endColor); 446 | background-image: linear-gradient(@startColor, @midColor @colorStop, @endColor); 447 | background-repeat: no-repeat; 448 | filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@startColor),argb(@endColor))); // IE9 and down, gets no color-stop at all for proper fallback 449 | } 450 | .radial(@innerColor: #555, @outerColor: #333) { 451 | background-color: @outerColor; 452 | background-image: -webkit-gradient(radial, center center, 0, center center, 460, from(@innerColor), to(@outerColor)); 453 | background-image: -webkit-radial-gradient(circle, @innerColor, @outerColor); 454 | background-image: -moz-radial-gradient(circle, @innerColor, @outerColor); 455 | background-image: -o-radial-gradient(circle, @innerColor, @outerColor); 456 | background-repeat: no-repeat; 457 | } 458 | .striped(@color: #555, @angle: 45deg) { 459 | background-color: @color; 460 | background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(.25, rgba(255,255,255,.15)), color-stop(.25, transparent), color-stop(.5, transparent), color-stop(.5, rgba(255,255,255,.15)), color-stop(.75, rgba(255,255,255,.15)), color-stop(.75, transparent), to(transparent)); 461 | background-image: -webkit-linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent); 462 | background-image: -moz-linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent); 463 | background-image: -o-linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent); 464 | background-image: linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent); 465 | } 466 | } 467 | // Reset filters for IE 468 | .reset-filter() { 469 | filter: e(%("progid:DXImageTransform.Microsoft.gradient(enabled = false)")); 470 | } 471 | 472 | 473 | 474 | // COMPONENT MIXINS 475 | // -------------------------------------------------- 476 | 477 | // Horizontal dividers 478 | // ------------------------- 479 | // Dividers (basically an hr) within dropdowns and nav lists 480 | .nav-divider(@top: #e5e5e5, @bottom: @white) { 481 | // IE7 needs a set width since we gave a height. Restricting just 482 | // to IE7 to keep the 1px left/right space in other browsers. 483 | // It is unclear where IE is getting the extra space that we need 484 | // to negative-margin away, but so it goes. 485 | *width: 100%; 486 | height: 1px; 487 | margin: ((@baseLineHeight / 2) - 1) 1px; // 8px 1px 488 | *margin: -5px 0 5px; 489 | overflow: hidden; 490 | background-color: @top; 491 | border-bottom: 1px solid @bottom; 492 | } 493 | 494 | // Button backgrounds 495 | // ------------------ 496 | .buttonBackground(@startColor, @endColor, @textColor: #fff, @textShadow: 0 -1px 0 rgba(0,0,0,.25)) { 497 | // gradientBar will set the background to a pleasing blend of these, to support IE<=9 498 | .gradientBar(@startColor, @endColor, @textColor, @textShadow); 499 | *background-color: @endColor; /* Darken IE7 buttons by default so they stand out more given they won't have borders */ 500 | .reset-filter(); 501 | 502 | // in these cases the gradient won't cover the background, so we override 503 | &:hover, &:active, &.active, &.disabled, &[disabled] { 504 | color: @textColor; 505 | background-color: @endColor; 506 | *background-color: darken(@endColor, 5%); 507 | } 508 | 509 | // IE 7 + 8 can't handle box-shadow to show active, so we darken a bit ourselves 510 | &:active, 511 | &.active { 512 | background-color: darken(@endColor, 10%) e("\9"); 513 | } 514 | } 515 | 516 | // Navbar vertical align 517 | // ------------------------- 518 | // Vertically center elements in the navbar. 519 | // Example: an element has a height of 30px, so write out `.navbarVerticalAlign(30px);` to calculate the appropriate top margin. 520 | .navbarVerticalAlign(@elementHeight) { 521 | margin-top: (@navbarHeight - @elementHeight) / 2; 522 | } 523 | 524 | 525 | 526 | // Grid System 527 | // ----------- 528 | 529 | // Centered container element 530 | .container-fixed() { 531 | margin-right: auto; 532 | margin-left: auto; 533 | .clearfix(); 534 | } 535 | 536 | // Table columns 537 | .tableColumns(@columnSpan: 1) { 538 | float: none; // undo default grid column styles 539 | width: ((@gridColumnWidth) * @columnSpan) + (@gridGutterWidth * (@columnSpan - 1)) - 16; // 16 is total padding on left and right of table cells 540 | margin-left: 0; // undo default grid column styles 541 | } 542 | 543 | // Make a Grid 544 | // Use .makeRow and .makeColumn to assign semantic layouts grid system behavior 545 | .makeRow() { 546 | margin-left: @gridGutterWidth * -1; 547 | .clearfix(); 548 | } 549 | .makeColumn(@columns: 1, @offset: 0) { 550 | float: left; 551 | margin-left: (@gridColumnWidth * @offset) + (@gridGutterWidth * (@offset - 1)) + (@gridGutterWidth * 2); 552 | width: (@gridColumnWidth * @columns) + (@gridGutterWidth * (@columns - 1)); 553 | } 554 | 555 | // The Grid 556 | #grid { 557 | 558 | .core (@gridColumnWidth, @gridGutterWidth) { 559 | 560 | .spanX (@index) when (@index > 0) { 561 | (.span@{index}) { .span(@index); } 562 | .spanX(@index - 1); 563 | } 564 | .spanX (0) {} 565 | 566 | .offsetX (@index) when (@index > 0) { 567 | (.offset@{index}) { .offset(@index); } 568 | .offsetX(@index - 1); 569 | } 570 | .offsetX (0) {} 571 | 572 | .offset (@columns) { 573 | margin-left: (@gridColumnWidth * @columns) + (@gridGutterWidth * (@columns + 1)); 574 | } 575 | 576 | .span (@columns) { 577 | width: (@gridColumnWidth * @columns) + (@gridGutterWidth * (@columns - 1)); 578 | } 579 | 580 | .row { 581 | margin-left: @gridGutterWidth * -1; 582 | .clearfix(); 583 | } 584 | 585 | [class*="span"] { 586 | float: left; 587 | min-height: 1px; // prevent collapsing columns 588 | margin-left: @gridGutterWidth; 589 | } 590 | 591 | // Set the container width, and override it for fixed navbars in media queries 592 | .container, 593 | .navbar-static-top .container, 594 | .navbar-fixed-top .container, 595 | .navbar-fixed-bottom .container { .span(@gridColumns); } 596 | 597 | // generate .spanX and .offsetX 598 | .spanX (@gridColumns); 599 | .offsetX (@gridColumns); 600 | 601 | } 602 | 603 | .fluid (@fluidGridColumnWidth, @fluidGridGutterWidth) { 604 | 605 | .spanX (@index) when (@index > 0) { 606 | (.span@{index}) { .span(@index); } 607 | .spanX(@index - 1); 608 | } 609 | .spanX (0) {} 610 | 611 | .offsetX (@index) when (@index > 0) { 612 | (.offset@{index}) { .offset(@index); } 613 | (.offset@{index}:first-child) { .offsetFirstChild(@index); } 614 | .offsetX(@index - 1); 615 | } 616 | .offsetX (0) {} 617 | 618 | .offset (@columns) { 619 | margin-left: (@fluidGridColumnWidth * @columns) + (@fluidGridGutterWidth * (@columns - 1)) + (@fluidGridGutterWidth*2); 620 | *margin-left: (@fluidGridColumnWidth * @columns) + (@fluidGridGutterWidth * (@columns - 1)) - (.5 / @gridRowWidth * 100 * 1%) + (@fluidGridGutterWidth*2) - (.5 / @gridRowWidth * 100 * 1%); 621 | } 622 | 623 | .offsetFirstChild (@columns) { 624 | margin-left: (@fluidGridColumnWidth * @columns) + (@fluidGridGutterWidth * (@columns - 1)) + (@fluidGridGutterWidth); 625 | *margin-left: (@fluidGridColumnWidth * @columns) + (@fluidGridGutterWidth * (@columns - 1)) - (.5 / @gridRowWidth * 100 * 1%) + @fluidGridGutterWidth - (.5 / @gridRowWidth * 100 * 1%); 626 | } 627 | 628 | .span (@columns) { 629 | width: (@fluidGridColumnWidth * @columns) + (@fluidGridGutterWidth * (@columns - 1)); 630 | *width: (@fluidGridColumnWidth * @columns) + (@fluidGridGutterWidth * (@columns - 1)) - (.5 / @gridRowWidth * 100 * 1%); 631 | } 632 | 633 | .row-fluid { 634 | width: 100%; 635 | .clearfix(); 636 | [class*="span"] { 637 | .input-block-level(); 638 | float: left; 639 | margin-left: @fluidGridGutterWidth; 640 | *margin-left: @fluidGridGutterWidth - (.5 / @gridRowWidth * 100 * 1%); 641 | } 642 | [class*="span"]:first-child { 643 | margin-left: 0; 644 | } 645 | 646 | // Space grid-sized controls properly if multiple per line 647 | .controls-row [class*="span"] + [class*="span"] { 648 | margin-left: @fluidGridGutterWidth; 649 | } 650 | 651 | // generate .spanX and .offsetX 652 | .spanX (@gridColumns); 653 | .offsetX (@gridColumns); 654 | } 655 | 656 | } 657 | 658 | .input(@gridColumnWidth, @gridGutterWidth) { 659 | .spanX (@index) when (@index > 0) { 660 | /* FIXME: thomas: this line provokes less compilation errors, so I 661 | /* broke it up in three lines 662 | (input.span@{index}, textarea.span@{index}, .uneditable-input.span@{index}) { .span(@index); } 663 | */ 664 | input.span@{index} { .span(@index); } 665 | textarea.span@{index} { .span(@index); } 666 | .uneditable-input.span@{index} { .span(@index); } 667 | 668 | .spanX(@index - 1); 669 | } 670 | .spanX (0) {} 671 | 672 | .span(@columns) { 673 | width: ((@gridColumnWidth) * @columns) + (@gridGutterWidth * (@columns - 1)) - 14; 674 | } 675 | 676 | input, 677 | textarea, 678 | .uneditable-input { 679 | margin-left: 0; // override margin-left from core grid system 680 | } 681 | 682 | // Space grid-sized controls properly if multiple per line 683 | .controls-row [class*="span"] + [class*="span"] { 684 | margin-left: @gridGutterWidth; 685 | } 686 | 687 | // generate .spanX 688 | .spanX (@gridColumns); 689 | 690 | } 691 | 692 | } 693 | -------------------------------------------------------------------------------- /app/styles/style.less: -------------------------------------------------------------------------------- 1 | @import "variables.less"; 2 | @import "mixins.less"; 3 | 4 | .date-picker-date-time { 5 | position: absolute; 6 | } 7 | 8 | .date-range .date-picker-date-time { 9 | position: inherit; 10 | } 11 | 12 | [date-picker-wrapper] { 13 | position: absolute; 14 | min-width: 220px; 15 | z-index: 10; 16 | display: block; 17 | font-size: 14px; 18 | } 19 | [date-time-append] [date-picker-wrapper] [date-picker] { 20 | margin-top: -30px; 21 | } 22 | 23 | [date-time-append] [date-picker]{ 24 | position: relative; 25 | margin-right: -1000px; 26 | margin-bottom: -1000px; 27 | } 28 | 29 | [date-range] [date-picker] { 30 | .after.before{ 31 | .buttonBackground(@btnInfoBackground, spin(@btnInfoBackgroundHighlight, 20)); 32 | } 33 | } 34 | 35 | [date-picker].hidden { display: none; } 36 | 37 | [date-picker] { 38 | .user-select(none); 39 | .border-radius(4px); 40 | background-color: #fff; 41 | 42 | /* GENERAL */ 43 | 44 | padding: 4px; 45 | 46 | table { 47 | margin: 0; 48 | } 49 | 50 | td, th { 51 | padding: 4px 5px; 52 | text-align: center; 53 | width: 20px; 54 | height: 20px; 55 | .border-radius(4px); 56 | border: none; 57 | 58 | } 59 | 60 | .switch{ 61 | width: 145px; 62 | } 63 | 64 | span { 65 | display: block; 66 | width: 23%; 67 | height: 26px; 68 | line-height: 25px; 69 | float: left; 70 | margin: 1%; 71 | cursor: pointer; 72 | .border-radius(4px); 73 | &:hover { 74 | background: @grayLighter; 75 | } 76 | &.disabled, &.disabled:hover { 77 | background:none; 78 | color: @grayLight; 79 | cursor: default; 80 | } 81 | 82 | } 83 | 84 | .active , .now{ 85 | .buttonBackground(@btnPrimaryBackground, spin(@btnPrimaryBackground, 20)); 86 | color: #fff; 87 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .25); 88 | } 89 | 90 | .now { 91 | .buttonBackground(@btnDangerBackground, spin(@btnDangerBackground, 20)); 92 | } 93 | 94 | .disabled { 95 | background: none; 96 | color: #999999 !important; 97 | cursor: default; 98 | } 99 | 100 | /* SPECIFIC */ 101 | 102 | [ng-switch-when="year"], [ng-switch-when="month"], [ng-switch-when="minutes"]{ 103 | span { 104 | height: 54px; 105 | line-height: 54px; 106 | } 107 | } 108 | [ng-switch-when="date"]{ 109 | td { 110 | padding: 0; 111 | } 112 | span{ 113 | width: 100%; 114 | height: 26px; 115 | line-height: 26px; 116 | } 117 | } 118 | 119 | th:hover, [ng-switch-when="date"] td span:hover{ 120 | background: @grayLighter; 121 | cursor: pointer; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /app/styles/variables.less: -------------------------------------------------------------------------------- 1 | // 2 | // Variables 3 | // -------------------------------------------------- 4 | 5 | 6 | // Global values 7 | // -------------------------------------------------- 8 | 9 | 10 | // Grays 11 | // ------------------------- 12 | @black: #000; 13 | @grayDarker: #222; 14 | @grayDark: #333; 15 | @gray: #555; 16 | @grayLight: #999; 17 | @grayLighter: #eee; 18 | @white: #fff; 19 | 20 | 21 | // Accent colors 22 | // ------------------------- 23 | @blue: #049cdb; 24 | @blueDark: #0064cd; 25 | @green: #46a546; 26 | @red: #9d261d; 27 | @yellow: #ffc40d; 28 | @orange: #f89406; 29 | @pink: #c3325f; 30 | @purple: #7a43b6; 31 | 32 | 33 | // Scaffolding 34 | // ------------------------- 35 | @bodyBackground: @white; 36 | @textColor: @grayDark; 37 | 38 | 39 | // Links 40 | // ------------------------- 41 | @linkColor: #08c; 42 | @linkColorHover: darken(@linkColor, 15%); 43 | 44 | 45 | // Typography 46 | // ------------------------- 47 | @sansFontFamily: "Helvetica Neue", Helvetica, Arial, sans-serif; 48 | @serifFontFamily: Georgia, "Times New Roman", Times, serif; 49 | @monoFontFamily: Monaco, Menlo, Consolas, "Courier New", monospace; 50 | 51 | @baseFontSize: 14px; 52 | @baseFontFamily: @sansFontFamily; 53 | @baseLineHeight: 20px; 54 | @altFontFamily: @serifFontFamily; 55 | 56 | @headingsFontFamily: inherit; // empty to use BS default, @baseFontFamily 57 | @headingsFontWeight: bold; // instead of browser default, bold 58 | @headingsColor: inherit; // empty to use BS default, @textColor 59 | 60 | 61 | // Component sizing 62 | // ------------------------- 63 | // Based on 14px font-size and 20px line-height 64 | 65 | @fontSizeLarge: @baseFontSize * 1.25; // ~18px 66 | @fontSizeSmall: @baseFontSize * 0.85; // ~12px 67 | @fontSizeMini: @baseFontSize * 0.75; // ~11px 68 | 69 | @paddingLarge: 11px 19px; // 44px 70 | @paddingSmall: 2px 10px; // 26px 71 | @paddingMini: 0 6px; // 22px 72 | 73 | @baseBorderRadius: 4px; 74 | @borderRadiusLarge: 6px; 75 | @borderRadiusSmall: 3px; 76 | 77 | 78 | // Tables 79 | // ------------------------- 80 | @tableBackground: transparent; // overall background-color 81 | @tableBackgroundAccent: #f9f9f9; // for striping 82 | @tableBackgroundHover: #f5f5f5; // for hover 83 | @tableBorder: #ddd; // table and cell border 84 | 85 | // Buttons 86 | // ------------------------- 87 | @btnBackground: @white; 88 | @btnBackgroundHighlight: darken(@white, 10%); 89 | @btnBorder: #bbb; 90 | 91 | @btnPrimaryBackground: @linkColor; 92 | @btnPrimaryBackgroundHighlight: spin(@btnPrimaryBackground, 20%); 93 | 94 | @btnInfoBackground: #5bc0de; 95 | @btnInfoBackgroundHighlight: #2f96b4; 96 | 97 | @btnSuccessBackground: #62c462; 98 | @btnSuccessBackgroundHighlight: #51a351; 99 | 100 | @btnWarningBackground: lighten(@orange, 15%); 101 | @btnWarningBackgroundHighlight: @orange; 102 | 103 | @btnDangerBackground: #ee5f5b; 104 | @btnDangerBackgroundHighlight: #bd362f; 105 | 106 | @btnInverseBackground: #444; 107 | @btnInverseBackgroundHighlight: @grayDarker; 108 | 109 | 110 | // Forms 111 | // ------------------------- 112 | @inputBackground: @white; 113 | @inputBorder: #ccc; 114 | @inputBorderRadius: @baseBorderRadius; 115 | @inputDisabledBackground: @grayLighter; 116 | @formActionsBackground: #f5f5f5; 117 | @inputHeight: @baseLineHeight + 10px; // base line-height + 8px vertical padding + 2px top/bottom border 118 | 119 | 120 | // Dropdowns 121 | // ------------------------- 122 | @dropdownBackground: @white; 123 | @dropdownBorder: rgba(0,0,0,.2); 124 | @dropdownDividerTop: #e5e5e5; 125 | @dropdownDividerBottom: @white; 126 | 127 | @dropdownLinkColor: @grayDark; 128 | @dropdownLinkColorHover: @white; 129 | @dropdownLinkColorActive: @white; 130 | 131 | @dropdownLinkBackgroundActive: @linkColor; 132 | @dropdownLinkBackgroundHover: @dropdownLinkBackgroundActive; 133 | 134 | 135 | 136 | // COMPONENT VARIABLES 137 | // -------------------------------------------------- 138 | 139 | 140 | // Z-index master list 141 | // ------------------------- 142 | // Used for a bird's eye view of components dependent on the z-axis 143 | // Try to avoid customizing these :) 144 | @zindexDropdown: 1000; 145 | @zindexPopover: 1010; 146 | @zindexTooltip: 1030; 147 | @zindexFixedNavbar: 1030; 148 | @zindexModalBackdrop: 1040; 149 | @zindexModal: 1050; 150 | 151 | 152 | // Sprite icons path 153 | // ------------------------- 154 | @iconSpritePath: "../img/glyphicons-halflings.png"; 155 | @iconWhiteSpritePath: "../img/glyphicons-halflings-white.png"; 156 | 157 | 158 | // Input placeholder text color 159 | // ------------------------- 160 | @placeholderText: @grayLight; 161 | 162 | 163 | // Hr border color 164 | // ------------------------- 165 | @hrBorder: @grayLighter; 166 | 167 | 168 | // Horizontal forms & lists 169 | // ------------------------- 170 | @horizontalComponentOffset: 180px; 171 | 172 | 173 | // Wells 174 | // ------------------------- 175 | @wellBackground: #f5f5f5; 176 | 177 | 178 | // Navbar 179 | // ------------------------- 180 | @navbarCollapseWidth: 979px; 181 | @navbarCollapseDesktopWidth: @navbarCollapseWidth + 1; 182 | 183 | @navbarHeight: 40px; 184 | @navbarBackgroundHighlight: #ffffff; 185 | @navbarBackground: darken(@navbarBackgroundHighlight, 5%); 186 | @navbarBorder: darken(@navbarBackground, 12%); 187 | 188 | @navbarText: #777; 189 | @navbarLinkColor: #777; 190 | @navbarLinkColorHover: @grayDark; 191 | @navbarLinkColorActive: @gray; 192 | @navbarLinkBackgroundHover: transparent; 193 | @navbarLinkBackgroundActive: darken(@navbarBackground, 5%); 194 | 195 | @navbarBrandColor: @navbarLinkColor; 196 | 197 | // Inverted navbar 198 | @navbarInverseBackground: #111111; 199 | @navbarInverseBackgroundHighlight: #222222; 200 | @navbarInverseBorder: #252525; 201 | 202 | @navbarInverseText: @grayLight; 203 | @navbarInverseLinkColor: @grayLight; 204 | @navbarInverseLinkColorHover: @white; 205 | @navbarInverseLinkColorActive: @navbarInverseLinkColorHover; 206 | @navbarInverseLinkBackgroundHover: transparent; 207 | @navbarInverseLinkBackgroundActive: @navbarInverseBackground; 208 | 209 | @navbarInverseSearchBackground: lighten(@navbarInverseBackground, 25%); 210 | @navbarInverseSearchBackgroundFocus: @white; 211 | @navbarInverseSearchBorder: @navbarInverseBackground; 212 | @navbarInverseSearchPlaceholderColor: #ccc; 213 | 214 | @navbarInverseBrandColor: @navbarInverseLinkColor; 215 | 216 | 217 | // Pagination 218 | // ------------------------- 219 | @paginationBackground: #fff; 220 | @paginationBorder: #ddd; 221 | @paginationActiveBackground: #f5f5f5; 222 | 223 | 224 | // Hero unit 225 | // ------------------------- 226 | @heroUnitBackground: @grayLighter; 227 | @heroUnitHeadingColor: inherit; 228 | @heroUnitLeadColor: inherit; 229 | 230 | 231 | // Form states and alerts 232 | // ------------------------- 233 | @warningText: #c09853; 234 | @warningBackground: #fcf8e3; 235 | @warningBorder: darken(spin(@warningBackground, -10), 3%); 236 | 237 | @errorText: #b94a48; 238 | @errorBackground: #f2dede; 239 | @errorBorder: darken(spin(@errorBackground, -10), 3%); 240 | 241 | @successText: #468847; 242 | @successBackground: #dff0d8; 243 | @successBorder: darken(spin(@successBackground, -10), 5%); 244 | 245 | @infoText: #3a87ad; 246 | @infoBackground: #d9edf7; 247 | @infoBorder: darken(spin(@infoBackground, -10), 7%); 248 | 249 | 250 | // Tooltips and popovers 251 | // ------------------------- 252 | @tooltipColor: #fff; 253 | @tooltipBackground: #000; 254 | @tooltipArrowWidth: 5px; 255 | @tooltipArrowColor: @tooltipBackground; 256 | 257 | @popoverBackground: #fff; 258 | @popoverArrowWidth: 10px; 259 | @popoverArrowColor: #fff; 260 | @popoverTitleBackground: darken(@popoverBackground, 3%); 261 | 262 | // Special enhancement for popovers 263 | @popoverArrowOuterWidth: @popoverArrowWidth + 1; 264 | @popoverArrowOuterColor: rgba(0,0,0,.25); 265 | 266 | 267 | 268 | // GRID 269 | // -------------------------------------------------- 270 | 271 | 272 | // Default 940px grid 273 | // ------------------------- 274 | @gridColumns: 12; 275 | @gridColumnWidth: 60px; 276 | @gridGutterWidth: 20px; 277 | @gridRowWidth: (@gridColumns * @gridColumnWidth) + (@gridGutterWidth * (@gridColumns - 1)); 278 | 279 | // 1200px min 280 | @gridColumnWidth1200: 70px; 281 | @gridGutterWidth1200: 30px; 282 | @gridRowWidth1200: (@gridColumns * @gridColumnWidth1200) + (@gridGutterWidth1200 * (@gridColumns - 1)); 283 | 284 | // 768px-979px 285 | @gridColumnWidth768: 42px; 286 | @gridGutterWidth768: 20px; 287 | @gridRowWidth768: (@gridColumns * @gridColumnWidth768) + (@gridGutterWidth768 * (@gridColumns - 1)); 288 | 289 | 290 | // Fluid grid 291 | // ------------------------- 292 | @fluidGridColumnWidth: percentage(@gridColumnWidth/@gridRowWidth); 293 | @fluidGridGutterWidth: percentage(@gridGutterWidth/@gridRowWidth); 294 | 295 | // 1200px min 296 | @fluidGridColumnWidth1200: percentage(@gridColumnWidth1200/@gridRowWidth1200); 297 | @fluidGridGutterWidth1200: percentage(@gridGutterWidth1200/@gridRowWidth1200); 298 | 299 | // 768px-979px 300 | @fluidGridColumnWidth768: percentage(@gridColumnWidth768/@gridRowWidth768); 301 | @fluidGridGutterWidth768: percentage(@gridGutterWidth768/@gridRowWidth768); 302 | -------------------------------------------------------------------------------- /app/templates/datepicker.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 22 | 23 |
17 | 20 |
24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 41 | 42 | 43 |
37 | 40 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 62 | 63 | 64 |
57 | 61 |
65 |
66 |
67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 82 | 83 | 84 |
78 | 81 |
85 |
86 |
87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 103 | 104 | 105 |
98 | 102 |
106 |
107 |
-------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-datepicker", 3 | "license": "MIT", 4 | "version": "2.1.3", 5 | "main": [ 6 | "./dist/angular-datepicker.js", 7 | "./dist/angular-datepicker.css" 8 | ], 9 | "ignore": [ 10 | ".gitignore", 11 | "README.md", 12 | "app", 13 | "circle.yml", 14 | ".travis.yml", 15 | "test", 16 | ".editorconfig", 17 | ".jshintrc", 18 | "Gruntfile.js", 19 | "karma-e2e.conf.js", 20 | "karma.conf.js", 21 | "package.json" 22 | ], 23 | "dependencies": { 24 | "angular": ">=1.2.14", 25 | "moment": "~2.10.6", 26 | "moment-timezone": "~0.4.1" 27 | }, 28 | "devDependencies": { 29 | "angular": "1.2.14", 30 | "angular-mocks": "1.2.14", 31 | "angular-scenario": "1.2.14", 32 | "angular-bootstrap": "~0.3.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | pre: 3 | - npm install -g grunt-cli bower 4 | - bower install 5 | cache_directories: 6 | - "node_modules" 7 | - "app/components" 8 | 9 | test: 10 | pre: 11 | - grunt test 12 | -------------------------------------------------------------------------------- /dist/angular-datepicker.css: -------------------------------------------------------------------------------- 1 | .clearfix { 2 | *zoom: 1; 3 | } 4 | .clearfix:before, 5 | .clearfix:after { 6 | display: table; 7 | content: ""; 8 | line-height: 0; 9 | } 10 | .clearfix:after { 11 | clear: both; 12 | } 13 | .hide-text { 14 | font: 0/0 a; 15 | color: transparent; 16 | text-shadow: none; 17 | background-color: transparent; 18 | border: 0; 19 | } 20 | .input-block-level { 21 | display: block; 22 | width: 100%; 23 | min-height: 30px; 24 | -webkit-box-sizing: border-box; 25 | -moz-box-sizing: border-box; 26 | box-sizing: border-box; 27 | } 28 | .date-picker-date-time { 29 | position: absolute; 30 | } 31 | .date-range .date-picker-date-time { 32 | position: inherit; 33 | } 34 | [date-picker-wrapper] { 35 | position: absolute; 36 | min-width: 220px; 37 | z-index: 10; 38 | display: block; 39 | font-size: 14px; 40 | } 41 | [date-time-append] [date-picker-wrapper] [date-picker] { 42 | margin-top: -30px; 43 | } 44 | [date-time-append] [date-picker] { 45 | position: relative; 46 | margin-right: -1000px; 47 | margin-bottom: -1000px; 48 | } 49 | [date-range] [date-picker] .after.before { 50 | color: #ffffff; 51 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 52 | background-color: #499dcd; 53 | background-image: -moz-linear-gradient(top, #5bc0de, #2f6ab4); 54 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f6ab4)); 55 | background-image: -webkit-linear-gradient(top, #5bc0de, #2f6ab4); 56 | background-image: -o-linear-gradient(top, #5bc0de, #2f6ab4); 57 | background-image: linear-gradient(to bottom, #5bc0de, #2f6ab4); 58 | background-repeat: repeat-x; 59 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2f6ab4', GradientType=0); 60 | border-color: #2f6ab4 #2f6ab4 #1f4677; 61 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 62 | *background-color: #2f6ab4; 63 | /* Darken IE7 buttons by default so they stand out more given they won't have borders */ 64 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 65 | } 66 | [date-range] [date-picker] .after.before:hover, 67 | [date-range] [date-picker] .after.before:active, 68 | [date-range] [date-picker] .after.before.active, 69 | [date-range] [date-picker] .after.before.disabled, 70 | [date-range] [date-picker] .after.before[disabled] { 71 | color: #ffffff; 72 | background-color: #2f6ab4; 73 | *background-color: #2a5ea0; 74 | } 75 | [date-range] [date-picker] .after.before:active, 76 | [date-range] [date-picker] .after.before.active { 77 | background-color: #24528c \9; 78 | } 79 | [date-picker].hidden { 80 | display: none; 81 | } 82 | [date-picker] { 83 | -webkit-user-select: none; 84 | -moz-user-select: none; 85 | -ms-user-select: none; 86 | -o-user-select: none; 87 | user-select: none; 88 | -webkit-border-radius: 4px; 89 | -moz-border-radius: 4px; 90 | border-radius: 4px; 91 | background-color: #fff; 92 | /* GENERAL */ 93 | padding: 4px; 94 | /* SPECIFIC */ 95 | } 96 | [date-picker] table { 97 | margin: 0; 98 | } 99 | [date-picker] td, 100 | [date-picker] th { 101 | padding: 4px 5px; 102 | text-align: center; 103 | width: 20px; 104 | height: 20px; 105 | -webkit-border-radius: 4px; 106 | -moz-border-radius: 4px; 107 | border-radius: 4px; 108 | border: none; 109 | } 110 | [date-picker] .switch { 111 | width: 145px; 112 | } 113 | [date-picker] span { 114 | display: block; 115 | width: 23%; 116 | height: 26px; 117 | line-height: 25px; 118 | float: left; 119 | margin: 1%; 120 | cursor: pointer; 121 | -webkit-border-radius: 4px; 122 | -moz-border-radius: 4px; 123 | border-radius: 4px; 124 | } 125 | [date-picker] span:hover { 126 | background: #eeeeee; 127 | } 128 | [date-picker] span.disabled, 129 | [date-picker] span.disabled:hover { 130 | background: none; 131 | color: #999999; 132 | cursor: default; 133 | } 134 | [date-picker] .active, 135 | [date-picker] .now { 136 | color: #ffffff; 137 | background-color: #006dcc; 138 | background-image: -moz-linear-gradient(top, #0088cc, #0044cc); 139 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); 140 | background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); 141 | background-image: -o-linear-gradient(top, #0088cc, #0044cc); 142 | background-image: linear-gradient(to bottom, #0088cc, #0044cc); 143 | background-repeat: repeat-x; 144 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0); 145 | border-color: #0044cc #0044cc #002a80; 146 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 147 | *background-color: #0044cc; 148 | /* Darken IE7 buttons by default so they stand out more given they won't have borders */ 149 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 150 | color: #fff; 151 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 152 | } 153 | [date-picker] .active:hover, 154 | [date-picker] .now:hover, 155 | [date-picker] .active:active, 156 | [date-picker] .now:active, 157 | [date-picker] .active.active, 158 | [date-picker] .now.active, 159 | [date-picker] .active.disabled, 160 | [date-picker] .now.disabled, 161 | [date-picker] .active[disabled], 162 | [date-picker] .now[disabled] { 163 | color: #ffffff; 164 | background-color: #0044cc; 165 | *background-color: #003bb3; 166 | } 167 | [date-picker] .active:active, 168 | [date-picker] .now:active, 169 | [date-picker] .active.active, 170 | [date-picker] .now.active { 171 | background-color: #003399 \9; 172 | } 173 | [date-picker] .now { 174 | color: #ffffff; 175 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 176 | background-color: #ee735b; 177 | background-image: -moz-linear-gradient(top, #ee5f5b, #ee905b); 178 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#ee905b)); 179 | background-image: -webkit-linear-gradient(top, #ee5f5b, #ee905b); 180 | background-image: -o-linear-gradient(top, #ee5f5b, #ee905b); 181 | background-image: linear-gradient(to bottom, #ee5f5b, #ee905b); 182 | background-repeat: repeat-x; 183 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffee905b', GradientType=0); 184 | border-color: #ee905b #ee905b #e56218; 185 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 186 | *background-color: #ee905b; 187 | /* Darken IE7 buttons by default so they stand out more given they won't have borders */ 188 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 189 | } 190 | [date-picker] .now:hover, 191 | [date-picker] .now:active, 192 | [date-picker] .now.active, 193 | [date-picker] .now.disabled, 194 | [date-picker] .now[disabled] { 195 | color: #ffffff; 196 | background-color: #ee905b; 197 | *background-color: #ec8044; 198 | } 199 | [date-picker] .now:active, 200 | [date-picker] .now.active { 201 | background-color: #e9712d \9; 202 | } 203 | [date-picker] .disabled { 204 | background: none; 205 | color: #999999 !important; 206 | cursor: default; 207 | } 208 | [date-picker] [ng-switch-when="year"] span, 209 | [date-picker] [ng-switch-when="month"] span, 210 | [date-picker] [ng-switch-when="minutes"] span { 211 | height: 54px; 212 | line-height: 54px; 213 | } 214 | [date-picker] [ng-switch-when="date"] td { 215 | padding: 0; 216 | } 217 | [date-picker] [ng-switch-when="date"] span { 218 | width: 100%; 219 | height: 26px; 220 | line-height: 26px; 221 | } 222 | [date-picker] th:hover, 223 | [date-picker] [ng-switch-when="date"] td span:hover { 224 | background: #eeeeee; 225 | cursor: pointer; 226 | } 227 | -------------------------------------------------------------------------------- /dist/angular-datepicker.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) {'use strict';var fnc;fnc = (typeof exports === 'object' && typeof module !== 'undefined') ? module.exports = factory(require('angular'), require('moment')) :(typeof define === 'function' && define.amd) ? define(['angular', 'moment'], factory) :factory(global.angular, global.moment);}(this, function (angular, moment) { 2 | //(function (global, factory) { 3 | // 'use strict'; 4 | // var fnc; 5 | // fnc = (typeof exports === 'object' && typeof module !== 'undefined') ? module.exports = factory(require('angular'), require('moment')) : 6 | // (typeof define === 'function' && define.amd) ? define(['angular', 'moment'], factory) : 7 | // factory(global.angular, global.moment); 8 | //}(this, function (angular, moment) { 9 | var Module = angular.module('datePicker', []); 10 | 11 | Module.constant('datePickerConfig', { 12 | template: 'templates/datepicker.html', 13 | view: 'month', 14 | views: ['year', 'month', 'date', 'hours', 'minutes'], 15 | momentNames: { 16 | year: 'year', 17 | month: 'month', 18 | date: 'day', 19 | hours: 'hours', 20 | minutes: 'minutes', 21 | }, 22 | viewConfig: { 23 | year: ['years', 'isSameYear'], 24 | month: ['months', 'isSameMonth'], 25 | hours: ['hours', 'isSameHour'], 26 | minutes: ['minutes', 'isSameMinutes'], 27 | }, 28 | step: 5 29 | }); 30 | 31 | //Moment format filter. 32 | Module.filter('mFormat', function () { 33 | return function (m, format, tz) { 34 | if (!(moment.isMoment(m))) { 35 | return (m) ? moment(m).format(format) : ''; 36 | } 37 | return tz ? moment.tz(m, tz).format(format) : m.format(format); 38 | }; 39 | }); 40 | 41 | Module.directive('datePicker', ['datePickerConfig', 'datePickerUtils', function datePickerDirective(datePickerConfig, datePickerUtils) { 42 | 43 | //noinspection JSUnusedLocalSymbols 44 | return { 45 | // this is a bug ? 46 | require: '?ngModel', 47 | template: '
', 48 | scope: { 49 | model: '=datePicker', 50 | after: '=?', 51 | before: '=?' 52 | }, 53 | link: function (scope, element, attrs, ngModel) { 54 | function prepareViews() { 55 | scope.views = datePickerConfig.views.concat(); 56 | scope.view = attrs.view || datePickerConfig.view; 57 | 58 | scope.views = scope.views.slice( 59 | scope.views.indexOf(attrs.maxView || 'year'), 60 | scope.views.indexOf(attrs.minView || 'minutes') + 1 61 | ); 62 | 63 | if (scope.views.length === 1 || scope.views.indexOf(scope.view) === -1) { 64 | scope.view = scope.views[0]; 65 | } 66 | } 67 | 68 | function getDate(name) { 69 | return datePickerUtils.getDate(scope, attrs, name); 70 | } 71 | 72 | var arrowClick = false, 73 | tz = scope.tz = attrs.timezone, 74 | createMoment = datePickerUtils.createMoment, 75 | eventIsForPicker = datePickerUtils.eventIsForPicker, 76 | step = parseInt(attrs.step || datePickerConfig.step, 10), 77 | partial = !!attrs.partial, 78 | minDate = getDate('minDate'), 79 | maxDate = getDate('maxDate'), 80 | pickerID = element[0].id, 81 | now = scope.now = createMoment(), 82 | selected = scope.date = createMoment(scope.model || now), 83 | autoclose = attrs.autoClose === 'true', 84 | // Either gets the 1st day from the attributes, or asks moment.js to give it to us as it is localized. 85 | firstDay = attrs.firstDay && attrs.firstDay >= 0 && attrs.firstDay <= 6 ? parseInt(attrs.firstDay, 10) : moment().weekday(0).day(), 86 | setDate, 87 | prepareViewData, 88 | isSame, 89 | clipDate, 90 | isNow, 91 | inValidRange; 92 | 93 | datePickerUtils.setParams(tz, firstDay); 94 | 95 | if (!scope.model) { 96 | selected.minute(Math.ceil(selected.minute() / step) * step).second(0); 97 | } 98 | 99 | scope.template = attrs.template || datePickerConfig.template; 100 | 101 | scope.watchDirectChanges = attrs.watchDirectChanges !== undefined; 102 | scope.callbackOnSetDate = attrs.dateChange ? datePickerUtils.findFunction(scope, attrs.dateChange) : undefined; 103 | 104 | prepareViews(); 105 | 106 | scope.setView = function (nextView) { 107 | if (scope.views.indexOf(nextView) !== -1) { 108 | scope.view = nextView; 109 | } 110 | }; 111 | 112 | scope.selectDate = function (date) { 113 | if (attrs.disabled) { 114 | return false; 115 | } 116 | if (isSame(scope.date, date)) { 117 | date = scope.date; 118 | } 119 | date = clipDate(date); 120 | if (!date) { 121 | return false; 122 | } 123 | scope.date = date; 124 | 125 | var nextView = scope.views[scope.views.indexOf(scope.view) + 1]; 126 | if ((!nextView || partial) || scope.model) { 127 | setDate(date); 128 | } 129 | 130 | if (nextView) { 131 | scope.setView(nextView); 132 | } else if (autoclose) { 133 | element.addClass('hidden'); 134 | scope.$emit('hidePicker'); 135 | } else { 136 | prepareViewData(); 137 | } 138 | }; 139 | 140 | setDate = function (date) { 141 | if (date) { 142 | scope.model = date; 143 | if (ngModel) { 144 | ngModel.$setViewValue(date); 145 | } 146 | } 147 | scope.$emit('setDate', scope.model, scope.view); 148 | 149 | //This is duplicated in the new functionality. 150 | if (scope.callbackOnSetDate) { 151 | scope.callbackOnSetDate(attrs.datePicker, scope.date); 152 | } 153 | }; 154 | 155 | function update() { 156 | var view = scope.view; 157 | datePickerUtils.setParams(tz, firstDay); 158 | 159 | if (scope.model && !arrowClick) { 160 | scope.date = createMoment(scope.model); 161 | arrowClick = false; 162 | } 163 | 164 | var date = scope.date; 165 | 166 | switch (view) { 167 | case 'year': 168 | scope.years = datePickerUtils.getVisibleYears(date); 169 | break; 170 | case 'month': 171 | scope.months = datePickerUtils.getVisibleMonths(date); 172 | break; 173 | case 'date': 174 | scope.weekdays = scope.weekdays || datePickerUtils.getDaysOfWeek(); 175 | scope.weeks = datePickerUtils.getVisibleWeeks(date); 176 | break; 177 | case 'hours': 178 | scope.hours = datePickerUtils.getVisibleHours(date); 179 | break; 180 | case 'minutes': 181 | scope.minutes = datePickerUtils.getVisibleMinutes(date, step); 182 | break; 183 | } 184 | 185 | prepareViewData(); 186 | } 187 | 188 | function watch() { 189 | if (scope.view !== 'date') { 190 | return scope.view; 191 | } 192 | return scope.date ? scope.date.month() : null; 193 | } 194 | 195 | scope.$watch(watch, update); 196 | 197 | if (scope.watchDirectChanges) { 198 | scope.$watch('model', function () { 199 | arrowClick = false; 200 | update(); 201 | }); 202 | } 203 | 204 | prepareViewData = function () { 205 | var view = scope.view, 206 | date = scope.date, 207 | classes = [], classList = '', 208 | i, j; 209 | 210 | datePickerUtils.setParams(tz, firstDay); 211 | 212 | if (view === 'date') { 213 | var weeks = scope.weeks, week; 214 | for (i = 0; i < weeks.length; i++) { 215 | week = weeks[i]; 216 | classes.push([]); 217 | for (j = 0; j < week.length; j++) { 218 | classList = ''; 219 | if (datePickerUtils.isSameDay(date, week[j])) { 220 | classList += 'active'; 221 | } 222 | if (isNow(week[j], view)) { 223 | classList += ' now'; 224 | } 225 | //if (week[j].month() !== date.month()) classList += ' disabled'; 226 | if (week[j].month() !== date.month() || !inValidRange(week[j])) { 227 | classList += ' disabled'; 228 | } 229 | classes[i].push(classList); 230 | } 231 | } 232 | } else { 233 | var params = datePickerConfig.viewConfig[view], 234 | dates = scope[params[0]], 235 | compareFunc = params[1]; 236 | 237 | for (i = 0; i < dates.length; i++) { 238 | classList = ''; 239 | if (datePickerUtils[compareFunc](date, dates[i])) { 240 | classList += 'active'; 241 | } 242 | if (isNow(dates[i], view)) { 243 | classList += ' now'; 244 | } 245 | if (!inValidRange(dates[i])) { 246 | classList += ' disabled'; 247 | } 248 | classes.push(classList); 249 | } 250 | } 251 | scope.classes = classes; 252 | }; 253 | 254 | scope.next = function (delta) { 255 | var date = moment(scope.date); 256 | delta = delta || 1; 257 | switch (scope.view) { 258 | case 'year': 259 | /*falls through*/ 260 | case 'month': 261 | date.year(date.year() + delta); 262 | break; 263 | case 'date': 264 | date.month(date.month() + delta); 265 | break; 266 | case 'hours': 267 | /*falls through*/ 268 | case 'minutes': 269 | date.hours(date.hours() + delta); 270 | break; 271 | } 272 | date = clipDate(date); 273 | if (date) { 274 | scope.date = date; 275 | arrowClick = true; 276 | update(); 277 | } 278 | }; 279 | 280 | inValidRange = function (date) { 281 | var valid = true; 282 | if (minDate && minDate.isAfter(date)) { 283 | valid = isSame(minDate, date); 284 | } 285 | if (maxDate && maxDate.isBefore(date)) { 286 | valid &= isSame(maxDate, date); 287 | } 288 | return valid; 289 | }; 290 | 291 | isSame = function (date1, date2) { 292 | return date1.isSame(date2, datePickerConfig.momentNames[scope.view]) ? true : false; 293 | }; 294 | 295 | clipDate = function (date) { 296 | if (minDate && minDate.isAfter(date)) { 297 | return minDate; 298 | } else if (maxDate && maxDate.isBefore(date)) { 299 | return maxDate; 300 | } else { 301 | return date; 302 | } 303 | }; 304 | 305 | isNow = function (date, view) { 306 | var is = true; 307 | 308 | switch (view) { 309 | case 'minutes': 310 | is &= ~~(now.minutes() / step) === ~~(date.minutes() / step); 311 | /* falls through */ 312 | case 'hours': 313 | is &= now.hours() === date.hours(); 314 | /* falls through */ 315 | case 'date': 316 | is &= now.date() === date.date(); 317 | /* falls through */ 318 | case 'month': 319 | is &= now.month() === date.month(); 320 | /* falls through */ 321 | case 'year': 322 | is &= now.year() === date.year(); 323 | } 324 | return is; 325 | }; 326 | 327 | scope.prev = function (delta) { 328 | return scope.next(-delta || -1); 329 | }; 330 | 331 | if (pickerID) { 332 | scope.$on('pickerUpdate', function (event, pickerIDs, data) { 333 | if (eventIsForPicker(pickerIDs, pickerID)) { 334 | var updateViews = false, updateViewData = false; 335 | 336 | if (angular.isDefined(data.minDate)) { 337 | minDate = data.minDate ? data.minDate : false; 338 | updateViewData = true; 339 | } 340 | if (angular.isDefined(data.maxDate)) { 341 | maxDate = data.maxDate ? data.maxDate : false; 342 | updateViewData = true; 343 | } 344 | 345 | if (angular.isDefined(data.minView)) { 346 | attrs.minView = data.minView; 347 | updateViews = true; 348 | } 349 | if (angular.isDefined(data.maxView)) { 350 | attrs.maxView = data.maxView; 351 | updateViews = true; 352 | } 353 | attrs.view = data.view || attrs.view; 354 | 355 | if (updateViews) { 356 | prepareViews(); 357 | } 358 | 359 | if (updateViewData) { 360 | update(); 361 | } 362 | } 363 | }); 364 | } 365 | } 366 | }; 367 | }]); 368 | //})); 369 | 370 | //(function (global, factory) { 371 | // 'use strict'; 372 | // var fnc; 373 | // fnc = (typeof exports === 'object' && typeof module !== 'undefined') ? module.exports = factory(require('angular'), require('moment')) : 374 | // (typeof define === 'function' && define.amd) ? define(['angular', 'moment'], factory) : 375 | // factory(global.angular, global.moment); 376 | //}(this, function (angular, moment) { 377 | angular.module('datePicker').factory('datePickerUtils', function () { 378 | var tz, firstDay; 379 | var createNewDate = function (year, month, day, hour, minute) { 380 | var utc = Date.UTC(year | 0, month | 0, day | 0, hour | 0, minute | 0); 381 | return tz ? moment.tz(utc, tz) : moment(utc); 382 | }; 383 | 384 | return { 385 | getVisibleMinutes: function (m, step) { 386 | var year = m.year(), 387 | month = m.month(), 388 | day = m.date(), 389 | hour = m.hours(), pushedDate, 390 | offset = m.utcOffset() / 60, 391 | minutes = [], minute; 392 | 393 | for (minute = 0; minute < 60; minute += step) { 394 | pushedDate = createNewDate(year, month, day, hour - offset, minute); 395 | minutes.push(pushedDate); 396 | } 397 | return minutes; 398 | }, 399 | getVisibleWeeks: function (m) { 400 | m = moment(m); 401 | var startYear = m.year(), 402 | startMonth = m.month(); 403 | 404 | //Set date to the first day of the month 405 | m.date(1); 406 | 407 | //Grab day of the week 408 | var day = m.day(); 409 | 410 | //Go back the required number of days to arrive at the previous week start 411 | m.date(firstDay - (day + (firstDay >= day ? 6 : -1))); 412 | 413 | var weeks = []; 414 | 415 | while (weeks.length < 6) { 416 | if (m.year() === startYear && m.month() > startMonth) { 417 | break; 418 | } 419 | weeks.push(this.getDaysOfWeek(m)); 420 | m.add(7, 'd'); 421 | } 422 | return weeks; 423 | }, 424 | getVisibleYears: function (d) { 425 | var m = moment(d), 426 | year = m.year(); 427 | 428 | m.year(year - (year % 10)); 429 | year = m.year(); 430 | 431 | var offset = m.utcOffset() / 60, 432 | years = [], 433 | pushedDate, 434 | actualOffset; 435 | 436 | for (var i = 0; i < 12; i++) { 437 | pushedDate = createNewDate(year, 0, 1, 0 - offset); 438 | actualOffset = pushedDate.utcOffset() / 60; 439 | if (actualOffset !== offset) { 440 | pushedDate = createNewDate(year, 0, 1, 0 - actualOffset); 441 | offset = actualOffset; 442 | } 443 | years.push(pushedDate); 444 | year++; 445 | } 446 | return years; 447 | }, 448 | getDaysOfWeek: function (m) { 449 | m = m ? m : (tz ? moment.tz(tz).day(firstDay) : moment().day(firstDay)); 450 | 451 | var year = m.year(), 452 | month = m.month(), 453 | day = m.date(), 454 | days = [], 455 | pushedDate, 456 | offset = m.utcOffset() / 60, 457 | actualOffset; 458 | 459 | for (var i = 0; i < 7; i++) { 460 | pushedDate = createNewDate(year, month, day, 0 - offset, 0, false); 461 | actualOffset = pushedDate.utcOffset() / 60; 462 | if (actualOffset !== offset) { 463 | pushedDate = createNewDate(year, month, day, 0 - actualOffset, 0, false); 464 | } 465 | days.push(pushedDate); 466 | day++; 467 | } 468 | return days; 469 | }, 470 | getVisibleMonths: function (m) { 471 | var year = m.year(), 472 | offset = m.utcOffset() / 60, 473 | months = [], 474 | pushedDate, 475 | actualOffset; 476 | 477 | for (var month = 0; month < 12; month++) { 478 | pushedDate = createNewDate(year, month, 1, 0 - offset, 0, false); 479 | actualOffset = pushedDate.utcOffset() / 60; 480 | if (actualOffset !== offset) { 481 | pushedDate = createNewDate(year, month, 1, 0 - actualOffset, 0, false); 482 | } 483 | months.push(pushedDate); 484 | } 485 | return months; 486 | }, 487 | getVisibleHours: function (m) { 488 | var year = m.year(), 489 | month = m.month(), 490 | day = m.date(), 491 | hours = [], 492 | hour, pushedDate, actualOffset, 493 | offset = m.utcOffset() / 60; 494 | 495 | for (hour = 0; hour < 24; hour++) { 496 | pushedDate = createNewDate(year, month, day, hour - offset, 0, false); 497 | actualOffset = pushedDate.utcOffset() / 60; 498 | if (actualOffset !== offset) { 499 | pushedDate = createNewDate(year, month, day, hour - actualOffset, 0, false); 500 | } 501 | hours.push(pushedDate); 502 | } 503 | 504 | return hours; 505 | }, 506 | isAfter: function (model, date) { 507 | return model && model.unix() >= date.unix(); 508 | }, 509 | isBefore: function (model, date) { 510 | return model.unix() <= date.unix(); 511 | }, 512 | isSameYear: function (model, date) { 513 | return model && model.year() === date.year(); 514 | }, 515 | isSameMonth: function (model, date) { 516 | return this.isSameYear(model, date) && model.month() === date.month(); 517 | }, 518 | isSameDay: function (model, date) { 519 | return this.isSameMonth(model, date) && model.date() === date.date(); 520 | }, 521 | isSameHour: function (model, date) { 522 | return this.isSameDay(model, date) && model.hours() === date.hours(); 523 | }, 524 | isSameMinutes: function (model, date) { 525 | return this.isSameHour(model, date) && model.minutes() === date.minutes(); 526 | }, 527 | setParams: function (zone, fd) { 528 | tz = zone; 529 | firstDay = fd; 530 | }, 531 | scopeSearch: function (scope, name, comparisonFn) { 532 | var parentScope = scope, 533 | nameArray = name.split('.'), 534 | target, i, j = nameArray.length; 535 | 536 | do { 537 | target = parentScope = parentScope.$parent; 538 | 539 | //Loop through provided names. 540 | for (i = 0; i < j; i++) { 541 | target = target[nameArray[i]]; 542 | if (!target) { 543 | continue; 544 | } 545 | } 546 | 547 | //If we reached the end of the list for this scope, 548 | //and something was found, trigger the comparison 549 | //function. If the comparison function is happy, return 550 | //found result. Otherwise, continue to the next parent scope 551 | if (target && comparisonFn(target)) { 552 | return target; 553 | } 554 | 555 | } while (parentScope.$parent); 556 | 557 | return false; 558 | }, 559 | findFunction: function (scope, name) { 560 | //Search scope ancestors for a matching function. 561 | return this.scopeSearch(scope, name, function (target) { 562 | //Property must also be a function 563 | return angular.isFunction(target); 564 | }); 565 | }, 566 | findParam: function (scope, name) { 567 | //Search scope ancestors for a matching parameter. 568 | return this.scopeSearch(scope, name, function () { 569 | //As long as the property exists, we're good 570 | return true; 571 | }); 572 | }, 573 | createMoment: function (m) { 574 | if (tz) { 575 | return moment.tz(m, tz); 576 | } else { 577 | //If input is a moment, and we have no TZ info, we need to remove TZ 578 | //info from the moment, otherwise the newly created moment will take 579 | //the timezone of the input moment. The easiest way to do that is to 580 | //take the unix timestamp, and use that to create a new moment. 581 | //The new moment will use the local timezone of the user machine. 582 | return moment.isMoment(m) ? moment.unix(m.unix()) : moment(m); 583 | } 584 | }, 585 | getDate: function (scope, attrs, name) { 586 | var result = false; 587 | if (attrs[name]) { 588 | result = this.createMoment(attrs[name]); 589 | if (!result.isValid()) { 590 | result = this.findParam(scope, attrs[name]); 591 | if (result) { 592 | result = this.createMoment(result); 593 | } 594 | } 595 | } 596 | 597 | return result; 598 | }, 599 | eventIsForPicker: function (targetIDs, pickerID) { 600 | //Checks if an event targeted at a specific picker, via either a string name, or an array of strings. 601 | return (angular.isArray(targetIDs) && targetIDs.indexOf(pickerID) > -1 || targetIDs === pickerID); 602 | } 603 | }; 604 | }); 605 | //})); 606 | 607 | //(function (global, factory) { 608 | // 'use strict'; 609 | // var fnc; 610 | // fnc = (typeof exports === 'object' && typeof module !== 'undefined') ? module.exports = factory(require('angular'), require('moment')) : 611 | // (typeof define === 'function' && define.amd) ? define(['angular', 'moment'], factory) : 612 | // factory(global.angular, global.moment); 613 | //}(this, function (angular, moment) { 614 | var Module = angular.module('datePicker'); 615 | 616 | Module.directive('dateRange', ['$compile', 'datePickerUtils', 'dateTimeConfig', function ($compile, datePickerUtils, dateTimeConfig) { 617 | function getTemplate(attrs, id, model, min, max) { 618 | return dateTimeConfig.template(angular.extend(attrs, { 619 | ngModel: model, 620 | minDate: min && moment.isMoment(min) ? min.format() : false, 621 | maxDate: max && moment.isMoment(max) ? max.format() : false 622 | }), id); 623 | } 624 | 625 | function randomName() { 626 | return 'picker' + Math.random().toString().substr(2); 627 | } 628 | 629 | return { 630 | scope: { 631 | start: '=', 632 | end: '=' 633 | }, 634 | link: function (scope, element, attrs) { 635 | var dateChange = null, 636 | pickerRangeID = element[0].id, 637 | pickerIDs = [randomName(), randomName()], 638 | createMoment = datePickerUtils.createMoment, 639 | eventIsForPicker = datePickerUtils.eventIsForPicker; 640 | 641 | scope.dateChange = function (modelName, newDate) { 642 | //Notify user if callback exists. 643 | if (dateChange) { 644 | dateChange(modelName, newDate); 645 | } 646 | }; 647 | 648 | function setMax(date) { 649 | scope.$broadcast('pickerUpdate', pickerIDs[0], { 650 | maxDate: date 651 | }); 652 | } 653 | 654 | function setMin(date) { 655 | scope.$broadcast('pickerUpdate', pickerIDs[1], { 656 | minDate: date 657 | }); 658 | } 659 | 660 | if (pickerRangeID) { 661 | scope.$on('pickerUpdate', function (event, targetIDs, data) { 662 | if (eventIsForPicker(targetIDs, pickerRangeID)) { 663 | //If we received an update event, dispatch it to the inner pickers using their IDs. 664 | scope.$broadcast('pickerUpdate', pickerIDs, data); 665 | } 666 | }); 667 | } 668 | 669 | datePickerUtils.setParams(attrs.timezone); 670 | 671 | scope.start = createMoment(scope.start); 672 | scope.end = createMoment(scope.end); 673 | 674 | scope.$watchGroup(['start', 'end'], function (dates) { 675 | //Scope data changed, update picker min/max 676 | setMin(dates[0]); 677 | setMax(dates[1]); 678 | }); 679 | 680 | if (angular.isDefined(attrs.dateChange)) { 681 | dateChange = datePickerUtils.findFunction(scope, attrs.dateChange); 682 | } 683 | 684 | attrs.onSetDate = 'dateChange'; 685 | 686 | var template = '
' + 687 | getTemplate(attrs, pickerIDs[0], 'start', false, scope.end) + 688 | '' + 689 | getTemplate(attrs, pickerIDs[1], 'end', scope.start, false) + 690 | '
'; 691 | 692 | var picker = $compile(template)(scope); 693 | element.append(picker); 694 | } 695 | }; 696 | }]); 697 | //})); 698 | 699 | //(function (global, factory) { 700 | // 'use strict'; 701 | // var fnc; 702 | // fnc = (typeof exports === 'object' && typeof module !== 'undefined') ? module.exports = factory(require('angular'), require('moment')) : 703 | // (typeof define === 'function' && define.amd) ? define(['angular', 'moment'], factory) : 704 | // factory(global.angular, global.moment); 705 | //}(this, function (angular, moment) { 706 | var PRISTINE_CLASS = 'ng-pristine', 707 | DIRTY_CLASS = 'ng-dirty'; 708 | 709 | var Module = angular.module('datePicker'); 710 | 711 | Module.constant('dateTimeConfig', { 712 | template: function (attrs, id) { 713 | return '' + 714 | '
'; 731 | }, 732 | format: 'YYYY-MM-DD HH:mm', 733 | views: ['date', 'year', 'month', 'hours', 'minutes'], 734 | autoClose: false, 735 | position: 'relative' 736 | }); 737 | 738 | Module.directive('dateTimeAppend', function () { 739 | return { 740 | link: function (scope, element) { 741 | element.bind('click', function () { 742 | element.find('input')[0].focus(); 743 | }); 744 | } 745 | }; 746 | }); 747 | 748 | Module.directive('dateTime', ['$compile', '$document', '$filter', 'dateTimeConfig', '$parse', 'datePickerUtils', function ($compile, $document, $filter, dateTimeConfig, $parse, datePickerUtils) { 749 | var body = $document.find('body'); 750 | var dateFilter = $filter('mFormat'); 751 | 752 | return { 753 | require: 'ngModel', 754 | scope: true, 755 | link: function (scope, element, attrs, ngModel) { 756 | var format = attrs.format || dateTimeConfig.format, 757 | parentForm = element.inheritedData('$formController'), 758 | views = $parse(attrs.views)(scope) || dateTimeConfig.views.concat(), 759 | view = attrs.view || views[0], 760 | index = views.indexOf(view), 761 | dismiss = attrs.autoClose ? $parse(attrs.autoClose)(scope) : dateTimeConfig.autoClose, 762 | picker = null, 763 | pickerID = element[0].id, 764 | position = attrs.position || dateTimeConfig.position, 765 | container = null, 766 | minDate = null, 767 | minValid = null, 768 | maxDate = null, 769 | maxValid = null, 770 | timezone = attrs.timezone || false, 771 | eventIsForPicker = datePickerUtils.eventIsForPicker, 772 | dateChange = null, 773 | shownOnce = false, 774 | template; 775 | 776 | if (index === -1) { 777 | views.splice(index, 1); 778 | } 779 | 780 | views.unshift(view); 781 | 782 | function formatter(value) { 783 | return dateFilter(value, format, timezone); 784 | } 785 | 786 | function parser(viewValue) { 787 | if (viewValue.length === format.length) { 788 | return viewValue; 789 | } 790 | return (viewValue.length === 0) ? viewValue : undefined; 791 | } 792 | 793 | function setMin(date) { 794 | minDate = date; 795 | attrs.minDate = date ? date.format() : date; 796 | minValid = moment.isMoment(date); 797 | } 798 | 799 | function setMax(date) { 800 | maxDate = date; 801 | attrs.maxDate = date ? date.format() : date; 802 | maxValid = moment.isMoment(date); 803 | } 804 | 805 | ngModel.$formatters.push(formatter); 806 | ngModel.$parsers.unshift(parser); 807 | 808 | if (angular.isDefined(attrs.minDate)) { 809 | setMin(datePickerUtils.findParam(scope, attrs.minDate)); 810 | 811 | ngModel.$validators.min = function (value) { 812 | //If we don't have a min / max value, then any value is valid. 813 | return minValid ? moment.isMoment(value) && (minDate.isSame(value) || minDate.isBefore(value)) : true; 814 | }; 815 | } 816 | 817 | if (angular.isDefined(attrs.maxDate)) { 818 | setMax(datePickerUtils.findParam(scope, attrs.maxDate)); 819 | 820 | ngModel.$validators.max = function (value) { 821 | return maxValid ? moment.isMoment(value) && (maxDate.isSame(value) || maxDate.isAfter(value)) : true; 822 | }; 823 | } 824 | 825 | if (angular.isDefined(attrs.dateChange)) { 826 | dateChange = datePickerUtils.findFunction(scope, attrs.dateChange); 827 | } 828 | 829 | function getTemplate() { 830 | template = dateTimeConfig.template(attrs); 831 | } 832 | 833 | 834 | function updateInput(event) { 835 | event.stopPropagation(); 836 | if (ngModel.$pristine) { 837 | ngModel.$dirty = true; 838 | ngModel.$pristine = false; 839 | element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS); 840 | if (parentForm) { 841 | parentForm.$setDirty(); 842 | } 843 | ngModel.$render(); 844 | } 845 | } 846 | 847 | function clear() { 848 | if (picker) { 849 | picker.remove(); 850 | picker = null; 851 | } 852 | if (container) { 853 | container.remove(); 854 | container = null; 855 | } 856 | } 857 | 858 | if (pickerID) { 859 | scope.$on('pickerUpdate', function (event, pickerIDs, data) { 860 | if (eventIsForPicker(pickerIDs, pickerID)) { 861 | if (picker) { 862 | //Need to handle situation where the data changed but the picker is currently open. 863 | //To handle this, we can create the inner picker with a random ID, then forward 864 | //any events received to it. 865 | } else { 866 | var validateRequired = false; 867 | if (angular.isDefined(data.minDate)) { 868 | setMin(data.minDate); 869 | validateRequired = true; 870 | } 871 | if (angular.isDefined(data.maxDate)) { 872 | setMax(data.maxDate); 873 | validateRequired = true; 874 | } 875 | 876 | if (angular.isDefined(data.minView)) { 877 | attrs.minView = data.minView; 878 | } 879 | if (angular.isDefined(data.maxView)) { 880 | attrs.maxView = data.maxView; 881 | } 882 | attrs.view = data.view || attrs.view; 883 | 884 | if (validateRequired) { 885 | ngModel.$validate(); 886 | } 887 | if (angular.isDefined(data.format)) { 888 | format = attrs.format = data.format || dateTimeConfig.format; 889 | ngModel.$modelValue = -1; //Triggers formatters. This value will be discarded. 890 | } 891 | getTemplate(); 892 | } 893 | } 894 | }); 895 | } 896 | 897 | function showPicker() { 898 | if (picker) { 899 | return; 900 | } 901 | // create picker element 902 | picker = $compile(template)(scope); 903 | scope.$digest(); 904 | 905 | //If the picker has already been shown before then we shouldn't be binding to events, as these events are already bound to in this scope. 906 | if (!shownOnce) { 907 | scope.$on('setDate', function (event, date, view) { 908 | updateInput(event); 909 | if (dateChange) { 910 | dateChange(attrs.ngModel, date); 911 | } 912 | if (dismiss && views[views.length - 1] === view) { 913 | clear(); 914 | } 915 | }); 916 | 917 | scope.$on('hidePicker', function () { 918 | element[0].blur(); 919 | }); 920 | 921 | scope.$on('$destroy', clear); 922 | 923 | shownOnce = true; 924 | } 925 | 926 | 927 | // move picker below input element 928 | 929 | if (position === 'absolute') { 930 | var pos = element[0].getBoundingClientRect(); 931 | // Support IE8 932 | var height = pos.height || element[0].offsetHeight; 933 | picker.css({top: (pos.top + height) + 'px', left: pos.left + 'px', display: 'block', position: position}); 934 | body.append(picker); 935 | } else { 936 | // relative 937 | container = angular.element('
'); 938 | element[0].parentElement.insertBefore(container[0], element[0]); 939 | container.append(picker); 940 | // this approach doesn't work 941 | // element.before(picker); 942 | picker.css({top: element[0].offsetHeight + 'px', display: 'block'}); 943 | } 944 | picker.bind('mousedown', function (evt) { 945 | evt.preventDefault(); 946 | }); 947 | } 948 | 949 | element.bind('focus', showPicker); 950 | element.bind('click', showPicker); 951 | element.bind('blur', clear); 952 | getTemplate(); 953 | } 954 | }; 955 | }]); 956 | //})); 957 | 958 | angular.module('datePicker').run(['$templateCache', function($templateCache) { 959 | $templateCache.put('templates/datepicker.html', 960 | "
\r" + 961 | "\n" + 962 | "
\r" + 963 | "\n" + 964 | " \r" + 965 | "\n" + 966 | " \r" + 967 | "\n" + 968 | " \r" + 969 | "\n" + 970 | " \r" + 971 | "\n" + 972 | " \r" + 973 | "\n" + 974 | " \r" + 975 | "\n" + 976 | " \r" + 977 | "\n" + 978 | " \r" + 979 | "\n" + 980 | " \r" + 981 | "\n" + 982 | " \r" + 983 | "\n" + 984 | " \r" + 985 | "\n" + 986 | " \r" + 987 | "\n" + 988 | " \r" + 989 | "\n" + 990 | " \r" + 999 | "\n" + 1000 | " \r" + 1001 | "\n" + 1002 | " \r" + 1003 | "\n" + 1004 | "
\r" + 991 | "\n" + 992 | " \r" + 997 | "\n" + 998 | "
\r" + 1005 | "\n" + 1006 | "
\r" + 1007 | "\n" + 1008 | "
\r" + 1009 | "\n" + 1010 | " \r" + 1011 | "\n" + 1012 | " \r" + 1013 | "\n" + 1014 | " \r" + 1015 | "\n" + 1016 | " \r" + 1017 | "\n" + 1018 | " \r" + 1019 | "\n" + 1020 | " \r" + 1021 | "\n" + 1022 | " \r" + 1023 | "\n" + 1024 | " \r" + 1025 | "\n" + 1026 | " \r" + 1027 | "\n" + 1028 | " \r" + 1029 | "\n" + 1030 | " \r" + 1039 | "\n" + 1040 | " \r" + 1041 | "\n" + 1042 | " \r" + 1043 | "\n" + 1044 | "
\r" + 1031 | "\n" + 1032 | " \r" + 1037 | "\n" + 1038 | "
\r" + 1045 | "\n" + 1046 | "
\r" + 1047 | "\n" + 1048 | "
\r" + 1049 | "\n" + 1050 | " \r" + 1051 | "\n" + 1052 | " \r" + 1053 | "\n" + 1054 | " \r" + 1055 | "\n" + 1056 | " \r" + 1057 | "\n" + 1058 | " \r" + 1059 | "\n" + 1060 | " \r" + 1061 | "\n" + 1062 | " \r" + 1063 | "\n" + 1064 | " \r" + 1065 | "\n" + 1066 | " \r" + 1067 | "\n" + 1068 | " \r" + 1069 | "\n" + 1070 | " \r" + 1081 | "\n" + 1082 | " \r" + 1083 | "\n" + 1084 | " \r" + 1085 | "\n" + 1086 | "
\r" + 1071 | "\n" + 1072 | " \r" + 1079 | "\n" + 1080 | "
\r" + 1087 | "\n" + 1088 | "
\r" + 1089 | "\n" + 1090 | "
\r" + 1091 | "\n" + 1092 | " \r" + 1093 | "\n" + 1094 | " \r" + 1095 | "\n" + 1096 | " \r" + 1097 | "\n" + 1098 | " \r" + 1099 | "\n" + 1100 | " \r" + 1101 | "\n" + 1102 | " \r" + 1103 | "\n" + 1104 | " \r" + 1105 | "\n" + 1106 | " \r" + 1107 | "\n" + 1108 | " \r" + 1109 | "\n" + 1110 | " \r" + 1111 | "\n" + 1112 | " \r" + 1121 | "\n" + 1122 | " \r" + 1123 | "\n" + 1124 | " \r" + 1125 | "\n" + 1126 | "
\r" + 1113 | "\n" + 1114 | " \r" + 1119 | "\n" + 1120 | "
\r" + 1127 | "\n" + 1128 | "
\r" + 1129 | "\n" + 1130 | "
\r" + 1131 | "\n" + 1132 | " \r" + 1133 | "\n" + 1134 | " \r" + 1135 | "\n" + 1136 | " \r" + 1137 | "\n" + 1138 | " \r" + 1139 | "\n" + 1140 | " \r" + 1141 | "\n" + 1142 | " \r" + 1143 | "\n" + 1144 | " \r" + 1145 | "\n" + 1146 | " \r" + 1147 | "\n" + 1148 | " \r" + 1149 | "\n" + 1150 | " \r" + 1151 | "\n" + 1152 | " \r" + 1163 | "\n" + 1164 | " \r" + 1165 | "\n" + 1166 | " \r" + 1167 | "\n" + 1168 | "
\r" + 1153 | "\n" + 1154 | " \r" + 1161 | "\n" + 1162 | "
\r" + 1169 | "\n" + 1170 | "
\r" + 1171 | "\n" + 1172 | "
" 1173 | ); 1174 | 1175 | }]); 1176 | })); 1177 | -------------------------------------------------------------------------------- /dist/angular-datepicker.min.css: -------------------------------------------------------------------------------- 1 | .clearfix:after,.clearfix:before{display:table;content:"";line-height:0}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}[date-picker],[date-picker] td,[date-picker] th{-webkit-border-radius:4px;-moz-border-radius:4px}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.date-picker-date-time{position:absolute}.date-range .date-picker-date-time{position:inherit}[date-picker-wrapper]{position:absolute;min-width:220px;z-index:10;display:block;font-size:14px}[date-time-append] [date-picker-wrapper] [date-picker]{margin-top:-30px}[date-time-append] [date-picker]{position:relative;margin-right:-1000px;margin-bottom:-1000px}[date-range] [date-picker] .after.before{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,.25);background-color:#499dcd;background-image:-moz-linear-gradient(top,#5bc0de,#2f6ab4);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#2f6ab4));background-image:-webkit-linear-gradient(top,#5bc0de,#2f6ab4);background-image:-o-linear-gradient(top,#5bc0de,#2f6ab4);background-image:linear-gradient(to bottom,#5bc0de,#2f6ab4);background-repeat:repeat-x;border-color:#2f6ab4 #2f6ab4 #1f4677;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}[date-range] [date-picker] .after.before.active,[date-range] [date-picker] .after.before.disabled,[date-range] [date-picker] .after.before:active,[date-range] [date-picker] .after.before:hover,[date-range] [date-picker] .after.before[disabled]{color:#fff;background-color:#2f6ab4}[date-range] [date-picker] .after.before.active,[date-range] [date-picker] .after.before:active{background-color:#24528c\9}[date-picker].hidden{display:none}[date-picker]{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none;border-radius:4px;background-color:#fff;padding:4px}[date-picker] table{margin:0}[date-picker] td,[date-picker] th{padding:4px 5px;text-align:center;width:20px;height:20px;border-radius:4px;border:none}[date-picker] .switch{width:145px}[date-picker] span{display:block;width:23%;height:26px;line-height:25px;float:left;margin:1%;cursor:pointer;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}[date-picker] span:hover{background:#eee}[date-picker] span.disabled,[date-picker] span.disabled:hover{background:0 0;color:#999;cursor:default}[date-picker] .active,[date-picker] .now{text-shadow:0 -1px 0 rgba(0,0,0,.25);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-color:#006dcc;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);border-color:#04c #04c #002a80;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);color:#fff}[date-picker] .active.active,[date-picker] .active.disabled,[date-picker] .active:active,[date-picker] .active:hover,[date-picker] .active[disabled],[date-picker] .now.active,[date-picker] .now.disabled,[date-picker] .now:active,[date-picker] .now:hover,[date-picker] .now[disabled]{color:#fff;background-color:#04c}[date-picker] .active.active,[date-picker] .active:active,[date-picker] .now.active,[date-picker] .now:active{background-color:#039\9}[date-picker] .now{color:#fff;background-color:#ee735b;background-image:-moz-linear-gradient(top,#ee5f5b,#ee905b);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#ee905b));background-image:-webkit-linear-gradient(top,#ee5f5b,#ee905b);background-image:-o-linear-gradient(top,#ee5f5b,#ee905b);background-image:linear-gradient(to bottom,#ee5f5b,#ee905b);border-color:#ee905b #ee905b #e56218;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25)}[date-picker] .now.active,[date-picker] .now.disabled,[date-picker] .now:active,[date-picker] .now:hover,[date-picker] .now[disabled]{color:#fff;background-color:#ee905b}[date-picker] .now.active,[date-picker] .now:active{background-color:#e9712d\9}[date-picker] .disabled{background:0 0;color:#999!important;cursor:default}[date-picker] [ng-switch-when=year] span,[date-picker] [ng-switch-when=month] span,[date-picker] [ng-switch-when=minutes] span{height:54px;line-height:54px}[date-picker] [ng-switch-when=date] td{padding:0}[date-picker] [ng-switch-when=date] span{width:100%;height:26px;line-height:26px}[date-picker] [ng-switch-when=date] td span:hover,[date-picker] th:hover{background:#eee;cursor:pointer} -------------------------------------------------------------------------------- /dist/angular-datepicker.min.js: -------------------------------------------------------------------------------- 1 | !function(a,b){"use strict";var c;c="object"==typeof exports&&"undefined"!=typeof module?module.exports=b(require("angular"),require("moment")):"function"==typeof define&&define.amd?define(["angular","moment"],b):b(a.angular,a.moment)}(this,function(a,b){var c=a.module("datePicker",[]);c.constant("datePickerConfig",{template:"templates/datepicker.html",view:"month",views:["year","month","date","hours","minutes"],momentNames:{year:"year",month:"month",date:"day",hours:"hours",minutes:"minutes"},viewConfig:{year:["years","isSameYear"],month:["months","isSameMonth"],hours:["hours","isSameHour"],minutes:["minutes","isSameMinutes"]},step:5}),c.filter("mFormat",function(){return function(a,c,d){return b.isMoment(a)?d?b.tz(a,d).format(c):a.format(c):b(a).format(c)}}),c.directive("datePicker",["datePickerConfig","datePickerUtils",function(c,d){return{require:"?ngModel",template:'
',scope:{model:"=datePicker",after:"=?",before:"=?"},link:function(e,f,g,h){function i(){e.views=c.views.concat(),e.view=g.view||c.view,e.views=e.views.slice(e.views.indexOf(g.maxView||"year"),e.views.indexOf(g.minView||"minutes")+1),(1===e.views.length||-1===e.views.indexOf(e.view))&&(e.view=e.views[0])}function j(a){return d.getDate(e,g,a)}function k(){var a=e.view;d.setParams(t,E),e.model&&!s&&(e.date=u(e.model),s=!1);var b=e.date;switch(a){case"year":e.years=d.getVisibleYears(b);break;case"month":e.months=d.getVisibleMonths(b);break;case"date":e.weekdays=e.weekdays||d.getDaysOfWeek(),e.weeks=d.getVisibleWeeks(b);break;case"hours":e.hours=d.getVisibleHours(b);break;case"minutes":e.minutes=d.getVisibleMinutes(b,w)}n()}function l(){return"date"!==e.view?e.view:e.date?e.date.month():null}var m,n,o,p,q,r,s=!1,t=e.tz=g.timezone,u=d.createMoment,v=d.eventIsForPicker,w=parseInt(g.step||c.step,10),x=!!g.partial,y=j("minDate"),z=j("maxDate"),A=f[0].id,B=e.now=u(),C=e.date=u(e.model||B),D="true"===g.autoClose,E=g.firstDay&&g.firstDay>=0&&g.firstDay<=6?parseInt(g.firstDay,10):b().weekday(0).day();d.setParams(t,E),e.model||C.minute(Math.ceil(C.minute()/w)*w).second(0),e.template=g.template||c.template,e.watchDirectChanges=void 0!==g.watchDirectChanges,e.callbackOnSetDate=g.dateChange?d.findFunction(e,g.dateChange):void 0,i(),e.setView=function(a){-1!==e.views.indexOf(a)&&(e.view=a)},e.selectDate=function(a){if(g.disabled)return!1;if(o(e.date,a)&&(a=e.date),a=p(a),!a)return!1;e.date=a;var b=e.views[e.views.indexOf(e.view)+1];(!b||x||e.model)&&m(a),b?e.setView(b):D?(f.addClass("hidden"),e.$emit("hidePicker")):n()},m=function(a){a&&(e.model=a,h&&h.$setViewValue(a)),e.$emit("setDate",e.model,e.view),e.callbackOnSetDate&&e.callbackOnSetDate(g.datePicker,e.date)},e.$watch(l,k),e.watchDirectChanges&&e.$watch("model",function(){s=!1,k()}),n=function(){var a,b,f=e.view,g=e.date,h=[],i="";if(d.setParams(t,E),"date"===f){var j,k=e.weeks;for(a=0;ad;d+=b)c=e(f,g,h,i-j,d),k.push(c);return k},getVisibleWeeks:function(a){a=b(a);var c=a.year(),e=a.month();a.date(1);var f=a.day();a.date(d-(f+(d>=f?6:-1)));for(var g=[];g.length<6&&!(a.year()===c&&a.month()>e);)g.push(this.getDaysOfWeek(a)),a.add(7,"d");return g},getVisibleYears:function(a){var c=b(a),d=c.year();c.year(d-d%10),d=c.year();for(var f,g,h=c.utcOffset()/60,i=[],j=0;12>j;j++)f=e(d,0,1,0-h),g=f.utcOffset()/60,g!==h&&(f=e(d,0,1,0-g),h=g),i.push(f),d++;return i},getDaysOfWeek:function(a){a=a?a:c?b.tz(c).day(d):b().day(d);for(var f,g,h=a.year(),i=a.month(),j=a.date(),k=[],l=a.utcOffset()/60,m=0;7>m;m++)f=e(h,i,j,0-l,0,!1),g=f.utcOffset()/60,g!==l&&(f=e(h,i,j,0-g,0,!1)),k.push(f),j++;return k},getVisibleMonths:function(a){for(var b,c,d=a.year(),f=a.utcOffset()/60,g=[],h=0;12>h;h++)b=e(d,h,1,0-f,0,!1),c=b.utcOffset()/60,c!==f&&(b=e(d,h,1,0-c,0,!1)),g.push(b);return g},getVisibleHours:function(a){var b,c,d,f=a.year(),g=a.month(),h=a.date(),i=[],j=a.utcOffset()/60;for(b=0;24>b;b++)c=e(f,g,h,b-j,0,!1),d=c.utcOffset()/60,d!==j&&(c=e(f,g,h,b-d,0,!1)),i.push(c);return i},isAfter:function(a,b){return a&&a.unix()>=b.unix()},isBefore:function(a,b){return a.unix()<=b.unix()},isSameYear:function(a,b){return a&&a.year()===b.year()},isSameMonth:function(a,b){return this.isSameYear(a,b)&&a.month()===b.month()},isSameDay:function(a,b){return this.isSameMonth(a,b)&&a.date()===b.date()},isSameHour:function(a,b){return this.isSameDay(a,b)&&a.hours()===b.hours()},isSameMinutes:function(a,b){return this.isSameHour(a,b)&&a.minutes()===b.minutes()},setParams:function(a,b){c=a,d=b},scopeSearch:function(a,b,c){var d,e,f=a,g=b.split("."),h=g.length;do{for(d=f=f.$parent,e=0;h>e;e++){d=d[g[e]]}if(d&&c(d))return d}while(f.$parent);return!1},findFunction:function(b,c){return this.scopeSearch(b,c,function(b){return a.isFunction(b)})},findParam:function(a,b){return this.scopeSearch(a,b,function(){return!0})},createMoment:function(a){return c?b.tz(a,c):b.isMoment(a)?b.unix(a.unix()):b(a)},getDate:function(a,b,c){var d=!1;return b[c]&&(d=this.createMoment(b[c]),d.isValid()||(d=this.findParam(a,b[c]),d&&(d=this.createMoment(d)))),d},eventIsForPicker:function(b,c){return a.isArray(b)&&b.indexOf(c)>-1||b===c}}});var c=a.module("datePicker");c.directive("dateRange",["$compile","datePickerUtils","dateTimeConfig",function(c,d,e){function f(c,d,f,g,h){return e.template(a.extend(c,{ngModel:f,minDate:g&&b.isMoment(g)?g.format():!1,maxDate:h&&b.isMoment(h)?h.format():!1}),d)}function g(){return"picker"+Math.random().toString().substr(2)}return{scope:{start:"=",end:"="},link:function(b,e,h){function i(a){b.$broadcast("pickerUpdate",m[0],{maxDate:a})}function j(a){b.$broadcast("pickerUpdate",m[1],{minDate:a})}var k=null,l=e[0].id,m=[g(),g()],n=d.createMoment,o=d.eventIsForPicker;b.dateChange=function(a,b){k&&k(a,b)},l&&b.$on("pickerUpdate",function(a,c,d){o(c,l)&&b.$broadcast("pickerUpdate",m,d)}),d.setParams(h.timezone),b.start=n(b.start),b.end=n(b.end),b.$watchGroup(["start","end"],function(a){j(a[0]),i(a[1])}),a.isDefined(h.dateChange)&&(k=d.findFunction(b,h.dateChange)),h.onSetDate="dateChange";var p='
'+f(h,m[0],"start",!1,b.end)+''+f(h,m[1],"end",b.start,!1)+"
",q=c(p)(b);e.append(q)}}}]);var d="ng-pristine",e="ng-dirty",c=a.module("datePicker");c.constant("dateTimeConfig",{template:function(a,b){return"
'},format:"YYYY-MM-DD HH:mm",views:["date","year","month","hours","minutes"],autoClose:!1,position:"relative"}),c.directive("dateTimeAppend",function(){return{link:function(a,b){b.bind("click",function(){b.find("input")[0].focus()})}}}),c.directive("dateTime",["$compile","$document","$filter","dateTimeConfig","$parse","datePickerUtils",function(c,f,g,h,i,j){var k=f.find("body"),l=g("mFormat");return{require:"ngModel",scope:!0,link:function(f,g,m,n){function o(a){return l(a,x,L)}function p(a){return a.length===x.length?a:void 0}function q(a){H=a,m.minDate=a?a.format():a,I=b.isMoment(a)}function r(a){J=a,m.maxDate=a?a.format():a,K=b.isMoment(a)}function s(){w=h.template(m)}function t(a){a.stopPropagation(),n.$pristine&&(n.$dirty=!0,n.$pristine=!1,g.removeClass(d).addClass(e),y&&y.$setDirty(),n.$render())}function u(){D&&(D.remove(),D=null),G&&(G.remove(),G=null)}function v(){if(!D){if(D=c(w)(f),f.$digest(),O||(f.$on("setDate",function(a,b,c){t(a),N&&N(m.ngModel,b),C&&z[z.length-1]===c&&u()}),f.$on("hidePicker",function(){g.triggerHandler("blur")}),f.$on("$destroy",u),O=!0),"absolute"===F){var b=g[0].getBoundingClientRect(),d=b.height||g[0].offsetHeight;D.css({top:b.top+d+"px",left:b.left+"px",display:"block",position:F}),k.append(D)}else G=a.element("
"),g[0].parentElement.insertBefore(G[0],g[0]),G.append(D),D.css({top:g[0].offsetHeight+"px",display:"block"});D.bind("mousedown",function(a){a.preventDefault()})}}var w,x=m.format||h.format,y=g.inheritedData("$formController"),z=i(m.views)(f)||h.views.concat(),A=m.view||z[0],B=z.indexOf(A),C=m.autoClose?i(m.autoClose)(f):h.autoClose,D=null,E=g[0].id,F=m.position||h.position,G=null,H=null,I=null,J=null,K=null,L=m.timezone||!1,M=j.eventIsForPicker,N=null,O=!1;-1===B&&z.splice(B,1),z.unshift(A),n.$formatters.push(o),n.$parsers.unshift(p),a.isDefined(m.minDate)&&(q(j.findParam(f,m.minDate)),n.$validators.min=function(a){return I?b.isMoment(a)&&(H.isSame(a)||H.isBefore(a)):!0}),a.isDefined(m.maxDate)&&(r(j.findParam(f,m.maxDate)),n.$validators.max=function(a){return K?b.isMoment(a)&&(J.isSame(a)||J.isAfter(a)):!0}),a.isDefined(m.dateChange)&&(N=j.findFunction(f,m.dateChange)),E&&f.$on("pickerUpdate",function(b,c,d){if(M(c,E))if(D);else{var e=!1;a.isDefined(d.minDate)&&(q(d.minDate),e=!0),a.isDefined(d.maxDate)&&(r(d.maxDate),e=!0),a.isDefined(d.minView)&&(m.minView=d.minView),a.isDefined(d.maxView)&&(m.maxView=d.maxView),m.view=d.view||m.view,e&&n.$validate(),a.isDefined(d.format)&&(x=m.format=d.format||h.format,n.$modelValue=-1),s()}}),g.bind("focus",v),g.bind("blur",u),s()}}}]),a.module("datePicker").run(["$templateCache",function(a){a.put("templates/datepicker.html",'
\r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n
\r\n
\r\n
')}])}); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('./dist/index.js'); 2 | module.exports = 'datePicker'; 3 | 4 | -------------------------------------------------------------------------------- /karma-e2e.conf.js: -------------------------------------------------------------------------------- 1 | // Karma E2E configuration 2 | 3 | // base path, that will be used to resolve files and exclude 4 | basePath = ''; 5 | 6 | // list of files / patterns to load in the browser 7 | files = [ 8 | ANGULAR_SCENARIO, 9 | ANGULAR_SCENARIO_ADAPTER, 10 | 'test/e2e/**/*.js' 11 | ]; 12 | 13 | // list of files to exclude 14 | exclude = []; 15 | 16 | // test results reporter to use 17 | // possible values: dots || progress || growl 18 | reporters = ['progress']; 19 | 20 | // web server port 21 | port = 8080; 22 | 23 | // cli runner port 24 | runnerPort = 9100; 25 | 26 | // enable / disable colors in the output (reporters and logs) 27 | colors = true; 28 | 29 | // level of logging 30 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 31 | logLevel = LOG_INFO; 32 | 33 | // enable / disable watching file and executing tests whenever any file changes 34 | autoWatch = false; 35 | 36 | // Start these browsers, currently available: 37 | // - Chrome 38 | // - ChromeCanary 39 | // - Firefox 40 | // - Opera 41 | // - Safari (only Mac) 42 | // - PhantomJS 43 | // - IE (only Windows) 44 | browsers = ['Chrome']; 45 | 46 | // If browser does not capture in given timeout [ms], kill it 47 | captureTimeout = 5000; 48 | 49 | // Continuous Integration mode 50 | // if true, it capture browsers, run tests and exit 51 | singleRun = false; 52 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | // base path, that will be used to resolve files and exclude 4 | basePath : '', 5 | 6 | // list of files / patterns to load in the browser 7 | files: [ 8 | 'app/components/angular/angular.js', 9 | 'app/components/angular-mocks/angular-mocks.js', 10 | 'app/components/moment/moment.js', 11 | 'app/components/moment-timezone/builds/moment-timezone-with-data.js', 12 | 'app/scripts/*.js', 13 | 'app/scripts/**/*.js', 14 | 'test/mock/**/*.js', 15 | 'test/spec/**/*.js', 16 | 'app/templates/**/*.html' 17 | ], 18 | 19 | // list of files to exclude 20 | exclude : [ 21 | 22 | ], 23 | 24 | preprocessors : { 25 | '**/*.html': ['ng-html2js'] 26 | }, 27 | 28 | proxies : { 29 | 30 | }, 31 | 32 | // test results reporter to use 33 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 34 | reporters : [ 'progress' ], 35 | 36 | // web server port 37 | port : 8080, 38 | 39 | // enable / disable colors in the output (reporters and logs) 40 | colors : true, 41 | 42 | // level of logging 43 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || 44 | // config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 45 | logLevel : config.LOG_INFO, 46 | 47 | autoWatch : true, 48 | 49 | // frameworks to use 50 | frameworks : [ 'jasmine' ], 51 | 52 | // Start these browsers, currently available: 53 | // - Chrome 54 | // - ChromeCanary 55 | // - Firefox 56 | // - Opera 57 | // - Safari (only Mac) 58 | // - PhantomJS 59 | // - IE (only Windows) 60 | browsers : [ 'Chrome' ], 61 | 62 | plugins : [ 63 | 'karma-chrome-launcher', 64 | 'karma-firefox-launcher', 65 | 'karma-script-launcher', 66 | 'karma-phantomjs-launcher', 67 | 'karma-jasmine', 68 | 'karma-ng-html2js-preprocessor' 69 | ], 70 | 71 | // If browser does not capture in given timeout [ms], kill it 72 | captureTimeout : 15000, 73 | 74 | // Continuous Integration mode 75 | // if true, it capture browsers, run tests and exit 76 | singleRun : false, 77 | 78 | ngHtml2JsPreprocessor: { 79 | // strip this from the file path 80 | stripPrefix: 'app/' 81 | } 82 | }); 83 | }; 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-datepicker", 3 | "version": "2.1.3", 4 | "main": "dist/angular-datepicker.js", 5 | "repository": { 6 | "url": "https://github.com/g00fy-/angular-datepicker.git" 7 | }, 8 | "dependencies": {}, 9 | "devDependencies": { 10 | "bower": "1.5.3", 11 | "grunt": "0.4.5", 12 | "grunt-angular-templates": "0.5.7", 13 | "grunt-bump": "0.3.1", 14 | "grunt-cli": "0.1.13", 15 | "grunt-concurrent": "1.0.1", 16 | "grunt-contrib-clean": "0.6.0", 17 | "grunt-contrib-concat": "0.5.1", 18 | "grunt-contrib-connect": "0.10.1", 19 | "grunt-contrib-copy": "0.8.0", 20 | "grunt-contrib-cssmin": "0.12.3", 21 | "grunt-contrib-jshint": "0.11.2", 22 | "grunt-contrib-less": "1.0.1", 23 | "grunt-contrib-livereload": "0.1.2", 24 | "grunt-contrib-uglify": "0.9.1", 25 | "grunt-contrib-watch": "0.6.1", 26 | "grunt-karma": "0.11.0", 27 | "grunt-ng-annotate": "1.0.1", 28 | "grunt-ngmin": "0.0.3", 29 | "grunt-open": "0.2.3", 30 | "jasmine-core": "^2.3.4", 31 | "karma": "0.12.36", 32 | "karma-chrome-launcher": "^0.1.12", 33 | "karma-firefox-launcher": "^0.1.6", 34 | "karma-jasmine": "^0.3.5", 35 | "karma-ng-html2js-preprocessor": "^0.2.1", 36 | "karma-phantomjs-launcher": "0.2.0", 37 | "karma-script-launcher": "^0.1.0", 38 | "load-grunt-tasks": "3.2.0", 39 | "phantomjs": "1.9.17" 40 | }, 41 | "engines": { 42 | "node": ">=0.8.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | End2end Test Runner 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/spec/datePickerTest.js: -------------------------------------------------------------------------------- 1 | describe('Test date Picker Filter', function(){ 2 | var mFormatFilter; 3 | 4 | beforeEach(angular.mock.module('datePicker')); 5 | 6 | beforeEach(angular.mock.inject(function($filter){ 7 | mFormatFilter = $filter('mFormat'); 8 | })); 9 | 10 | it('returns a formatted date when receiving a Moment instance', function(){ 11 | var date = moment(new Date(2015, 0, 2)); 12 | var formattedDate = mFormatFilter(date, 'YYYY_MM_DD'); 13 | 14 | expect(formattedDate).toBe('2015_01_02'); 15 | }); 16 | 17 | it('returns a formatted date when receiving a Date instance', function(){ 18 | var date = new Date(2015, 0, 2); 19 | var formattedDate = mFormatFilter(date, 'YYYY_MM_DD'); 20 | 21 | expect(formattedDate).toBe('2015_01_02'); 22 | }); 23 | 24 | it('returns a formatted date when receiving a string', function(){ 25 | var date = '2015-01-02T03:00:00.000Z'; 26 | var formattedDate = mFormatFilter(date, 'YYYY_MM_DD'); 27 | 28 | expect(formattedDate).toBe('2015_01_02'); 29 | }); 30 | }); 31 | 32 | describe('Test date Picker Directive', function(){ 33 | var $rootScope, $compile; 34 | 35 | function compileAndDigest(template) { 36 | var el = $compile(template)($rootScope); 37 | $rootScope.$digest(); 38 | return el; 39 | } 40 | 41 | beforeEach(angular.mock.module('datePicker')); 42 | 43 | beforeEach(angular.mock.module('templates/datepicker.html')); 44 | 45 | beforeEach(angular.mock.inject(function(_$compile_, _$rootScope_){ 46 | $compile = _$compile_; 47 | $rootScope = _$rootScope_; 48 | })); 49 | 50 | it('appends a div element with an ng-include attribute', function(){ 51 | var t = '
'; 52 | var el = compileAndDigest(t); 53 | expect(el.find(':scope > div[ng-include]').length).toBeGreaterThan(0); 54 | }); 55 | 56 | it('stands on the date of the model', function(){ 57 | $rootScope.date = moment(new Date(1973, 4, 7)); 58 | var t = '
'; 59 | var el = compileAndDigest(t); 60 | expect(el.find('.switch').text()).toBe('1973'); 61 | }); 62 | 63 | describe('next button', function(){ 64 | 65 | it('moves the view to the next year', function(){ 66 | $rootScope.date = moment(new Date(1973, 4, 7)); 67 | var t = '
'; 68 | var el = compileAndDigest(t); 69 | el.find('.switch').next().triggerHandler('click'); 70 | expect(el.find('.switch').text()).toBe('1974'); 71 | }); 72 | 73 | }); 74 | 75 | describe('prev button', function(){ 76 | 77 | it('moves the view to the previous year', function(){ 78 | $rootScope.date = moment(new Date(1973, 4, 7)); 79 | var t = '
'; 80 | var el = compileAndDigest(t); 81 | el.find('.switch').parent().children().eq(0).triggerHandler('click'); 82 | expect(el.find('.switch').text()).toBe('1972'); 83 | }); 84 | 85 | it('does not moves the view to the previous year if its less than min-date', function(){ 86 | $rootScope.date = moment(new Date(1973, 4, 7)); 87 | $rootScope.minDate = moment(new Date(1973, 0, 1)); 88 | var t = '
'; 89 | var el = compileAndDigest(t); 90 | el.find('.switch').parent().children().eq(0).triggerHandler('click'); 91 | expect(el.find('.switch').text()).toBe('1973'); 92 | }); 93 | 94 | }); 95 | 96 | describe('date cell button', function(){ 97 | 98 | it('is set to active if it corresponds to the model month', function(){ 99 | $rootScope.date = moment(new Date(1973, 4, 7)); 100 | var t = '
'; 101 | var el = compileAndDigest(t); 102 | expect(el.find('.active').text()).toBe('May'); 103 | }); 104 | 105 | it('goes to the next view', function(){ 106 | $rootScope.date = moment(new Date(1973, 4, 7)); 107 | var t = '
'; 108 | var el = compileAndDigest(t); 109 | el.find('table td span:first-child').triggerHandler('click'); 110 | expect(el.find('.switch').text()).toBe('1973 January'); 111 | }); 112 | 113 | it('clips date if its less than min-date', function(){ 114 | $rootScope.date = moment(new Date(1973, 4, 7)); 115 | $rootScope.minDate = moment(new Date(1973, 2, 1)); 116 | var t = '
'; 117 | var el = compileAndDigest(t); 118 | el.find('table td span:first-child').triggerHandler('click'); 119 | expect(el.find('.switch').text()).toBe('1973 March'); 120 | }); 121 | 122 | it('is set to disabled if its date is less than min-date', function(){ 123 | $rootScope.date = moment(new Date(1973, 4, 7)); 124 | $rootScope.minDate = moment(new Date(1973, 1, 1)); 125 | var t = '
'; 126 | var el = compileAndDigest(t); 127 | expect(el.find('.disabled').text()).toBe('Jan'); 128 | }); 129 | 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /test/spec/datePickerUtilsTest.js: -------------------------------------------------------------------------------- 1 | describe('Test date Picker Utils', function(){ 2 | var utils, constants, tz = 'UTC', firstDay = 0 //Sunday; 3 | 4 | /** 5 | * Creates a moment object from an iso8601 string, using a pre-set timezone. 6 | */ 7 | function dateBuilder(iso8601) { 8 | return moment.tz(iso8601, tz); 9 | } 10 | 11 | var model = dateBuilder('2014-06-29T19:00:00+00:00'); // sunday 12 | 13 | beforeEach(angular.mock.module('datePicker')); 14 | 15 | beforeEach(angular.mock.inject(function($injector){ 16 | utils = $injector.get('datePickerUtils'); 17 | constants = $injector.get('datePickerConfig'); 18 | utils.setParams(tz, firstDay); 19 | })); 20 | 21 | /* 22 | * At no point do we ever need to get minutes, weeks, years, months, or hours for an unknown date. 23 | * * The only date building util function which is called without a specific date is getDaysOfWeek. 24 | * * This is called via the update() function in datePicker.js if scope.weekdays is falsy. 25 | * * These tests have therefore been removed. 26 | */ 27 | 28 | it('get visible mins provided date', function(){ 29 | var start = dateBuilder('2014-06-29T19:00:00+00:00'); // sunday 30 | var end = dateBuilder('2014-06-29T19:55:00+00:00'); // sunday 31 | var chosen = dateBuilder('2014-06-29T19:00:00+00:00'); // sunday 32 | var mins = utils.getVisibleMinutes(chosen, constants.step); 33 | 34 | expect(mins).toBeDefined(); 35 | expect(start.isSame(mins[0])).toBe(true); 36 | expect(end.isSame(mins[mins.length-1])).toBe(true); 37 | }); 38 | 39 | it('get visible weeks provided date', function(){ 40 | var start = dateBuilder('2014-05-25T00:00:00+00:00'); // sunday 41 | var end = dateBuilder('2014-07-05T00:00:00+00:00'); // saturday 42 | var chosen = dateBuilder('2014-06-29T19:00:00+00:00'); // sunday 43 | var weeks = utils.getVisibleWeeks(chosen, constants.step); 44 | 45 | expect(weeks).toBeDefined(); 46 | expect(start.isSame(weeks[0][0])).toBe(true); 47 | expect(end.isSame(weeks[5][weeks[5].length - 1])).toBe(true); 48 | }); 49 | 50 | it('get visible years provided date', function(){ 51 | var start = dateBuilder('2010-01-01T00:00:00+00:00'); // friday 52 | var end = dateBuilder('2021-01-01T00:00:00+00:00'); // friday 53 | var chosen = dateBuilder('2014-06-29T19:00:00+00:00'); // sunday 54 | var years = utils.getVisibleYears(chosen, constants.step); 55 | expect(years).toBeDefined(); 56 | expect(start.isSame(years[0])).toBe(true); 57 | expect(end.isSame(years[years.length - 1])).toBe(true); 58 | }); 59 | 60 | it('get default days of week', function(){ 61 | var days = utils.getDaysOfWeek(null); 62 | 63 | expect(days).toBeDefined(); 64 | }); 65 | 66 | it('get days of week provided date', function(){ 67 | var start = dateBuilder('2014-05-26T00:00:00+00:00'); // monday 68 | var end = dateBuilder('2014-06-01T00:00:00+00:00'); // sunday 69 | var days = utils.getDaysOfWeek(start); 70 | 71 | expect(days).toBeDefined(); 72 | expect(start.isSame(days[0])).toBe(true); 73 | expect(end.isSame(days[days.length-1])).toBe(true); 74 | }); 75 | 76 | 77 | it('get default months provided date', function(){ 78 | var start = dateBuilder('2014-01-01T00:00:00+00:00'); // wednesday 79 | var end = dateBuilder('2014-12-01T00:00:00+00:00'); // monday 80 | var chosen = dateBuilder('2014-06-29T19:00:00+00:00'); // sunday 81 | var months = utils.getVisibleMonths(chosen); 82 | 83 | expect(months).toBeDefined(); 84 | expect(start.isSame(months[0])).toBe(true); 85 | expect(end.isSame(months[months.length-1])).toBe(true); 86 | }); 87 | 88 | 89 | it('get default hours provided date', function(){ 90 | var start = dateBuilder('2014-06-29T00:00:00+00:00'); // sunday 91 | var end = dateBuilder('2014-06-29T23:00:00+00:00'); // sunday 92 | var chosen = dateBuilder('2014-06-29T19:00:00+00:00'); // sunday 93 | var hours = utils.getVisibleHours(chosen); 94 | 95 | expect(hours).toBeDefined(); 96 | expect(start.isSame(hours[0])).toBe(true); 97 | expect(end.isSame(hours[hours.length-1])).toBe(true); 98 | }); 99 | 100 | it('model is after date', function(){ 101 | // model is 19h, dateAfter is 20h, so model should be before and not after 102 | var dateAfter = dateBuilder('2014-06-29T20:00:00+00:00'); // sunday 103 | 104 | expect(utils.isAfter(model, dateAfter)).toBe(false); 105 | expect(utils.isBefore(model, dateAfter)).toBe(true); 106 | }); 107 | 108 | it('model is before date', function(){ 109 | // model is 19h, dateAfter is 18h, so model should be after and not before 110 | var dateAfter = dateBuilder('2014-06-29T18:00:00+00:00'); // sunday 111 | 112 | expect(utils.isAfter(model, dateAfter)).toBe(true); 113 | expect(utils.isBefore(model, dateAfter)).toBe(false); 114 | }); 115 | 116 | it('model is almost same', function(){ 117 | var dateSimilar = dateBuilder('2014-06-29T19:00:55.555'); // sunday 118 | 119 | expect(utils.isSameYear(model, dateSimilar)).toBe(true); 120 | expect(utils.isSameMonth(model, dateSimilar)).toBe(true); 121 | expect(utils.isSameDay(model, dateSimilar)).toBe(true); 122 | expect(utils.isSameHour(model, dateSimilar)).toBe(true); 123 | expect(utils.isSameMinutes(model, dateSimilar)).toBe(true); 124 | }); 125 | 126 | //it('angular date format is a moment format', function () { 127 | // //Angular formats: https://docs.angularjs.org/api/ng/filter/date 128 | // //Moment formats: http://momentjs.com/docs/#/parsing/string-format/ 129 | // expect(utils.toMomentFormat('dd-MM-yyyy')).toBe('DD-MM-YYYY'); 130 | // expect(utils.toMomentFormat('EEEE MM/yy')).toBe('dddd MM/YY'); 131 | // expect(utils.toMomentFormat('dd MMMM yyyy HH:mm:ss.sss')).toBe('DD MMMM YYYY HH:mm:ss.SSS'); 132 | //}); 133 | }); 134 | -------------------------------------------------------------------------------- /test/spec/inputTest.js: -------------------------------------------------------------------------------- 1 | describe('Test date time input Directive', function(){ 2 | var $rootScope, $compile; 3 | 4 | function compileAndDigest(template) { 5 | var el = $compile(template)($rootScope); 6 | $rootScope.$digest(); 7 | return el; 8 | } 9 | 10 | beforeEach(angular.mock.module('datePicker')); 11 | 12 | beforeEach(angular.mock.module('templates/datepicker.html')); 13 | 14 | beforeEach(angular.mock.inject(function(_$compile_, _$rootScope_){ 15 | $compile = _$compile_; 16 | $rootScope = _$rootScope_; 17 | })); 18 | 19 | it('shows the model with the default format', function(){ 20 | $rootScope.date = moment(new Date(1972, 2, 5)); 21 | var t = ''; 22 | var el = compileAndDigest(t); 23 | expect(el.val()).toBe('1972-03-05 00:00'); 24 | }); 25 | 26 | it('shows the model with the attribute format', function(){ 27 | $rootScope.date = moment(new Date(1972, 2, 5)); 28 | var t = ''; 29 | var el = compileAndDigest(t); 30 | expect(el.val()).toBe('1972_03_05'); 31 | }); 32 | 33 | it('updates the view when model is changed', function(){ 34 | $rootScope.date = moment(new Date(1972, 2, 5)); 35 | var t = ''; 36 | var el = compileAndDigest(t); 37 | $rootScope.date = moment(new Date(1971, 1, 4)); 38 | $rootScope.$digest(); 39 | expect(el.val()).toBe('1971-02-04 00:00'); 40 | }); 41 | 42 | it('opens the datepicker on focus', function(){ 43 | $rootScope.date = moment(new Date(1972, 2, 5)); 44 | var t = '
'; 45 | var el = compileAndDigest(t); 46 | 47 | expect(el.children().length).toBe(1); 48 | 49 | el.children().triggerHandler('focus'); 50 | 51 | expect(el.children().length).toBe(2); 52 | expect(el.find('[date-picker]').length).toBe(1); 53 | }); 54 | 55 | it('hides the datepicker on blur', function(){ 56 | $rootScope.date = moment(new Date(1972, 2, 5)); 57 | var t = '
'; 58 | var el = compileAndDigest(t); 59 | 60 | el.children().triggerHandler('focus'); 61 | el.children().triggerHandler('blur'); 62 | 63 | expect(el.children().length).toBe(1); 64 | expect(el.find('[date-picker]').length).toBe(0); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/spec/querySelectorFind.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.element.prototype.find = function(find){ 5 | if (this.length) { 6 | return angular.element(this[0].querySelectorAll(find)); 7 | } 8 | return this; 9 | } 10 | 11 | })(); 12 | --------------------------------------------------------------------------------