Do not allow manual entry of insane years into datepickers
Fixes FOO-4288 flag=none It's all too easy to inadvertently manually enter an incorrect year when typing into either the jQuery or the InstUI date input components. For example, while '20' is read as the year 2020, '2' becomes 1902 and '202' is left as the year 202. This has created issues in custommer accounts. Since Canvas dates are always related to some school term that is likely to be in or near the present, it makes the most sense to simply have the datepickers reject any year earlier than 1980. That figure was arbitrarily chosen but seems reasonable. Test plan: * Try both datepickers (suggest the course module "Lock Until" setting for the jQuery picker and the "Course Participation" start and end dates in the main course settings page). * Free-form type a date. Any date that resolves to a year after 1980 should work just as always; any earlier year should be rejected and the displayed error ("suggestion") below the picker field should indicate that the year is too far in the past. Change-Id: Ib53d8c5cca1f9e76319323c5eda316eb63c34ef1 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/344411 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: August Thornton <august@instructure.com> QA-Review: August Thornton <august@instructure.com> Product-Review: Charley Kline <ckline@instructure.com>
This commit is contained in:
parent
28a2c34666
commit
edbed718bf
|
@ -19,7 +19,7 @@
|
|||
import london from 'timezone/Europe/London'
|
||||
import * as tz from '@canvas/datetime'
|
||||
import coupleTimeFields from '@canvas/calendar/jquery/coupleTimeFields'
|
||||
import DatetimeField from '@canvas/datetime/jquery/DatetimeField'
|
||||
import DatetimeField, {PARSE_RESULTS} from '@canvas/datetime/jquery/DatetimeField'
|
||||
import $ from 'jquery'
|
||||
import 'jquery-migrate'
|
||||
import fakeENV from 'helpers/fakeENV'
|
||||
|
@ -80,7 +80,7 @@ test('leaves invalid start alone', function () {
|
|||
this.end.setTime(fixed)
|
||||
coupleTimeFields(this.$start, this.$end)
|
||||
equal(this.$start.val(), 'invalid')
|
||||
equal(this.start.invalid, true)
|
||||
equal(this.start.valid, PARSE_RESULTS.ERROR)
|
||||
})
|
||||
|
||||
test('leaves invalid end alone', function () {
|
||||
|
@ -89,7 +89,7 @@ test('leaves invalid end alone', function () {
|
|||
this.end.setFromValue()
|
||||
coupleTimeFields(this.$start, this.$end)
|
||||
equal(this.$end.val(), 'invalid')
|
||||
equal(this.end.invalid, true)
|
||||
equal(this.end.valid, PARSE_RESULTS.ERROR)
|
||||
})
|
||||
|
||||
test('interprets time as occurring on date', function () {
|
||||
|
@ -156,7 +156,7 @@ test('leaves invalid start alone', function () {
|
|||
this.end.setTime(fixed)
|
||||
this.$end.trigger('blur')
|
||||
equal(this.$start.val(), 'invalid')
|
||||
equal(this.start.invalid, true)
|
||||
equal(this.start.valid, PARSE_RESULTS.ERROR)
|
||||
})
|
||||
|
||||
test('leaves invalid end alone', function () {
|
||||
|
@ -165,7 +165,7 @@ test('leaves invalid end alone', function () {
|
|||
this.end.setFromValue()
|
||||
this.$start.trigger('blur')
|
||||
equal(this.$end.val(), 'invalid')
|
||||
equal(this.end.invalid, true)
|
||||
equal(this.end.valid, PARSE_RESULTS.ERROR)
|
||||
})
|
||||
|
||||
test('does not rewrite blurred input', function () {
|
||||
|
|
|
@ -20,6 +20,7 @@ import DatetimeField, {
|
|||
DATE_FORMAT_OPTIONS,
|
||||
DATETIME_FORMAT_OPTIONS,
|
||||
TIME_FORMAT_OPTIONS,
|
||||
PARSE_RESULTS,
|
||||
} from '@canvas/datetime/jquery/DatetimeField'
|
||||
import '@canvas/datetime/jquery'
|
||||
import $ from 'jquery'
|
||||
|
@ -32,8 +33,8 @@ import juneau from 'timezone/America/Juneau'
|
|||
import fakeENV from 'helpers/fakeENV'
|
||||
import moment from 'moment'
|
||||
|
||||
const moonwalk = new Date('1969-07-21T02:56:00Z')
|
||||
const john_glenn = new Date('1962-02-20T14:47:39')
|
||||
const challenger = new Date('1986-01-28T16:39:00Z')
|
||||
const columbia = new Date('2003-02-01T13:59:00Z')
|
||||
|
||||
QUnit.module('processTimeOptions', {
|
||||
setup() {
|
||||
|
@ -222,9 +223,9 @@ test('should add hidden input when requested', function () {
|
|||
})
|
||||
|
||||
test('should initialize from the inputdate data', function () {
|
||||
this.$field.data('inputdate', '1969-07-21T02:56:00Z')
|
||||
this.$field.data('inputdate', '1986-01-28T16:39:00Z')
|
||||
const field = new DatetimeField(this.$field, {})
|
||||
equal(field.$suggest.text(), 'Sun, Jul 20, 1969, 9:56 PM')
|
||||
equal(field.$suggest.text(), 'Tue, Jan 28, 1986, 11:39 AM')
|
||||
})
|
||||
|
||||
test('should initialize from the initial value attribute if present, and then remove it', function () {
|
||||
|
@ -239,8 +240,8 @@ test('should initialize from the initial value attribute if present, and then re
|
|||
test('should tie it to update on keyup only', function () {
|
||||
const field = new DatetimeField(this.$field, {})
|
||||
|
||||
this.$field.val('Jul 21, 1969 5:56am').trigger('keyup')
|
||||
equal(field.$suggest.text(), 'Mon, Jul 21, 1969, 5:56 AM')
|
||||
this.$field.val('Jan 28, 1986 5:56am').trigger('keyup')
|
||||
equal(field.$suggest.text(), 'Tue, Jan 28, 1986, 5:56 AM')
|
||||
})
|
||||
|
||||
QUnit.module('setFromValue', {
|
||||
|
@ -269,9 +270,9 @@ test('should set data fields', function () {
|
|||
})
|
||||
|
||||
test('should set suggest text', function () {
|
||||
this.$field.val('Jul 21, 1969 at 2:56am')
|
||||
this.$field.val('Jan 28, 1986 at 2:56am')
|
||||
this.field.setFromValue()
|
||||
equal(this.field.$suggest.text(), 'Mon, Jul 21, 1969, 2:56 AM')
|
||||
equal(this.field.$suggest.text(), 'Tue, Jan 28, 1986, 2:56 AM')
|
||||
})
|
||||
|
||||
QUnit.module('parseValue', {
|
||||
|
@ -282,15 +283,15 @@ QUnit.module('parseValue', {
|
|||
})
|
||||
|
||||
test('sets @fudged according to browser (fudged) timezone', function () {
|
||||
this.$field.val(tz.format(moonwalk, '%b %-e, %Y at %-l:%M%P')).change()
|
||||
this.$field.val(tz.format(challenger, '%b %-e, %Y at %-l:%M%P')).change()
|
||||
this.field.parseValue()
|
||||
equal(+this.field.fudged, +$.fudgeDateForProfileTimezone(moonwalk))
|
||||
equal(+this.field.fudged, +$.fudgeDateForProfileTimezone(challenger))
|
||||
})
|
||||
|
||||
test('sets @datetime according to profile timezone', function () {
|
||||
this.$field.val(tz.format(moonwalk, '%b %-e, %Y at %-l:%M%P')).change()
|
||||
this.$field.val(tz.format(challenger, '%b %-e, %Y at %-l:%M%P')).change()
|
||||
this.field.parseValue()
|
||||
equal(+this.field.datetime, +moonwalk)
|
||||
equal(+this.field.datetime, +challenger)
|
||||
})
|
||||
|
||||
test('sets @showTime true by default', function () {
|
||||
|
@ -300,14 +301,14 @@ test('sets @showTime true by default', function () {
|
|||
})
|
||||
|
||||
test('sets @showTime false when value is midnight in profile timezone', function () {
|
||||
this.$field.val('Jan 1, 1970 at 12:00am').change()
|
||||
this.$field.val('Jan 1, 1990 at 12:00am').change()
|
||||
this.field.parseValue()
|
||||
equal(this.field.showTime, false)
|
||||
})
|
||||
|
||||
test('sets @showTime true for midnight if @alwaysShowTime', function () {
|
||||
this.field.alwaysShowTime = true
|
||||
this.$field.val('Jan 1, 1970 at 12:00am').change()
|
||||
this.$field.val('Jan 1, 1990 at 12:00am').change()
|
||||
this.field.parseValue()
|
||||
equal(this.field.showTime, true)
|
||||
})
|
||||
|
@ -320,17 +321,17 @@ test('sets @showTime false for non-midnight if not @allowTime', function () {
|
|||
})
|
||||
|
||||
test('sets not @blank and not @invalid on valid input', function () {
|
||||
this.$field.val('Jan 1, 1970 at 12:00am').change()
|
||||
this.$field.val('Jan 1, 1990 at 12:00am').change()
|
||||
this.field.parseValue()
|
||||
equal(this.field.blank, false)
|
||||
equal(this.field.invalid, false)
|
||||
equal(this.field.valid, PARSE_RESULTS.VALID)
|
||||
})
|
||||
|
||||
test('sets @blank and not @invalid and null dates when no input', function () {
|
||||
this.$field.val('').change()
|
||||
this.field.parseValue()
|
||||
equal(this.field.blank, true)
|
||||
equal(this.field.invalid, false)
|
||||
equal(this.field.valid, PARSE_RESULTS.VALID)
|
||||
equal(this.field.datetime, null)
|
||||
equal(this.field.fudged, null)
|
||||
})
|
||||
|
@ -339,7 +340,7 @@ test('sets @invalid and not @blank and null dates when invalid input', function
|
|||
this.$field.val('invalid').change()
|
||||
this.field.parseValue()
|
||||
equal(this.field.blank, false)
|
||||
equal(this.field.invalid, true)
|
||||
equal(this.field.valid, PARSE_RESULTS.ERROR)
|
||||
equal(this.field.datetime, null)
|
||||
equal(this.field.fudged, null)
|
||||
})
|
||||
|
@ -363,18 +364,18 @@ test('interprets bare numbers >= 8 in time-only fields as 24-hour', function ()
|
|||
|
||||
test('interprets time-only fields as occurring on implicit date if set', function () {
|
||||
this.field.showDate = false
|
||||
this.field.setDate(moonwalk)
|
||||
this.field.setDate(challenger)
|
||||
this.$field.val('12PM').change()
|
||||
this.field.parseValue()
|
||||
equal(tz.format(this.field.datetime, '%F %T'), `${tz.format(moonwalk, '%F ')}12:00:00`)
|
||||
equal(tz.format(this.field.datetime, '%F %T'), `${tz.format(challenger, '%F ')}12:00:00`)
|
||||
})
|
||||
|
||||
test('setDate changes the date of an existing time field', function () {
|
||||
this.field.showDate = false
|
||||
this.field.setDate(moonwalk)
|
||||
this.field.setDate(challenger)
|
||||
this.$field.val('12PM').change()
|
||||
this.field.setDate(john_glenn)
|
||||
equal(tz.format(this.field.datetime, '%F %T'), `${tz.format(john_glenn, '%F ')}12:00:00`)
|
||||
this.field.setDate(columbia)
|
||||
equal(tz.format(this.field.datetime, '%F %T'), `${tz.format(columbia, '%F ')}12:00:00`)
|
||||
})
|
||||
|
||||
QUnit.module('updateData', {
|
||||
|
@ -388,10 +389,10 @@ QUnit.module('updateData', {
|
|||
fakeENV.setup({TIMEZONE: 'America/Detroit'})
|
||||
|
||||
this.$field = $('<input type="text" name="due_at">')
|
||||
this.$field.val('Jan 1, 1970 at 12:01am')
|
||||
this.$field.val('Jan 1, 1990 at 12:01am')
|
||||
this.field = new DatetimeField(this.$field, {})
|
||||
this.field.datetime = moonwalk
|
||||
this.field.fudged = $.fudgeDateForProfileTimezone(moonwalk)
|
||||
this.field.datetime = challenger
|
||||
this.field.fudged = $.fudgeDateForProfileTimezone(challenger)
|
||||
},
|
||||
|
||||
teardown() {
|
||||
|
@ -407,7 +408,7 @@ test('sets date field to fudged time', function () {
|
|||
|
||||
test('sets unfudged-date field to actual time', function () {
|
||||
this.field.updateData()
|
||||
equal(+this.$field.data('unfudged-date'), +moonwalk)
|
||||
equal(+this.$field.data('unfudged-date'), +challenger)
|
||||
})
|
||||
|
||||
test('sets invalid field', function () {
|
||||
|
@ -428,16 +429,16 @@ test('sets value of hiddenInput, if present, to fudged time as ISO8601', functio
|
|||
|
||||
test('sets time-* to fudged, 12-hour values', function () {
|
||||
this.field.updateData()
|
||||
equal(this.$field.data('time-hour'), '9')
|
||||
equal(this.$field.data('time-minute'), '56')
|
||||
equal(this.$field.data('time-ampm'), 'PM')
|
||||
equal(this.$field.data('time-hour'), '11')
|
||||
equal(this.$field.data('time-minute'), '39')
|
||||
equal(this.$field.data('time-ampm'), 'AM')
|
||||
})
|
||||
|
||||
test('sets time-* to fudged, 24-hour values', function () {
|
||||
ENV.LOCALE = 'pt-BR'
|
||||
this.field.updateData()
|
||||
equal(this.$field.data('time-hour'), '21')
|
||||
equal(this.$field.data('time-minute'), '56')
|
||||
equal(this.$field.data('time-hour'), '11')
|
||||
equal(this.$field.data('time-minute'), '39')
|
||||
equal(this.$field.data('time-ampm'), null)
|
||||
})
|
||||
|
||||
|
@ -458,7 +459,7 @@ test('clear time-* to null if blank', function () {
|
|||
})
|
||||
|
||||
test('clear time-* to null if invalid', function () {
|
||||
this.field.invalid = true
|
||||
this.field.valid = PARSE_RESULTS.ERROR
|
||||
this.field.updateData()
|
||||
equal(this.$field.data('time-hour'), null)
|
||||
})
|
||||
|
@ -510,7 +511,7 @@ test('omits course suggest text if formatSuggestContext is empty', function () {
|
|||
test('adds invalid_datetime class to suggest if invalid', function () {
|
||||
this.field.updateSuggest()
|
||||
ok(!this.field.$suggest.hasClass('invalid_datetime'))
|
||||
this.field.invalid = true
|
||||
this.field.valid = PARSE_RESULTS.ERROR
|
||||
this.field.updateSuggest()
|
||||
ok(this.field.$suggest.hasClass('invalid_datetime'))
|
||||
})
|
||||
|
@ -533,6 +534,12 @@ test('should alert screenreader on an invalid parse no matter what', function ()
|
|||
ok(this.field.debouncedSRFME.withArgs("That's not a date!").called)
|
||||
})
|
||||
|
||||
test('should alert on an impossible year entered', function () {
|
||||
this.$field.val('Jul 19 1964')
|
||||
this.$field.change()
|
||||
ok(this.field.debouncedSRFME.withArgs('Year is too far in the past.').called)
|
||||
})
|
||||
|
||||
test('flashes suggest text to screenreader on typed input', function () {
|
||||
const value = 'suggested value'
|
||||
this.field.formatSuggest = () => value
|
||||
|
@ -573,7 +580,7 @@ QUnit.module('formatSuggest', {
|
|||
})
|
||||
fakeENV.setup({TIMEZONE: 'America/Detroit'})
|
||||
this.$field = $('<input type="text" name="due_at">')
|
||||
this.$field.val('Jul 20, 1969 at 9:56pm')
|
||||
this.$field.val('Jan 28, 1986 at 11:39am')
|
||||
this.field = new DatetimeField(this.$field, {})
|
||||
},
|
||||
|
||||
|
@ -584,7 +591,7 @@ QUnit.module('formatSuggest', {
|
|||
})
|
||||
|
||||
test('returns result formatted in profile timezone', function () {
|
||||
equal(this.field.formatSuggest(), 'Sun, Jul 20, 1969, 9:56 PM')
|
||||
equal(this.field.formatSuggest(), 'Tue, Jan 28, 1986, 11:39 AM')
|
||||
})
|
||||
|
||||
test('returns "" if @blank', function () {
|
||||
|
@ -593,23 +600,23 @@ test('returns "" if @blank', function () {
|
|||
})
|
||||
|
||||
test('returns error message if @invalid', function () {
|
||||
this.field.invalid = true
|
||||
this.field.valid = PARSE_RESULTS.ERROR
|
||||
equal(this.field.formatSuggest(), this.field.parseError)
|
||||
})
|
||||
|
||||
test('returns date only if @showTime false', function () {
|
||||
this.field.showTime = false
|
||||
equal(this.field.formatSuggest(), 'Sun, Jul 20, 1969')
|
||||
equal(this.field.formatSuggest(), 'Tue, Jan 28, 1986')
|
||||
})
|
||||
|
||||
test('returns time only if @showDate false', function () {
|
||||
this.field.showDate = false
|
||||
equal(this.field.formatSuggest(), '9:56 PM')
|
||||
equal(this.field.formatSuggest(), '11:39 AM')
|
||||
})
|
||||
|
||||
test('localizes formatting of dates and times', function () {
|
||||
ENV.LOCALE = 'pt-BR'
|
||||
equal(this.field.formatSuggest(), 'dom., 20 de jul. de 1969, 21:56')
|
||||
equal(this.field.formatSuggest(), 'ter., 28 de jan. de 1986, 11:39')
|
||||
})
|
||||
|
||||
QUnit.module('formatSuggestContext', {
|
||||
|
@ -623,7 +630,7 @@ QUnit.module('formatSuggestContext', {
|
|||
})
|
||||
fakeENV.setup({TIMEZONE: 'America/Detroit', CONTEXT_TIMEZONE: 'America/Juneau'})
|
||||
this.$field = $('<input type="text" name="due_at">')
|
||||
this.$field.val('Jul 20, 1969 at 9:56pm')
|
||||
this.$field.val('Jan 28, 1986 at 11:39am')
|
||||
this.field = new DatetimeField(this.$field, {})
|
||||
},
|
||||
|
||||
|
@ -634,7 +641,7 @@ QUnit.module('formatSuggestContext', {
|
|||
})
|
||||
|
||||
test('returns result formatted in course timezone', function () {
|
||||
equal(this.field.formatSuggestContext(), 'Sun, Jul 20, 1969, 7:56 PM')
|
||||
equal(this.field.formatSuggestContext(), 'Tue, Jan 28, 1986, 7:39 AM')
|
||||
})
|
||||
|
||||
test('returns "" if @blank', function () {
|
||||
|
@ -643,7 +650,7 @@ test('returns "" if @blank', function () {
|
|||
})
|
||||
|
||||
test('returns "" if @invalid', function () {
|
||||
this.field.invalid = true
|
||||
this.field.valid = PARSE_RESULTS.ERROR
|
||||
equal(this.field.formatSuggestContext(), '')
|
||||
})
|
||||
|
||||
|
@ -654,7 +661,7 @@ test('returns "" if @showTime false', function () {
|
|||
|
||||
test('returns time only if @showDate false', function () {
|
||||
this.field.showDate = false
|
||||
equal(this.field.formatSuggestContext(), '7:56 PM')
|
||||
equal(this.field.formatSuggestContext(), '7:39 AM')
|
||||
})
|
||||
|
||||
QUnit.module('normalizeValue', {
|
||||
|
@ -740,30 +747,30 @@ test('sets to blank with null value', function () {
|
|||
equal(this.field.datetime, null)
|
||||
equal(this.field.fudged, null)
|
||||
equal(this.field.blank, true)
|
||||
equal(this.field.invalid, false)
|
||||
equal(this.field.valid, PARSE_RESULTS.VALID)
|
||||
equal(this.$field.val(), '')
|
||||
})
|
||||
|
||||
test('treats value as unfudged', function () {
|
||||
this.field.setFormattedDatetime(moonwalk, DATETIME_FORMAT_OPTIONS)
|
||||
equal(+this.field.datetime, +moonwalk)
|
||||
equal(+this.field.fudged, +$.fudgeDateForProfileTimezone(moonwalk))
|
||||
this.field.setFormattedDatetime(challenger, DATETIME_FORMAT_OPTIONS)
|
||||
equal(+this.field.datetime, +challenger)
|
||||
equal(+this.field.fudged, +$.fudgeDateForProfileTimezone(challenger))
|
||||
equal(this.field.blank, false)
|
||||
equal(this.field.invalid, false)
|
||||
equal(this.$field.val(), 'Sun, Jul 20, 1969, 9:56 PM')
|
||||
equal(this.field.valid, PARSE_RESULTS.VALID)
|
||||
equal(this.$field.val(), 'Tue, Jan 28, 1986, 11:39 AM')
|
||||
})
|
||||
|
||||
test('formats value into val() according to date/time requests', function () {
|
||||
this.field.setFormattedDatetime(moonwalk, DATE_FORMAT_OPTIONS)
|
||||
equal(this.$field.val(), 'Sun, Jul 20, 1969')
|
||||
this.field.setFormattedDatetime(moonwalk, TIME_FORMAT_OPTIONS)
|
||||
equal(this.$field.val(), '9:56 PM')
|
||||
this.field.setFormattedDatetime(challenger, DATE_FORMAT_OPTIONS)
|
||||
equal(this.$field.val(), 'Tue, Jan 28, 1986')
|
||||
this.field.setFormattedDatetime(challenger, TIME_FORMAT_OPTIONS)
|
||||
equal(this.$field.val(), '11:39 AM')
|
||||
})
|
||||
|
||||
test('localizes value', function () {
|
||||
ENV.LOCALE = 'de'
|
||||
this.field.setFormattedDatetime(moonwalk, DATETIME_FORMAT_OPTIONS)
|
||||
equal(this.$field.val(), 'So., 20. Juli 1969, 21:56')
|
||||
this.field.setFormattedDatetime(challenger, DATETIME_FORMAT_OPTIONS)
|
||||
equal(this.$field.val(), 'Di., 28. Jan. 1986, 11:39')
|
||||
})
|
||||
|
||||
QUnit.module('setDate/setTime/setDatetime', {
|
||||
|
@ -786,16 +793,16 @@ QUnit.module('setDate/setTime/setDatetime', {
|
|||
})
|
||||
|
||||
test('setDate formats into val() with just date', function () {
|
||||
this.field.setDate(moonwalk)
|
||||
equal(this.$field.val(), 'Sun, Jul 20, 1969')
|
||||
this.field.setDate(challenger)
|
||||
equal(this.$field.val(), 'Tue, Jan 28, 1986')
|
||||
})
|
||||
|
||||
test('setTime formats into val() with just time', function () {
|
||||
this.field.setTime(moonwalk)
|
||||
equal(this.$field.val(), '9:56 PM')
|
||||
this.field.setTime(challenger)
|
||||
equal(this.$field.val(), '11:39 AM')
|
||||
})
|
||||
|
||||
test('setDatetime formats into val() with full date and time', function () {
|
||||
this.field.setDatetime(moonwalk)
|
||||
equal(this.$field.val(), 'Sun, Jul 20, 1969, 9:56 PM')
|
||||
this.field.setDatetime(challenger)
|
||||
equal(this.$field.val(), 'Tue, Jan 28, 1986, 11:39 AM')
|
||||
})
|
||||
|
|
|
@ -79,9 +79,9 @@ export default function CourseAvailabilityOptions({canManage, viewPastLocked, vi
|
|||
|
||||
const formatDate = date => tz.format(date, 'date.formats.full')
|
||||
|
||||
const parseDate = (date, tz) => {
|
||||
const parseDate = (date, originTZ) => {
|
||||
const dateObj = new Date(date)
|
||||
const parsedDate = changeTimezone(dateObj, {originTZ: tz, desiredTZ: ENV.TIMEZONE})
|
||||
const parsedDate = changeTimezone(dateObj, {originTZ, desiredTZ: ENV.TIMEZONE})
|
||||
return formatDate(parsedDate)
|
||||
}
|
||||
|
||||
|
|
|
@ -40,14 +40,23 @@ const DATE_FORMAT_OPTIONS = {
|
|||
year: 'numeric',
|
||||
}
|
||||
|
||||
const EARLIEST_YEAR = 1980 // do not allow any manually entered year before this
|
||||
|
||||
const PARSE_RESULTS = {
|
||||
VALID: 0,
|
||||
ERROR: 1,
|
||||
BAD_YEAR: 2,
|
||||
}
|
||||
|
||||
const DATETIME_FORMAT_OPTIONS = {...DATE_FORMAT_OPTIONS, ...TIME_FORMAT_OPTIONS}
|
||||
|
||||
Object.freeze(TIME_FORMAT_OPTIONS)
|
||||
Object.freeze(DATE_FORMAT_OPTIONS)
|
||||
Object.freeze(DATETIME_FORMAT_OPTIONS)
|
||||
Object.freeze(PARSE_RESULTS)
|
||||
|
||||
// for tests only
|
||||
export {TIME_FORMAT_OPTIONS, DATE_FORMAT_OPTIONS, DATETIME_FORMAT_OPTIONS}
|
||||
export {TIME_FORMAT_OPTIONS, DATE_FORMAT_OPTIONS, DATETIME_FORMAT_OPTIONS, PARSE_RESULTS}
|
||||
|
||||
function formatter(zone, formatOptions = DATETIME_FORMAT_OPTIONS) {
|
||||
const options = {...formatOptions}
|
||||
|
@ -149,6 +158,10 @@ export default class DatetimeField {
|
|||
}
|
||||
}
|
||||
|
||||
invalid() {
|
||||
return this.valid !== PARSE_RESULTS.VALID
|
||||
}
|
||||
|
||||
processTimeOptions(options) {
|
||||
// default undefineds to false
|
||||
let {timeOnly, dateOnly} = options
|
||||
|
@ -285,26 +298,29 @@ export default class DatetimeField {
|
|||
}
|
||||
|
||||
parseValue(val) {
|
||||
const previousDate = this.datetime
|
||||
if (typeof val === 'undefined' && this.$field.data('inputdate')) {
|
||||
const inputdate = this.$field.data('inputdate')
|
||||
this.datetime = inputdate instanceof Date ? inputdate : new Date(inputdate)
|
||||
this.blank = false
|
||||
this.invalid = this.datetime === null
|
||||
this.valid = PARSE_RESULTS.VALID
|
||||
if (this.datetime === null) this.valid = PARSE_RESULTS.ERROR
|
||||
if (this.datetime && this.datetime.getFullYear() < EARLIEST_YEAR)
|
||||
this.valid = PARSE_RESULTS.BAD_YEAR
|
||||
this.$field.data('inputdate', null)
|
||||
} else {
|
||||
const previousDate = this.datetime
|
||||
if (val) {
|
||||
this.setFormattedDatetime(val, TIME_FORMAT_OPTIONS)
|
||||
}
|
||||
if (val) this.setFormattedDatetime(val, this.intlFormatType())
|
||||
const value = this.normalizeValue(this.$field.val())
|
||||
this.datetime = tz.parse(value)
|
||||
this.blank = !value
|
||||
this.invalid = !this.blank && this.datetime === null
|
||||
this.valid = PARSE_RESULTS.VALID
|
||||
if (!this.blank && this.datetime === null) this.valid = PARSE_RESULTS.ERROR
|
||||
if (this.datetime && this.datetime.getFullYear() < EARLIEST_YEAR)
|
||||
this.valid = PARSE_RESULTS.BAD_YEAR
|
||||
}
|
||||
// If the date is invalid, revert to the previous date
|
||||
if (this.invalid) {
|
||||
this.datetime = previousDate
|
||||
}
|
||||
}
|
||||
if (this.invalid()) this.datetime = previousDate
|
||||
|
||||
if (this.datetime && !this.showDate && this.implicitDate) {
|
||||
this.datetime = tz.mergeTimeAndDate(this.datetime, this.implicitDate)
|
||||
}
|
||||
|
@ -326,7 +342,7 @@ export default class DatetimeField {
|
|||
this.fudged = null
|
||||
this.$field.val('')
|
||||
}
|
||||
this.invalid = false
|
||||
this.valid = PARSE_RESULTS.VALID
|
||||
this.showTime = this.alwaysShowTime || (this.allowTime && !tz.isMidnight(this.datetime))
|
||||
this.update()
|
||||
this.updateSuggest(false)
|
||||
|
@ -344,7 +360,7 @@ export default class DatetimeField {
|
|||
date: this.fudged,
|
||||
iso8601,
|
||||
blank: this.blank,
|
||||
invalid: this.invalid,
|
||||
invalid: this.invalid(),
|
||||
})
|
||||
|
||||
if (this.$hiddenInput) {
|
||||
|
@ -354,7 +370,7 @@ export default class DatetimeField {
|
|||
// date_fields and time_fields don't have timepicker data fields
|
||||
if (!(this.showDate && this.allowTime)) return
|
||||
|
||||
if (this.invalid || this.blank || !this.showTime) {
|
||||
if (this.invalid() || this.blank || !this.showTime) {
|
||||
this.$field.data({
|
||||
'time-hour': null,
|
||||
'time-minute': null,
|
||||
|
@ -384,8 +400,8 @@ export default class DatetimeField {
|
|||
}
|
||||
this.$contextSuggest.text(contextText).show()
|
||||
}
|
||||
this.$suggest.toggleClass('invalid_datetime', this.invalid).text(localText)
|
||||
if (show || this.$contextSuggest || this.invalid) {
|
||||
this.$suggest.toggleClass('invalid_datetime', this.invalid()).text(localText)
|
||||
if (show || this.$contextSuggest || this.invalid()) {
|
||||
this.$suggest.show()
|
||||
return
|
||||
}
|
||||
|
@ -407,7 +423,7 @@ export default class DatetimeField {
|
|||
}
|
||||
|
||||
updateAria() {
|
||||
this.$field.attr('aria-invalid', !!this.invalid)
|
||||
this.$field.attr('aria-invalid', this.invalid())
|
||||
}
|
||||
|
||||
intlFormatType() {
|
||||
|
@ -417,13 +433,13 @@ export default class DatetimeField {
|
|||
}
|
||||
|
||||
formatSuggest() {
|
||||
if (this.invalid) return this.parseError
|
||||
if (this.invalid()) return this.parseError
|
||||
if (this.blank) return ''
|
||||
return formatter(ENV.TIMEZONE, this.intlFormatType()).format(this.datetime)
|
||||
}
|
||||
|
||||
formatSuggestContext() {
|
||||
if (this.invalid || !this.showTime || this.blank) return ''
|
||||
if (this.invalid() || !this.showTime || this.blank) return ''
|
||||
return formatter(this.contextTimezone, this.intlFormatType()).format(this.datetime)
|
||||
}
|
||||
|
||||
|
@ -438,6 +454,7 @@ export default class DatetimeField {
|
|||
}
|
||||
|
||||
get parseError() {
|
||||
if (this.valid === PARSE_RESULTS.BAD_YEAR) return I18n.t('Year is too far in the past.')
|
||||
return I18n.t('errors.not_a_date', "That's not a date!")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,8 @@ type BlurReturn = SyntheticEvent<Element, Event> | KeyboardEvent<DateInputProps>
|
|||
|
||||
const I18n = useI18nScope('app_shared_components_canvas_date_time')
|
||||
|
||||
const EARLIEST_YEAR = 1980 // do not allow any manually entered year before this
|
||||
|
||||
export type CanvasDateInputProps = {
|
||||
/**
|
||||
* Represents the initial date to be selected. May be `undefined` for no selected date.
|
||||
|
@ -288,10 +290,22 @@ export default function CanvasDateInput({
|
|||
if (isShowingCalendar && withRunningValue) handleHideCalendar()
|
||||
const newDate = tz.parse(value, timezone)
|
||||
if (newDate) {
|
||||
const year = newDate.getFullYear()
|
||||
if (year < EARLIEST_YEAR) {
|
||||
setInternalMessages([
|
||||
{
|
||||
type: 'error',
|
||||
text: I18n.t('Year %{year} is too far in the past', {year: String(year)}),
|
||||
},
|
||||
])
|
||||
return
|
||||
}
|
||||
const msgs: Messages = withRunningValue ? [{type: 'success', text: formatDate(newDate)}] : []
|
||||
setRenderedMoment(moment.tz(newDate, timezone))
|
||||
setInternalMessages(msgs)
|
||||
} else if (value === '') {
|
||||
return
|
||||
}
|
||||
if (value === '') {
|
||||
setInternalMessages([])
|
||||
} else {
|
||||
const text = invalidText(value)
|
||||
|
|
Loading…
Reference in New Issue