├── .gitignore ├── CHANGELOG.md ├── README.md ├── bower.json ├── dist └── datepicker.js ├── example ├── index.html ├── main.js └── main.scss ├── gulpfile.js ├── package.json └── src ├── bind.js ├── calendar.js ├── datepicker.html ├── display.js ├── events.js ├── helpers.js ├── picker.js ├── plugin.js └── wrapper.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | .tmp 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.3.1 4 | 5 | * Remove outputTo value on remoev 6 | 7 | ## 0.3.0 8 | 9 | * Change `formattedVal` to output current state, rather than internal state 10 | 11 | ## 0.2.1 12 | 13 | * Fix prefill option 14 | * Fix state when using prefilled input 15 | * Add onInitialize callback, event 16 | 17 | ## 0.2.0 18 | 19 | * Add 'Remove' functionality 20 | * Remove 'Today' functionality 21 | * Change 'Done' functionality. Date object will no longer be output until Save is clicked. 22 | 23 | ## 0.1.0 24 | 25 | * Initial release 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # datepicker 2 | 3 | A Trello-like datepicker with freeform time. 4 | 5 | ![](http://cl.ly/image/0r0w001L3a0o/datepicker.mov.gif) 6 | 7 | ## Usage 8 | 9 | ```javascript 10 | // jQuery 11 | $('input').picker() 12 | 13 | // Vanilla 14 | new Picker(document.getElementByTagName('input')) 15 | ``` 16 | 17 | ## Options 18 | 19 | **startDate** 20 | 21 | Default: `'-0d'` 22 | 23 | **dateFormat** 24 | 25 | Default: `'MM/DD/YYYY'` 26 | 27 | **defaultTimeRange** 28 | 29 | Default: `{ hours: 1 }` 30 | 31 | * Only used with an input range 32 | 33 | **doneText** 34 | 35 | Default: `'Done'` 36 | 37 | **timeFormat** 38 | 39 | Default: `'h:mm A'` 40 | 41 | **template** 42 | 43 | Default: `JST.datepicker` 44 | 45 | **outputTo** 46 | 47 | Default: `this.$el` 48 | 49 | **prefill** 50 | 51 | Default: `true` 52 | 53 | **onChange** 54 | 55 | Default: `noop` bound to `this` (picker instance) 56 | 57 | ## Dependencies 58 | 59 | * jQuery 60 | * moment 61 | * bootstrap-datepicker 62 | 63 | ## Development 64 | 65 | * `npm install && bower install` 66 | * `gulp` 67 | * Open http://0.0.0.0:8000/example 68 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datetime-picker", 3 | "dependencies": { 4 | "bootstrap-datepicker": "~1.3.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /dist/datepicker.js: -------------------------------------------------------------------------------- 1 | /* 2 | * datepicker.js 0.8.0 3 | * https://github.com/Zeumo/datepicker.js 4 | * 5 | * /!\ Don't edit this file directly! 6 | * It was generated by the datepicker.js build system. 7 | */ 8 | 9 | (function(window) { 10 | var Picker, pluginName = 'picker', templates = {}; 11 | 12 | var t = function(s,d){ 13 | for(var p in d) s=s.replace(new RegExp('{'+p+'}','g'), d[p]); 14 | return s; 15 | }; 16 | 17 | Picker = function(el, options) { 18 | this.$el = $(el); 19 | this._initialized = false; 20 | this.range = this.initializeRange(options); 21 | 22 | if (this.hasRange()) { 23 | this._initialized = true; 24 | return this; 25 | } 26 | 27 | // Options 28 | this.options = $.extend({ 29 | startDate: '-0d', 30 | dateFormat: 'MM/DD/YYYY', 31 | timeFormat: 'h:mm A', 32 | template: templates.datepicker, 33 | doneText: 'Save', 34 | removeText: 'Remove', 35 | prefill: false, 36 | defaultTimeRange: { hours: 1 }, 37 | outputTo: this.$el, 38 | onChange: function() {}, 39 | onRemove: function() {}, 40 | onInitialize: function() {} 41 | }, options); 42 | 43 | this.$startPicker = this.options.startPicker; 44 | this.$endPicker = this.options.endPicker; 45 | 46 | this.options.onChange = this.options.onChange.bind(this); 47 | this.options.onRemove = this.options.onRemove.bind(this); 48 | this.options.onInitialize = this.options.onInitialize.bind(this); 49 | 50 | // Events 51 | this.events = { 52 | 'click': this.onClick 53 | }; 54 | 55 | if (this.isInput()) { 56 | this.events = { 57 | 'focus': this.onClick 58 | }; 59 | } 60 | 61 | this.pickerEvents = { 62 | 'click .done': this.onDone, 63 | 'click .remove': this.onRemove 64 | }; 65 | 66 | this.startPickerEvents = { 67 | 'change': this.setTimeAfterStartPicker 68 | } 69 | 70 | this.endPickerEvents = { 71 | 'change': this.setTimeToBeforeEndPicker 72 | } 73 | 74 | // Convenience vars 75 | this.$body = $('body'); 76 | this.$picker = $(this.render()); 77 | this.$date = this.$picker.find('[name=date]'); 78 | this.$time = this.$picker.find('[name=time]'); 79 | 80 | // Standardize outputTo 81 | if (!this.options.outputTo) { 82 | this.options.outputTo = this.$el; 83 | } 84 | if (!this.options.outputTo.jquery) { 85 | this.options.outputTo = $(this.options.outputTo); 86 | } 87 | 88 | // Set current date and time 89 | var m = moment(new Date(this.options.outputTo.val())); 90 | this.setDateTime(this.dateTime()); 91 | 92 | if (m.isValid()) { 93 | this.setDateTime({ 94 | date: m.format(this.options.dateFormat), 95 | time: m.format(this.options.timeFormat) 96 | }); 97 | this.outputDateTime(); 98 | } 99 | 100 | // Prefill empty field 101 | if (this.options.prefill && !m.isValid()) { 102 | this.outputDateTime(); 103 | } 104 | 105 | // Delegate events 106 | this.delegateEvents(this.events, this.$el); 107 | this.delegateEvents(this.pickerEvents, this.$picker); 108 | this.handlePickerClose(); 109 | if (this.isEndPicker()) { 110 | this.delegateEvents(this.startPickerEvents, this.$startPicker); 111 | } else if (this.isStartPicker()) { 112 | this.delegateEvents(this.endPickerEvents, this.$endPicker); 113 | } 114 | 115 | // Initialize calendar picker 116 | this.initializeCalendar(); 117 | 118 | this.options.onInitialize(); 119 | this.$el.trigger('initialize'); 120 | 121 | this._initialized = true; 122 | return this; 123 | }; 124 | 125 | Picker.prototype.onClick = function(e) { 126 | e.preventDefault(); 127 | 128 | this.show(); 129 | this.$date.focus(); 130 | }; 131 | 132 | Picker.prototype.onChangeDate = function() { 133 | this.setDateTime(this.serialize()); 134 | this.outputDateTime(); 135 | this.updateCalendar(); 136 | }; 137 | 138 | Picker.prototype.onChangeTime = function(e) { 139 | this.setDateTime(this.serialize()); 140 | }; 141 | 142 | Picker.prototype.onDone = function(e) { 143 | e.preventDefault(); 144 | this.close(); 145 | this.onChangeDate(); 146 | }; 147 | 148 | Picker.prototype.onRemove = function(e) { 149 | e.preventDefault(); 150 | delete this.savedVal; 151 | this.close(); 152 | this.unsetDateTime(); 153 | }; 154 | 155 | Picker.prototype.delegateEvents = function(events, $el) { 156 | for(var key in events) { 157 | var match = key.match(/^(\S+)\s*(.*)$/); 158 | var eventName = match[1]; 159 | var handler = match[2]; 160 | var method = events[key]; 161 | 162 | $el.on(eventName, handler, method.bind(this)); 163 | } 164 | }; 165 | 166 | Picker.prototype.handlePickerClose = function() { 167 | var self = this; 168 | 169 | var handler = function(e) { 170 | var isEl = !!$(e.target).closest(self.$el).length, 171 | isDetached = !$(document).find(e.target).length, 172 | isPicker = !!$(e.target).closest('#datepicker').length; 173 | 174 | if (isEl || isDetached || isPicker) return; 175 | this.close(); 176 | }; 177 | 178 | $(document).on('click', handler.bind(this)); 179 | 180 | $(document).on('keyup', function(e) { 181 | // Esc 182 | if (e.which === 27) this.close(); 183 | }.bind(this)); 184 | }; 185 | 186 | Picker.prototype.show = function() { 187 | var elHeight = this.$el.outerHeight(true); 188 | elBottom = elHeight + this.$el.offset().top, 189 | elLeft = this.$el.offset().left; 190 | 191 | this.$picker.find('.remove').toggleClass('hidden', !this.savedVal); 192 | 193 | this.$picker.css({ 194 | top: elBottom + 5 + 'px', 195 | left: elLeft + 'px', 196 | position: 'absolute' 197 | }); 198 | 199 | this.closeAll(); 200 | this.$body.append(this.$picker); 201 | 202 | var pickerHeight = this.$picker.outerHeight(true); 203 | var pickerBottom = pickerHeight + this.$picker.offset().top; 204 | 205 | if (pickerBottom > window.innerHeight) { 206 | this.$picker.css({ 207 | top: this.$el.offset().top - pickerHeight + 5 + 'px', 208 | position: 'absolute' 209 | }); 210 | } 211 | }; 212 | 213 | Picker.prototype.render = function() { 214 | var options = $.extend({}, 215 | this.dateTime(), 216 | { val: this._val }, 217 | this.options); 218 | 219 | return $(t(this.options.template, options)); 220 | }; 221 | 222 | Picker.prototype.closeAll = function() { 223 | $('#datepicker').detach(); 224 | }; 225 | 226 | Picker.prototype.close = function() { 227 | this.$picker.detach(); 228 | }; 229 | 230 | Picker.prototype.dateTime = function(offsetHours) { 231 | offsetHours = offsetHours || 1; 232 | 233 | if (this.hasPrecedingPicker() || this.isEndPicker()) { 234 | offsetHours += 1; 235 | } 236 | 237 | return { 238 | date: moment().format(this.options.dateFormat), 239 | time: moment().add(offsetHours, 'hour').startOf('hour') 240 | .format(this.options.timeFormat) 241 | }; 242 | }; 243 | 244 | Picker.prototype.setDateTime = function(obj) { 245 | var date = obj.date, 246 | time = this.normalizeTime(obj.time), 247 | datetime; 248 | 249 | this._val = moment(new Date([date, time].join(' '))); 250 | 251 | // Reset the moment object if we got an invalid date 252 | if (!this._val.isValid()) { 253 | datetime = this.dateTime(); 254 | this._val = moment([datetime.date, datetime.time].join(' ')); 255 | } 256 | 257 | this.$date.val(this._val.format(this.options.dateFormat)); 258 | this.$time.val(this._val.format(this.options.timeFormat)); 259 | }; 260 | 261 | Picker.prototype.outputDateTime = function() { 262 | this.savedVal = this._val; 263 | formattedVal = this.formattedVal(); 264 | 265 | this.options.outputTo.val(formattedVal); 266 | 267 | if (this._initialized) { 268 | if (this.isInput()) { 269 | this.$el.trigger('change'); 270 | } 271 | 272 | this.options.onChange(); 273 | } 274 | }; 275 | 276 | Picker.prototype.unsetDateTime = function(obj) { 277 | this.options.outputTo.val(''); 278 | this.$el.trigger('datepicker.remove', this.options.outputTo); 279 | this.options.onRemove(); 280 | }; 281 | 282 | Picker.prototype.formattedVal = function() { 283 | if (!this.savedVal) return; 284 | return this.savedVal.format([this.options.dateFormat, this.options.timeFormat].join(' ')); 285 | }; 286 | 287 | Picker.prototype.normalizeTime = function(time) { 288 | // Normalize minutes 289 | if (!(/\d:\d{2}/).test(time)) { 290 | time = time.replace(/(^\d+)/, "$1:00"); 291 | } 292 | 293 | // Normalize spacing 294 | if (!(/\s[a|p]/i).test(time)) { 295 | time = time.replace(/(a|p)/i, " $1"); 296 | } 297 | 298 | // Normalize meridian 299 | if (!(/m/i).test(time)) { 300 | time = time.replace(/(a|p)/i, "$1m"); 301 | } 302 | 303 | return time; 304 | }; 305 | 306 | Picker.prototype.isInput = function() { 307 | return this.$el[0].tagName === 'INPUT'; 308 | }; 309 | 310 | Picker.prototype.serialize = function() { 311 | return { 312 | date: this.$date.val(), 313 | time: this.$time.val().toUpperCase() 314 | }; 315 | }; 316 | 317 | Picker.prototype.hasPrecedingPicker = function() { 318 | var dtp = this.$el.siblings('input').data(pluginName); 319 | if (dtp) return true; 320 | }; 321 | 322 | Picker.prototype.isEndPicker = function() { 323 | return !!this.$startPicker; 324 | }; 325 | 326 | Picker.prototype.isStartPicker = function() { 327 | return !!this.$endPicker; 328 | }; 329 | 330 | Picker.prototype.hasRange = function() { 331 | return !!this.range.length; 332 | } 333 | 334 | Picker.prototype.initializeRange = function(options) { 335 | var children = this.$el.find('input'); 336 | 337 | if (children.length !== 2) return []; 338 | 339 | return children.map(function(index) { 340 | var rangeOptions; 341 | 342 | if (index === 1) { 343 | rangeOptions = $.extend({ startPicker: $(children[0]) }, options); 344 | } else { 345 | rangeOptions = $.extend({ endPicker: $(children[1]) }, options); 346 | } 347 | return new Picker(this, rangeOptions); 348 | }); 349 | }; 350 | 351 | Picker.prototype.startPickerDate = function() { 352 | return new Date(this.$startPicker.val()); 353 | }; 354 | 355 | Picker.prototype.endPickerDate = function() { 356 | return new Date(this.$endPicker.val()); 357 | }; 358 | 359 | Picker.prototype.selectedMoment = function() { 360 | return moment(new Date(this.options.outputTo.val())); 361 | }; 362 | 363 | Picker.prototype.setTimeAfterStartPicker = function() { 364 | var startTime = this.startPickerDate(); 365 | var newEndTime = moment(startTime).add(this.options.defaultTimeRange); 366 | var currentEndTime = this.selectedMoment() 367 | 368 | // Don't update dateTime if the currentEndTime is already > than startTime 369 | if (currentEndTime > startTime) return; 370 | 371 | if (newEndTime.isValid()) { 372 | this.setDateTime({ 373 | date: newEndTime.format(this.options.dateFormat), 374 | time: newEndTime.format(this.options.timeFormat) 375 | }); 376 | this.outputDateTime(); 377 | this.updateCalendar(); 378 | } 379 | }; 380 | 381 | Picker.prototype.setTimeToBeforeEndPicker = function() { 382 | var endTime = this.endPickerDate(); 383 | var newStartTime = moment(endTime).subtract(this.options.defaultTimeRange); 384 | var currentStartTime = this.selectedMoment() 385 | 386 | // Don't update dateTime if the currentStartTime is already < than endTime 387 | if (currentStartTime < endTime) return; 388 | 389 | if (newStartTime.isValid()) { 390 | this.setDateTime({ 391 | date: newStartTime.format(this.options.dateFormat), 392 | time: newStartTime.format(this.options.timeFormat) 393 | }); 394 | this.outputDateTime(); 395 | this.updateCalendar(); 396 | } 397 | }; 398 | 399 | Picker.prototype.initializeCalendar = function() { 400 | this.calendarEvents = { 401 | 'changeDate': this.onCalendarChangeDate 402 | }; 403 | 404 | this.$calendar = this.$picker.find('.calendar').datepicker({ 405 | startDate: this.options.startDate 406 | }); 407 | 408 | this.updateCalendar(); 409 | this.delegateEvents(this.calendarEvents, this.$calendar); 410 | }; 411 | 412 | Picker.prototype.updateCalendar = function() { 413 | this.$calendar.datepicker('update', this.$date.val()); 414 | }; 415 | 416 | Picker.prototype.onCalendarChangeDate = function(e) { 417 | var date = e.format(); 418 | 419 | if (date) { 420 | this.$date.val(date); 421 | this.setDateTime(this.serialize()); 422 | } 423 | }; 424 | 425 | $.fn[pluginName] = function (options) { 426 | this.each(function() { 427 | if (!$.data(this, pluginName)) { 428 | $.data(this, pluginName, new Picker(this, options)); 429 | } 430 | }); 431 | return this; 432 | }; 433 | 434 | if (!Function.prototype.bind) { 435 | Function.prototype.bind = function(oThis) { 436 | if (typeof this !== 'function') { 437 | // closest thing possible to the ECMAScript 5 438 | // internal IsCallable function 439 | throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); 440 | } 441 | 442 | var aArgs = Array.prototype.slice.call(arguments, 1), 443 | fToBind = this, 444 | fNOP = function() {}, 445 | fBound = function() { 446 | return fToBind.apply(this instanceof fNOP 447 | ? this 448 | : oThis, 449 | aArgs.concat(Array.prototype.slice.call(arguments))); 450 | }; 451 | 452 | fNOP.prototype = this.prototype; 453 | fBound.prototype = new fNOP(); 454 | 455 | return fBound; 456 | }; 457 | } 458 | 459 | templates["datepicker"] = "
{doneText} {removeText}
"; 460 | 461 | window.Picker = Picker; 462 | }(this)); 463 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DatePicker Example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 | 23 | to 24 | 25 |
26 | 27 |
28 | Schedule 29 | 30 |
31 | 32 |
33 | 34 |
35 |
36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /example/main.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | $('.date-range-picker').picker({ 3 | onInitialize: function() { 4 | console.log(this.formattedVal()); 5 | } 6 | }); 7 | 8 | $('.button-picker a').picker({ 9 | outputTo: $('.button-picker input'), 10 | prefill: true, 11 | onInitialize: function() { 12 | console.log(this.formattedVal()); 13 | } 14 | }); 15 | 16 | $('.prefilled-picker input').picker(); 17 | 18 | $('.button-picker a').on('datepicker.remove', function(e, el) { 19 | $(el).val(''); 20 | }); 21 | 22 | $('input').on('datepicker.remove', function(e) { 23 | $(this).val(''); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /example/main.scss: -------------------------------------------------------------------------------- 1 | #datepicker { 2 | background: #f7f7f7; 3 | box-shadow: 0 2px 7px rgba(0,0,0,0.2); 4 | border-radius: 3px; 5 | width: 300px; 6 | padding: 10px; 7 | margin-top: 10px; 8 | margin-bottom: 20px; 9 | 10 | input { 11 | margin-bottom: 15px; 12 | } 13 | 14 | .datepicker-inline { 15 | width: 100%; 16 | } 17 | 18 | .datepicker table { 19 | width: 100% !important; 20 | } 21 | } 22 | 23 | 24 | /* For example */ 25 | 26 | #root { 27 | margin: 50px; 28 | text-align: center; 29 | } 30 | 31 | .example + .example { 32 | margin-top: 50px; 33 | } 34 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | eventStream = require('event-stream'), 3 | pkg = require('./package.json'), 4 | gulp = require('gulp'), 5 | concat = require('gulp-concat'), 6 | wrap = require('gulp-wrap'), 7 | sass = require('gulp-sass'), 8 | webserver = require('gulp-webserver'); 9 | 10 | var compileTemplate = function() { 11 | var transform = function(file, cb) { 12 | var name = file.relative.replace(/\.html$/, ''); 13 | var contents = file.contents.toString() 14 | .replace(/\"/g, '\\"') 15 | .replace(/\n/g, ''); 16 | 17 | var content = 'templates["'+name+'"] = "'+contents+'";'; 18 | 19 | file.contents = new Buffer(String(content)); 20 | cb(null, file); 21 | }; 22 | 23 | return eventStream.map(transform); 24 | }; 25 | 26 | gulp.task('default', ['build', 'sass', 'watch', 'server']); 27 | 28 | gulp.task('watch', function() { 29 | gulp.watch(['src/*', 'package.json'], ['build']); 30 | gulp.watch('example/*.scss', ['sass']); 31 | }); 32 | 33 | gulp.task('sass', function() { 34 | gulp.src('example/*.scss') 35 | .pipe(sass({ 36 | includePaths: ['bower_components'] 37 | })) 38 | .pipe(gulp.dest('.tmp/')); 39 | }); 40 | 41 | gulp.task('templates', function(){ 42 | return gulp.src('src/*.html') 43 | .pipe(compileTemplate()) 44 | .pipe(concat('templates.js')) 45 | .pipe(gulp.dest('.tmp')); 46 | }); 47 | 48 | gulp.task('build', ['templates'], function() { 49 | gulp.src([ 50 | 'src/picker.js', 51 | 'src/events.js', 52 | 'src/display.js', 53 | 'src/helpers.js', 54 | 'src/calendar.js', 55 | 'src/plugin.js', 56 | 'src/bind.js', 57 | '.tmp/templates.js' 58 | ]) 59 | .pipe(concat('datepicker.js')) 60 | .pipe(wrap({ 61 | src: 'src/wrapper.js', 62 | }, 63 | { 64 | version: pkg.version 65 | })) 66 | .pipe(gulp.dest('dist/')); 67 | }); 68 | 69 | gulp.task('server', function() { 70 | gulp.src('.') 71 | .pipe(webserver({ 72 | livereload: true, 73 | open: true 74 | })); 75 | }); 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datetime-picker", 3 | "version": "0.8.0", 4 | "description": "A Trello-like datepicker with freeform time", 5 | "bugs": { 6 | "url": "https://github.com/Zeumo/datetime-picker/issues" 7 | }, 8 | "homepage": "https://github.com/Zeumo/datetime-picker", 9 | "main": "dist/datepicker.js", 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "author": "Nic Aitch", 14 | "license": "MIT", 15 | "dependencies": { 16 | "jquery": "^1.11.1", 17 | "moment": "^2.8.2" 18 | }, 19 | "devDependencies": { 20 | "bootstrap": "^3.2.0", 21 | "event-stream": "^3.3.0", 22 | "gulp": "^3.8.7", 23 | "gulp-concat": "^2.3.5", 24 | "gulp-declare": "^0.3.0", 25 | "gulp-sass": "^2.0.4", 26 | "gulp-webserver": "^0.7.2", 27 | "gulp-wrap": "^0.3.0", 28 | "lodash": "^3.10.1", 29 | "normalize.css": "^3.0.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/bind.js: -------------------------------------------------------------------------------- 1 | if (!Function.prototype.bind) { 2 | Function.prototype.bind = function(oThis) { 3 | if (typeof this !== 'function') { 4 | // closest thing possible to the ECMAScript 5 5 | // internal IsCallable function 6 | throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); 7 | } 8 | 9 | var aArgs = Array.prototype.slice.call(arguments, 1), 10 | fToBind = this, 11 | fNOP = function() {}, 12 | fBound = function() { 13 | return fToBind.apply(this instanceof fNOP 14 | ? this 15 | : oThis, 16 | aArgs.concat(Array.prototype.slice.call(arguments))); 17 | }; 18 | 19 | fNOP.prototype = this.prototype; 20 | fBound.prototype = new fNOP(); 21 | 22 | return fBound; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/calendar.js: -------------------------------------------------------------------------------- 1 | Picker.prototype.initializeCalendar = function() { 2 | this.calendarEvents = { 3 | 'changeDate': this.onCalendarChangeDate 4 | }; 5 | 6 | this.$calendar = this.$picker.find('.calendar').datepicker({ 7 | startDate: this.options.startDate 8 | }); 9 | 10 | this.updateCalendar(); 11 | this.delegateEvents(this.calendarEvents, this.$calendar); 12 | }; 13 | 14 | Picker.prototype.updateCalendar = function() { 15 | this.$calendar.datepicker('update', this.$date.val()); 16 | }; 17 | 18 | Picker.prototype.onCalendarChangeDate = function(e) { 19 | var date = e.format(); 20 | 21 | if (date) { 22 | this.$date.val(date); 23 | this.setDateTime(this.serialize()); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/datepicker.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 |
7 |
8 | 9 | 10 |
11 |
12 | 13 |
14 | 15 | {doneText} 16 | 17 |
18 | -------------------------------------------------------------------------------- /src/display.js: -------------------------------------------------------------------------------- 1 | Picker.prototype.show = function() { 2 | var elHeight = this.$el.outerHeight(true); 3 | elBottom = elHeight + this.$el.offset().top, 4 | elLeft = this.$el.offset().left; 5 | 6 | this.$picker.find('.remove').toggleClass('hidden', !this.savedVal); 7 | 8 | this.$picker.css({ 9 | top: elBottom + 5 + 'px', 10 | left: elLeft + 'px', 11 | position: 'absolute' 12 | }); 13 | 14 | this.closeAll(); 15 | this.$body.append(this.$picker); 16 | 17 | var pickerHeight = this.$picker.outerHeight(true); 18 | var pickerBottom = pickerHeight + this.$picker.offset().top; 19 | 20 | if (pickerBottom > window.innerHeight) { 21 | this.$picker.css({ 22 | top: this.$el.offset().top - pickerHeight + 5 + 'px', 23 | position: 'absolute' 24 | }); 25 | } 26 | }; 27 | 28 | Picker.prototype.render = function() { 29 | var options = $.extend({}, 30 | this.dateTime(), 31 | { val: this._val }, 32 | this.options); 33 | 34 | return $(t(this.options.template, options)); 35 | }; 36 | 37 | Picker.prototype.closeAll = function() { 38 | $('#datepicker').detach(); 39 | }; 40 | 41 | Picker.prototype.close = function() { 42 | this.$picker.detach(); 43 | }; 44 | -------------------------------------------------------------------------------- /src/events.js: -------------------------------------------------------------------------------- 1 | Picker.prototype.delegateEvents = function(events, $el) { 2 | for(var key in events) { 3 | var match = key.match(/^(\S+)\s*(.*)$/); 4 | var eventName = match[1]; 5 | var handler = match[2]; 6 | var method = events[key]; 7 | 8 | $el.on(eventName, handler, method.bind(this)); 9 | } 10 | }; 11 | 12 | Picker.prototype.handlePickerClose = function() { 13 | var self = this; 14 | 15 | var handler = function(e) { 16 | var isEl = !!$(e.target).closest(self.$el).length, 17 | isDetached = !$(document).find(e.target).length, 18 | isPicker = !!$(e.target).closest('#datepicker').length; 19 | 20 | if (isEl || isDetached || isPicker) return; 21 | this.close(); 22 | }; 23 | 24 | $(document).on('click', handler.bind(this)); 25 | 26 | $(document).on('keyup', function(e) { 27 | // Esc 28 | if (e.which === 27) this.close(); 29 | }.bind(this)); 30 | }; 31 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | Picker.prototype.dateTime = function(offsetHours) { 2 | offsetHours = offsetHours || 1; 3 | 4 | if (this.hasPrecedingPicker() || this.isEndPicker()) { 5 | offsetHours += 1; 6 | } 7 | 8 | return { 9 | date: moment().format(this.options.dateFormat), 10 | time: moment().add(offsetHours, 'hour').startOf('hour') 11 | .format(this.options.timeFormat) 12 | }; 13 | }; 14 | 15 | Picker.prototype.setDateTime = function(obj) { 16 | var date = obj.date, 17 | time = this.normalizeTime(obj.time), 18 | datetime; 19 | 20 | this._val = moment(new Date([date, time].join(' '))); 21 | 22 | // Reset the moment object if we got an invalid date 23 | if (!this._val.isValid()) { 24 | datetime = this.dateTime(); 25 | this._val = moment([datetime.date, datetime.time].join(' ')); 26 | } 27 | 28 | this.$date.val(this._val.format(this.options.dateFormat)); 29 | this.$time.val(this._val.format(this.options.timeFormat)); 30 | }; 31 | 32 | Picker.prototype.outputDateTime = function() { 33 | this.savedVal = this._val; 34 | formattedVal = this.formattedVal(); 35 | 36 | this.options.outputTo.val(formattedVal); 37 | 38 | if (this._initialized) { 39 | if (this.isInput()) { 40 | this.$el.trigger('change'); 41 | } 42 | 43 | this.options.onChange(); 44 | } 45 | }; 46 | 47 | Picker.prototype.unsetDateTime = function(obj) { 48 | this.options.outputTo.val(''); 49 | this.$el.trigger('datepicker.remove', this.options.outputTo); 50 | this.options.onRemove(); 51 | }; 52 | 53 | Picker.prototype.formattedVal = function() { 54 | if (!this.savedVal) return; 55 | return this.savedVal.format([this.options.dateFormat, this.options.timeFormat].join(' ')); 56 | }; 57 | 58 | Picker.prototype.normalizeTime = function(time) { 59 | // Normalize minutes 60 | if (!(/\d:\d{2}/).test(time)) { 61 | time = time.replace(/(^\d+)/, "$1:00"); 62 | } 63 | 64 | // Normalize spacing 65 | if (!(/\s[a|p]/i).test(time)) { 66 | time = time.replace(/(a|p)/i, " $1"); 67 | } 68 | 69 | // Normalize meridian 70 | if (!(/m/i).test(time)) { 71 | time = time.replace(/(a|p)/i, "$1m"); 72 | } 73 | 74 | return time; 75 | }; 76 | 77 | Picker.prototype.isInput = function() { 78 | return this.$el[0].tagName === 'INPUT'; 79 | }; 80 | 81 | Picker.prototype.serialize = function() { 82 | return { 83 | date: this.$date.val(), 84 | time: this.$time.val().toUpperCase() 85 | }; 86 | }; 87 | 88 | Picker.prototype.hasPrecedingPicker = function() { 89 | var dtp = this.$el.siblings('input').data(pluginName); 90 | if (dtp) return true; 91 | }; 92 | 93 | Picker.prototype.isEndPicker = function() { 94 | return !!this.$startPicker; 95 | }; 96 | 97 | Picker.prototype.isStartPicker = function() { 98 | return !!this.$endPicker; 99 | }; 100 | 101 | Picker.prototype.hasRange = function() { 102 | return !!this.range.length; 103 | } 104 | 105 | Picker.prototype.initializeRange = function(options) { 106 | var children = this.$el.find('input'); 107 | 108 | if (children.length !== 2) return []; 109 | 110 | return children.map(function(index) { 111 | var rangeOptions; 112 | 113 | if (index === 1) { 114 | rangeOptions = $.extend({ startPicker: $(children[0]) }, options); 115 | } else { 116 | rangeOptions = $.extend({ endPicker: $(children[1]) }, options); 117 | } 118 | return new Picker(this, rangeOptions); 119 | }); 120 | }; 121 | 122 | Picker.prototype.startPickerDate = function() { 123 | return new Date(this.$startPicker.val()); 124 | }; 125 | 126 | Picker.prototype.endPickerDate = function() { 127 | return new Date(this.$endPicker.val()); 128 | }; 129 | 130 | Picker.prototype.selectedMoment = function() { 131 | return moment(new Date(this.options.outputTo.val())); 132 | }; 133 | 134 | Picker.prototype.setTimeAfterStartPicker = function() { 135 | var startTime = this.startPickerDate(); 136 | var newEndTime = moment(startTime).add(this.options.defaultTimeRange); 137 | var currentEndTime = this.selectedMoment() 138 | 139 | // Don't update dateTime if the currentEndTime is already > than startTime 140 | if (currentEndTime > startTime) return; 141 | 142 | if (newEndTime.isValid()) { 143 | this.setDateTime({ 144 | date: newEndTime.format(this.options.dateFormat), 145 | time: newEndTime.format(this.options.timeFormat) 146 | }); 147 | this.outputDateTime(); 148 | this.updateCalendar(); 149 | } 150 | }; 151 | 152 | Picker.prototype.setTimeToBeforeEndPicker = function() { 153 | var endTime = this.endPickerDate(); 154 | var newStartTime = moment(endTime).subtract(this.options.defaultTimeRange); 155 | var currentStartTime = this.selectedMoment() 156 | 157 | // Don't update dateTime if the currentStartTime is already < than endTime 158 | if (currentStartTime < endTime) return; 159 | 160 | if (newStartTime.isValid()) { 161 | this.setDateTime({ 162 | date: newStartTime.format(this.options.dateFormat), 163 | time: newStartTime.format(this.options.timeFormat) 164 | }); 165 | this.outputDateTime(); 166 | this.updateCalendar(); 167 | } 168 | }; 169 | -------------------------------------------------------------------------------- /src/picker.js: -------------------------------------------------------------------------------- 1 | Picker = function(el, options) { 2 | this.$el = $(el); 3 | this._initialized = false; 4 | this.range = this.initializeRange(options); 5 | 6 | if (this.hasRange()) { 7 | this._initialized = true; 8 | return this; 9 | } 10 | 11 | // Options 12 | this.options = $.extend({ 13 | startDate: '-0d', 14 | dateFormat: 'MM/DD/YYYY', 15 | timeFormat: 'h:mm A', 16 | template: templates.datepicker, 17 | doneText: 'Save', 18 | removeText: 'Remove', 19 | prefill: false, 20 | defaultTimeRange: { hours: 1 }, 21 | outputTo: this.$el, 22 | onChange: function() {}, 23 | onRemove: function() {}, 24 | onInitialize: function() {} 25 | }, options); 26 | 27 | this.$startPicker = this.options.startPicker; 28 | this.$endPicker = this.options.endPicker; 29 | 30 | this.options.onChange = this.options.onChange.bind(this); 31 | this.options.onRemove = this.options.onRemove.bind(this); 32 | this.options.onInitialize = this.options.onInitialize.bind(this); 33 | 34 | // Events 35 | this.events = { 36 | 'click': this.onClick 37 | }; 38 | 39 | if (this.isInput()) { 40 | this.events = { 41 | 'focus': this.onClick 42 | }; 43 | } 44 | 45 | this.pickerEvents = { 46 | 'click .done': this.onDone, 47 | 'click .remove': this.onRemove 48 | }; 49 | 50 | this.startPickerEvents = { 51 | 'change': this.setTimeAfterStartPicker 52 | } 53 | 54 | this.endPickerEvents = { 55 | 'change': this.setTimeToBeforeEndPicker 56 | } 57 | 58 | // Convenience vars 59 | this.$body = $('body'); 60 | this.$picker = $(this.render()); 61 | this.$date = this.$picker.find('[name=date]'); 62 | this.$time = this.$picker.find('[name=time]'); 63 | 64 | // Standardize outputTo 65 | if (!this.options.outputTo) { 66 | this.options.outputTo = this.$el; 67 | } 68 | if (!this.options.outputTo.jquery) { 69 | this.options.outputTo = $(this.options.outputTo); 70 | } 71 | 72 | // Set current date and time 73 | var m = moment(new Date(this.options.outputTo.val())); 74 | this.setDateTime(this.dateTime()); 75 | 76 | if (m.isValid()) { 77 | this.setDateTime({ 78 | date: m.format(this.options.dateFormat), 79 | time: m.format(this.options.timeFormat) 80 | }); 81 | this.outputDateTime(); 82 | } 83 | 84 | // Prefill empty field 85 | if (this.options.prefill && !m.isValid()) { 86 | this.outputDateTime(); 87 | } 88 | 89 | // Delegate events 90 | this.delegateEvents(this.events, this.$el); 91 | this.delegateEvents(this.pickerEvents, this.$picker); 92 | this.handlePickerClose(); 93 | if (this.isEndPicker()) { 94 | this.delegateEvents(this.startPickerEvents, this.$startPicker); 95 | } else if (this.isStartPicker()) { 96 | this.delegateEvents(this.endPickerEvents, this.$endPicker); 97 | } 98 | 99 | // Initialize calendar picker 100 | this.initializeCalendar(); 101 | 102 | this.options.onInitialize(); 103 | this.$el.trigger('initialize'); 104 | 105 | this._initialized = true; 106 | return this; 107 | }; 108 | 109 | Picker.prototype.onClick = function(e) { 110 | e.preventDefault(); 111 | 112 | this.show(); 113 | this.$date.focus(); 114 | }; 115 | 116 | Picker.prototype.onChangeDate = function() { 117 | this.setDateTime(this.serialize()); 118 | this.outputDateTime(); 119 | this.updateCalendar(); 120 | }; 121 | 122 | Picker.prototype.onChangeTime = function(e) { 123 | this.setDateTime(this.serialize()); 124 | }; 125 | 126 | Picker.prototype.onDone = function(e) { 127 | e.preventDefault(); 128 | this.close(); 129 | this.onChangeDate(); 130 | }; 131 | 132 | Picker.prototype.onRemove = function(e) { 133 | e.preventDefault(); 134 | delete this.savedVal; 135 | this.close(); 136 | this.unsetDateTime(); 137 | }; 138 | -------------------------------------------------------------------------------- /src/plugin.js: -------------------------------------------------------------------------------- 1 | $.fn[pluginName] = function (options) { 2 | this.each(function() { 3 | if (!$.data(this, pluginName)) { 4 | $.data(this, pluginName, new Picker(this, options)); 5 | } 6 | }); 7 | return this; 8 | }; 9 | -------------------------------------------------------------------------------- /src/wrapper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * datepicker.js <%= version %> 3 | * https://github.com/Zeumo/datepicker.js 4 | * 5 | * /!\ Don't edit this file directly! 6 | * It was generated by the datepicker.js build system. 7 | */ 8 | 9 | (function(window) { 10 | var Picker, pluginName = 'picker', templates = {}; 11 | 12 | var t = function(s,d){ 13 | for(var p in d) s=s.replace(new RegExp('{'+p+'}','g'), d[p]); 14 | return s; 15 | }; 16 | 17 | <%= contents %> 18 | 19 | window.Picker = Picker; 20 | }(this)); 21 | --------------------------------------------------------------------------------