init
This commit is contained in:
@@ -0,0 +1,715 @@
|
||||
import { ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, HostBinding, Input, OnDestroy, OnInit, Output, Renderer2, TemplateRef, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
|
||||
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
|
||||
import { Overlay } from '@angular/cdk/overlay';
|
||||
import { TemplatePortal } from '@angular/cdk/portal';
|
||||
import { MatCalendarCellCssClasses, MatMonthView } from '@angular/material/datepicker';
|
||||
import { Subject } from 'rxjs';
|
||||
import * as moment from 'moment';
|
||||
import { Moment } from 'moment';
|
||||
|
||||
@Component({
|
||||
selector : 'treo-date-range',
|
||||
templateUrl : './date-range.component.html',
|
||||
styleUrls : ['./date-range.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
exportAs : 'treoDateRange',
|
||||
providers : [
|
||||
{
|
||||
provide : NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => TreoDateRangeComponent),
|
||||
multi : true
|
||||
}
|
||||
]
|
||||
})
|
||||
export class TreoDateRangeComponent implements ControlValueAccessor, OnInit, OnDestroy
|
||||
{
|
||||
// Range changed
|
||||
@Output()
|
||||
readonly rangeChanged: EventEmitter<{ start: string, end: string }>;
|
||||
|
||||
activeDates: { month1: Moment, month2: Moment };
|
||||
setWhichDate: 'start' | 'end';
|
||||
startTimeFormControl: FormControl;
|
||||
endTimeFormControl: FormControl;
|
||||
|
||||
// Private
|
||||
@HostBinding('class.treo-date-range')
|
||||
private _defaultClassNames;
|
||||
|
||||
@ViewChild('matMonthView1')
|
||||
private _matMonthView1: MatMonthView<any>;
|
||||
|
||||
@ViewChild('matMonthView2')
|
||||
private _matMonthView2: MatMonthView<any>;
|
||||
|
||||
@ViewChild('pickerPanelOrigin', {read: ElementRef})
|
||||
private _pickerPanelOrigin: ElementRef;
|
||||
|
||||
@ViewChild('pickerPanel')
|
||||
private _pickerPanel: TemplateRef<any>;
|
||||
|
||||
private _dateFormat: string;
|
||||
private _onChange: (value: any) => void;
|
||||
private _onTouched: (value: any) => void;
|
||||
private _programmaticChange: boolean;
|
||||
private _range: { start: Moment, end: Moment };
|
||||
private _timeFormat: string;
|
||||
private _timeRange: boolean;
|
||||
private readonly _timeRegExp: RegExp;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {ChangeDetectorRef} _changeDetectorRef
|
||||
* @param {ElementRef} _elementRef
|
||||
* @param {Overlay} _overlay
|
||||
* @param {Renderer2} _renderer2
|
||||
* @param {ViewContainerRef} _viewContainerRef
|
||||
*/
|
||||
constructor(
|
||||
private _changeDetectorRef: ChangeDetectorRef,
|
||||
private _elementRef: ElementRef,
|
||||
private _overlay: Overlay,
|
||||
private _renderer2: Renderer2,
|
||||
private _viewContainerRef: ViewContainerRef
|
||||
)
|
||||
{
|
||||
// Set the private defaults
|
||||
this._defaultClassNames = true;
|
||||
this._onChange = () => {
|
||||
};
|
||||
this._onTouched = () => {
|
||||
};
|
||||
this._range = {
|
||||
start: null,
|
||||
end : null
|
||||
};
|
||||
this._timeRegExp = new RegExp('^(0[0-9]|1[0-9]|2[0-4]|[0-9]):([0-5][0-9])(A|(?:AM)|P|(?:PM))?$', 'i');
|
||||
this._unsubscribeAll = new Subject();
|
||||
|
||||
// Set the defaults
|
||||
this.activeDates = {
|
||||
month1: null,
|
||||
month2: null
|
||||
};
|
||||
this.dateFormat = 'DD/MM/YYYY';
|
||||
this.rangeChanged = new EventEmitter();
|
||||
this.setWhichDate = 'start';
|
||||
this.timeFormat = '12';
|
||||
|
||||
// Initialize the component
|
||||
this._init();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Accessors
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Setter and getter for dateFormat input
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
@Input()
|
||||
set dateFormat(value: string)
|
||||
{
|
||||
// Return, if the values are the same
|
||||
if ( this._dateFormat === value )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the value
|
||||
this._dateFormat = value;
|
||||
}
|
||||
|
||||
get dateFormat(): string
|
||||
{
|
||||
return this._dateFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter and getter for timeFormat input
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
@Input()
|
||||
set timeFormat(value: string)
|
||||
{
|
||||
// Return, if the values are the same
|
||||
if ( this._timeFormat === value )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Set format based on the time format input
|
||||
this._timeFormat = value === '12' ? 'hh:mmA' : 'HH:mm';
|
||||
}
|
||||
|
||||
get timeFormat(): string
|
||||
{
|
||||
return this._timeFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter and getter for timeRange input
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
@Input()
|
||||
set timeRange(value: boolean)
|
||||
{
|
||||
// Return, if the values are the same
|
||||
if ( this._timeRange === value )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the value
|
||||
this._timeRange = value;
|
||||
|
||||
// If the time range turned off...
|
||||
if ( !value )
|
||||
{
|
||||
this.range = {
|
||||
start: this._range.start.clone().startOf('day'),
|
||||
end : this._range.end.clone().endOf('day')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
get timeRange(): boolean
|
||||
{
|
||||
return this._timeRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter and getter for range input
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
@Input()
|
||||
set range(value)
|
||||
{
|
||||
if ( !value )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the value is an object and has 'start' and 'end' values
|
||||
if ( !value.start || !value.end )
|
||||
{
|
||||
console.error('Range input must have "start" and "end" properties!');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we are setting an individual date or both of them
|
||||
const whichDate = value.whichDate || null;
|
||||
|
||||
// Get the start and end dates as moment
|
||||
const start = moment(value.start);
|
||||
const end = moment(value.end);
|
||||
|
||||
// If we are only setting the start date...
|
||||
if ( whichDate === 'start' )
|
||||
{
|
||||
// Set the start date
|
||||
this._range.start = start.clone();
|
||||
|
||||
// If the selected start date is after the end date...
|
||||
if ( this._range.start.isAfter(this._range.end) )
|
||||
{
|
||||
// Set the end date to the start date but keep the end date's time
|
||||
const endDate = start.clone().hours(this._range.end.hours()).minutes(this._range.end.minutes()).seconds(this._range.end.seconds());
|
||||
|
||||
// Test this new end date to see if it's ahead of the start date
|
||||
if ( this._range.start.isBefore(endDate) )
|
||||
{
|
||||
// If it's, set the new end date
|
||||
this._range.end = endDate;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, set the end date same as the start date
|
||||
this._range.end = start.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we are only setting the end date...
|
||||
if ( whichDate === 'end' )
|
||||
{
|
||||
// Set the end date
|
||||
this._range.end = end.clone();
|
||||
|
||||
// If the selected end date is before the start date...
|
||||
if ( this._range.start.isAfter(this._range.end) )
|
||||
{
|
||||
// Set the start date to the end date but keep the start date's time
|
||||
const startDate = end.clone().hours(this._range.start.hours()).minutes(this._range.start.minutes()).seconds(this._range.start.seconds());
|
||||
|
||||
// Test this new end date to see if it's ahead of the start date
|
||||
if ( this._range.end.isAfter(startDate) )
|
||||
{
|
||||
// If it's, set the new start date
|
||||
this._range.start = startDate;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, set the start date same as the end date
|
||||
this._range.start = end.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we are setting both dates...
|
||||
if ( !whichDate )
|
||||
{
|
||||
// Set the start date
|
||||
this._range.start = start.clone();
|
||||
|
||||
// If the start date is before the end date, set the end date as normal.
|
||||
// If the start date is after the end date, set the end date same as the start date.
|
||||
this._range.end = start.isBefore(end) ? end.clone() : start.clone();
|
||||
}
|
||||
|
||||
// Prepare another range object that holds the ISO formatted range dates
|
||||
const range = {
|
||||
start: this._range.start.clone().toISOString(),
|
||||
end : this._range.end.clone().toISOString()
|
||||
};
|
||||
|
||||
// Emit the range changed event with the range
|
||||
this.rangeChanged.emit(range);
|
||||
|
||||
// Update the model with the range if the change was not a programmatic change
|
||||
// Because programmatic changes trigger writeValue which triggers onChange and onTouched
|
||||
// internally causing them to trigger twice which breaks the form's pristine and touched
|
||||
// statuses.
|
||||
if ( !this._programmaticChange )
|
||||
{
|
||||
this._onTouched(range);
|
||||
this._onChange(range);
|
||||
}
|
||||
|
||||
// Set the active dates
|
||||
this.activeDates = {
|
||||
month1: this._range.start.clone(),
|
||||
month2: this._range.start.clone().add(1, 'month')
|
||||
};
|
||||
|
||||
// Set the time form controls
|
||||
this.startTimeFormControl.setValue(this._range.start.clone().format(this._timeFormat).toString());
|
||||
this.endTimeFormControl.setValue(this._range.end.clone().format(this._timeFormat).toString());
|
||||
|
||||
// Run ngAfterContentInit on month views to trigger
|
||||
// re-render on month views if they are available
|
||||
if ( this._matMonthView1 && this._matMonthView2 )
|
||||
{
|
||||
this._matMonthView1.ngAfterContentInit();
|
||||
this._matMonthView2.ngAfterContentInit();
|
||||
}
|
||||
|
||||
// Reset the programmatic change status
|
||||
this._programmaticChange = false;
|
||||
}
|
||||
|
||||
get range(): any
|
||||
{
|
||||
// Clone the range start and end
|
||||
const start = this._range.start.clone();
|
||||
const end = this._range.end.clone();
|
||||
|
||||
// Build and return the range object
|
||||
return {
|
||||
startDate: start.clone().format(this.dateFormat),
|
||||
startTime: this.timeRange ? start.clone().format(this.timeFormat) : null,
|
||||
endDate : end.clone().format(this.dateFormat),
|
||||
endTime : this.timeRange ? end.clone().format(this.timeFormat) : null
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Control Value Accessor
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Update the form model on change
|
||||
*
|
||||
* @param fn
|
||||
*/
|
||||
registerOnChange(fn: any): void
|
||||
{
|
||||
this._onChange = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the form model on blur
|
||||
*
|
||||
* @param fn
|
||||
*/
|
||||
registerOnTouched(fn: any): void
|
||||
{
|
||||
this._onTouched = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write to view from model when the form model changes programmatically
|
||||
*
|
||||
* @param range
|
||||
*/
|
||||
writeValue(range: { start: string, end: string }): void
|
||||
{
|
||||
// Set this change as a programmatic one
|
||||
this._programmaticChange = true;
|
||||
|
||||
// Set the range
|
||||
this.range = range;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
|
||||
// @ TODO: Workaround until "angular/issues/20007" resolved
|
||||
this.writeValue = () => {
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Initialize
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private _init(): void
|
||||
{
|
||||
// Start and end time form controls
|
||||
this.startTimeFormControl = new FormControl('', [Validators.pattern(this._timeRegExp)]);
|
||||
this.endTimeFormControl = new FormControl('', [Validators.pattern(this._timeRegExp)]);
|
||||
|
||||
// Set the default range
|
||||
this._programmaticChange = true;
|
||||
this.range = {
|
||||
start: moment().startOf('day').toISOString(),
|
||||
end : moment().add(1, 'day').endOf('day').toISOString()
|
||||
};
|
||||
|
||||
// Set the default time range
|
||||
this._programmaticChange = true;
|
||||
this.timeRange = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the time from the inputs
|
||||
*
|
||||
* @param value
|
||||
* @private
|
||||
*/
|
||||
private _parseTime(value: string): Moment
|
||||
{
|
||||
// Parse the time using the time regexp
|
||||
const timeArr = value.split(this._timeRegExp).filter((part) => part !== '');
|
||||
|
||||
// Get the meridiem
|
||||
const meridiem = timeArr[2] || null;
|
||||
|
||||
// If meridiem exists...
|
||||
if ( meridiem )
|
||||
{
|
||||
// Create a moment using 12-hours format and return it
|
||||
return moment(value, 'hh:mmA').seconds(0);
|
||||
}
|
||||
|
||||
// If meridiem doesn't exist, create a moment using 24-hours format and return in
|
||||
return moment(value, 'HH:mm').seconds(0);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Open the picker panel
|
||||
*/
|
||||
openPickerPanel(): void
|
||||
{
|
||||
// Create the overlay
|
||||
const overlayRef = this._overlay.create({
|
||||
panelClass : 'treo-date-range-panel',
|
||||
backdropClass : '',
|
||||
hasBackdrop : true,
|
||||
scrollStrategy : this._overlay.scrollStrategies.reposition(),
|
||||
positionStrategy: this._overlay.position()
|
||||
.flexibleConnectedTo(this._pickerPanelOrigin)
|
||||
.withPositions([
|
||||
{
|
||||
originX : 'start',
|
||||
originY : 'bottom',
|
||||
overlayX: 'start',
|
||||
overlayY: 'top',
|
||||
offsetY : 8
|
||||
},
|
||||
{
|
||||
originX : 'start',
|
||||
originY : 'top',
|
||||
overlayX: 'start',
|
||||
overlayY: 'bottom',
|
||||
offsetY : -8
|
||||
}
|
||||
])
|
||||
});
|
||||
|
||||
// Create a portal from the template
|
||||
const templatePortal = new TemplatePortal(this._pickerPanel, this._viewContainerRef);
|
||||
|
||||
// On backdrop click
|
||||
overlayRef.backdropClick().subscribe(() => {
|
||||
|
||||
// If template portal exists and attached...
|
||||
if ( templatePortal && templatePortal.isAttached )
|
||||
{
|
||||
// Detach it
|
||||
templatePortal.detach();
|
||||
}
|
||||
|
||||
// If overlay exists and attached...
|
||||
if ( overlayRef && overlayRef.hasAttached() )
|
||||
{
|
||||
// Detach it
|
||||
overlayRef.detach();
|
||||
overlayRef.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
// Attach the portal to the overlay
|
||||
overlayRef.attach(templatePortal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get month label
|
||||
*
|
||||
* @param month
|
||||
*/
|
||||
getMonthLabel(month: number): string
|
||||
{
|
||||
if ( month === 1 )
|
||||
{
|
||||
return this.activeDates.month1.clone().format('MMMM Y');
|
||||
}
|
||||
|
||||
return this.activeDates.month2.clone().format('MMMM Y');
|
||||
}
|
||||
|
||||
/**
|
||||
* Date class function to add/remove class names to calendar days
|
||||
*/
|
||||
dateClass(): any
|
||||
{
|
||||
return (date: Moment): MatCalendarCellCssClasses => {
|
||||
|
||||
// If the date is both start and end date...
|
||||
if ( date.isSame(this._range.start, 'day') && date.isSame(this._range.end, 'day') )
|
||||
{
|
||||
return ['treo-date-range', 'treo-date-range-start', 'treo-date-range-end'];
|
||||
}
|
||||
|
||||
// If the date is the start date...
|
||||
if ( date.isSame(this._range.start, 'day') )
|
||||
{
|
||||
return ['treo-date-range', 'treo-date-range-start'];
|
||||
}
|
||||
|
||||
// If the date is the end date...
|
||||
if ( date.isSame(this._range.end, 'day') )
|
||||
{
|
||||
return ['treo-date-range', 'treo-date-range-end'];
|
||||
}
|
||||
|
||||
// If the date is in between start and end dates...
|
||||
if ( date.isBetween(this._range.start, this._range.end, 'day') )
|
||||
{
|
||||
return ['treo-date-range', 'treo-date-range-mid'];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Date filter to enable/disable calendar days
|
||||
*/
|
||||
dateFilter(): any
|
||||
{
|
||||
return (date: Moment): boolean => {
|
||||
|
||||
// If we are selecting the end date, disable all the dates that comes before the start date
|
||||
return !(this.setWhichDate === 'end' && date.isBefore(this._range.start, 'day'));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* On selected date change
|
||||
*
|
||||
* @param date
|
||||
*/
|
||||
onSelectedDateChange(date: Moment): void
|
||||
{
|
||||
// Create a new range object
|
||||
const newRange = {
|
||||
start : this._range.start.clone().toISOString(),
|
||||
end : this._range.end.clone().toISOString(),
|
||||
whichDate: null
|
||||
};
|
||||
|
||||
// Replace either the start or the end date with the new one
|
||||
// depending on which date we are setting
|
||||
if ( this.setWhichDate === 'start' )
|
||||
{
|
||||
newRange.start = moment(newRange.start).year(date.year()).month(date.month()).date(date.date()).toISOString();
|
||||
}
|
||||
else
|
||||
{
|
||||
newRange.end = moment(newRange.end).year(date.year()).month(date.month()).date(date.date()).toISOString();
|
||||
}
|
||||
|
||||
// Append the which date to the new range object
|
||||
newRange.whichDate = this.setWhichDate;
|
||||
|
||||
// Switch which date to set on the next run
|
||||
this.setWhichDate = this.setWhichDate === 'start' ? 'end' : 'start';
|
||||
|
||||
// Set the range
|
||||
this.range = newRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to previous month on both views
|
||||
*/
|
||||
prev(): void
|
||||
{
|
||||
this.activeDates.month1 = moment(this.activeDates.month1).subtract(1, 'month');
|
||||
this.activeDates.month2 = moment(this.activeDates.month2).subtract(1, 'month');
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to next month on both views
|
||||
*/
|
||||
next(): void
|
||||
{
|
||||
this.activeDates.month1 = moment(this.activeDates.month1).add(1, 'month');
|
||||
this.activeDates.month2 = moment(this.activeDates.month2).add(1, 'month');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the start time
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
updateStartTime(event): void
|
||||
{
|
||||
// Parse the time
|
||||
const parsedTime = this._parseTime(event.target.value);
|
||||
|
||||
// Go back to the previous value if the form control is not valid
|
||||
if ( this.startTimeFormControl.invalid )
|
||||
{
|
||||
// Override the time
|
||||
const time = this._range.start.clone().format(this._timeFormat);
|
||||
|
||||
// Set the time
|
||||
this.startTimeFormControl.setValue(time);
|
||||
|
||||
// Do not update the range
|
||||
return;
|
||||
}
|
||||
|
||||
// Append the new time to the start date
|
||||
const startDate = this._range.start.clone().hours(parsedTime.hours()).minutes(parsedTime.minutes());
|
||||
|
||||
// If the new start date is after the current end date,
|
||||
// use the end date's time and set the start date again
|
||||
if ( startDate.isAfter(this._range.end) )
|
||||
{
|
||||
const endDateHours = this._range.end.hours();
|
||||
const endDateMinutes = this._range.end.minutes();
|
||||
|
||||
// Set the start date
|
||||
startDate.hours(endDateHours).minutes(endDateMinutes);
|
||||
}
|
||||
|
||||
// If everything is okay, set the new date
|
||||
this.range = {
|
||||
start : startDate.toISOString(),
|
||||
end : this._range.end.clone().toISOString(),
|
||||
whichDate: 'start'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the end time
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
updateEndTime(event): void
|
||||
{
|
||||
// Parse the time
|
||||
const parsedTime = this._parseTime(event.target.value);
|
||||
|
||||
// Go back to the previous value if the form control is not valid
|
||||
if ( this.endTimeFormControl.invalid )
|
||||
{
|
||||
// Override the time
|
||||
const time = this._range.end.clone().format(this._timeFormat);
|
||||
|
||||
// Set the time
|
||||
this.endTimeFormControl.setValue(time);
|
||||
|
||||
// Do not update the range
|
||||
return;
|
||||
}
|
||||
|
||||
// Append the new time to the end date
|
||||
const endDate = this._range.end.clone().hours(parsedTime.hours()).minutes(parsedTime.minutes());
|
||||
|
||||
// If the new end date is before the current start date,
|
||||
// use the start date's time and set the end date again
|
||||
if ( endDate.isBefore(this._range.start) )
|
||||
{
|
||||
const startDateHours = this._range.start.hours();
|
||||
const startDateMinutes = this._range.start.minutes();
|
||||
|
||||
// Set the end date
|
||||
endDate.hours(startDateHours).minutes(startDateMinutes);
|
||||
}
|
||||
|
||||
// If everything is okay, set the new date
|
||||
this.range = {
|
||||
start : this._range.start.clone().toISOString(),
|
||||
end : endDate.toISOString(),
|
||||
whichDate: 'end'
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user