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.
This commit is contained in:
parent
d49cef2201
commit
e772e87de5
|
|
@ -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()}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
$(document).on('render',function(){$('[data-control="datepicker"]').datePicker()});}(window.jQuery);
|
||||
|
|
@ -11,6 +11,5 @@
|
|||
=require ../vendor/pikaday/js/pikaday.jquery.js
|
||||
=require ../vendor/clockpicker/js/jquery-clockpicker.js
|
||||
=require datepicker.js
|
||||
=require timepicker.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()
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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 = ['<div class="clockpicker-am-pm-block">',
|
||||
var amPmButtonsTemplate = ['<div class="clockpicker-am-pm-block">',
|
||||
'<button type="button" class="btn btn-sm btn-default clockpicker-button clockpicker-am-button">',
|
||||
'AM</button>',
|
||||
'<button type="button" class="btn btn-sm btn-default clockpicker-button clockpicker-pm-button">',
|
||||
|
|
@ -367,7 +371,7 @@
|
|||
fromnow: 0, // set default time to * milliseconds from now (using with default = 'now')
|
||||
placement: 'bottom', // clock popover placement
|
||||
align: 'left', // popover arrow align
|
||||
donetext: '完成', // done button text
|
||||
donetext: 'Done', // done button text
|
||||
autoclose: false, // auto close when minute is selected
|
||||
twelvehour: false, // change to 12 hour AM/PM clock from 24 hour
|
||||
vibrate: true // vibrate the device when dragging clock hand
|
||||
|
|
@ -676,10 +680,11 @@
|
|||
this.hide();
|
||||
var last = this.input.prop('value'),
|
||||
value = leadingZero(this.hours) + ':' + leadingZero(this.minutes);
|
||||
if (this.options.twelvehour) {
|
||||
value = value + this.amOrPm;
|
||||
|
||||
if (this.options.twelvehour) {
|
||||
value = value + ' ' +this.amOrPm;
|
||||
}
|
||||
|
||||
|
||||
this.input.prop('value', value);
|
||||
if (value !== last) {
|
||||
this.input.triggerHandler('change');
|
||||
|
|
|
|||
|
|
@ -2,101 +2,38 @@
|
|||
<div class="form-control"><?= $value ?></div>
|
||||
<?php else: ?>
|
||||
|
||||
<?php if ($mode == 'date'): ?>
|
||||
<div
|
||||
id="<?= $this->getId() ?>"
|
||||
class="field-datepicker"
|
||||
data-control="datepicker"
|
||||
<?php if ($minDate): ?>data-min-date="<?= $minDate ?>"<?php endif ?>
|
||||
<?php if ($maxDate): ?>data-max-date="<?= $maxDate ?>"<?php endif ?>
|
||||
>
|
||||
|
||||
<div
|
||||
id="<?= $this->getId() ?>"
|
||||
class="field-datepicker"
|
||||
data-control="datepicker"
|
||||
data-format="<?= $format ?>"
|
||||
<?php if ($minDate): ?>data-min-date="<?= $minDate ?>"<?php endif ?>
|
||||
<?php if ($maxDate): ?>data-max-date="<?= $maxDate ?>"<?php endif ?>
|
||||
>
|
||||
<div class="input-with-icon right-align">
|
||||
<i class="icon icon-calendar-o"></i>
|
||||
<input
|
||||
type="text"
|
||||
id="<?= $this->getId('input') ?>"
|
||||
name="<?= $name ?>"
|
||||
value="<?= $value ?>"
|
||||
class="form-control align-right"
|
||||
autocomplete="off"
|
||||
<?= $field->getAttributes() ?> />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php elseif ($mode == 'datetime'): ?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<!-- Date -->
|
||||
<div
|
||||
id="<?= $this->getId() ?>"
|
||||
class="field-datepicker"
|
||||
data-control="datepicker"
|
||||
data-format="<?= $format ?>"
|
||||
<?php if ($minDate): ?>data-min-date="<?= $minDate ?>"<?php endif ?>
|
||||
<?php if ($maxDate): ?>data-max-date="<?= $maxDate ?>"<?php endif ?>
|
||||
>
|
||||
<div class="input-with-icon right-align">
|
||||
<i class="icon icon-calendar-o"></i>
|
||||
<input
|
||||
type="text"
|
||||
id="<?= $this->getId('input') ?>"
|
||||
name="<?= $name ?>"
|
||||
value="<?= $value ?>"
|
||||
class="form-control align-right"
|
||||
autocomplete="off"
|
||||
<?= $field->getAttributes() ?> />
|
||||
</div>
|
||||
<?php if ($mode == 'date'): ?>
|
||||
<?= $this->makePartial('picker_date') ?>
|
||||
<?php elseif ($mode == 'datetime'): ?>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<?= $this->makePartial('picker_date') ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<?= $this->makePartial('picker_time') ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<!-- Time -->
|
||||
<div
|
||||
id="<?= $this->getId() ?>-time"
|
||||
data-control="timepicker"
|
||||
class="field-timepicker clockpicker">
|
||||
<div class="input-with-icon right-align">
|
||||
<i class="icon icon-clock-o"></i>
|
||||
<input
|
||||
type="text"
|
||||
id="<?= $this->getId('input') ?>-time"
|
||||
name="<?= $timeName ?>"
|
||||
value="<?= $timeValue ?>"
|
||||
class="form-control align-right"
|
||||
autocomplete="off"
|
||||
data-autoclose="true"
|
||||
data-placement="bottom"
|
||||
data-align="right"
|
||||
<?= $field->getAttributes() ?> />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php elseif ($mode == 'time'): ?>
|
||||
<?= $this->makePartial('picker_time') ?>
|
||||
<?php endif ?>
|
||||
|
||||
<?php elseif ($mode == 'time'): ?>
|
||||
<!-- Data locker -->
|
||||
<input
|
||||
type="hidden"
|
||||
name="<?= $field->getName() ?>"
|
||||
id="<?= $field->getId() ?>"
|
||||
value="<?= e($value) ?>"
|
||||
data-datetime-value
|
||||
/>
|
||||
|
||||
<div
|
||||
id="<?= $this->getId() ?>"
|
||||
data-control="timepicker"
|
||||
class="field-timepicker clockpicker">
|
||||
<div class="input-with-icon right-align">
|
||||
<i class="icon icon-clock-o"></i>
|
||||
<input
|
||||
type="text"
|
||||
id="<?= $this->getId('input') ?>"
|
||||
name="<?= $name ?>"
|
||||
value="<?= $value ?>"
|
||||
class="form-control align-right"
|
||||
autocomplete="off"
|
||||
data-autoclose="true"
|
||||
data-placement="bottom"
|
||||
data-align="right"
|
||||
<?= $field->getAttributes() ?> />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php endif ?>
|
||||
</div>
|
||||
|
||||
<?php endif ?>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Date -->
|
||||
<div class="input-with-icon right-align">
|
||||
<i class="icon icon-calendar-o"></i>
|
||||
<input
|
||||
type="text"
|
||||
id="<?= $this->getId('date') ?>"
|
||||
class="form-control align-right"
|
||||
autocomplete="off"
|
||||
<?= $field->getAttributes() ?>
|
||||
data-datepicker />
|
||||
</div>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Date -->
|
||||
<div class="input-with-icon right-align">
|
||||
<i class="icon icon-clock-o"></i>
|
||||
<input
|
||||
type="text"
|
||||
id="<?= $this->getId('time') ?>"
|
||||
class="form-control align-right"
|
||||
autocomplete="off"
|
||||
<?= $field->getAttributes() ?>
|
||||
data-timepicker />
|
||||
</div>
|
||||
Loading…
Reference in New Issue