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 () { diff --git a/src/core/dom.js b/src/core/dom.js index 01529d838..80bd3e6ad 100644 --- a/src/core/dom.js +++ b/src/core/dom.js @@ -1,11 +1,14 @@ /* Utilities for DOM traversal or navigation */ import logging from "./logging"; +import create_uuid from "./uuid"; 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. * @@ -539,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"); }; @@ -571,17 +562,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(); + }); +}); 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; + }); +}); diff --git a/src/lib/dependshandler.js b/src/lib/dependshandler.js index 770ad487d..299b2b204 100644 --- a/src/lib/dependshandler.js +++ b/src/lib/dependshandler.js @@ -1,59 +1,87 @@ -import $ from "jquery"; import parser from "./depends_parse"; -function DependsHandler($el, expression) { - var $context = $el.closest("form"); - if (!$context.length) $context = $(document); - this.$el = $el; - this.$context = $context; - this.ast = parser.parse(expression); // TODO: handle parse exceptions here -} +class DependsHandler { + constructor(el, expression) { + this.el = el; + this.context = el.closest("form") || document; + this.ast = parser.parse(expression); // TODO: handle parse exceptions here + } -DependsHandler.prototype = { - _findInputs: function (name) { - var $input = this.$context.find(":input[name='" + name + "']"); - if (!$input.length) $input = $("#" + name); - return $input; - }, + _findInputs(name) { + // In case of radio buttons, there might be multiple inputs. + // "name" in parentheses, because it can be any value. Common is: + // `somename:list` for a radio input list. + let inputs = this.context.querySelectorAll(` + input[name="${name}"], + select[name="${name}"], + textarea[name="${name}"], + button[name="${name}"] + `); + if (!inputs.length) { + // This should really only find one instance. + inputs = document.querySelectorAll(`#${name}`); + } + return inputs; + } - _getValue: function (name) { - var $input = this._findInputs(name); - if (!$input.length) return null; + _getValue(name) { + let inputs = this._findInputs(name); - if ($input.attr("type") === "radio" || $input.attr("type") === "checkbox") - return $input.filter(":checked").val() || null; - else return $input.val(); - }, + inputs = [...inputs].filter((input) => { + if (input.type === "radio" && input.checked === false) { + return false; + } + if (input.type === "checkbox" && input.checked === false) { + return false; + } + if (input.disabled) { + return false; + } + return true; + }); - getAllInputs: function () { - var todo = [this.ast], - $inputs = $(), - node; + if (inputs.length === 0) { + return null; + } + + return inputs[0].value; + } + + getAllInputs() { + const todo = [this.ast]; + const all_inputs = new Set(); while (todo.length) { - node = todo.shift(); - if (node.input) $inputs = $inputs.add(this._findInputs(node.input)); - if (node.children && node.children.length) + const node = todo.shift(); + if (node.input) { + const inputs = this._findInputs(node.input); + for (const input of inputs) { + all_inputs.add(input); + } + } + if (node.children && node.children.length) { todo.push.apply(todo, node.children); + } } - return $inputs; - }, + return [...all_inputs]; + } - _evaluate: function (node) { - var value = node.input ? this._getValue(node.input) : null, - i; + _evaluate(node) { + const value = node.input ? this._getValue(node.input) : null; switch (node.type) { case "NOT": return !this._evaluate(node.children[0]); - case "AND": - for (i = 0; i < node.children.length; i++) - if (!this._evaluate(node.children[i])) return false; - return true; - case "OR": - for (i = 0; i < node.children.length; i++) - if (this._evaluate(node.children[i])) return true; - return false; + case "AND": { + // As soon as one child evaluates to false, the AND expression is false. + const is_false = node.children.some((child) => !this._evaluate(child)); + return !is_false; + } + case "OR": { + // As soon as one child evaluates to true, the OR expression is true. + const is_true = node.children.some((child) => this._evaluate(child)); + return is_true; + } case "comparison": switch (node.operator) { case "=": @@ -69,21 +97,25 @@ DependsHandler.prototype = { case ">=": return value >= node.value; case "~=": - if (value === null) return false; + if (value === null) { + return false; + } return value.indexOf(node.value) != -1; case "=~": - if (value === null || !node.value) return false; + if (value === null || !node.value) { + return false; + } return node.value.indexOf(value) != -1; } break; case "truthy": return !!value; } - }, + } - evaluate: function () { + evaluate() { return this._evaluate(this.ast); - }, -}; + } +} export default DependsHandler; diff --git a/src/lib/dependshandler.test.js b/src/lib/dependshandler.test.js index fc347d61c..5f03c4f74 100644 --- a/src/lib/dependshandler.test.js +++ b/src/lib/dependshandler.test.js @@ -1,106 +1,252 @@ -import $ from "jquery"; import DependsHandler from "./dependshandler"; describe("pat-dependshandler", function () { - beforeEach(function () { - $("
", { id: "lab" }).appendTo(document.body); - }); + describe("Basic tests", function () { + it("uses a form as context, if there is one.", function () { + document.body.innerHTML = ` +
+ +
+ `; + + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "lab"); + + expect(handler.context).toBe(document.querySelector("form")); + }); - afterEach(function () { - $("#lab").remove(); + it("uses the document as context, if there is no form", function () { + document.body.innerHTML = ` +
+ +
+ `; + + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "lab"); + + expect(handler.context).toBe(document); + }); }); describe("_findInputs", function () { - it("Reference to unknown input", function () { - var handler = new DependsHandler($("#lab"), "foo"); + it("no input, nothing found", function () { + document.body.innerHTML = ` +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "foo"); + expect(handler._findInputs("foo").length).toBe(0); }); - it("Reference to input name", function () { - var $lab = $("#lab"), - handler = new DependsHandler($("#lab"), "foo"); - $lab.append(''); + it("find input by name", function () { + document.body.innerHTML = ` +
+ +
+ `; + const lab = document.getElementById("lab"); + const foo = document.querySelector("[name=foo]"); + const handler = new DependsHandler(lab, "foo"); + expect(handler._findInputs("foo").length).toBe(1); + expect(handler._findInputs("foo")[0]).toBe(foo); }); - it("Reference to input id", function () { - var $lab = $("#lab"), - handler = new DependsHandler($("#lab"), "foo"); - $lab.append(''); + it("find input by id", function () { + document.body.innerHTML = ` +
+ +
+ `; + const lab = document.getElementById("lab"); + const bar = document.getElementById("bar"); + const handler = new DependsHandler(lab, "bar"); + expect(handler._findInputs("bar").length).toBe(1); + expect(handler._findInputs("bar")[0]).toBe(bar); }); it("Restrict searches to current form", function () { - $("#lab").html( - [ - '', - "
", - '
', - ' ', - "", - ].join("\n") - ); - var handler = new DependsHandler($("#context"), "foo"), - $inputs = handler._findInputs("foo"); - expect($inputs.length).toBe(1); - expect($inputs[0].type).toBe("radio"); + document.body.innerHTML = ` + +
+
+ +
+ `; + const lab = document.getElementById("lab"); + const foo1 = document.getElementById("foo1"); + const handler = new DependsHandler(lab, "foo"); + + expect(handler._findInputs("foo").length).toBe(1); + expect(handler._findInputs("foo")[0]).toBe(foo1); + }); + + it("find multiple inputs of the same name, e.g. for radio buttons", function () { + document.body.innerHTML = ` +
+ + + +
+ `; + const lab = document.getElementById("lab"); + const foos = document.querySelectorAll("[name='foo:list']"); + const handler = new DependsHandler(lab, "foo:list"); + expect(handler._findInputs("foo:list").length).toBe(3); + expect(handler._findInputs("foo:list")[0]).toBe(foos[0]); + expect(handler._findInputs("foo:list")[1]).toBe(foos[1]); + expect(handler._findInputs("foo:list")[2]).toBe(foos[2]); }); }); describe("_getValue", function () { - it("Unchecked checkbox", function () { - $("#lab").append(''); - var handler = new DependsHandler($("#lab"), "foo"); - expect(handler._getValue("foo")).toBeNull(); - }); + it("Just get the text value.", function () { + document.body.innerHTML = ` +
+ +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "foo"); - it("Checked checkbox", function () { - $("#lab").append( - '' - ); - var handler = new DependsHandler($("#lab"), "foo"); expect(handler._getValue("foo")).toBe("bar"); }); - it("Unchecked radio button", function () { - $("#lab").append(''); - var handler = new DependsHandler($("#lab"), "foo"); + it("Get an empty value from an empty input.", function () { + document.body.innerHTML = ` +
+ +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "foo"); + + expect(handler._getValue("foo")).toBe(""); + }); + + it("Get null from a disabled input.", function () { + document.body.innerHTML = ` +
+ +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "foo"); + + expect(handler._getValue("foo")).toBe(null); + }); + + it("Unchecked checkbox returns no value", function () { + document.body.innerHTML = ` +
+ +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "foo"); + expect(handler._getValue("foo")).toBeNull(); }); - it("Unchecked radio button", function () { - $("#lab").append(''); - var handler = new DependsHandler($("#lab"), "foo"); + it("Checked checkbox returns value", function () { + document.body.innerHTML = ` +
+ +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "foo"); + + expect(handler._getValue("foo")).toBe("bar"); + }); + + it("Unchecked radio button returns no value", function () { + document.body.innerHTML = ` +
+ +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "foo"); + expect(handler._getValue("foo")).toBeNull(); }); it("Checked radio button", function () { - $("#lab").append( - '' - ); - var handler = new DependsHandler($("#lab"), "foo"); + document.body.innerHTML = ` +
+ +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "foo"); + expect(handler._getValue("foo")).toBe("bar"); }); + + it("Returns the value of the checked radio buttons in a list of multiple radio buttons.", function () { + document.body.innerHTML = ` +
+ + + + +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "foo:list"); + + expect(handler._getValue("foo:list")).toBe("fuzz"); + }); + + it("Returns no value in a list of multiple radio buttons of no one is checked.", function () { + document.body.innerHTML = ` +
+ + + + +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "foo:list"); + + expect(handler._getValue("foo:list")).toBe(null); + }); + }); describe("getAllInputs", function () { it("Simple expression", function () { - $("#lab").append(''); - var handler = new DependsHandler($("#lab"), "foo"), - inputs = handler.getAllInputs(); + document.body.innerHTML = ` +
+ + +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "foo"); + const inputs = handler.getAllInputs(); + expect(inputs.length).toBe(1); - expect(inputs[0].type).toBe("radio"); + expect(inputs[0].name).toBe("foo"); }); it("Nested expression", function () { - $("#lab").html( - [ - '', - '', - ].join("\n") - ); - var handler = new DependsHandler($("#lab"), "foo or (foo and buz)"), - inputs = handler.getAllInputs(); + document.body.innerHTML = ` +
+ + + +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "foo or (foo and buz)"); + const inputs = handler.getAllInputs(); + expect(inputs.length).toBe(2); expect(inputs[0].name).toBe("foo"); expect(inputs[1].name).toBe("buz"); @@ -110,102 +256,196 @@ describe("pat-dependshandler", function () { describe("evaluate", function () { describe("truthy", function () { it("Text input with value", function () { - $("#lab").append(''); - var handler = new DependsHandler($("#lab"), "foo"); + document.body.innerHTML = ` +
+ +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "foo"); + expect(handler.evaluate()).toBe(true); }); it("Text input without value", function () { - $("#lab").append(''); - var handler = new DependsHandler($("#lab"), "foo"); + document.body.innerHTML = ` +
+ +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "foo"); + expect(handler.evaluate()).toBe(false); }); }); - describe("Comparison", function () { - it("Positive number input below value", function () { - $("#lab").append(''); - var handler = new DependsHandler($("#lab"), "foo<15"); + describe("Number comparisons", function () { + it("Number below expression value", function () { + document.body.innerHTML = ` +
+ +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "foo<15"); + expect(handler.evaluate()).toBe(true); }); - it("Negative number input below value", function () { - $("#lab").append(''); - var handler = new DependsHandler($("#lab"), "foo<15"); + it("Number above expression value", function () { + document.body.innerHTML = ` +
+ +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "foo<15"); + expect(handler.evaluate()).toBe(false); }); - it("Positive equal to value", function () { - $("#lab").append(''); - var handler = new DependsHandler($("#lab"), "foo=bar"); + it("Negative number below expression value", function () { + document.body.innerHTML = ` +
+ +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "foo<15"); + + expect(handler.evaluate()).toBe(true); + }); + + describe.skip("Negative expression values not yet supported.", function () { + it("Negative number above negative expression value", function () { + document.body.innerHTML = ` +
+ +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "foo<-15"); + + expect(handler.evaluate()).toBe(false); + }); + }); + + it("Number equal to expression value", function () { + document.body.innerHTML = ` +
+ +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "foo<=15"); + + expect(handler.evaluate()).toBe(true); + }); + + it("Text equal to expression value", function () { + document.body.innerHTML = ` +
+ +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "foo=buz"); + expect(handler.evaluate()).toBe(true); }); - it("Negative equal to value", function () { - $("#lab").append(''); - var handler = new DependsHandler($("#lab"), "foo=bar"); + it("Text not equal to expression value", function () { + document.body.innerHTML = ` +
+ +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "foo=buz"); + expect(handler.evaluate()).toBe(false); }); }); - describe("Negate test", function () { - it("Text input with value", function () { - $("#lab").append(''); - var handler = new DependsHandler($("#lab"), "not foo"); + describe("NOT expressions", function () { + it("Text input with value where expressions expects no value", function () { + document.body.innerHTML = ` +
+ +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "not foo"); + expect(handler.evaluate()).toBe(false); }); - it("Text input without value", function () { - $("#lab").append(''); - var handler = new DependsHandler($("#lab"), "not foo"); + it("Text input without value where expression expects no value", function () { + document.body.innerHTML = ` +
+ +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "not foo"); + expect(handler.evaluate()).toBe(true); }); }); - describe("AND multiple expressions", function () { + describe("AND expressions", function () { it("All options true", function () { - $("#lab").html( - [ - '', - '', - ].join("\n") - ); - var handler = new DependsHandler($("#lab"), "one and two"); + document.body.innerHTML = ` +
+ + +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "one and two"); + expect(handler.evaluate()).toBe(true); }); it("Not all options true", function () { - $("#lab").html( - [ - '', - '', - ].join("\n") - ); - var handler = new DependsHandler($("#lab"), "one and two"); + document.body.innerHTML = ` +
+ + +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "one and two"); + expect(handler.evaluate()).toBe(false); }); }); - describe("OR multiple expressions", function () { + describe("OR expressions", function () { it("No options true", function () { - $("#lab").html( - [ - '', - '', - ].join("\n") - ); - var handler = new DependsHandler($("#lab"), "one or two"); + document.body.innerHTML = ` +
+ + +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "one or two"); + expect(handler.evaluate()).toBe(false); }); it("One option true", function () { - $("#lab").html( - [ - '', - '', - ].join("\n") - ); - var handler = new DependsHandler($("#lab"), "one or two"); + document.body.innerHTML = ` +
+ + +
+ `; + const lab = document.getElementById("lab"); + const handler = new DependsHandler(lab, "one or two"); + expect(handler.evaluate()).toBe(true); }); }); diff --git a/src/pat/depends/depends.js b/src/pat/depends/depends.js index 2cc2a5e8c..43c62f427 100644 --- a/src/pat/depends/depends.js +++ b/src/pat/depends/depends.js @@ -1,8 +1,11 @@ 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 +16,109 @@ 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 (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"); + const inputs = dom.find_inputs(this.el); + for (const input of inputs) { + input.disabled = false; + // Trigger the change event after disabling so that any other bound actions can react on that. + input.dispatchEvent(events.change_event()); } - this.$el.removeClass("disabled"); + if (this.el.tagName === "A") { + events.remove_event_listener(this.el, "pat-depends--click"); + } + 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()); + const inputs = dom.find_inputs(this.el); + for (const input of inputs) { + input.disabled = true; + // Trigger the change event after disabling so that any other bound actions can react on that. + input.dispatchEvent(events.change_event()); + } + 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 +128,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 +137,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..433846ef4 100644 --- a/src/pat/depends/depends.test.js +++ b/src/pat/depends/depends.test.js @@ -1,4 +1,5 @@ import $ from "jquery"; +import dom from "../../core/dom"; import pattern from "./depends"; import utils from "../../core/utils"; @@ -19,11 +20,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 +35,12 @@ describe("pat-depends", function () { '