1 // ==========================================================================
  2 // Project:   The M-Project - Mobile HTML5 Application Framework
  3 // Copyright: (c) 2010 M-Way Solutions GmbH. All rights reserved.
  4 //            (c) 2011 panacoda GmbH. All rights reserved.
  5 // Creator:   Dominik
  6 // Date:      04.02.2011
  7 // License:   Dual licensed under the MIT or GPL Version 2 licenses.
  8 //            http://github.com/mwaylabs/The-M-Project/blob/master/MIT-LICENSE
  9 //            http://github.com/mwaylabs/The-M-Project/blob/master/GPL-LICENSE
 10 // ==========================================================================
 11 
 12 /**
 13  * @class
 14  *
 15  * This defines the prototype for a date picker view. A date picker is a special view, that can
 16  * be called out of a controller. It is shown as a date picker popup, based on the mobiscroll
 17  * library. You can either connect a date picker with an existing view and automatically pass
 18  * the selected date to the source's value property, or you can simply use the date picker to
 19  * select a date, return it to the controller (respectively the callback) and handle the date
 20  * by yourself.
 21  *
 22  * @extends M.View
 23  */
 24 M.DatePickerView = M.View.extend(
 25 /** @scope M.DatePickerView.prototype */ {
 26 
 27     /**
 28      * The type of this object.
 29      *
 30      * @type String
 31      */
 32     type: 'M.DatePickerView',
 33 
 34     /**
 35      * This property is used to link the date picker to a source. You can either pass the DOM id of
 36      * the corresponding source or the javascript object itself. Linking the date picker directly
 37      * to a source results in automatic value updates of this source.
 38      *
 39      * Note: Valid sources need to provide a setValue() method.
 40      *
 41      * If you do not pass a source, the date picker isn't linked to any view. It simply returns the
 42      * selected value/date to given callbacks. So you can call the date picker out of a controller
 43      * and handle the selected date all by yourself.
 44      *
 45      * @type String|Object
 46      */
 47     source: null,
 48 
 49     /**
 50      * This property can be used to specify several callbacks for the date picker view. There are
 51      * three types of callbacks available:
 52      *
 53      *     - before
 54      *         This callback gets called, right before the date picker is shown. It passes along two
 55      *         parameters:
 56      *             - value      -> The initial date of the date picker, formatted as a string
 57      *             - date       -> The initial date of the date picker as d8 object
 58      *     - confirm
 59      *         This callback gets called, when a selected date was confirmed. It passes along two
 60      *         parameters:
 61      *             - value      -> The selected date of the date picker, formatted as a string
 62      *             - date       -> The selected date of the date picker as d8 object
 63      *     - cancel
 64      *         This callback gets called, when the cancel button is hit. It doesn't pass any
 65      *         parameters.
 66      *
 67      * Setting up one of those callbacks works the same as with other controls of The-M-Project. You
 68      * simply have to specify an object containing a target function, e.g.:
 69      *
 70      * callbacks: {
 71      *     confirm: {
 72      *         target: this,
 73      *         action: 'dateSelected'
 74      *     },
 75      *     cancel: {
 76      *         action: function() {
 77      *             // do something
 78      *         }
 79      *     }
 80      * }
 81      *
 82      * @type Object
 83      */
 84     callbacks: null,
 85 
 86     /**
 87      * This property can be used to specify the initial date for the date picker. If you use the
 88      * date picker without a source, this date is always picked as the initial date. If nothing is
 89      * specified, the current date will be displayed.
 90      *
 91      * If you use the date picker with a valid source, the initial date is picked as long as there
 92      * is no valid date available by the source. Once a date was selected and assigned to the source,
 93      * this is taken as initial date the next time the date picker is opened.
 94      *
 95      * @type Object|String
 96      */
 97     initialDate: null,
 98 
 99     /**
100      * This property can be used to determine whether to use the data source's value as initial date
101      * or not. If there is no source specified, this property is irrelevant.
102      *
103      * Note: If there is a source specified and this property is set to NO, the 'initialDate' property
104      * will be used anyway if there is no date value available for the source!
105      *
106      * @type Boolean
107      */
108     useSourceDateAsInitialDate: YES,
109 
110     /**
111      * This property can be used to specify whether to show scrollers for picking a date or not.
112      *
113      * Note: If both this and the 'showTimePicker' property are set to NO, no date picker will
114      * be shown!
115      *
116      * @type Boolean
117      */
118     showDatePicker: YES,
119 
120     /**
121      * This property can be used to specify whether to show scrollers for picking a time or not.
122      *
123      * Note: If both this and the 'showDatePicker' property are set to NO, no date picker will
124      * be shown!
125      *
126      * @type Boolean
127      */
128     showTimePicker: YES,
129 
130     /**
131      * This property can be used to specify whether or not to show labels above of the scrollers.
132      * If set to YES, the labels specified with the '...Label' properties are displayed above of
133      * the corresponding scroller.
134      *
135      * @type Boolean
136      */
137     showLabels: YES,
138 
139     /**
140      * This property specified the label shown above of the 'year' scroller.
141      *
142      * Note: This label is only shown if the 'showLabels' property is set to YES.
143      *
144      * @type String
145      */
146     yearLabel: 'Year',
147 
148     /**
149      * This property specified the label shown above of the 'month' scroller.
150      *
151      * Note: This label is only shown if the 'showLabels' property is set to YES.
152      *
153      * @type String
154      */
155     monthLabel: 'Month',
156 
157     /**
158      * This property specified the label shown above of the 'day' scroller.
159      *
160      * Note: This label is only shown if the 'showLabels' property is set to YES.
161      *
162      * @type String
163      */
164     dayLabel: 'Day',
165 
166     /**
167      * This property specified the label shown above of the 'hours' scroller.
168      *
169      * Note: This label is only shown if the 'showLabels' property is set to YES.
170      *
171      * @type String
172      */
173     hoursLabel: 'Hours',
174 
175     /**
176      * This property specified the label shown above of the 'minutes' scroller.
177      *
178      * Note: This label is only shown if the 'showLabels' property is set to YES.
179      *
180      * @type String
181      */
182     minutesLabel: 'Minutes',
183 
184     /**
185      * You can use this property to enable or disable the AM/PM scroller. If set to NO, the
186      * date picker will use the 24h format.
187      *
188      * @type Boolean
189      */
190     showAmPm: YES,
191 
192     /**
193      * This property can be used to specify the first year of the 'year' scroller. By default,
194      * this will be set to 20 years before the current year.
195      *
196      * @type Number
197      */
198     startYear: null,
199 
200     /**
201      * This property can be used to specify the last year of the 'year' scroller. By default,
202      * this will be set to 20 years after the current year.
203      *
204      * @type Number
205      */
206     endYear: null,
207 
208     /**
209      * This property can be used to customize the date format of the date picker. This is important
210      * if you use the date picker on a valid source since the date picker will then automatically
211      * push the selected date/datetime to the 'value' property of the source - based on this format.
212      *
213      * The possible keys:
214      *
215      *     - m      -> month (without leading zero)
216      *     - mm     -> month (two-digit)
217      *     - M      -> month name (short)
218      *     - MM     -> month name (long)
219      *     - d      -> day (without leading zero)
220      *     - d      -> day (two digit)
221      *     - D      -> day name (short)
222      *     - DD     -> day name (long)
223      *     - y      -> year (two digit)
224      *     - yy     -> year (four digit)
225      *
226      * @type String
227      */
228     dateFormat: 'M dd, yy',
229 
230     /**
231      * This property can be used to customize the date format of the date picker if it is associated
232      * with a text input with the type 'month'. It works the same as the dateFormat property.
233      *
234      * @type String
235      */
236     dateFormatMonthOnly: 'MM yy',
237 
238     /**
239      * This property can be used to customize the time format of the date picker. This is important
240      * if you use the date picker on a valid source since the date picker will then automatically
241      * push the selected time/datetime to the 'value' property of the source - based on this format.
242      *
243      * The possible keys:
244      *
245      *     - h      -> hours (without leading zero, 12h format)
246      *     - hh     -> hours (two-digit, 12h format)
247      *     - H      -> hours (without leading zero, 24h format)
248      *     - HH     -> hours (two-digit, 24h format)
249      *     - i      -> minutes (without leading zero)
250      *     - ii     -> minutes (two-digit)
251      *     - A      -> AM/PM
252      *
253      * @type String
254      */
255     timeFormat: 'h:ii A',
256 
257     /**
258      * This property determines the order and formating of the date scrollers. The following keys
259      * are possible:
260      *
261      *     - m      -> month (without leading zero)
262      *     - mm     -> month (two-digit)
263      *     - M      -> month name (short)
264      *     - MM     -> month name (long)
265      *     - d      -> day (without leading zero)
266      *     - d      -> day (two digit)
267      *     - y      -> year (two digit)
268      *     - yy     -> year (four digit)
269      *
270      * By default, we use this format: Mddyy
271      *
272      * @type String
273      */
274     dateOrder: 'Mddyy',
275 
276     /**
277      * This property determines the order and formating of the date scrollers if it is associated
278      * with an input field of type 'month'. It works the same as the dateOrder property.
279      *
280      * By default, we use this format: MMyy
281      *
282      * @type String
283      */
284     dateOrderMonthOnly: 'MMyy',
285 
286 
287 
288     /**
289      * This property specifies a list of full month names.
290      *
291      * @type Array
292      */
293     monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
294 
295     /**
296      * This property specifies a list of short month names.
297      *
298      * @type Array
299      */
300     monthNamesShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
301 
302     /**
303      * This property specifies a list of full day names.
304      *
305      * @type Array
306      */
307     dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
308 
309     /**
310      * This property specifies a list of short day names.
311      *
312      * @type Array
313      */
314     dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
315 
316     /**
317      * This property can be used to specify the label of the date picker's cancel button. By default
318      * it shows 'Cancel'.
319      *
320      * @type String
321      */
322     cancelButtonValue: 'Cancel',
323 
324     /**
325      * This property can be used to specify the label of the date picker's cancel button. By default
326      * it shows 'Ok'.
327      *
328      * @type String
329      */
330     confirmButtonValue: 'Ok',
331 
332     /**
333      * This property can be used to specify the steps between hours in the time / date-time picker.
334      *
335      * @type Number
336      */
337     stepHour: 1,
338 
339     /**
340      * This property can be used to specify the steps between minutes in the time / date-time picker.
341      *
342      * @type Number
343      */
344     stepMinute: 1,
345 
346     /**
347      * This property can be used to specify the steps between seconds in the time / date-time picker.
348      *
349      * @type Number
350      */
351     stepSecond: 1,
352 
353     /**
354      * This property can be used to activate the seconds wheel on a time/date-time picker.
355      *
356      * @type Boolean
357      */
358     seconds: NO,
359 
360     /**
361      * This property is used internally to indicate whether the current date picker works on a valid
362      * source or was called without one. This is important for stuff like auto-updating the source's
363      * DOM representation.
364      *
365      * @private
366      */
367     hasSource: YES,
368 
369     /**
370      * This property is used internally to state whether a value, respectively a date, was selected
371      * or not.
372      *
373      * @private
374      * @type Boolean
375      */
376     isValueSelected: NO,
377 
378     /**
379      * This property is used internally to state whether a the date picker is currently activated
380      * or not.
381      *
382      * @private
383      * @type Boolean
384      */
385     isActive: NO,
386 
387     /**
388      * This method is the only important method of a date picker view for 'the outside world'. From within
389      * an application, simply call this method and pass along an object, containing all the properties
390      * you want to set, different from default.
391      *
392      * A sample call:
393      *
394      * M.DatePickerView.show({
395      *     source: M.ViewManager.getView('mainPage', 'myTextField')
396      *     initialDate: D8.create('30.04.1985 10:30'),
397      *     callbacks: {
398      *          confirm: {
399      *              target: this,
400      *              action: function(value, date) {
401      *                  // do something...
402      *              }
403      *          }
404      *     }
405      * });
406      *
407      * @param obj
408      */
409     show: function(obj) {
410         var datepicker = M.DatePickerView.design(obj);
411 
412         /* if a datepicker is active already, return */
413         if(Object.getPrototypeOf(datepicker).isActive) {
414             return;
415         /* otherwise go on and set the flag to active */
416         } else {
417             Object.getPrototypeOf(datepicker).isActive = YES;
418         }
419 
420         /* check if it's worth the work at all */
421         if(!(datepicker.showDatePicker || datepicker.showTimePicker)) {
422             M.Logger.log('In order to use the M.DatepickerView, you have to set the \'showDatePicker\' or \'showTimePicker\' property to YES.', M.ERR);
423             return;
424         }
425 
426         /* calculate the default start and end years */
427         this.startYear = this.startYear ? this.startYear : D8.now().format('yyyy') - 20;
428         this.endYear = this.endYear ? this.endYear : D8.now().format('yyyy') + 20;
429 
430         /* check if we got a valid source */
431         if(datepicker.source) {
432             /* if we got a view, get its id */
433             datepicker.source = typeof(datepicker.source) === 'object' && datepicker.source.type ? datepicker.source.id : datepicker.source;
434 
435             var view = M.ViewManager.getViewById(datepicker.source);
436             if(view && typeof(view.setValue) === 'function' && $('#' + datepicker.source) && $('#' + datepicker.source).length > 0) {
437                 datepicker.init();
438             } else {
439                 M.Logger.log('The specified source for the M.DatepickerView is invalid!', M.ERR);
440             }
441         } else {
442             /* use default source (the current page) */
443             datepicker.hasSource = NO;
444             var page = M.ViewManager.getCurrentPage();
445             if(page) {
446                 datepicker.source = page.id;
447                 datepicker.init();
448             }
449         }
450     },
451 
452     /**
453      * This method is used internally to communicate with the mobiscroll library. It actually initializes
454      * the creation of the date picker and is responsible for reacting on events. If the cancel or confirm
455      * button is hit, this method dispatches the events to the corresponding callbacks.
456      *
457      * @private
458      */
459     init: function() {
460         var that = this;
461         $('#' + this.source).scroller({
462             preset: (this.showDatePicker && this.showTimePicker ? 'datetime' : (this.showDatePicker ? 'date' : (this.showTimePicker ? 'time' : null))),
463             ampm: this.showAmPm,
464             startYear: this.startYear,
465             endYear: this.endYear,
466             dateFormat: this.dateFormat,
467             timeFormat: this.timeFormat,
468             dateOrder: this.dateOrder,
469             dayText: this.dayLabel,
470             hourText: this.hoursLabel,
471             minuteText: this.minutesLabel,
472             monthText: this.monthLabel,
473             yearText: this.yearLabel,
474             monthNames: this.monthNames,
475             monthNamesShort: this.monthNamesShort,
476             dayNames: this.dayNames,
477             dayNamesShort: this.dayNamesShort,
478             cancelText: this.cancelButtonValue,
479             setText: this.confirmButtonValue,
480             stepHour: this.stepHour,
481             stepMinute: this.stepMinute,
482             stepSecond: this.stepSecond,
483             seconds: this.seconds,
484 
485             /* now set the width of the scrollers */
486             width: (M.Environment.getWidth() - 20) / 3 - 20 > 90 ? 90 : (M.Environment.getWidth() - 20) / 3 - 20,
487 
488             beforeShow: function(input, scroller) {
489                 that.bindToCaller(that, that.beforeShow, [input, scroller])();
490             },
491             onClose: function(value, scroller) {
492                 that.bindToCaller(that, that.onClose, [value, scroller])();
493             },
494             onSelect: function(value, scroller) {
495                 that.bindToCaller(that, that.onSelect, [value, scroller])();
496             }
497         });
498         $('#' + this.source).scroller('show');
499     },
500 
501     /**
502      * This method is used internally to handle the 'beforeShow' event. It does some adjustments to the
503      * rendered scroller by mobiscroll and finally calls the application's 'before' callback, if it is
504      * defined.
505      *
506      * @param source
507      * @param scroller
508      */
509     beforeShow: function(source, scroller) {
510         var source = null;
511         var date = null;
512 
513         /* try to set the date picker's initial date based on its source */
514         if(this.hasSource && this.useSourceDateAsInitialDate) {
515             source = M.ViewManager.getViewById(this.source);
516             if(source.value) {
517                 try {
518                     date = D8.create(source.value);
519                 } catch(e) {
520 
521                 }
522                 if(date) {
523                     if(date.format('yyyy') < this.startYear) {
524                         if(this.hasOwnProperty('startYear')) {
525                             M.Logger.log('The given date of the source (' + date.format('yyyy') + ') conflicts with the \'startYear\' property (' + this.startYear + ') and therefore will be ignored!', M.WARN);
526                         } else {
527                             M.Logger.log('The date picker\'s default \'startYear\' property (' + this.startYear + ') conflicts with the given date of the source (' + date.format('yyyy') + ') and therefore will be ignored!', M.WARN);
528                             $('#' + this.source).scroller('option', 'startYear', date.format('yyyy'));
529                             $('#' + this.source).scroller('setDate', date.date);
530                         }
531                     } else {
532                         $('#' + this.source).scroller('setDate', date.date);
533                     }
534                 }
535             }
536         }
537 
538         /* if there is no source or the retrieval of the date went wrong, try to set it based on the initial date property */
539         if(this.initialDate && !date) {
540             if(this.initialDate.date) {
541                 date = this.initialDate;
542             } else {
543                 try {
544                     date = D8.create(this.initialDate);
545                 } catch(e) {
546 
547                 }
548             }
549             if(date) {
550                 if(date.format('yyyy') < this.startYear) {
551                     if(this.hasOwnProperty('startYear')) {
552                         M.Logger.log('The specified initial date (' + date.format('yyyy') + ') conflicts with the \'startYear\' property (' + this.startYear + ') and therefore will be ignored!', M.WARN);
553                     } else {
554                         M.Logger.log('The date picker\'s default \'startYear\' property (' + this.startYear + ') conflicts with the specified initial date (' + date.format('yyyy') + ') and therefore will be ignored!', M.WARN);
555                         $('#' + this.source).scroller('option', 'startYear', date.format('yyyy'));
556                         $('#' + this.source).scroller('setDate', date.date);
557                     }
558                 } else {
559                     $('#' + this.source).scroller('setDate', date.date);
560                 }
561             }
562         }
563 
564         /* now we got the date (or use the current date as default), lets compute this as a formatted text for the callback */
565         value = scroller.formatDate(
566             this.showDatePicker ? this.dateFormat + (this.showTimePicker ? ' ' + this.timeFormat : '') : this.timeFormat,
567             scroller.getDate()
568         );
569 
570         /* kill parts of the scoller */
571         $('.dwv').remove();
572 
573         /* inject TMP buttons*/
574         var confirmButton = M.ButtonView.design({
575             value: this.confirmButtonValue,
576             cssClass: 'b tmp-dialog-smallerbtn-confirm',
577             events: {
578                 tap: {
579                     action: function() {
580                         $('#dw_set').trigger('click');
581                     }
582                 }
583             }
584         });
585         var cancelButton = M.ButtonView.design({
586             value: this.cancelButtonValue,
587             cssClass: 'd tmp-dialog-smallerbtn-confirm',
588             events: {
589                 tap: {
590                     action: function() {
591                         $('#dw_cancel').trigger('click');
592                     }
593                 }
594             }
595         });
596 
597         if(this.showDatePicker) {
598             var grid = M.GridView.design({
599                 childViews: 'confirm cancel',
600                 layout: M.TWO_COLUMNS,
601                 cssClass: 'tmp-datepicker-buttongrid',
602                 confirm: confirmButton,
603                 cancel: cancelButton
604             });
605 
606             var html = grid.render();
607             $('.dw').append(html);
608             grid.theme();
609             grid.registerEvents();
610         } else {
611             var html = confirmButton.render();
612             html += cancelButton.render();
613             $('.dw').append(html);
614             confirmButton.theme();
615             confirmButton.registerEvents();
616             cancelButton.theme();
617             cancelButton.registerEvents();
618         }
619 
620         /* hide default buttons */
621         $('#dw_cancel').hide();
622         $('#dw_set').hide();
623 
624         /* add class to body as selector for showing/hiding labels */
625         if(!this.showLabels) {
626             $('body').addClass('tmp-datepicker-no-label');
627         }
628 
629         /* call callback */
630         if(this.callbacks && M.EventDispatcher.checkHandler(this.callbacks['before'])) {
631             M.EventDispatcher.callHandler(this.callbacks['before'], null, NO, [value, date]);
632         }
633     },
634 
635     onClose: function(value, scroller) {
636         /* set value if one was selected */
637         var source = null;
638         var date = null;
639         if(this.isValueSelected) {
640             /* first compute the date */
641             try {
642                 date = D8.create(scroller.getDate());
643             } catch(e) {
644 
645             }
646 
647             /* now, if there is a source, auto-update its value */
648             if(this.hasSource) {
649                 source = M.ViewManager.getViewById(this.source);
650                 if(source) {
651                     source.setValue(value, NO, YES);
652                 }
653             }
654         }
655 
656         /* remove class from body as selector for showing/hiding labels */
657         if(!this.showLabels) {
658             $('body').removeClass('tmp-datepicker-no-label');
659         }
660 
661         /* call cancel callback */
662         if(!this.isValueSelected && this.callbacks && M.EventDispatcher.checkHandler(this.callbacks['cancel'])) {
663             M.EventDispatcher.callHandler(this.callbacks['cancel'], null, NO, []);
664         } else if(this.isValueSelected && this.callbacks && M.EventDispatcher.checkHandler(this.callbacks['confirm'])) {
665             M.EventDispatcher.callHandler(this.callbacks['confirm'], null, NO, [value, date]);
666         }
667 
668         /* kill the datepicker */
669         Object.getPrototypeOf(this).isActive = NO;
670         $('#' + this.source).scroller('destroy');
671         $('.dwo').remove();
672         $('.dw').remove();
673         this.destroy();
674     },
675 
676     onSelect: function(value) {
677         /* mark the datepicker as 'valueSelected' */
678         this.isValueSelected = YES;
679     }
680 
681 });