Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/core/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ const ensureArray = (it, force_array) => {

const localized_isodate = (date) => {
// Return a iso date (date only) in the current timezone instead of a
// UTC ISO 8602 date+time component which toISOString returns.
// UTC ISO 8601 date+time component which toISOString returns.

const day = date.getDate().toString().padStart(2, "0");
const month = (date.getMonth() + 1).toString().padStart(2, "0");
Expand Down Expand Up @@ -628,6 +628,20 @@ const unescape_html = (escaped_html) => {
.replace(/"/g, '"');
};

/**
* Return true, if the given value is a valid ISO 8601 date/time string with or without an optional time component.
*
* @param {String} value - The date/time value to be checked.
* @param {Boolean} [optional_time=false] - True, if time component is optional.
* @return {Boolean} - True, if the given value is a valid Date string. False if not.
*/
const is_iso_date_time = (value, optional_time = false) => {
const re_date_time = optional_time
? /^\d{4}-[01]\d-[0-3]\d(T[0-2]\d:[0-5]\d)?$/
: /^\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d$/;
return re_date_time.test(value);
};

var utils = {
// pattern pimping - own module?
jqueryPlugin: jqueryPlugin,
Expand Down Expand Up @@ -658,6 +672,7 @@ var utils = {
localized_isodate: localized_isodate,
escape_html: escape_html,
unescape_html: unescape_html,
is_iso_date_time: is_iso_date_time,
getCSSValue: dom.get_css_value, // BBB: moved to dom. TODO: Remove in upcoming version.
};

Expand Down
28 changes: 28 additions & 0 deletions src/core/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -721,3 +721,31 @@ describe("escape/unescape html ...", function () {
expect(utils.unescape_html(undefined)).toBe("");
});
});

describe("is_iso_date_time ...", function () {
it("detects valid date/time objects", () => {
expect(utils.is_iso_date_time("2022-05-04T21:00")).toBe(true);

// We actually do not strictly check for a valid datetime, just if the
// format is correct.
expect(utils.is_iso_date_time("2222-19-39T29:59")).toBe(true);

// But some basic constraints are in place
expect(utils.is_iso_date_time("2222-20-40T30:60")).toBe(false);

// And this is for sure no valid date/time
expect(utils.is_iso_date_time("not2-ok-40T30:60")).toBe(false);

// Also, the time component cannot be left out
expect(utils.is_iso_date_time("2022-05-04")).toBe(false);

// Not even partially.
expect(utils.is_iso_date_time("2022-05-04T21")).toBe(false);

// Unless we set optional_time to true.
expect(utils.is_iso_date_time("2022-05-04", true)).toBe(true);

// But still, partial time component does not pass.
expect(utils.is_iso_date_time("2022-05-04T21", true)).toBe(false);
});
});
59 changes: 30 additions & 29 deletions src/pat/validation/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,53 +138,54 @@ export default Base.extend({
const msg = input_options.message.date || input_options.message.datetime;

let not_after;
let not_before;
let not_after_el;
let not_before_el;
const date = new Date(input.value);
if (isNaN(date)) {
// Should not happen or input only partially typed in.
return;
}
if (input_options.not.after) {
// Handle value as date.
not_after = new Date(input_options.not.after);
if (isNaN(not_after)) {
if (utils.is_iso_date_time(input_options.not.after, true)) {
not_after = new Date(input_options.not.after);
} else {
// Handle value as selector
not_after_el = document.querySelector(input_options.not.after);
not_after = not_after_el?.value;
not_after =
not_after &&
new Date(
document.querySelector(input_options.not.after).value
);
not_after = not_after_el?.value
? new Date(not_after_el?.value)
: undefined;
}

// Use null if no valid date.
not_after = isNaN(not_after) ? null : not_after;
}

let not_before;
let not_before_el;
if (input_options.not.before) {
// Handle value as date.
not_before = new Date(input_options.not.before);
if (isNaN(not_before)) {
if (utils.is_iso_date_time(input_options.not.before, true)) {
not_before = new Date(input_options.not.before);
} else {
// Handle value as selector
not_before_el = document.querySelector(input_options.not.before);
not_before = not_before_el?.value;
not_before =
not_before &&
new Date(
document.querySelector(input_options.not.before).value
);
not_before = not_before_el?.value
? new Date(not_before_el?.value)
: undefined;
}

// Use null if no valid date.
not_before = isNaN(not_before) ? null : not_before;
}
if (not_after && date > not_after) {
this.set_validity({ input: input, msg: msg });
} else if (not_before && date < not_before) {
this.set_validity({ input: input, msg: msg });

if (
input.value &&
utils.is_iso_date_time(input.value, true) &&
!isNaN(new Date(input.value))
) {
// That's 1 valid date!
const date = new Date(input.value);

if (not_after && date > not_after) {
this.set_validity({ input: input, msg: msg });
} else if (not_before && date < not_before) {
this.set_validity({ input: input, msg: msg });
}
}

// always check the other input to clear/set errors
!stop && // do not re-check when stop is set to avoid infinite loops
not_after_el &&
Expand Down
42 changes: 42 additions & 0 deletions src/pat/validation/validation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,20 @@ describe("pat-validation", function () {
inp_start.dispatchEvent(events.change_event());
await utils.timeout(1); // wait a tick for async to settle.
expect(el.querySelectorAll("em.warning").length).toBe(0);

// Violate the constraint again...
inp_start.value = "2020-10-11";
inp_start.dispatchEvent(events.change_event());
inp_end.value = "2020-10-10";
inp_end.dispatchEvent(events.change_event());
await utils.timeout(1); // wait a tick for async to settle.
expect(el.querySelectorAll("em.warning").length).toBe(2);

// Clearing one of the optional values should clear all errors.
inp_start.value = "";
inp_start.dispatchEvent(events.change_event());
await utils.timeout(1); // wait a tick for async to settle.
expect(el.querySelectorAll("em.warning").length).toBe(0);
});

it("5.6 - doesn't validate empty optional dates", async function () {
Expand Down Expand Up @@ -992,6 +1006,34 @@ describe("pat-validation", function () {
expect(el.querySelectorAll("em.warning").length).toBe(0);
});

it("5.9 - Do not interpret ``ok-1`` as a valid date.", async function () {
// This issue popped up in Chrome but not in Firefox.
// A date like ``ok-1`` was interpreted as ``2000-12-31T23:00:00.000Z``.
// Explicitly checking for a valid ISO 8601 date fixes this.

document.body.innerHTML = `
<form class="pat-validation">
<input
type="date"
name="date"
data-pat-validation="message-date: Wong date!; not-after: ok-1"
/>
</form>
`;

const el = document.querySelector(".pat-validation");
const inp = el.querySelector("[name=date]");

new Pattern(el);
await utils.timeout(1); // wait a tick for async to settle.

inp.value = "2022-01-01";
inp.dispatchEvent(events.change_event());
await utils.timeout(1); // wait a tick for async to settle.

expect(el.querySelectorAll("em.warning").length).toBe(0);
});

it("6.1 - validates radio buttons", async function () {
document.body.innerHTML = `
<form class="pat-validation"
Expand Down