├── .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 | 
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"] = "
";
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 |
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 |
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 |
--------------------------------------------------------------------------------