Skip to content

Commit

Permalink
Date-Picker honours time zone
Browse files Browse the repository at this point in the history
fix #175 : Date-Picker honours time zone
  • Loading branch information
jrief committed Jan 20, 2025
1 parent 9ccc343 commit 404c37d
Show file tree
Hide file tree
Showing 11 changed files with 126 additions and 101 deletions.
59 changes: 34 additions & 25 deletions client/django-formset/Calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ export class CalendarSheet extends Widget {
}

private get todayDateString() : string {
return this.asUTCDate(new Date()).toISOString().slice(0, this.settings.dateOnly ? 10 : 16);
const isoString = this.asUTCDate(new Date()).toISOString();
return this.settings.dateOnly ? `${isoString.slice(0, 10)}T00:00` : isoString.slice(0, 16);
}

private asUTCDate(date: Date) : Date {
Expand Down Expand Up @@ -246,10 +247,10 @@ export class CalendarSheet extends Widget {
}

private registerWeeksView() {
const todayDateString = this.todayDateString.slice(0, 10);
const todayDateString = `${this.todayDateString.slice(0, 10)}T00:00`;
this.calendarItems.forEach(elem => {
elem.classList.toggle('today', elem.getAttribute('data-date') === todayDateString);
const date = this.getDate(elem);
elem.classList.toggle('today', elem.getAttribute('data-date') === todayDateString);
if (this.minWeekDate && date < this.minWeekDate || this.maxWeekDate && date > this.maxWeekDate) {
elem.toggleAttribute('disabled', true);
}
Expand All @@ -263,11 +264,10 @@ export class CalendarSheet extends Widget {
}

private registerMonthsView() {
const todayMonthString = this.todayDateString.slice(0, 7);
const todayMonthString = `${this.todayDateString.slice(0, 7)}-01T00:00`;
this.calendarItems.forEach(elem => {
const date = this.getDate(elem);
const monthString = elem.getAttribute('data-date')?.slice(0, 7);
elem.classList.toggle('today', monthString === todayMonthString);
elem.classList.toggle('today', elem.getAttribute('data-date') === todayMonthString);
if (this.minMonthDate && date < this.minMonthDate || this.maxMonthDate && date > this.maxMonthDate) {
elem.toggleAttribute('disabled', true);
}
Expand All @@ -281,11 +281,10 @@ export class CalendarSheet extends Widget {
}

private registerYearsView() {
const todayYearString = this.todayDateString.slice(0, 4);
const todayYearString = `${this.todayDateString.slice(0, 4)}-01-01T00:00`;
this.calendarItems.forEach(elem => {
const date = this.getDate(elem);
const yearString = elem.getAttribute('data-date')?.slice(0, 4);
elem.classList.toggle('today', yearString === todayYearString);
elem.classList.toggle('today', elem.getAttribute('data-date') === todayYearString);
if (this.minYearDate && date < this.minYearDate || this.maxYearDate && date > this.maxYearDate) {
elem.toggleAttribute('disabled', true);
}
Expand Down Expand Up @@ -338,11 +337,11 @@ export class CalendarSheet extends Widget {
break;
case 'Enter':
if (this.preselectedDate) {
const dateString = this.asUTCDate(this.preselectedDate).toISOString().slice(0, this.viewMode === ViewMode.hours ? 16 : 10);
const dateString = this.asUTCDate(this.preselectedDate).toISOString().slice(0, 16);
element = this.element.querySelector(`.sheet-body li[data-date="${dateString}"]`);
} else {
const date = this.upperRange ? this.dateRange[1] : this.dateRange[0];
const dateString = date ? this.asUTCDate(date).toISOString().slice(0, this.viewMode === ViewMode.hours ? 16 : 10) : '';
const dateString = date ? this.asUTCDate(date).toISOString().slice(0, 16) : '';
element = this.element.querySelector(`.sheet-body li[data-date="${dateString}"]`);
}
if (element) {
Expand Down Expand Up @@ -383,13 +382,17 @@ export class CalendarSheet extends Widget {
[Direction.down, +10080],
[Direction.left, -1440],
]);
const nextDate = new Date(lastDate);
let nextDate: Date;
switch (this.viewMode) {
case ViewMode.hours:
return new Date(lastDate.getTime() + 60000 * deltaHours.get(direction)!);
nextDate = new Date(lastDate.getTime() + 60000 * deltaHours.get(direction)!);
break;
case ViewMode.weeks:
return new Date(lastDate.getTime() + 60000 * deltaWeeks.get(direction)!);
nextDate = new Date(lastDate.getTime() + 60000 * deltaWeeks.get(direction)!);
nextDate.setHours(0, 0, 0);
break;
case ViewMode.months:
nextDate = new Date(lastDate);
switch (direction) {
case Direction.up:
nextDate.setMonth(nextDate.getMonth() - 3);
Expand All @@ -404,8 +407,11 @@ export class CalendarSheet extends Widget {
nextDate.setMonth(nextDate.getMonth() - 1);
break;
}
nextDate.setDate(1);
nextDate.setHours(0, 0, 0);
break;
case ViewMode.years:
nextDate = new Date(lastDate);
switch (direction) {
case Direction.up:
nextDate.setFullYear(lastDate.getFullYear() - 4);
Expand All @@ -420,6 +426,9 @@ export class CalendarSheet extends Widget {
nextDate.setFullYear(lastDate.getFullYear() - 1);
break;
}
nextDate.setMonth(0);
nextDate.setDate(1);
nextDate.setHours(0, 0, 0);
break;
}
return nextDate;
Expand Down Expand Up @@ -473,20 +482,20 @@ export class CalendarSheet extends Widget {
dateString = utcDateString.slice(0, 16);
break;
case ViewMode.weeks:
dateString = utcDateString.slice(0, 10);
dateString = `${utcDateString.slice(0, 10)}T00:00`;
break;
case ViewMode.months:
dateString = `${utcDateString.slice(0, 7)}-01`;
dateString = `${utcDateString.slice(0, 7)}-01T00:00`;
break;
case ViewMode.years:
dateString = `${utcDateString.slice(0, 4)}-01-01`;
dateString = `${utcDateString.slice(0, 4)}-01-01T00:00`;
break;
}
return `li[data-date="${dateString}"]`;
}

private indexOfCalendarItem(date: Date) : number {
const dateSelector= this.getDateSelector(date);
const dateSelector = this.getDateSelector(date);
const selectedElement = this.element.querySelector(dateSelector);
return Array.from(this.calendarItems).indexOf(selectedElement as HTMLLIElement);
}
Expand Down Expand Up @@ -588,7 +597,7 @@ export class CalendarSheet extends Widget {
this.calendarItems.item(upperIndex)?.classList.add('selected');
}
} else if (this.dateRange[0]) {
const dateSelector= this.getDateSelector(this.dateRange[0]);
const dateSelector = this.getDateSelector(this.dateRange[0]);
this.element.querySelector(dateSelector)?.classList.add('selected');
}
if (this.preselectedDate) {
Expand All @@ -602,7 +611,7 @@ export class CalendarSheet extends Widget {
);
}
} else if (this.dateRange[0]) {
const dateSelector= this.getDateSelector(this.dateRange[0]);
const dateSelector = this.getDateSelector(this.dateRange[0]);
this.element.querySelector(dateSelector)?.classList.add('selected');
}
}
Expand Down Expand Up @@ -680,7 +689,7 @@ export class CalendarSheet extends Widget {
}

private async selectToday() {
const todayDateString = this.todayDateString.slice(0, 10);
const todayDateString = this.todayDateString;
let todayElem = this.element.querySelector(`li[data-date="${todayDateString}"]`);
if (!todayElem) {
await this.fetchCalendar(new Date(), ViewMode.weeks);
Expand Down Expand Up @@ -766,7 +775,7 @@ export class CalendarSheet extends Widget {
}
const nextDate = this.getDelta(direction, selectedDate);
this.preselectedDate = this.settings.withRange ? nextDate : null;
const dataDateString = this.asUTCDate(nextDate).toISOString().slice(0, this.viewMode === ViewMode.hours ? 16 : 10);
const dataDateString = this.asUTCDate(nextDate).toISOString().slice(0, 16);
let nextItem: Element|null = null;
if (this.viewMode !== ViewMode.weeks || selectedDate.getMonth() === nextDate.getMonth()) {
nextItem = this.element.querySelector(`.sheet-body li[data-date="${dataDateString}"]`);
Expand Down Expand Up @@ -946,7 +955,7 @@ export class DateCalendarElement extends HTMLInputElement {

this[CAL] = new CalendarSheet(calendarElement as HTMLElement, settings);
if (this.value) {
this[CAL].updateDate(new Date(this.value), null);
this[CAL].updateDate(new Date(`${this.value}T00:00`), null);
}
this.hidden = true;
}
Expand Down Expand Up @@ -1010,8 +1019,8 @@ export class DateRangeCalendarElement extends HTMLInputElement {
pure: true,
updateDate: (lowerDate: Date, upperDate?: Date) => {
const dateStrings = [
lowerDate.toISOString().slice(0, 10),
upperDate?.toISOString().slice(0, 10) ?? '',
`${lowerDate.toISOString().slice(0, 10)}T00:00`,
upperDate ? `${upperDate.toISOString().slice(0, 10)}T00:00` : '',
];
this.value = dateStrings.join(';');
this.dispatchEvent(new Event('input'));
Expand Down
34 changes: 22 additions & 12 deletions client/django-formset/DateTime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,16 @@ class DateTimeField extends Widget {
}

private setInitialDate() {
const timestamp = (value: string) => value.length === 10 ? value + 'T00:00' : value;

const value = this.inputElement.value;
if (value) {
if (this.withRange) {
const [start, end] = value.split(';');
this.currentDate = start ? new Date(start) : null;
this.extendedDate = end ? new Date(end) : null;
this.currentDate = start ? new Date(timestamp(start)) : null;
this.extendedDate = end ? new Date(timestamp(end)) : null;
} else {
this.currentDate = new Date(value);
this.currentDate = new Date(timestamp(value));
this.extendedDate = null;
}
} else {
Expand Down Expand Up @@ -244,13 +246,19 @@ class DateTimeField extends Widget {
};
if (this.currentDate) {
setDateParts(this.currentDate, FieldPart.year, FieldPart.month, FieldPart.day, FieldPart.hour, FieldPart.minute);
this.inputElement.value = this.currentDate.toISOString().slice(0, this.dateOnly ? 10 : 16);
if (this.extendedDate) {
setDateParts(this.extendedDate, FieldPart.yearExt, FieldPart.monthExt, FieldPart.dayExt, FieldPart.hourExt, FieldPart.minuteExt);
this.inputElement.value = [
this.inputElement.value,
this.extendedDate.toISOString().slice(0, this.dateOnly ? 10 : 16),
].join(';');
const isoString = this.currentDate.toISOString();
if (this.withRange) {
this.inputElement.value = this.dateOnly ? `${isoString.slice(0, 10)}T00:00` : isoString.slice(0, 16);
if (this.extendedDate) {
setDateParts(this.extendedDate, FieldPart.yearExt, FieldPart.monthExt, FieldPart.dayExt, FieldPart.hourExt, FieldPart.minuteExt);
const isoString = this.extendedDate.toISOString();
this.inputElement.value = [
this.inputElement.value,
this.dateOnly ? `${isoString.slice(0, 10)}T00:00` : isoString.slice(0, 16),
].join(';');
}
} else {
this.inputElement.value = isoString.slice(0, this.dateOnly ? 10 : 16);
}
} else {
this.inputFields.forEach(field => field.innerText = '');
Expand All @@ -267,8 +275,10 @@ class DateTimeField extends Widget {
'-',
Math.min(Math.max(parseInt(this.inputFields[day].innerText), 1), 31).toString().padStart(2, '0'),
];
if (!this.dateOnly) {
dateParts.push('T');
dateParts.push('T');
if (this.dateOnly) {
dateParts.push('00:00');
} else {
dateParts.push(Math.min(Math.max(parseInt(this.inputFields[hour].innerText), 0), 23).toString().padStart(2, '0'));
dateParts.push(':');
dateParts.push(Math.min(Math.max(parseInt(this.inputFields[minute].innerText), 0), 59).toString().padStart(2, '0'));
Expand Down
33 changes: 18 additions & 15 deletions formset/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ def get_context_hours(self, hour12, interval):
hour_localized = '12am' if hour12 else '24h'
context['shifts'].append([(next_day.replace(hour=0, minute=0).isoformat()[:16], hour_localized, None)])
context.update(
prev_day=(start_datetime - timedelta(days=1)).isoformat()[:10],
next_day=(start_datetime + timedelta(days=1)).isoformat()[:10],
prev_day=(start_datetime - timedelta(days=1)).isoformat()[:16],
next_day=(start_datetime + timedelta(days=1)).isoformat()[:16],
)
return context

Expand All @@ -87,18 +87,18 @@ def get_context_weeks(self):
safe_day = min(start_datetime.day, 28) # prevent date arithmetic errors on adjacent months
if start_datetime.month == 1:
context.update(
prev_month=start_datetime.replace(day=safe_day, month=12, year=start_datetime.year - 1).isoformat()[:10],
next_month=start_datetime.replace(day=safe_day, month=start_datetime.month + 1).isoformat()[:10],
prev_month=start_datetime.replace(day=safe_day, month=12, year=start_datetime.year - 1).isoformat()[:16],
next_month=start_datetime.replace(day=safe_day, month=start_datetime.month + 1).isoformat()[:16],
)
elif start_datetime.month == 12:
context.update(
prev_month=start_datetime.replace(day=safe_day, month=start_datetime.month - 1).isoformat()[:10],
next_month=start_datetime.replace(day=safe_day, month=1, year=start_datetime.year + 1).isoformat()[:10],
prev_month=start_datetime.replace(day=safe_day, month=start_datetime.month - 1).isoformat()[:16],
next_month=start_datetime.replace(day=safe_day, month=1, year=start_datetime.year + 1).isoformat()[:16],
)
else:
context.update(
prev_month=start_datetime.replace(day=safe_day, month=start_datetime.month - 1).isoformat()[:10],
next_month=start_datetime.replace(day=safe_day, month=start_datetime.month + 1).isoformat()[:10],
prev_month=start_datetime.replace(day=safe_day, month=start_datetime.month - 1).isoformat()[:16],
next_month=start_datetime.replace(day=safe_day, month=start_datetime.month + 1).isoformat()[:16],
)
monthdays = []
for day in cal.itermonthdays3(start_datetime.year, start_datetime.month):
Expand All @@ -107,7 +107,8 @@ def get_context_weeks(self):
css_classes = []
if monthday.month != start_datetime.month:
css_classes.append('adjacent')
context['monthdays'].append((monthday.isoformat()[:10], monthday.day, ' '.join(css_classes)))
timestamp = f'{monthday.isoformat()[:10]}T00:00'
context['monthdays'].append((timestamp, monthday.day, ' '.join(css_classes)))
context['weekdays'] = [(date_format(day, 'D'), date_format(day, 'l')) for day in monthdays[:7]]
return context

Expand All @@ -117,12 +118,13 @@ def get_context_months(self):
'months': [],
}
context.update(
prev_year=start_datetime.replace(year=start_datetime.year - 1, month=1).isoformat()[:10],
next_year=start_datetime.replace(year=start_datetime.year + 1, month=1).isoformat()[:10],
prev_year=start_datetime.replace(year=start_datetime.year - 1, month=1).isoformat()[:16],
next_year=start_datetime.replace(year=start_datetime.year + 1, month=1).isoformat()[:16],
)
for m in range(1, 13):
month_date = date(start_datetime.year, m, 1)
context['months'].append((month_date.isoformat()[:10], date_format(month_date, 'F')))
timestamp = f'{month_date.isoformat()[:10]}T00:00'
context['months'].append((timestamp, date_format(month_date, 'F')))
return context

def get_context_years(self):
Expand All @@ -132,12 +134,13 @@ def get_context_years(self):
}
start_epoch = int(start_datetime.year / 20) * 20
context.update(
prev_epoch=start_datetime.replace(year=start_epoch - 20, month=1).isoformat()[:10],
next_epoch=start_datetime.replace(year=start_epoch + 20, month=1).isoformat()[:10],
prev_epoch=start_datetime.replace(year=start_epoch - 20, month=1).isoformat()[:16],
next_epoch=start_datetime.replace(year=start_epoch + 20, month=1).isoformat()[:16],
)
for y in range(start_epoch, start_epoch + 20):
year_date = date(y, 1, 1)
context['years'].append((year_date.isoformat()[:10], date_format(year_date, 'Y')))
timestamp = f'{year_date.isoformat()[:10]}T00:00'
context['years'].append((timestamp, date_format(year_date, 'Y')))
return context

def get_context(self, hour12=False, interval=None):
Expand Down
22 changes: 14 additions & 8 deletions formset/ranges.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class DateRangeCalendar(DateCalendar):
def __init__(self, attrs=None, calendar_renderer=None):
default_attrs = {
'type': 'regex',
'pattern': r'\d{4}-\d{2}-\d{2};\d{4}-\d{2}-\d{2}',
'pattern': r'\d{4}-\d{2}-\d{2}T00:00;\d{4}-\d{2}-\d{2}T00:00',
'is': 'django-daterangecalendar',
}
if attrs:
Expand All @@ -22,7 +22,7 @@ class DateRangePicker(DatePicker):
def __init__(self, attrs=None, calendar_renderer=None):
default_attrs = {
'type': 'regex',
'pattern': r'\d{4}-\d{2}-\d{2};\d{4}-\d{2}-\d{2}',
'pattern': r'\d{4}-\d{2}-\d{2}T00:00;\d{4}-\d{2}-\d{2}T00:00',
'is': 'django-daterangepicker',
}
if attrs:
Expand All @@ -34,7 +34,7 @@ class DateRangeTextbox(DateTextbox):
def __init__(self, attrs=None):
default_attrs = {
'type': 'regex',
'pattern': r'\d{4}-\d{2}-\d{2};\d{4}-\d{2}-\d{2}',
'pattern': r'\d{4}-\d{2}-\d{2}T00:00;\d{4}-\d{2}-\d{2}T00:00',
'is': 'django-daterangefield',
}
if attrs:
Expand Down Expand Up @@ -99,9 +99,7 @@ def __init__(self, widget, **kwargs):
super().__init__(widget=widget, **kwargs)

def prepare_value(self, values):
if isinstance(values, (list, tuple)) and len(values) == 2:
return ';'.join((values[0].isoformat()[:self.num_digits], values[1].isoformat()[:self.num_digits]))
return ''
raise NotImplementedError("Subclasses must implement this method.")

def compress(self, values):
if not values:
Expand All @@ -123,17 +121,25 @@ class DateRangeField(BaseRangeField):
'bound_ordering': _("The start date must be before the end date."),
}
base_field = fields.DateField
num_digits = 10

def __init__(self, **kwargs):
kwargs.setdefault('widget', DateRangePicker())
super().__init__(**kwargs)

def prepare_value(self, values):
if isinstance(values, (list, tuple)) and len(values) == 2:
return ';'.join(map(lambda v: f'{v.isoformat()[:10]}T00:00', values))
return ''


class DateTimeRangeField(DateRangeField):
base_field = fields.DateTimeField
num_digits = 16

def __init__(self, **kwargs):
kwargs.setdefault('widget', DateTimeRangePicker())
super().__init__(**kwargs)

def prepare_value(self, values):
if isinstance(values, (list, tuple)) and len(values) == 2:
return ';'.join(map(lambda v: v.isoformat()[:16], values))
return ''
Loading

0 comments on commit 404c37d

Please sign in to comment.