diff --git a/src/core/dom.js b/src/core/dom.js index f61c0f617..c0c901bd9 100644 --- a/src/core/dom.js +++ b/src/core/dom.js @@ -102,16 +102,11 @@ const is_visible = (el) => { /** * Test, if a element is a input-type element. * - * This is taken from Sizzle/jQuery at: - * https://github.com/jquery/sizzle/blob/f2a2412e5e8a5d9edf168ae3b6633ac8e6bd9f2e/src/sizzle.js#L139 - * https://github.com/jquery/sizzle/blob/f2a2412e5e8a5d9edf168ae3b6633ac8e6bd9f2e/src/sizzle.js#L1773 - * * @param {Node} el - The DOM node to test. * @returns {Boolean} - True if the element is a input-type element. */ const is_input = (el) => { - const re_input = /^(?:input|select|textarea|button)$/i; - return re_input.test(el.nodeName); + return el.matches("button, input, select, textarea"); }; /** diff --git a/src/core/registry.js b/src/core/registry.js index 03fd0430c..cbd57d5ef 100644 --- a/src/core/registry.js +++ b/src/core/registry.js @@ -133,6 +133,16 @@ const registry = { }, orderPatterns(patterns) { + // Resort patterns and set those with `sort_early` to the beginning. + // NOTE: Only use when necessary and it's not guaranteed that a pattern + // with `sort_early` is set to the beginning. Last come, first serve. + for (const name of [...patterns]) { + if (registry[name]?.sort_early) { + patterns.splice(patterns.indexOf(name), 1); + patterns.unshift(name); + } + } + // Always add pat-validation as first pattern, so that it can prevent // other patterns from reacting to submit events if form validation // fails. @@ -140,6 +150,7 @@ const registry = { patterns.splice(patterns.indexOf("validation"), 1); patterns.unshift("validation"); } + // Add clone-code to the very beginning - we want to copy the markup // before any other patterns changed the markup. if (patterns.includes("clone-code")) { @@ -180,17 +191,16 @@ const registry = { ); matches = matches.filter((el) => { // Filter out patterns: - // - with class ``.disable-patterns`` - // - wrapped in ``.disable-patterns`` elements + // - with class ``.disable-patterns`` or wrapped within. // - wrapped in ``
`` elements
// - wrapped in ```` elements
return (
- !el.matches(".disable-patterns") &&
- !el?.parentNode?.closest?.(".disable-patterns") &&
+ !el?.closest?.(".disable-patterns") &&
!el?.parentNode?.closest?.("pre") &&
- !el?.parentNode?.closest?.("template") && // NOTE: not strictly necessary. Template is a DocumentFragment and not reachable except for IE.
- !el.matches(".cant-touch-this") && // BBB. TODO: Remove with next major version.
- !el?.parentNode?.closest?.(".cant-touch-this") // BBB. TODO: Remove with next major version.
+ // BBB. TODO: Remove with next major version.
+ !el?.closest?.(".cant-touch-this")
+ // NOTE: templates are not reachabne anyways.
+ //!el?.parentNode?.closest?.("template")
);
});
diff --git a/src/lib/input-change-events.js b/src/lib/input-change-events.js
index 3048c4f18..5bd38fb1f 100644
--- a/src/lib/input-change-events.js
+++ b/src/lib/input-change-events.js
@@ -1,25 +1,30 @@
// helper functions to make all input elements
import $ from "jquery";
+import dom from "../core/dom";
import logging from "../core/logging";
-var namespace = "input-change-events";
+
+const namespace = "input-change-events";
const log = logging.getLogger(namespace);
-var _ = {
- setup: function ($el, pat) {
+const _ = {
+ setup($el, pat) {
if (!pat) {
log.error("The name of the calling pattern has to be set.");
return;
}
+
// list of patterns that installed input-change-event handlers
- var patterns = $el.data(namespace) || [];
+ const patterns = $el.data(namespace) || [];
log.debug("setup handlers for " + pat);
+ const el = $el[0];
+
if (!patterns.length) {
log.debug("installing handlers");
- _.setupInputHandlers($el);
+ this.setupInputHandlers(el);
- $el.on("patterns-injected." + namespace, function (event) {
- _.setupInputHandlers($(event.target));
+ $el.on("patterns-injected." + namespace, (event) => {
+ this.setupInputHandlers(event.target);
});
}
if (patterns.indexOf(pat) === -1) {
@@ -28,53 +33,69 @@ var _ = {
}
},
- setupInputHandlers: function ($el) {
- if (!$el.is(":input")) {
+ setupInputHandlers(el) {
+ if (dom.is_input(el)) {
+ // The element itself is an input, se we simply register a
+ // handler fot it.
+ console.log("1");
+ this.registerHandlersForElement({ trigger_source: el, trigger_target: el });
+ } else {
// We've been given an element that is not a form input. We
// therefore assume that it's a container of form inputs and
// register handlers for its children.
- $el.findInclusive(":input").each(_.registerHandlersForElement);
- } else {
- // The element itself is an input, se we simply register a
- // handler fot it.
- _.registerHandlersForElement.bind($el)();
+ console.log("2");
+ const form = el.closest("form");
+ for (const _el of form.elements) {
+ console.log("3", _el);
+ // Search for all form elements, also those outside the form
+ // container.
+ if (!dom.is_input(_el)) {
+ // form.elements also catches fieldsets, object, output,
+ // which we do not want to handle here.
+ continue;
+ }
+ this.registerHandlersForElement({
+ trigger_source: _el,
+ trigger_target: form,
+ });
+ }
}
},
- registerHandlersForElement: function () {
- var $el = $(this),
- isNumber = $el.is("input[type=number]"),
- isText = $el.is("input:text, input[type=search], textarea");
+ registerHandlersForElement({ trigger_source, trigger_target }) {
+ const $trigger_source = $(trigger_source);
+ const $trigger_target = $(trigger_target);
+ const isNumber = trigger_source.matches("input[type=number]");
+ const isText = trigger_source.matches(
+ "input:not(type), input[type=text], input[type=search], textarea"
+ );
if (isNumber) {
- // for we want to trigger the change
- // on keyup
- if ("onkeyup" in window) {
- $el.on("keyup." + namespace, function () {
- log.debug("translating keyup");
- $el.trigger("input-change");
- });
- }
+ // for number inputs we want to trigger the change on keyup
+ $trigger_source.on("keyup." + namespace, function () {
+ log.debug("translating keyup");
+ $trigger_target.trigger("input-change");
+ });
}
if (isText || isNumber) {
- $el.on("input." + namespace, function () {
+ $trigger_source.on("input." + namespace, function () {
log.debug("translating input");
- $el.trigger("input-change");
+ $trigger_target.trigger("input-change");
});
} else {
- $el.on("change." + namespace, function () {
+ $trigger_source.on("change." + namespace, function () {
log.debug("translating change");
- $el.trigger("input-change");
+ $trigger_target.trigger("input-change");
});
}
- $el.on("blur", function () {
- $el.trigger("input-defocus");
+ $trigger_source.on("blur", function () {
+ $trigger_target.trigger("input-defocus");
});
},
- remove: function ($el, pat) {
- var patterns = $el.data(namespace) || [];
+ remove($el, pat) {
+ let patterns = $el.data(namespace) || [];
if (patterns.indexOf(pat) === -1) {
log.warn("input-change-events were never installed for " + pat);
} else {
diff --git a/src/pat/auto-submit/auto-submit.js b/src/pat/auto-submit/auto-submit.js
index 6786ef050..1e129670c 100644
--- a/src/pat/auto-submit/auto-submit.js
+++ b/src/pat/auto-submit/auto-submit.js
@@ -118,6 +118,7 @@ export default Base.extend({
},
onInputChange(e) {
+ console.log("onInputChange", e);
e.stopPropagation();
this.$el.submit();
log.debug("triggered by " + e.type);
diff --git a/src/pat/auto-submit/auto-submit.test.js b/src/pat/auto-submit/auto-submit.test.js
index c4fe59a85..853aa8265 100644
--- a/src/pat/auto-submit/auto-submit.test.js
+++ b/src/pat/auto-submit/auto-submit.test.js
@@ -62,14 +62,16 @@ describe("pat-autosubmit", function () {
`;
const el = document.querySelector(".pat-autosubmit");
const instance = new Pattern(el);
- const spy = jest.spyOn(instance, "refreshListeners");
+ const spy = jest
+ .spyOn(instance, "refreshListeners")
+ .mockImplementation(() => {});
$(el).trigger("pat-update", { pattern: "clone" });
expect(spy).toHaveBeenCalled();
});
});
describe("2 - Trigger a submit", function () {
- it("when a change on a single input happens", async function () {
+ it("2.1 - when a change on a single input happens", async function () {
document.body.innerHTML = `
@@ -128,7 +130,7 @@ describe("pat-autosubmit", function () {
expect(spy).toHaveBeenCalled();
});
- it("when pat-sortable changes the sorting", function () {
+ it("2.4 - when pat-sortable changes the sorting", function () {
document.body.innerHTML = `
@@ -139,9 +141,84 @@ describe("pat-autosubmit", function () {
$(el).trigger("pat-update", { pattern: "sortable" });
expect(spy).toHaveBeenCalled();
});
+
+ it("2.5 - input outside form: change on input 1", async function () {
+ document.body.innerHTML = `
+
+
+ `;
+ const input = document.querySelector("[name=name]");
+ const form = document.querySelector("form");
+
+ let submit_input = false;
+ let submit_form = false;
+ input.addEventListener("submit", () => {
+ submit_input = true;
+ // NOTE: In a real browser a submit on an input outside a form
+ // would submit the form too. In jsdom this is not the case, so
+ // we need to trigger it manually. This is making this test a
+ // bit useless.
+ form.dispatchEvent(events.submit_event());
+ });
+ form.addEventListener("submit", () => {
+ submit_form = true;
+ });
+
+ const instance = new Pattern(input);
+ await events.await_pattern_init(instance);
+
+ jest.spyOn(instance.$el, "submit").mockImplementation(() => {
+ input.dispatchEvent(events.submit_event());
+ });
+
+ input.dispatchEvent(events.input_event());
+
+ expect(submit_input).toBe(true);
+ expect(submit_form).toBe(true);
+ });
+
+ it("2.6 - input outside form: change on input 2", async function () {
+ document.body.innerHTML = `
+
+
+ `;
+ const input = document.querySelector("[name=name]");
+ const form = document.querySelector("form");
+
+ let submit_input = false;
+ let submit_form = false;
+ input.addEventListener("submit", () => (submit_input = true));
+ form.addEventListener("submit", () => (submit_form = true));
+
+ const instance = new Pattern(form);
+ await events.await_pattern_init(instance);
+
+ jest.spyOn(instance.$el, "submit").mockImplementation(() => {
+ input.dispatchEvent(events.submit_event());
+ });
+
+ input.dispatchEvent(events.input_event());
+
+ expect(submit_input).toBe(true);
+ expect(submit_form).toBe(true);
+ });
});
- describe("3 - Parsing of the delay option", function () {
+ describe("3 - Input outside form: Trigger a submit", function () {});
+
+ describe("4 - Parsing of the delay option", function () {
it("can be done in shorthand notation", function () {
let pat = new Pattern(``);
expect(pat.options.delay).toBe(500);
diff --git a/src/pat/validation/validation.js b/src/pat/validation/validation.js
index 2a2d152b2..d00cddfdd 100644
--- a/src/pat/validation/validation.js
+++ b/src/pat/validation/validation.js
@@ -71,12 +71,12 @@ class Pattern extends BasePattern {
}
initialize_inputs() {
- this.inputs = [
- ...this.el.querySelectorAll("input[name], select[name], textarea[name]"),
- ];
- this.disabled_elements = [
- ...this.el.querySelectorAll(this.options.disableSelector),
- ];
+ this.inputs = [...this.el.elements].filter((el) =>
+ el.matches("input[name], select[name], textarea[name]")
+ );
+ this.disabled_elements = [...this.el.elements].filter((el) =>
+ el.matches(this.options.disableSelector)
+ );
for (const [cnt, input] of this.inputs.entries()) {
// Cancelable debouncer.
diff --git a/src/pat/validation/validation.test.js b/src/pat/validation/validation.test.js
index 56471f449..a9fa1decc 100644
--- a/src/pat/validation/validation.test.js
+++ b/src/pat/validation/validation.test.js
@@ -1455,4 +1455,80 @@ describe("pat-validation", function () {
expect(el.querySelectorAll("em.warning").length).toBe(0);
expect(el.querySelector("#form-buttons-create").disabled).toBe(false);
});
+
+ it("8.1 - input ouside form: validates inputs part of the form but outside the form container", async function () {
+ document.body.innerHTML = `
+
+
+ `;
+ const el = document.querySelector(".pat-validation");
+ const inp = document.querySelector("[name=name]");
+
+ const instance = new Pattern(el);
+ await events.await_pattern_init(instance);
+
+ inp.value = "";
+ inp.dispatchEvent(events.change_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+
+ expect(document.querySelectorAll("em.warning").length).toBe(1);
+ });
+
+ it("8.2 - input outside form: removes the error when the field becomes valid.", async function () {
+ document.body.innerHTML = `
+
+
+ `;
+ const el = document.querySelector(".pat-validation");
+ const inp = document.querySelector("[name=name]");
+
+ const instance = new Pattern(el);
+ await events.await_pattern_init(instance);
+
+ inp.value = "";
+ inp.dispatchEvent(events.change_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+
+ expect(document.querySelectorAll("em.warning").length).toBe(1);
+
+ inp.value = "abc";
+ inp.dispatchEvent(events.change_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+
+ expect(document.querySelectorAll("em.warning").length).toBe(0);
+ });
+
+ it("8.3 - input outside form: can disable certain form elements when validation fails", async function () {
+ // Tests the disable-selector argument
+ document.body.innerHTML = `
+
+
+
+ `;
+
+ const el = document.querySelector(".pat-validation");
+ const inp = document.querySelector("[name=input]");
+ const but = document.querySelector("button");
+
+ const instance = new Pattern(el);
+ await events.await_pattern_init(instance);
+
+ inp.value = "";
+ inp.dispatchEvent(events.change_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+ expect(document.querySelectorAll("em.warning").length).toBe(1);
+ expect(but.disabled).toBe(true);
+
+ inp.value = "ok";
+ inp.dispatchEvent(events.change_event());
+ await utils.timeout(1); // wait a tick for async to settle.
+ expect(document.querySelectorAll("em.warning").length).toBe(0);
+ expect(but.disabled).toBe(false);
+ });
});