diff --git a/src/core/utils.js b/src/core/utils.js index 597c48a1e..dabb624b7 100644 --- a/src/core/utils.js +++ b/src/core/utils.js @@ -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"); @@ -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, @@ -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. }; diff --git a/src/core/utils.test.js b/src/core/utils.test.js index 3674e02ec..b2bfb8cea 100644 --- a/src/core/utils.test.js +++ b/src/core/utils.test.js @@ -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); + }); +}); diff --git a/src/pat/validation/validation.js b/src/pat/validation/validation.js index 16187e19b..d7f0e8fd0 100644 --- a/src/pat/validation/validation.js +++ b/src/pat/validation/validation.js @@ -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 && diff --git a/src/pat/validation/validation.test.js b/src/pat/validation/validation.test.js index bfdd5838e..6f218e20f 100644 --- a/src/pat/validation/validation.test.js +++ b/src/pat/validation/validation.test.js @@ -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 () { @@ -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 = ` +
+ `; + + 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 = `