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

Scrum 2615 depends #1210

Merged
merged 11 commits into from
Jan 6, 2025
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
Loading