Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: diverged branch of https://github.com/Patternslib/Patterns/pull/1154 #1223

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
7 changes: 1 addition & 6 deletions src/core/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
};

/**
Expand Down
24 changes: 17 additions & 7 deletions src/core/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,24 @@ 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.
if (patterns.includes("validation")) {
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")) {
Expand Down Expand Up @@ -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 ``<pre>`` elements
// - wrapped in ``<template>`` 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")
);
});

Expand Down
89 changes: 55 additions & 34 deletions src/lib/input-change-events.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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 <input type="number" /> 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 {
Expand Down
1 change: 1 addition & 0 deletions src/pat/auto-submit/auto-submit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
87 changes: 82 additions & 5 deletions src/pat/auto-submit/auto-submit.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<form>
<input
Expand Down Expand Up @@ -116,7 +118,7 @@ describe("pat-autosubmit", function () {
expect(spy).toHaveBeenCalled();
});

it("when pat-clone removes an element", function () {
it("2.3 - when pat-clone removes an element", function () {
document.body.innerHTML = `
<form class="pat-autosubmit">
</form>
Expand All @@ -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 = `
<form class="pat-autosubmit">
</form>
Expand All @@ -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 = `
<form id="form-el">
</form>
<input
form="form-el"
class="pat-autosubmit"
name="name"
data-pat-autosubmit="delay: 0"
/>
`;
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 = `
<form id="form-el" class="pat-auto-submit"
data-pat-autosubmit="delay: 0"
>
</form>
<input
form="form-el"
name="name"
/>
`;
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(`<input data-pat-autosubmit="500ms"/>`);
expect(pat.options.delay).toBe(500);
Expand Down
12 changes: 6 additions & 6 deletions src/pat/validation/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading