Skip to content

Commit

Permalink
Merge pull request #1210 from Patternslib/scrum-2615--depends-basepat…
Browse files Browse the repository at this point in the history
…tern

Scrum 2615  depends
  • Loading branch information
thet authored Jan 6, 2025
2 parents e73a98e + 430167b commit 0cc28c3
Show file tree
Hide file tree
Showing 11 changed files with 898 additions and 422 deletions.
2 changes: 2 additions & 0 deletions src/core/basepattern.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/
import events from "./events";
import logging from "./logging";
import create_uuid from "./uuid";

const log = logging.getLogger("basepattern");

Expand Down Expand Up @@ -35,6 +36,7 @@ class BasePattern {
el = el[0];
}
this.el = el;
this.uuid = create_uuid();

// Notify pre-init
this.el.dispatchEvent(
Expand Down
4 changes: 4 additions & 0 deletions src/core/basepattern.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
27 changes: 13 additions & 14 deletions src/core/dom.js
Original file line number Diff line number Diff line change
@@ -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.
*
Expand Down Expand Up @@ -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");
};
Expand All @@ -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,
Expand Down
41 changes: 41 additions & 0 deletions src/core/dom.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<p>hello</p>
<fieldset>
<div>
<input type="text" />
</div>
<select>
<option>1</option>
<option>2</option>
</select>
<textarea></textarea>
</fieldset>
<button>Click me!</button>
`;
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();
});
});
21 changes: 21 additions & 0 deletions src/core/uuid.js
Original file line number Diff line number Diff line change
@@ -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;
22 changes: 22 additions & 0 deletions src/core/uuid.test.js
Original file line number Diff line number Diff line change
@@ -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;
});
});
126 changes: 79 additions & 47 deletions src/lib/dependshandler.js
Original file line number Diff line number Diff line change
@@ -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 "=":
Expand All @@ -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;
Loading

0 comments on commit 0cc28c3

Please sign in to comment.