From e772e87de5c678fd12e1eb68bfbced97fb90072f Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Sun, 24 Apr 2016 09:33:39 +1000 Subject: [PATCH] Complete rebuild of datepicker form widget The datepicker now handles timezones and locale mainly on the client side. When a user selects a date/time, the value is chosen in their timezone preference, the script will then convert the value to the application timezone (UTC) for storage in the database. The reverse is true: when the value is loaded, it is converted from UTC to the user preference. The entire process is seamless. Dates are also formatted in the locale preference. Example scenario: This fixes the issue when selecting the blog post published date. In some cases, the date could be set to 24th April but the server time is 23rd April, so the post appears unpublished against the user's intent. There is still some issues around DATE and TIME column types stored in the database. It is best to always use TIMESTAMP/DATETIME to retain the timezone conversions. DATE will reset the time to 00:00 UTC which can cause issues and TIME does not play nicely with Carbon at all. We should still try to add support for these columns in the datepicker, even though they are not recommended. --- modules/backend/assets/js/october-min.js | 2 +- modules/backend/assets/js/october.datetime.js | 3 +- modules/backend/formwidgets/DatePicker.php | 69 ++-- .../datepicker/assets/js/build-min.js | 132 +++++--- .../formwidgets/datepicker/assets/js/build.js | 1 - .../datepicker/assets/js/datepicker.js | 295 +++++++++++++++--- .../datepicker/assets/js/timepicker.js | 75 ----- .../clockpicker/js/jquery-clockpicker.js | 15 +- .../datepicker/partials/_datepicker.htm | 119 ++----- .../datepicker/partials/_picker_date.htm | 11 + .../datepicker/partials/_picker_time.htm | 11 + 11 files changed, 425 insertions(+), 308 deletions(-) delete mode 100644 modules/backend/formwidgets/datepicker/assets/js/timepicker.js create mode 100644 modules/backend/formwidgets/datepicker/partials/_picker_date.htm create mode 100644 modules/backend/formwidgets/datepicker/partials/_picker_time.htm diff --git a/modules/backend/assets/js/october-min.js b/modules/backend/assets/js/october-min.js index 2df20ecf4..b85128ca4 100644 --- a/modules/backend/assets/js/october-min.js +++ b/modules/backend/assets/js/october-min.js @@ -1732,7 +1732,7 @@ this.appTimezone=$('meta[name="app-timezone"]').attr('content') if(!this.appTimezone){this.appTimezone='UTC'}} DateTimeConverter.prototype.getDateTimeValue=function(){this.datetime=this.$el.attr('datetime') var momentObj=moment.tz(this.datetime,this.appTimezone),result -if(this.options.locale){moment.locale(this.options.locale)} +if(this.options.locale){momentObj=momentObj.locale(this.options.locale)} if(this.options.timezone){momentObj=momentObj.tz(this.options.timezone)} if(this.options.timeSince){result=momentObj.fromNow()} else if(this.options.timeTense){result=momentObj.calendar()} diff --git a/modules/backend/assets/js/october.datetime.js b/modules/backend/assets/js/october.datetime.js index 5437ad9d8..4a93201e3 100644 --- a/modules/backend/assets/js/october.datetime.js +++ b/modules/backend/assets/js/october.datetime.js @@ -81,7 +81,7 @@ result if (this.options.locale) { - moment.locale(this.options.locale) + momentObj = momentObj.locale(this.options.locale) } if (this.options.timezone) { @@ -164,7 +164,6 @@ return this } - // Add this only if required $(document).render(function (){ $('time[data-datetime-control]').dateTimeConverter() }) diff --git a/modules/backend/formwidgets/DatePicker.php b/modules/backend/formwidgets/DatePicker.php index 8c8824e61..a2179d513 100644 --- a/modules/backend/formwidgets/DatePicker.php +++ b/modules/backend/formwidgets/DatePicker.php @@ -3,6 +3,7 @@ use Carbon\Carbon; use Backend\Classes\FormField; use Backend\Classes\FormWidgetBase; +use System\Helpers\DateTime as DateTimeHelper; /** * Date picker @@ -13,17 +14,10 @@ use Backend\Classes\FormWidgetBase; */ class DatePicker extends FormWidgetBase { - const TIME_PREFIX = '___time_'; - // // Configurable properties // - /** - * @var string Display format. - */ - public $format = 'YYYY-MM-DD'; - /** * @var bool Display mode: datetime, date, time. */ @@ -93,45 +87,42 @@ class DatePicker extends FormWidgetBase { $this->vars['name'] = $this->formField->getName(); - $this->vars['timeName'] = self::TIME_PREFIX.$this->formField->getName(false); - $this->vars['timeValue'] = null; if ($value = $this->getLoadValue()) { - + $value = $value instanceof Carbon ? $value->toDateTimeString() : $value; /* * Date / Time */ - if ($this->mode == 'datetime') { - if (is_object($value)) { - $value = $value->toDateTimeString(); - } + // if ($this->mode == 'datetime') { + // if (is_object($value)) { + // $value = $value->toDateTimeString(); + // } - $dateTime = explode(' ', $value); - $value = $dateTime[0]; - $this->vars['timeValue'] = isset($dateTime[1]) ? substr($dateTime[1], 0, 5) : ''; - } + // $dateTime = explode(' ', $value); + // $value = $dateTime[0]; + // $this->vars['timeValue'] = isset($dateTime[1]) ? substr($dateTime[1], 0, 5) : ''; + // } /* * Date */ - elseif ($this->mode == 'date') { - if (is_string($value)) { - $value = substr($value, 0, 10); - } - elseif (is_object($value)) { - $value = $value->toDateString(); - } - } - elseif ($this->mode == 'time') { - if (is_object($value)) { - $value = $value->toTimeString(); - } - } + // elseif ($this->mode == 'date') { + // if (is_string($value)) { + // $value = substr($value, 0, 10); + // } + // elseif (is_object($value)) { + // $value = $value->toDateString(); + // } + // } + // elseif ($this->mode == 'time') { + // if (is_object($value)) { + // $value = $value->toTimeString(); + // } + // } } $this->vars['value'] = $value ?: ''; $this->vars['field'] = $this->formField; - $this->vars['format'] = $this->format; $this->vars['mode'] = $this->mode; $this->vars['minDate'] = $this->minDate; $this->vars['maxDate'] = $this->maxDate; @@ -161,13 +152,13 @@ class DatePicker extends FormWidgetBase return null; } - $timeValue = post(self::TIME_PREFIX . $this->formField->getName(false)); - if ($this->mode == 'datetime' && $timeValue) { - $value .= ' ' . $timeValue . ':00'; - } - elseif ($this->mode == 'time') { - $value = substr($value, 0, 5) . ':00'; - } + // $timeValue = post(self::TIME_PREFIX . $this->formField->getName(false)); + // if ($this->mode == 'datetime' && $timeValue) { + // $value .= ' ' . $timeValue . ':00'; + // } + // elseif ($this->mode == 'time') { + // $value = substr($value, 0, 5) . ':00'; + // } return $value; } diff --git a/modules/backend/formwidgets/datepicker/assets/js/build-min.js b/modules/backend/formwidgets/datepicker/assets/js/build-min.js index 9a0d02c5f..d13d83722 100644 --- a/modules/backend/formwidgets/datepicker/assets/js/build-min.js +++ b/modules/backend/formwidgets/datepicker/assets/js/build-min.js @@ -152,7 +152,7 @@ plate.prepend(canvas);clearTimeout(movingTimer);$body.removeClass('clockpicker-m if(svgSupported){var canvas=popover.find('.clockpicker-canvas'),svg=createSvgElement('svg');svg.setAttribute('class','clockpicker-svg');svg.setAttribute('width',diameter);svg.setAttribute('height',diameter);var g=createSvgElement('g');g.setAttribute('transform','translate('+dialRadius+','+dialRadius+')');var bearing=createSvgElement('circle');bearing.setAttribute('class','clockpicker-canvas-bearing');bearing.setAttribute('cx',0);bearing.setAttribute('cy',0);bearing.setAttribute('r',2);var hand=createSvgElement('line');hand.setAttribute('x1',0);hand.setAttribute('y1',0);var bg=createSvgElement('circle');bg.setAttribute('class','clockpicker-canvas-bg');bg.setAttribute('r',tickRadius);var fg=createSvgElement('circle');fg.setAttribute('class','clockpicker-canvas-fg');fg.setAttribute('r',3.5);g.appendChild(hand);g.appendChild(bg);g.appendChild(fg);g.appendChild(bearing);svg.appendChild(g);canvas.append(svg);this.hand=hand;this.bg=bg;this.fg=fg;this.bearing=bearing;this.g=g;this.canvas=canvas;} raiseCallback(this.options.init);} function raiseCallback(callbackFunction){if(callbackFunction&&typeof callbackFunction==="function"){callbackFunction();}} -ClockPicker.DEFAULTS={'default':'',fromnow:0,placement:'bottom',align:'left',donetext:'完成',autoclose:false,twelvehour:false,vibrate:true};ClockPicker.prototype.toggle=function(){this[this.isShown?'hide':'show']();};ClockPicker.prototype.locate=function(){var element=this.element,popover=this.popover,offset=element.offset(),width=element.outerWidth(),height=element.outerHeight(),placement=this.options.placement,align=this.options.align,styles={},self=this;popover.show();switch(placement){case'bottom':styles.top=offset.top+height;break;case'right':styles.left=offset.left+width;break;case'top':styles.top=offset.top-popover.outerHeight();break;case'left':styles.left=offset.left-popover.outerWidth();break;} +ClockPicker.DEFAULTS={'default':'',fromnow:0,placement:'bottom',align:'left',donetext:'Done',autoclose:false,twelvehour:false,vibrate:true};ClockPicker.prototype.toggle=function(){this[this.isShown?'hide':'show']();};ClockPicker.prototype.locate=function(){var element=this.element,popover=this.popover,offset=element.offset(),width=element.outerWidth(),height=element.outerHeight(),placement=this.options.placement,align=this.options.align,styles={},self=this;popover.show();switch(placement){case'bottom':styles.top=offset.top+height;break;case'right':styles.left=offset.left+width;break;case'top':styles.top=offset.top-popover.outerHeight();break;case'left':styles.left=offset.left-popover.outerWidth();break;} switch(align){case'left':styles.left=offset.left;break;case'right':styles.left=offset.left+width-popover.outerWidth();break;case'top':styles.top=offset.top;break;case'bottom':styles.top=offset.top+height-popover.outerHeight();break;} popover.css(styles);};ClockPicker.prototype.show=function(e){if(this.isShown){return;} raiseCallback(this.options.beforeShow);var self=this;if(!this.isAppended){$body=$(document.body).append(this.popover);$win.on('resize.clockpicker'+this.id,function(){if(self.isShown){self.locate();}});this.isAppended=true;} @@ -167,54 +167,100 @@ if(value===60){value=0;}}} if(this[this.currentView]!==value){if(vibrate&&this.options.vibrate){if(!this.vibrateTimer){navigator[vibrate](10);this.vibrateTimer=setTimeout($.proxy(function(){this.vibrateTimer=null;},this),100);}}} this[this.currentView]=value;this[isHours?'spanHours':'spanMinutes'].html(leadingZero(value));if(!svgSupported){this[isHours?'hoursView':'minutesView'].find('.clockpicker-tick').each(function(){var tick=$(this);tick.toggleClass('active',value===+tick.html());});return;} if(dragging||(!isHours&&value%5)){this.g.insertBefore(this.hand,this.bearing);this.g.insertBefore(this.bg,this.fg);this.bg.setAttribute('class','clockpicker-canvas-bg clockpicker-canvas-bg-trans');}else{this.g.insertBefore(this.hand,this.bg);this.g.insertBefore(this.fg,this.bg);this.bg.setAttribute('class','clockpicker-canvas-bg');} -var cx=Math.sin(radian)*radius,cy=-Math.cos(radian)*radius;this.hand.setAttribute('x2',cx);this.hand.setAttribute('y2',cy);this.bg.setAttribute('cx',cx);this.bg.setAttribute('cy',cy);this.fg.setAttribute('cx',cx);this.fg.setAttribute('cy',cy);};ClockPicker.prototype.done=function(){raiseCallback(this.options.beforeDone);this.hide();var last=this.input.prop('value'),value=leadingZero(this.hours)+':'+leadingZero(this.minutes);if(this.options.twelvehour){value=value+this.amOrPm;} +var cx=Math.sin(radian)*radius,cy=-Math.cos(radian)*radius;this.hand.setAttribute('x2',cx);this.hand.setAttribute('y2',cy);this.bg.setAttribute('cx',cx);this.bg.setAttribute('cy',cy);this.fg.setAttribute('cx',cx);this.fg.setAttribute('cy',cy);};ClockPicker.prototype.done=function(){raiseCallback(this.options.beforeDone);this.hide();var last=this.input.prop('value'),value=leadingZero(this.hours)+':'+leadingZero(this.minutes);if(this.options.twelvehour){value=value+' '+this.amOrPm;} this.input.prop('value',value);if(value!==last){this.input.triggerHandler('change');if(!this.isInput){this.element.trigger('change');}} if(this.options.autoclose){this.input.trigger('blur');} raiseCallback(this.options.afterDone);};ClockPicker.prototype.remove=function(){this.element.removeData('clockpicker');this.input.off('focus.clockpicker click.clockpicker');this.addon.off('click.clockpicker');if(this.isShown){this.hide();} -if(this.isAppended){$win.off('resize.clockpicker'+this.id);this.popover.remove();}};$.fn.clockpicker=function(option){var args=Array.prototype.slice.call(arguments,1);return this.each(function(){var $this=$(this),data=$this.data('clockpicker');if(!data){var options=$.extend({},ClockPicker.DEFAULTS,$this.data(),typeof option=='object'&&option);$this.data('clockpicker',new ClockPicker($this,options));}else{if(typeof data[option]==='function'){data[option].apply(data,args);}}});};}());+function($){"use strict";var DatePicker=function(element,options){var self=this -this.options=options -this.$el=$(element) -this.$input=this.$el.find('input:first') -var $form=this.$el.closest('form'),changeMonitor=$form.data('oc.changeMonitor') -if(changeMonitor!==undefined) -changeMonitor.pause() -var pikadayOptions={yearRange:options.yearRange,format:options.format,setDefaultDate:moment(this.$input.val()).toDate(),i18n:$.oc.lang.get('datepicker'),onOpen:function(){var $field=$(this._o.trigger) -$(this.el).css({left:'auto',right:$(window).width()-$field.offset().left-$field.outerWidth()})}} -if(options.minDate){pikadayOptions.minDate=new Date(options.minDate)} -if(options.maxDate){pikadayOptions.maxDate=new Date(options.maxDate)} -this.$input.pikaday(pikadayOptions) -if(changeMonitor!==undefined) -changeMonitor.resume()} +if(this.isAppended){$win.off('resize.clockpicker'+this.id);this.popover.remove();}};$.fn.clockpicker=function(option){var args=Array.prototype.slice.call(arguments,1);return this.each(function(){var $this=$(this),data=$this.data('clockpicker');if(!data){var options=$.extend({},ClockPicker.DEFAULTS,$this.data(),typeof option=='object'&&option);$this.data('clockpicker',new ClockPicker($this,options));}else{if(typeof data[option]==='function'){data[option].apply(data,args);}}});};}());+function($){"use strict";var Base=$.oc.foundation.base,BaseProto=Base.prototype +var DatePicker=function(element,options){this.$el=$(element) +this.options=options||{} +$.oc.foundation.controlUtils.markDisposable(element) +Base.call(this) +this.init()} +DatePicker.prototype=Object.create(BaseProto) +DatePicker.prototype.constructor=DatePicker +DatePicker.prototype.init=function(){var self=this,$form=this.$el.closest('form'),changeMonitor=$form.data('oc.changeMonitor') +if(changeMonitor!==undefined){changeMonitor.pause()} +this.dbDateTimeFormat='YYYY-MM-DD HH:mm:ss' +this.dbDateFormat='YYYY-MM-DD' +this.dbTimeFormat='HH:mm:ss' +this.$dataLocker=$('[data-datetime-value]',this.$el) +this.$datePicker=$('[data-datepicker]',this.$el) +this.$timePicker=$('[data-timepicker]',this.$el) +this.hasDate=!!this.$datePicker.length +this.hasTime=!!this.$timePicker.length +this.initRegion() +if(this.hasDate){this.initDatePicker()} +if(this.hasTime){this.initTimePicker()} +if(changeMonitor!==undefined){changeMonitor.resume()} +this.$timePicker.on('change.oc.datepicker',function(){if(!$.trim($(this).val())){self.emptyValues()}}) +this.$datePicker.on('change.oc.datepicker',function(){if(!$.trim($(this).val())){self.emptyValues()}}) +this.$el.one('dispose-control',this.proxy(this.dispose))} +DatePicker.prototype.dispose=function(){this.$timePicker.off('change.oc.datepicker') +this.$datePicker.off('change.oc.datepicker') +this.$el.off('dispose-control',this.proxy(this.dispose)) +this.$el.removeData('oc.datePicker') +this.$el=null +this.options=null +BaseProto.dispose.call(this)} +DatePicker.prototype.initDatePicker=function(){var self=this +var pikadayOptions={yearRange:this.options.yearRange,format:this.getDateFormat(),setDefaultDate:moment().tz(this.timezone).format('l'),i18n:$.oc.lang.get('datepicker'),onOpen:function(){var $field=$(this._o.trigger) +$(this.el).css({left:'auto',right:$(window).width()-$field.offset().left-$field.outerWidth()})},onSelect:function(){self.onSelectDatePicker.call(self,this.getMoment())}} +this.$datePicker.val(this.getDataLockerValue('l')) +if(this.options.minDate){pikadayOptions.minDate=new Date(this.options.minDate)} +if(this.options.maxDate){pikadayOptions.maxDate=new Date(this.options.maxDate)} +this.$datePicker.pikaday(pikadayOptions)} +DatePicker.prototype.onSelectDatePicker=function(pickerMoment){var pickerValue=pickerMoment.format(this.dbDateFormat) +var timeValue=this.getTimePickerValue() +var momentObj=moment.tz(pickerValue+' '+timeValue,this.dbDateTimeFormat,this.timezone).tz(this.appTimezone) +var lockerValue=momentObj.format(this.dbDateTimeFormat) +this.$dataLocker.val(lockerValue)} +DatePicker.prototype.getDatePickerValue=function(){var value=this.$datePicker.val() +if(!this.hasDate||!value){return moment.tz(this.appTimezone).tz(this.timezone).format(this.dbDateFormat)} +return moment(value,this.getDateFormat()).format(this.dbDateFormat)} +DatePicker.prototype.getDateFormat=function(){var format=this.options.format +if(this.locale){format=moment().locale(this.locale).localeData().longDateFormat('l')} +return format} +DatePicker.prototype.initTimePicker=function(){this.$timePicker.clockpicker({autoclose:'true',placement:'bottom',align:'right',twelvehour:this.isTimeTwelveHour(),afterDone:this.proxy(this.onSelectTimePicker)}) +this.$timePicker.val(this.getDataLockerValue(this.getTimeFormat()))} +DatePicker.prototype.onSelectTimePicker=function(){var pickerValue=this.$timePicker.val() +var timeValue=moment(pickerValue,this.getTimeFormat()).format(this.dbTimeFormat) +var dateValue=this.getDatePickerValue() +var momentObj=moment.tz(dateValue+' '+timeValue,this.dbDateTimeFormat,this.timezone).tz(this.appTimezone) +var lockerValue=momentObj.format(this.dbDateTimeFormat) +this.$dataLocker.val(lockerValue)} +DatePicker.prototype.getTimePickerValue=function(){var value=this.$timePicker.val() +if(!this.hasTime||!value){return moment.tz(this.appTimezone).tz(this.timezone).format(this.dbTimeFormat)} +return moment(value,this.getTimeFormat()).format(this.dbTimeFormat)} +DatePicker.prototype.getTimeFormat=function(){return this.isTimeTwelveHour()?'hh:mm A':'HH:mm'} +DatePicker.prototype.isTimeTwelveHour=function(){var momentObj=moment() +if(this.locale){momentObj=momentObj.locale(this.locale)} +return momentObj.localeData().longDateFormat('LT').indexOf('A')!==-1;} +DatePicker.prototype.emptyValues=function(){this.$dataLocker.val('') +this.$datePicker.val('') +this.$timePicker.val('')} +DatePicker.prototype.getDataLockerValue=function(format){var value=this.$dataLocker.val() +return value?this.getMomentLoadValue(value,format):null} +DatePicker.prototype.getMomentLoadValue=function(value,format){var momentObj=moment.tz(value,this.appTimezone) +if(this.locale){momentObj=momentObj.locale(this.locale)} +momentObj=momentObj.tz(this.timezone) +return momentObj.format(format)} +DatePicker.prototype.initRegion=function(){this.locale=$('meta[name="backend-locale"]').attr('content') +this.timezone=$('meta[name="backend-timezone"]').attr('content') +this.appTimezone=$('meta[name="app-timezone"]').attr('content') +if(!this.appTimezone){this.appTimezone='UTC'} +if(!this.timezone){this.timezone='UTC'}} DatePicker.DEFAULTS={minDate:null,maxDate:null,format:'YYYY-MM-DD',yearRange:10} var old=$.fn.datePicker -$.fn.datePicker=function(option){var args=Array.prototype.slice.call(arguments,1) -return this.each(function(){var $this=$(this) -var data=$this.data('oc.datepicker') +$.fn.datePicker=function(option){var args=Array.prototype.slice.call(arguments,1),items,result +items=this.each(function(){var $this=$(this) +var data=$this.data('oc.datePicker') var options=$.extend({},DatePicker.DEFAULTS,$this.data(),typeof option=='object'&&option) -if(!data)$this.data('oc.datepicker',(data=new DatePicker(this,options))) -else if(typeof option=='string')data[option].apply(data,args)})} +if(!data)$this.data('oc.datePicker',(data=new DatePicker(this,options))) +if(typeof option=='string')result=data[option].apply(data,args) +if(typeof result!='undefined')return false}) +return result?result:items} $.fn.datePicker.Constructor=DatePicker $.fn.datePicker.noConflict=function(){$.fn.datePicker=old return this} -$(document).on('render',function(){$('[data-control="datepicker"]').datePicker()});}(window.jQuery);+function($){"use strict";var TimePicker=function(element,options){var self=this -this.options=options -this.$el=$(element) -this.$input=this.$el.find('input:first') -var $form=this.$el.closest('form'),changeMonitor=$form.data('oc.changeMonitor') -if(changeMonitor!==undefined) -changeMonitor.pause() -this.$input.clockpicker() -if(changeMonitor!==undefined) -changeMonitor.resume()} -TimePicker.DEFAULTS={} -var old=$.fn.timePicker -$.fn.timePicker=function(option){var args=Array.prototype.slice.call(arguments,1) -return this.each(function(){var $this=$(this) -var data=$this.data('oc.timepicker') -var options=$.extend({},TimePicker.DEFAULTS,$this.data(),typeof option=='object'&&option) -if(!data)$this.data('oc.timepicker',(data=new TimePicker(this,options))) -else if(typeof option=='string')data[option].apply(data,args)})} -$.fn.timePicker.Constructor=TimePicker -$.fn.timePicker.noConflict=function(){$.fn.timePicker=old -return this} -$(document).on('render',function(){$('[data-control="timepicker"]').timePicker()});}(window.jQuery); \ No newline at end of file +$(document).on('render',function(){$('[data-control="datepicker"]').datePicker()});}(window.jQuery); \ No newline at end of file diff --git a/modules/backend/formwidgets/datepicker/assets/js/build.js b/modules/backend/formwidgets/datepicker/assets/js/build.js index ade2af3d0..08dc42101 100644 --- a/modules/backend/formwidgets/datepicker/assets/js/build.js +++ b/modules/backend/formwidgets/datepicker/assets/js/build.js @@ -11,6 +11,5 @@ =require ../vendor/pikaday/js/pikaday.jquery.js =require ../vendor/clockpicker/js/jquery-clockpicker.js =require datepicker.js -=require timepicker.js */ diff --git a/modules/backend/formwidgets/datepicker/assets/js/datepicker.js b/modules/backend/formwidgets/datepicker/assets/js/datepicker.js index 0b8ebf63d..2fb27b0bd 100644 --- a/modules/backend/formwidgets/datepicker/assets/js/datepicker.js +++ b/modules/backend/formwidgets/datepicker/assets/js/datepicker.js @@ -1,45 +1,90 @@ -/* - * DatePicker plugin - * - * Data attributes: - * - data-control="datepicker" - enables the plugin on an element - * - data-format="value" - display format - * - data-min-date="value" - minimum date to allow - * - data-max-date="value" - maximum date to allow - * - data-year-range="value" - range of years to display - * - * JavaScript API: - * $('a#someElement').datePicker({ option: 'value' }) - * - * Dependences: - * - Pikaday plugin (pikaday.js) - * - Pikaday jQuery addon (pikaday.jquery.js) - * - Moment library (moment.js) - */ - +function ($) { "use strict"; + var Base = $.oc.foundation.base, + BaseProto = Base.prototype - // DATEPICKER CLASS DEFINITION - // ============================ + var DatePicker = function (element, options) { + this.$el = $(element) + this.options = options || {} - var DatePicker = function(element, options) { - var self = this - this.options = options - this.$el = $(element) - this.$input = this.$el.find('input:first') + $.oc.foundation.controlUtils.markDisposable(element) + Base.call(this) + this.init() + } - // Init + DatePicker.prototype = Object.create(BaseProto) + DatePicker.prototype.constructor = DatePicker - var $form = this.$el.closest('form'), + DatePicker.prototype.init = function() { + var self = this, + $form = this.$el.closest('form'), changeMonitor = $form.data('oc.changeMonitor') - if (changeMonitor !== undefined) + if (changeMonitor !== undefined) { changeMonitor.pause() + } + + this.dbDateTimeFormat = 'YYYY-MM-DD HH:mm:ss' + this.dbDateFormat = 'YYYY-MM-DD' + this.dbTimeFormat = 'HH:mm:ss' + + this.$dataLocker = $('[data-datetime-value]', this.$el) + this.$datePicker = $('[data-datepicker]', this.$el) + this.$timePicker = $('[data-timepicker]', this.$el) + this.hasDate = !!this.$datePicker.length + this.hasTime = !!this.$timePicker.length + + this.initRegion() + + if (this.hasDate) { + this.initDatePicker() + } + + if (this.hasTime) { + this.initTimePicker() + } + + if (changeMonitor !== undefined) { + changeMonitor.resume() + } + + this.$timePicker.on('change.oc.datepicker', function() { + if (!$.trim($(this).val())) { + self.emptyValues() + } + }) + + this.$datePicker.on('change.oc.datepicker', function() { + if (!$.trim($(this).val())) { + self.emptyValues() + } + }) + + this.$el.one('dispose-control', this.proxy(this.dispose)) + } + + DatePicker.prototype.dispose = function() { + this.$timePicker.off('change.oc.datepicker') + this.$datePicker.off('change.oc.datepicker') + this.$el.off('dispose-control', this.proxy(this.dispose)) + this.$el.removeData('oc.datePicker') + + this.$el = null + this.options = null + + BaseProto.dispose.call(this) + } + + // + // Datepicker + // + + DatePicker.prototype.initDatePicker = function() { + var self = this var pikadayOptions = { - yearRange: options.yearRange, - format: options.format, - setDefaultDate: moment(this.$input.val()).toDate(), + yearRange: this.options.yearRange, + format: this.getDateFormat(), + setDefaultDate: moment().tz(this.timezone).format('l'), // now i18n: $.oc.lang.get('datepicker'), onOpen: function() { var $field = $(this._o.trigger) @@ -48,21 +93,171 @@ left: 'auto', right: $(window).width() - $field.offset().left - $field.outerWidth() }) + }, + onSelect: function() { + self.onSelectDatePicker.call(self, this.getMoment()) } } - if (options.minDate) { - pikadayOptions.minDate = new Date(options.minDate) + this.$datePicker.val(this.getDataLockerValue('l')) + + if (this.options.minDate) { + pikadayOptions.minDate = new Date(this.options.minDate) } - if (options.maxDate) { - pikadayOptions.maxDate = new Date(options.maxDate) + if (this.options.maxDate) { + pikadayOptions.maxDate = new Date(this.options.maxDate) } - this.$input.pikaday(pikadayOptions) + this.$datePicker.pikaday(pikadayOptions) + } - if (changeMonitor !== undefined) - changeMonitor.resume() + DatePicker.prototype.onSelectDatePicker = function(pickerMoment) { + var pickerValue = pickerMoment.format(this.dbDateFormat) + + var timeValue = this.getTimePickerValue() + + var momentObj = moment + .tz(pickerValue + ' ' + timeValue, this.dbDateTimeFormat, this.timezone) + .tz(this.appTimezone) + + var lockerValue = momentObj.format(this.dbDateTimeFormat) + + this.$dataLocker.val(lockerValue) + } + + // Returns in user preference timezone + DatePicker.prototype.getDatePickerValue = function() { + var value = this.$datePicker.val() + + if (!this.hasDate || !value) { + return moment.tz(this.appTimezone) + .tz(this.timezone) + .format(this.dbDateFormat) + } + + return moment(value, this.getDateFormat()).format(this.dbDateFormat) + } + + DatePicker.prototype.getDateFormat = function() { + var format = this.options.format + + if (this.locale) { + format = moment() + .locale(this.locale) + .localeData() + .longDateFormat('l') + } + + return format + } + + // + // Timepicker + // + + DatePicker.prototype.initTimePicker = function() { + this.$timePicker.clockpicker({ + autoclose: 'true', + placement: 'bottom', + align: 'right', + twelvehour: this.isTimeTwelveHour(), + afterDone: this.proxy(this.onSelectTimePicker) + }) + + this.$timePicker.val(this.getDataLockerValue(this.getTimeFormat())) + } + + DatePicker.prototype.onSelectTimePicker = function() { + var pickerValue = this.$timePicker.val() + + var timeValue = moment(pickerValue, this.getTimeFormat()).format(this.dbTimeFormat) + + var dateValue = this.getDatePickerValue() + + var momentObj = moment + .tz(dateValue + ' ' + timeValue, this.dbDateTimeFormat, this.timezone) + .tz(this.appTimezone) + + var lockerValue = momentObj.format(this.dbDateTimeFormat) + + this.$dataLocker.val(lockerValue) + } + + // Returns in user preference timezone + DatePicker.prototype.getTimePickerValue = function() { + var value = this.$timePicker.val() + + if (!this.hasTime || !value) { + return moment.tz(this.appTimezone) + .tz(this.timezone) + .format(this.dbTimeFormat) + } + + return moment(value, this.getTimeFormat()).format(this.dbTimeFormat) + } + + DatePicker.prototype.getTimeFormat = function() { + return this.isTimeTwelveHour() + ? 'hh:mm A' + : 'HH:mm' + } + + DatePicker.prototype.isTimeTwelveHour = function() { + var momentObj = moment() + + if (this.locale) { + momentObj = momentObj.locale(this.locale) + } + + return momentObj + .localeData() + .longDateFormat('LT') + .indexOf('A') !== -1; + } + + // + // Utilities + // + + DatePicker.prototype.emptyValues = function() { + this.$dataLocker.val('') + this.$datePicker.val('') + this.$timePicker.val('') + } + + DatePicker.prototype.getDataLockerValue = function(format) { + var value = this.$dataLocker.val() + + return value + ? this.getMomentLoadValue(value, format) + : null + } + + DatePicker.prototype.getMomentLoadValue = function(value, format) { + var momentObj = moment.tz(value, this.appTimezone) + + if (this.locale) { + momentObj = momentObj.locale(this.locale) + } + + momentObj = momentObj.tz(this.timezone) + + return momentObj.format(format) + } + + DatePicker.prototype.initRegion = function() { + this.locale = $('meta[name="backend-locale"]').attr('content') + this.timezone = $('meta[name="backend-timezone"]').attr('content') + this.appTimezone = $('meta[name="app-timezone"]').attr('content') + + if (!this.appTimezone) { + this.appTimezone = 'UTC' + } + + if (!this.timezone) { + this.timezone = 'UTC' + } } DatePicker.DEFAULTS = { @@ -72,35 +267,33 @@ yearRange: 10 } - // DATEPICKER PLUGIN DEFINITION + // PLUGIN DEFINITION // ============================ var old = $.fn.datePicker $.fn.datePicker = function (option) { - var args = Array.prototype.slice.call(arguments, 1) - return this.each(function () { + var args = Array.prototype.slice.call(arguments, 1), items, result + + items = this.each(function () { var $this = $(this) - var data = $this.data('oc.datepicker') + var data = $this.data('oc.datePicker') var options = $.extend({}, DatePicker.DEFAULTS, $this.data(), typeof option == 'object' && option) - if (!data) $this.data('oc.datepicker', (data = new DatePicker(this, options))) - else if (typeof option == 'string') data[option].apply(data, args) + if (!data) $this.data('oc.datePicker', (data = new DatePicker(this, options))) + if (typeof option == 'string') result = data[option].apply(data, args) + if (typeof result != 'undefined') return false }) + + return result ? result : items } $.fn.datePicker.Constructor = DatePicker - // DATEPICKER NO CONFLICT - // ================= - $.fn.datePicker.noConflict = function () { $.fn.datePicker = old return this } - // DATEPICKER DATA-API - // =============== - $(document).on('render', function() { $('[data-control="datepicker"]').datePicker() }); diff --git a/modules/backend/formwidgets/datepicker/assets/js/timepicker.js b/modules/backend/formwidgets/datepicker/assets/js/timepicker.js deleted file mode 100644 index 8b6684a90..000000000 --- a/modules/backend/formwidgets/datepicker/assets/js/timepicker.js +++ /dev/null @@ -1,75 +0,0 @@ -/* - * TimePicker plugin - * - * Data attributes: - * - data-control="timepicker" - enables the plugin on an element - * - * JavaScript API: - * $('a#someElement').timePicker({ option: 'value' }) - * - * Dependences: - * - Clockpicker plugin (jquery-clockpicker.js) - */ - -+function ($) { "use strict"; - - // DATEPICKER CLASS DEFINITION - // ============================ - - var TimePicker = function(element, options) { - var self = this - this.options = options - this.$el = $(element) - this.$input = this.$el.find('input:first') - - // Init - - var $form = this.$el.closest('form'), - changeMonitor = $form.data('oc.changeMonitor') - - if (changeMonitor !== undefined) - changeMonitor.pause() - - this.$input.clockpicker() - - if (changeMonitor !== undefined) - changeMonitor.resume() - } - - TimePicker.DEFAULTS = { - } - - // DATEPICKER PLUGIN DEFINITION - // ============================ - - var old = $.fn.timePicker - - $.fn.timePicker = function (option) { - var args = Array.prototype.slice.call(arguments, 1) - return this.each(function () { - var $this = $(this) - var data = $this.data('oc.timepicker') - var options = $.extend({}, TimePicker.DEFAULTS, $this.data(), typeof option == 'object' && option) - if (!data) $this.data('oc.timepicker', (data = new TimePicker(this, options))) - else if (typeof option == 'string') data[option].apply(data, args) - }) - } - - $.fn.timePicker.Constructor = TimePicker - - // DATEPICKER NO CONFLICT - // ================= - - $.fn.timePicker.noConflict = function () { - $.fn.timePicker = old - return this - } - - // DATEPICKER DATA-API - // =============== - - $(document).on('render', function() { - $('[data-control="timepicker"]').timePicker() - }); - -}(window.jQuery); \ No newline at end of file diff --git a/modules/backend/formwidgets/datepicker/assets/vendor/clockpicker/js/jquery-clockpicker.js b/modules/backend/formwidgets/datepicker/assets/vendor/clockpicker/js/jquery-clockpicker.js index e930b4ff1..05eab6f1e 100644 --- a/modules/backend/formwidgets/datepicker/assets/vendor/clockpicker/js/jquery-clockpicker.js +++ b/modules/backend/formwidgets/datepicker/assets/vendor/clockpicker/js/jquery-clockpicker.js @@ -2,6 +2,10 @@ * ClockPicker v0.0.7 (http://weareoutman.github.io/clockpicker/) * Copyright 2014 Wang Shenwei. * Licensed under MIT (https://github.com/weareoutman/clockpicker/blob/gh-pages/LICENSE) + + * This library is no longer maintained by Weareoutman, hence + * no further updates will be provided. Consider this file + * a forked version and you a free to make changes. */ ;(function(){ @@ -121,7 +125,7 @@ // Setup for for 12 hour clock if option is selected if (options.twelvehour) { - var amPmButtonsTemplate = ['
', + var amPmButtonsTemplate = ['
', '', '