Skip to content

Commit

Permalink
Switch to using Luxon to parse dates for the date time picker. Also,
Browse files Browse the repository at this point in the history
instead of using javascript Intl formats for date/times, use format
strings chosen to match the perl DateTime::Locale formats.  We have to
use formats in any case to correctly parse strings back to a unix
timestamp.  So we might as well use them both ways, and make the perl
display match that of the javascript better.  There is only one
exception.  That is for the Korean language.  The perl DateTime::Locale
does not translate the AM/PM meridian, and the javascript does.  There
isn't much that can be done about that except for fixing the perl
DateTime::Locale data.
  • Loading branch information
drgrice1 committed Apr 4, 2022
1 parent 300b45a commit a47d789
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 53 deletions.
70 changes: 44 additions & 26 deletions htdocs/js/apps/DatePicker/datepicker.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,24 @@
(() => {
// Date/time formats for the languages supported by webwork.
// Note that these formats are chosen to match the perl DateTime::Locale formats.
// Make sure that anytime a new language is added, its format is added here.
const datetimeFormats = {
en: 'L/d/yy, h:mm a',
'en-US': 'L/d/yy, h:mm a',
'cs-CZ': 'dd.LL.yy H:mm',
de: 'dd.LL.yy, HH:mm',
es: 'd/L/yy H:mm',
'fr-CA': "yyyy-LL-dd HH 'h' mm",
fr: 'dd/LL/yyyy HH:mm',
'he-IL': 'd.L.yyyy, H:mm',
hu: 'yyyy. LL. dd. H:mm',
ko: 'yy. L. d. a h:mm',
'ru-RU': 'dd.LL.yyyy, HH:mm',
tr: 'd.LL.yyyy HH:mm',
'zh-CN': 'yyyy/L/d ah:mm',
'zh-HK': 'yyyy/L/d ah:mm'
};

document.querySelectorAll('.datepicker-group').forEach((open_rule) => {
const name = open_rule.name.replace('.open_date', '');

Expand All @@ -24,6 +44,8 @@
for (const rule of groupRules) {
const orig_value = rule.value;

luxon.Settings.defaultLocale = rule.dataset.locale?.replaceAll(/_/g, '-') ?? 'en';

// Compute the time difference between the current browser timezone and the the course timezone.
// flatpickr gives the time in the browser's timezone, and this is used to adjust to the course timezone.
// Note that this is converted to seconds.
Expand All @@ -40,6 +62,8 @@
minuteIncrement: 1,
altInput: true,
dateFormat: 'U',
altFormat: datetimeFormats[luxon.Settings.defaultLocale],
ariaDateFormat: datetimeFormats[luxon.Settings.defaultLocale],
defaultDate: orig_value,
defaultHour: 0,
locale: rule.dataset.locale ? rule.dataset.locale.substring(0, 2) : 'en',
Expand All @@ -48,10 +72,6 @@
wrap: true,
plugins: [ new confirmDatePlugin({ confirmText: rule.dataset.doneText ?? 'Done', showAlways: true }) ],
onChange(selectedDates) {
// If the altInput field has been emptied, then the formatDate method still sets the hidden input.
// So set that back to empty again.
if (!selectedDates.length) this.input.value = '';

if (this.input.value === orig_value) this.altInput.classList.remove('changed');
else this.altInput.classList.add('changed');
},
Expand All @@ -63,34 +83,32 @@
this.altInput.after(this.input);

this.altInput.addEventListener('blur', update);

// If the inital value is empty, then the formatDate method still sets the hidden input.
// So set that back to empty again.
if (!selectedDates.length) this.input.value = '';
},
parseDate(datestr, format) {
// Deal with the case of a unix timestamp on initial load. At this time the timezone needs to be
// adjusted backward as flatpickr is going to use the browser's time zone.
// Deal with the case of a unix timestamp. The timezone needs to be adjusted back as this is for
// the unix timestamp stored in the hidden input whose value will be sent to the server.
if (format === 'U') return new Date(parseInt(datestr) * 1000 - timezoneAdjustment);
// Next attempt to parse the datestr with the current format. This should not be adjusted.
const date = new Date(Date.parse(datestr, format));
if (!isNaN(date.getTime())) return date;
// Finally, fall back to the previous value in the original input if that failed. This also needs
// to be adjusted back since the adjusted timestamp is saved in the input.
return new Date(parseInt(rule.value) * 1000 - timezoneAdjustment);

// Next attempt to parse the datestr with the current format. This should not be adjusted. It is
// for display only.
const date = luxon.DateTime.fromFormat(datestr, format);
if (date.isValid) return date.toJSDate();

// Finally, fall back to the previous value in the original input if that failed. This is the case
// that the user typed a time that isn't in the valid format. So fallback to the last valid time
// that was displayed. This also should not be adjusted.
return new Date(this.lastFormattedDate.getTime());
},
formatDate(date) {
// flatpickr gives the date in the browser's time zone. So it needs to be adjusted to the timezone
// of the course.
formatDate(date, format) {
// Save this date for the fallback in parseDate.
this.lastFormattedDate = date;

// Flatpickr sets the value of the original input to the parsed time.
// So set that back to the unix timestamp.
rule.value = date.getTime() / 1000 + timezoneAdjustment / 1000;
// In this case the date provided is in the browser's time zone. So it needs to be adjusted to the
// timezone of the course.
if (format === 'U') return (date.getTime() + timezoneAdjustment) / 1000;

// Return the localized time string.
return Intl.DateTimeFormat(rule.dataset.locale?.replaceAll(/_/g, '-') ?? 'en',
{ dateStyle: 'short', timeStyle: 'short', timeZone: rule.dataset.timezone ?? 'UTC' })
.format(new Date(date.getTime() + timezoneAdjustment));
return luxon.DateTime.fromMillis(date.getTime()).toFormat(
datetimeFormats[luxon.Settings.defaultLocale]);
}
});
}
Expand Down
72 changes: 45 additions & 27 deletions htdocs/js/apps/ProblemSetList/problemsetlist.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,32 @@
});
}

