From db271ce9265c3c020fa6a237984178c92cd7995b Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Tue, 31 Dec 2024 17:17:37 +0100 Subject: [PATCH 01/11] feat(core dom): Add find_inputs to find all inputs in a node tree, including the node itself. --- src/core/dom.js | 12 +++++++++++- src/core/dom.test.js | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/core/dom.js b/src/core/dom.js index 01529d838..493c621d7 100644 --- a/src/core/dom.js +++ b/src/core/dom.js @@ -6,6 +6,8 @@ const logger = logging.getLogger("core dom"); const DATA_PREFIX = "__patternslib__data_prefix__"; const DATA_STYLE_DISPLAY = "__patternslib__style__display"; +const INPUT_SELECTOR = "input, select, textarea, button"; + /** * Return an array of DOM nodes. * @@ -571,17 +573,25 @@ const find_form = (el) => { const form = el.closest(".pat-subform") || // Special Patternslib subform concept has precedence. el.form || - el.querySelector("input, select, textarea, button")?.form || + el.querySelector(INPUT_SELECTOR)?.form || el.closest("form"); return form; }; +/** + * Find any input type. + */ +const find_inputs = (el) => { + return querySelectorAllAndMe(el, INPUT_SELECTOR); +}; + const dom = { toNodeArray: toNodeArray, querySelectorAllAndMe: querySelectorAllAndMe, wrap: wrap, hide: hide, show: show, + find_inputs: find_inputs, find_parents: find_parents, find_scoped: find_scoped, get_parents: get_parents, diff --git a/src/core/dom.test.js b/src/core/dom.test.js index 1283e8e29..caa17cbcb 100644 --- a/src/core/dom.test.js +++ b/src/core/dom.test.js @@ -1017,3 +1017,44 @@ describe("find_form", function () { expect(dom.find_form(el)).toBe(subform); }); }); + +describe("find_inputs", () => { + it("finds an input within a node structure.", (done) => { + const wrapper = document.createElement("div"); + wrapper.innerHTML = ` +

hello

+
+
+ +
+ + +
+ + `; + const inputs = dom.find_inputs(wrapper); + const input_types = inputs.map((node) => node.nodeName); + + expect(inputs.length).toBe(4); + expect(input_types.includes("INPUT")).toBeTruthy(); + expect(input_types.includes("SELECT")).toBeTruthy(); + expect(input_types.includes("TEXTAREA")).toBeTruthy(); + expect(input_types.includes("BUTTON")).toBeTruthy(); + + done(); + }); + + it("finds the input on the node itself.", (done) => { + const wrapper = document.createElement("input"); + const inputs = dom.find_inputs(wrapper); + const input_types = inputs.map((node) => node.nodeName); + + expect(inputs.length).toBe(1); + expect(input_types.includes("INPUT")).toBeTruthy(); + + done(); + }); +}); From 8b70876f7c132d521a3c9b591ba75a94442363be Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Thu, 2 Jan 2025 12:48:28 +0100 Subject: [PATCH 02/11] feat(core uuid): Add utility function to generate a uuid. --- src/core/dom.js | 15 ++------------- src/core/uuid.js | 21 +++++++++++++++++++++ src/core/uuid.test.js | 22 ++++++++++++++++++++++ 3 files changed, 45 insertions(+), 13 deletions(-) create mode 100644 src/core/uuid.js create mode 100644 src/core/uuid.test.js diff --git a/src/core/dom.js b/src/core/dom.js index 493c621d7..80bd3e6ad 100644 --- a/src/core/dom.js +++ b/src/core/dom.js @@ -1,5 +1,6 @@ /* Utilities for DOM traversal or navigation */ import logging from "./logging"; +import create_uuid from "./uuid"; const logger = logging.getLogger("core dom"); @@ -541,19 +542,7 @@ const escape_css_id = (id) => { */ const element_uuid = (el) => { if (!get_data(el, "uuid", false)) { - let uuid; - if (window.crypto.randomUUID) { - // Create a real UUID - // window.crypto.randomUUID does only exist in browsers with secure - // context. - // See: https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID - uuid = window.crypto.randomUUID(); - } else { - // Create a sufficiently unique ID - const array = new Uint32Array(4); - uuid = window.crypto.getRandomValues(array).join(""); - } - set_data(el, "uuid", uuid); + set_data(el, "uuid", create_uuid()); } return get_data(el, "uuid"); }; diff --git a/src/core/uuid.js b/src/core/uuid.js new file mode 100644 index 000000000..9eb2a7d1e --- /dev/null +++ b/src/core/uuid.js @@ -0,0 +1,21 @@ +/** + * Get a universally unique id (uuid). + * + * @returns {String} - The uuid. + */ +const create_uuid = () => { + let uuid; + if (window.crypto.randomUUID) { + // Create a real UUID + // window.crypto.randomUUID does only exist in browsers with secure + // context. + // See: https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID + uuid = window.crypto.randomUUID(); + } else { + // Create a sufficiently unique ID + const array = new Uint32Array(4); + uuid = window.crypto.getRandomValues(array).join(""); + } + return uuid; +}; +export default create_uuid; diff --git a/src/core/uuid.test.js b/src/core/uuid.test.js new file mode 100644 index 000000000..d70c337a0 --- /dev/null +++ b/src/core/uuid.test.js @@ -0,0 +1,22 @@ +import create_uuid from "./uuid"; + +describe("uuid", function () { + it("returns a UUIDv4", function () { + const uuid = create_uuid(); + expect(uuid).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/ + ); + }); + + it("returns a sufficiently unique id", function () { + // Mock window.crypto.randomUUID not existing, like in browser with + // non-secure context. + const orig_randomUUID = window.crypto.randomUUID; + window.crypto.randomUUID = undefined; + + const uuid = create_uuid(); + expect(uuid).toMatch(/^[0-9]*$/); + + window.crypto.randomUUID = orig_randomUUID; + }); +}); From c14221498693bda204f14a7f4baeccd2f77509df Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Thu, 2 Jan 2025 12:58:15 +0100 Subject: [PATCH 03/11] feat(core basepattern): Assign each pattern a UUID. --- src/core/basepattern.js | 2 ++ src/core/basepattern.test.js | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/core/basepattern.js b/src/core/basepattern.js index 9e6394a07..cf7975626 100644 --- a/src/core/basepattern.js +++ b/src/core/basepattern.js @@ -8,6 +8,7 @@ */ import events from "./events"; import logging from "./logging"; +import create_uuid from "./uuid"; const log = logging.getLogger("basepattern"); @@ -35,6 +36,7 @@ class BasePattern { el = el[0]; } this.el = el; + this.uuid = create_uuid(); // Notify pre-init this.el.dispatchEvent( diff --git a/src/core/basepattern.test.js b/src/core/basepattern.test.js index 63c61c497..4b3a43f1f 100644 --- a/src/core/basepattern.test.js +++ b/src/core/basepattern.test.js @@ -35,6 +35,10 @@ describe("Basepattern class tests", function () { expect(pat.name).toBe("example"); expect(pat.trigger).toBe(".example"); expect(typeof pat.parser.parse).toBe("function"); + + // Test more attributes + expect(pat.el).toBe(el); + expect(pat.uuid).toMatch(/^[0-9a-f\-]*$/); }); it("1.2 - Options are created with grouping per default.", async function () { From c83bb0af71b50e44c097ea7220fcfd8a3ffb77eb Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Wed, 1 Jan 2025 17:18:57 +0100 Subject: [PATCH 04/11] maint(pat-depends): Rework to class based pattern. --- src/lib/dependshandler.js | 3 +- src/pat/depends/depends.js | 168 ++++++++++++++------------------ src/pat/depends/depends.test.js | 70 ++++++------- 3 files changed, 112 insertions(+), 129 deletions(-) diff --git a/src/lib/dependshandler.js b/src/lib/dependshandler.js index 770ad487d..defa0d0e7 100644 --- a/src/lib/dependshandler.js +++ b/src/lib/dependshandler.js @@ -1,7 +1,8 @@ import $ from "jquery"; import parser from "./depends_parse"; -function DependsHandler($el, expression) { +function DependsHandler(el, expression) { + const $el = $(el); var $context = $el.closest("form"); if (!$context.length) $context = $(document); this.$el = $el; diff --git a/src/pat/depends/depends.js b/src/pat/depends/depends.js index 2cc2a5e8c..25ff162fc 100644 --- a/src/pat/depends/depends.js +++ b/src/pat/depends/depends.js @@ -1,8 +1,10 @@ -import $ from "jquery"; -import Base from "../../core/base"; -import utils from "../../core/utils"; +import { BasePattern } from "@patternslib/patternslib/src/core/basepattern"; +import events from "../../core/events"; +import dom from "../../core/dom"; import logging from "../../core/logging"; -import Parser from "../../core/parser"; +import Parser from "@patternslib/patternslib/src/core/parser"; +import registry from "@patternslib/patternslib/src/core/registry"; +import utils from "../../core/utils"; const log = logging.getLogger("depends"); @@ -13,127 +15,101 @@ parser.addArgument("transition", "none", ["none", "css", "fade", "slide"]); parser.addArgument("effect-duration", "fast"); parser.addArgument("effect-easing", "swing"); -export default Base.extend({ - name: "depends", - trigger: ".pat-depends", - jquery_plugin: true, +class Pattern extends BasePattern { + static name = "depends"; + static trigger = ".pat-depends"; + static parser = parser; - async init($el, opts) { + async init() { + this.$el = $(this.el); const DependsHandler = (await import("../../lib/dependshandler")).default; // prettier-ignore - const dependent = this.$el[0]; - const options = parser.parse(this.$el, opts); - this.$modal = this.$el.parents(".pat-modal"); - - let handler; try { - handler = new DependsHandler(this.$el, options.condition); + this.handler = new DependsHandler(this.el, this.options.condition); } catch (e) { - log.error("Invalid condition: " + e.message, dependent); + log.error("Invalid condition: " + e.message, this.el); return; } - let state = handler.evaluate(); - switch (options.action) { - case "show": - utils.hideOrShow($el, state, options, this.name); - this.updateModal(); - break; - case "enable": - if (state) this.enable(); - else this.disable(); - break; - case "both": - if (state) { - utils.hideOrShow($el, state, options, this.name); - this.updateModal(); - this.enable(); - } else { - utils.hideOrShow($el, state, options, this.name); - this.updateModal(); - this.disable(); - } - break; - } + // Initialize + this.set_state(); - const data = { - handler: handler, - options: options, - dependent: dependent, - }; + for (const input of this.handler.getAllInputs()) { + events.add_event_listener( + input, + "change", + `pat-depends--change--${this.uuid}`, // We need to support multiple events per dependant ... + this.set_state.bind(this) + ); + events.add_event_listener( + input, + "keyup", + `pat-depends--keyup--${this.uuid}`, // ... therefore we need to add a uuid to the event id ... + this.set_state.bind(this) + ); - for (let input of handler.getAllInputs()) { if (input.form) { - let $form = $(input.form); - let dependents = $form.data("patDepends.dependents"); - if (!dependents) { - dependents = [data]; - $form.on("reset.pat-depends", () => this.onReset); - } else if (dependents.indexOf(data) === -1) dependents.push(data); - $form.data("patDepends.dependents", dependents); + events.add_event_listener( + input.form, + "reset", + `pat-depends--reset--${this.uuid}`, // ... to not override previously set event handlers. + async () => { + // TODO: note sure, what this timeout is for. + await utils.timeout(50); + this.set_state.bind(this); + } + ); } - $(input).on("change.pat-depends", null, data, this.onChange.bind(this)); - $(input).on("keyup.pat-depends", null, data, this.onChange.bind(this)); } - }, + } - async onReset(event) { - const dependents = $(event.target).data("patDepends.dependents"); - await utils.timeout(50); - for (let dependent of dependents) { - event.data = dependent; - this.onChange(event); - } - }, + update_modal() { + const modal = this.el.closest(".pat-modal"); - updateModal() { // If we're in a modal, make sure that it gets resized. - if (this.$modal.length) { + if (this.modal) { $(document).trigger("pat-update", { pattern: "depends" }); } - }, + } enable() { - if (this.$el.is(":input")) { - this.$el[0].disabled = null; - } else if (this.$el.is("a")) { - this.$el.off("click.patternDepends"); + if (dom.is_input(this.el)) { + this.el.disabled = false; + } else if (this.el.tagName === "A") { + events.remove_event_listener(this.el, "pat-depends--click"); } - this.$el.removeClass("disabled"); + this.el.classList.remove("disabled"); this.$el.trigger("pat-update", { pattern: "depends", action: "attribute-changed", - dom: this.$el[0], + dom: this.el, enabled: true, }); - }, + } disable() { - if (this.$el.is(":input")) { - this.$el[0].disabled = "disabled"; - } else if (this.$el.is("a")) { - this.$el.on("click.patternDepends", (e) => e.preventDefault()); + if (dom.is_input(this.el)) { + this.el.disabled = true; + } else if (this.el.tagName === "A") { + events.add_event_listener(this.el, "click", "pat-depends--click", (e) => + e.preventDefault() + ); } - this.$el.addClass("disabled"); + this.el.classList.add("disabled"); this.$el.trigger("pat-update", { pattern: "depends", action: "attribute-changed", - dom: this.$el[0], + dom: this.el, enabled: false, }); - }, - - onChange(event) { - const handler = event.data.handler; - const options = event.data.options; - const dependent = event.data.dependent; - const $depdendent = $(dependent); - const state = handler.evaluate(); + } - switch (options.action) { + set_state() { + const state = this.handler.evaluate(); + switch (this.options.action) { case "show": - utils.hideOrShow($depdendent, state, options, this.name); - this.updateModal(); + utils.hideOrShow(this.el, state, this.options, this.name); + this.update_modal(); break; case "enable": if (state) { @@ -143,8 +119,8 @@ export default Base.extend({ } break; case "both": - utils.hideOrShow($depdendent, state, options, this.name); - this.updateModal(); + utils.hideOrShow(this.el, state, this.options, this.name); + this.update_modal(); if (state) { this.enable(); } else { @@ -152,5 +128,11 @@ export default Base.extend({ } break; } - }, -}); + } +} + +// Register Pattern class in the global pattern registry +registry.register(Pattern); + +// Make it available +export default Pattern; diff --git a/src/pat/depends/depends.test.js b/src/pat/depends/depends.test.js index 1b9832398..e482f8a8f 100644 --- a/src/pat/depends/depends.test.js +++ b/src/pat/depends/depends.test.js @@ -19,11 +19,12 @@ describe("pat-depends", function () { '
', ].join("\n") ); - var $dependent = $("#dependent"); - pattern.init($dependent, { condition: "control" }); + + const el = document.querySelector(".pat-depends"); + new pattern(el, { condition: "control" }); await utils.timeout(1); // wait a tick for async to settle. - expect($dependent.css("display")).toBe("none"); + expect($(el).css("display")).toBe("none"); }); it("Show if condition is not met initially", async function () { @@ -33,10 +34,12 @@ describe("pat-depends", function () { '