// Date/time formats for the languages supported by webwork.
// Note that these formats are chosen to match the perl DateTime::Locale formats.
// Make sure that anytime a new language is added, its format is added here.
const datetimeFormats = {
en: 'L/d/yy, h:mm a',
'en-US': 'L/d/yy, h:mm a',
'cs-CZ': 'dd.LL.yy H:mm',
de: 'dd.LL.yy, HH:mm',
es: 'd/L/yy H:mm',
'fr-CA': "yyyy-LL-dd HH 'h' mm",
fr: 'dd/LL/yyyy HH:mm',
'he-IL': 'd.L.yyyy, H:mm',
hu: 'yyyy. LL. dd. H:mm',
ko: 'yy. L. d. a h:mm',
'ru-RU': 'dd.LL.yyyy, HH:mm',
tr: 'd.LL.yyyy HH:mm',
'zh-CN': 'yyyy/L/d ah:mm',
'zh-HK': 'yyyy/L/d ah:mm'
};

// Initialize the date/time picker for the import form.
const importDateShift = document.getElementById('import_date_shift');
if (importDateShift) {

luxon.Settings.defaultLocale = importDateShift.dataset.locale?.replaceAll(/_/g, '-') ?? 'en';

// Compute the time difference between the current browser timezone and the the course timezone.
// flatpickr gives the time in the browser's timezone, and this is used to adjust to the course timezone.
// Note that this is converted to microseconds.
Expand All @@ -60,6 +83,8 @@
minuteIncrement: 1,
altInput: true,
dateFormat: 'U',
altFormat: datetimeFormats[luxon.Settings.defaultLocale],
ariaDateFormat: datetimeFormats[luxon.Settings.defaultLocale],
defaultHour: 0,
locale: importDateShift.dataset.locale ? importDateShift.dataset.locale.substring(0, 2) : 'en',
clickOpens: false,
Expand All @@ -71,39 +96,32 @@
// bootstrap input group styling. So move the now hidden original input after the created alternate
// input to fix that.
this.altInput.after(this.input);

// If the inital value is empty, then the formatDate method still sets the hidden input.
// So set that back to empty again.
if (!selectedDates.length) this.input.value = '';
},
onChange(selectedDates) {
// If the altInput field has been emptied, then the formatDate method still sets the hidden input.
// So set that back to empty again.
if (!selectedDates.length) this.input.value = '';
},
parseDate(datestr, format) {
// Deal with the case of a unix timestamp on initial load. At this time the timezone needs to be
// adjusted backward as flatpickr is going to use the browser's time zone.
// Deal with the case of a unix timestamp. The timezone needs to be adjusted back as this is for
// the unix timestamp stored in the hidden input whose value will be sent to the server.
if (format === 'U') return new Date(parseInt(datestr) * 1000 - timezoneAdjustment);
// Next attempt to parse the datestr with the current format. This should not be adjusted.
const date = new Date(Date.parse(datestr, format));
if (!isNaN(date.getTime())) return date;
// Finally, fall back to the previous value in the original input if that failed. This also needs
// to be adjusted back since the adjusted timestamp is saved in the input.
return new Date(parseInt(importDateShift.value) * 1000 - timezoneAdjustment);

// Next attempt to parse the datestr with the current format. This should not be adjusted. It is
// for display only.
const date = luxon.DateTime.fromFormat(datestr, format);
if (date.isValid) return date.toJSDate();

// Finally, fall back to the previous value in the original input if that failed. This is the case
// that the user typed a time that isn't in the valid format. So fallback to the last valid time
// that was displayed. This also should not be adjusted.
return new Date(this.lastFormattedDate.getTime());
},
formatDate(date) {
// flatpickr gives the date in the browser's time zone. So it needs to be adjusted to the timezone
// of the course.
formatDate(date, format) {
// Save this date for the fallback in parseDate.
this.lastFormattedDate = date;

// Flatpickr sets the value of the original input to the parsed time.
// So set that back to the unix timestamp.
importDateShift.value = date.getTime() / 1000 + timezoneAdjustment / 1000;
// In this case the date provided is in the browser's time zone. So it needs to be adjusted to the
// timezone of the course.
if (format === 'U') return (date.getTime() + timezoneAdjustment) / 1000;

// Return the localized time string.
return Intl.DateTimeFormat(importDateShift.dataset.locale?.replaceAll(/_/g, '-') ?? 'en',
{ dateStyle: 'short', timeStyle: 'short', timeZone: importDateShift.dataset.timezone ?? 'UTC' })
.format(new Date(date.getTime() + timezoneAdjustment));
return luxon.DateTime.fromMillis(date.getTime()).toFormat(
datetimeFormats[luxon.Settings.defaultLocale]);
}
});
}
Expand Down
1 change: 1 addition & 0 deletions htdocs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"iframe-resizer": "^4.3.2",
"jquery": "^3.6.0",
"jquery-ui-dist": "^1.13.1",
"luxon": "^2.3.1",
"mathjax": "^3.2.0",
"sortablejs": "^1.14.0"
},
Expand Down
1 change: 1 addition & 0 deletions htdocs/third-party-assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"node_modules/jquery-ui-dist/jquery-ui.min.css": "https://cdn.jsdelivr.net/npm/[email protected]/jquery-ui.min.css",
"node_modules/jquery-ui-dist/jquery-ui.min.js": "https://cdn.jsdelivr.net/npm/[email protected]/jquery-ui.min.js",
"node_modules/jquery/dist/jquery.min.js": "https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js",
"node_modules/luxon/build/global/luxon.min.js": "https://cdn.jsdelivr.net/npm/[email protected]/build/global/luxon.min.js",
"node_modules/mathjax/es5/tex-chtml.js": "https://cdn.jsdelivr.net/npm/[email protected]/es5/tex-chtml.js",
"node_modules/sortablejs/Sortable.min.js": "https://cdn.jsdelivr.net/npm/[email protected]/Sortable.min.js"
}
Original file line number Diff line number Diff line change
Expand Up @@ -2894,6 +2894,7 @@ sub output_JS {
rel => 'stylesheet',
href => getAssetURL($ce, 'node_modules/flatpickr/dist/plugins/confirmDate/confirmDate.css')
});
print CGI::script({ src => getAssetURL($ce, 'node_modules/luxon/build/global/luxon.min.js'), defer => undef }, '');
print CGI::script({ src => getAssetURL($ce, 'node_modules/flatpickr/dist/flatpickr.min.js'), defer => undef }, '');
if ($ce->{language} !~ /^en/) {
print CGI::script(
Expand Down
1 change: 1 addition & 0 deletions lib/WeBWorK/ContentGenerator/Instructor/ProblemSetList.pm
Original file line number Diff line number Diff line change
Expand Up @@ -2744,6 +2744,7 @@ sub output_JS {
rel => 'stylesheet',
href => getAssetURL($ce, 'node_modules/flatpickr/dist/plugins/confirmDate/confirmDate.css')
});
print CGI::script({ src => getAssetURL($ce, 'node_modules/luxon/build/global/luxon.min.js'), defer => undef }, '');
print CGI::script({ src => getAssetURL($ce, 'node_modules/flatpickr/dist/flatpickr.min.js'), defer => undef }, '');
if ($ce->{language} !~ /^en/) {
print CGI::script(
Expand Down
1 change: 1 addition & 0 deletions lib/WeBWorK/ContentGenerator/Instructor/UserDetail.pm
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,7 @@ sub output_JS {
rel => 'stylesheet',
href => getAssetURL($ce, 'node_modules/flatpickr/dist/plugins/confirmDate/confirmDate.css')
});
print CGI::script({ src => getAssetURL($ce, 'node_modules/luxon/build/global/luxon.min.js'), defer => undef }, '');
print CGI::script({ src => getAssetURL($ce, 'node_modules/flatpickr/dist/flatpickr.min.js'), defer => undef }, '');
if ($ce->{language} !~ /^en/) {
print CGI::script(
Expand Down

0 comments on commit a47d789

Please sign in to comment.