Skip to content

Commit 0cc28c3

Browse files
authored
Merge pull request #1210 from Patternslib/scrum-2615--depends-basepattern
Scrum 2615 depends
2 parents e73a98e + 430167b commit 0cc28c3

11 files changed

+898
-422
lines changed

src/core/basepattern.js

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99
import events from "./events";
1010
import logging from "./logging";
11+
import create_uuid from "./uuid";
1112

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

@@ -35,6 +36,7 @@ class BasePattern {
3536
el = el[0];
3637
}
3738
this.el = el;
39+
this.uuid = create_uuid();
3840

3941
// Notify pre-init
4042
this.el.dispatchEvent(

src/core/basepattern.test.js

+4
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ describe("Basepattern class tests", function () {
3535
expect(pat.name).toBe("example");
3636
expect(pat.trigger).toBe(".example");
3737
expect(typeof pat.parser.parse).toBe("function");
38+
39+
// Test more attributes
40+
expect(pat.el).toBe(el);
41+
expect(pat.uuid).toMatch(/^[0-9a-f\-]*$/);
3842
});
3943

4044
it("1.2 - Options are created with grouping per default.", async function () {

src/core/dom.js

+13-14
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
/* Utilities for DOM traversal or navigation */
22
import logging from "./logging";
3+
import create_uuid from "./uuid";
34

45
const logger = logging.getLogger("core dom");
56

67
const DATA_PREFIX = "__patternslib__data_prefix__";
78
const DATA_STYLE_DISPLAY = "__patternslib__style__display";
89

10+
const INPUT_SELECTOR = "input, select, textarea, button";
11+
912
/**
1013
* Return an array of DOM nodes.
1114
*
@@ -539,19 +542,7 @@ const escape_css_id = (id) => {
539542
*/
540543
const element_uuid = (el) => {
541544
if (!get_data(el, "uuid", false)) {
542-
let uuid;
543-
if (window.crypto.randomUUID) {
544-
// Create a real UUID
545-
// window.crypto.randomUUID does only exist in browsers with secure
546-
// context.
547-
// See: https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID
548-
uuid = window.crypto.randomUUID();
549-
} else {
550-
// Create a sufficiently unique ID
551-
const array = new Uint32Array(4);
552-
uuid = window.crypto.getRandomValues(array).join("");
553-
}
554-
set_data(el, "uuid", uuid);
545+
set_data(el, "uuid", create_uuid());
555546
}
556547
return get_data(el, "uuid");
557548
};
@@ -571,17 +562,25 @@ const find_form = (el) => {
571562
const form =
572563
el.closest(".pat-subform") || // Special Patternslib subform concept has precedence.
573564
el.form ||
574-
el.querySelector("input, select, textarea, button")?.form ||
565+
el.querySelector(INPUT_SELECTOR)?.form ||
575566
el.closest("form");
576567
return form;
577568
};
578569

570+
/**
571+
* Find any input type.
572+
*/
573+
const find_inputs = (el) => {
574+
return querySelectorAllAndMe(el, INPUT_SELECTOR);
575+
};
576+
579577
const dom = {
580578
toNodeArray: toNodeArray,
581579
querySelectorAllAndMe: querySelectorAllAndMe,
582580
wrap: wrap,
583581
hide: hide,
584582
show: show,
583+
find_inputs: find_inputs,
585584
find_parents: find_parents,
586585
find_scoped: find_scoped,
587586
get_parents: get_parents,

src/core/dom.test.js

+41
Original file line numberDiff line numberDiff line change
@@ -1017,3 +1017,44 @@ describe("find_form", function () {
10171017
expect(dom.find_form(el)).toBe(subform);
10181018
});
10191019
});
1020+
1021+
describe("find_inputs", () => {
1022+
it("finds an input within a node structure.", (done) => {
1023+
const wrapper = document.createElement("div");
1024+
wrapper.innerHTML = `
1025+
<p>hello</p>
1026+
<fieldset>
1027+
<div>
1028+
<input type="text" />
1029+
</div>
1030+
<select>
1031+
<option>1</option>
1032+
<option>2</option>
1033+
</select>
1034+
<textarea></textarea>
1035+
</fieldset>
1036+
<button>Click me!</button>
1037+
`;
1038+
const inputs = dom.find_inputs(wrapper);
1039+
const input_types = inputs.map((node) => node.nodeName);
1040+
1041+
expect(inputs.length).toBe(4);
1042+
expect(input_types.includes("INPUT")).toBeTruthy();
1043+
expect(input_types.includes("SELECT")).toBeTruthy();
1044+
expect(input_types.includes("TEXTAREA")).toBeTruthy();
1045+
expect(input_types.includes("BUTTON")).toBeTruthy();
1046+
1047+
done();
1048+
});
1049+
1050+
it("finds the input on the node itself.", (done) => {
1051+
const wrapper = document.createElement("input");
1052+
const inputs = dom.find_inputs(wrapper);
1053+
const input_types = inputs.map((node) => node.nodeName);
1054+
1055+
expect(inputs.length).toBe(1);
1056+
expect(input_types.includes("INPUT")).toBeTruthy();
1057+
1058+
done();
1059+
});
1060+
});

src/core/uuid.js

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Get a universally unique id (uuid).
3+
*
4+
* @returns {String} - The uuid.
5+
*/
6+
const create_uuid = () => {
7+
let uuid;
8+
if (window.crypto.randomUUID) {
9+
// Create a real UUID
10+
// window.crypto.randomUUID does only exist in browsers with secure
11+
// context.
12+
// See: https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID
13+
uuid = window.crypto.randomUUID();
14+
} else {
15+
// Create a sufficiently unique ID
16+
const array = new Uint32Array(4);
17+
uuid = window.crypto.getRandomValues(array).join("");
18+
}
19+
return uuid;
20+
};
21+
export default create_uuid;

src/core/uuid.test.js

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import create_uuid from "./uuid";
2+
3+
describe("uuid", function () {
4+
it("returns a UUIDv4", function () {
5+
const uuid = create_uuid();
6+
expect(uuid).toMatch(
7+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
8+
);
9+
});
10+
11+
it("returns a sufficiently unique id", function () {
12+
// Mock window.crypto.randomUUID not existing, like in browser with
13+
// non-secure context.
14+
const orig_randomUUID = window.crypto.randomUUID;
15+
window.crypto.randomUUID = undefined;
16+
17+
const uuid = create_uuid();
18+
expect(uuid).toMatch(/^[0-9]*$/);
19+
20+
window.crypto.randomUUID = orig_randomUUID;
21+
});
22+
});

src/lib/dependshandler.js

+79-47
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,87 @@
1-
import $ from "jquery";
21
import parser from "./depends_parse";
32

4-
function DependsHandler($el, expression) {
5-
var $context = $el.closest("form");
6-
if (!$context.length) $context = $(document);
7-
this.$el = $el;
8-
this.$context = $context;
9-
this.ast = parser.parse(expression); // TODO: handle parse exceptions here
10-
}
3+
class DependsHandler {
4+
constructor(el, expression) {
5+
this.el = el;
6+
this.context = el.closest("form") || document;
7+
this.ast = parser.parse(expression); // TODO: handle parse exceptions here
8+
}
119

12-
DependsHandler.prototype = {
13-
_findInputs: function (name) {
14-
var $input = this.$context.find(":input[name='" + name + "']");
15-
if (!$input.length) $input = $("#" + name);
16-
return $input;
17-
},
10+
_findInputs(name) {
11+
// In case of radio buttons, there might be multiple inputs.
12+
// "name" in parentheses, because it can be any value. Common is:
13+
// `somename:list` for a radio input list.
14+
let inputs = this.context.querySelectorAll(`
15+
input[name="${name}"],
16+
select[name="${name}"],
17+
textarea[name="${name}"],
18+
button[name="${name}"]
19+
`);
20+
if (!inputs.length) {
21+
// This should really only find one instance.
22+
inputs = document.querySelectorAll(`#${name}`);
23+
}
24+
return inputs;
25+
}
1826

19-
_getValue: function (name) {
20-
var $input = this._findInputs(name);
21-
if (!$input.length) return null;
27+
_getValue(name) {
28+
let inputs = this._findInputs(name);
2229

23-
if ($input.attr("type") === "radio" || $input.attr("type") === "checkbox")
24-
return $input.filter(":checked").val() || null;
25-
else return $input.val();
26-
},
30+
inputs = [...inputs].filter((input) => {
31+
if (input.type === "radio" && input.checked === false) {
32+
return false;
33+
}
34+
if (input.type === "checkbox" && input.checked === false) {
35+
return false;
36+
}
37+
if (input.disabled) {
38+
return false;
39+
}
40+
return true;
41+
});
2742

28-
getAllInputs: function () {
29-
var todo = [this.ast],
30-
$inputs = $(),
31-
node;
43+
if (inputs.length === 0) {
44+
return null;
45+
}
46+
47+
return inputs[0].value;
48+
}
49+
50+
getAllInputs() {
51+
const todo = [this.ast];
52+
const all_inputs = new Set();
3253

3354
while (todo.length) {
34-
node = todo.shift();
35-
if (node.input) $inputs = $inputs.add(this._findInputs(node.input));
36-
if (node.children && node.children.length)
55+
const node = todo.shift();
56+
if (node.input) {
57+
const inputs = this._findInputs(node.input);
58+
for (const input of inputs) {
59+
all_inputs.add(input);
60+
}
61+
}
62+
if (node.children && node.children.length) {
3763
todo.push.apply(todo, node.children);
64+
}
3865
}
39-
return $inputs;
40-
},
66+
return [...all_inputs];
67+
}
4168

42-
_evaluate: function (node) {
43-
var value = node.input ? this._getValue(node.input) : null,
44-
i;
69+
_evaluate(node) {
70+
const value = node.input ? this._getValue(node.input) : null;
4571

4672
switch (node.type) {
4773
case "NOT":
4874
return !this._evaluate(node.children[0]);
49-
case "AND":
50-
for (i = 0; i < node.children.length; i++)
51-
if (!this._evaluate(node.children[i])) return false;
52-
return true;
53-
case "OR":
54-
for (i = 0; i < node.children.length; i++)
55-
if (this._evaluate(node.children[i])) return true;
56-
return false;
75+
case "AND": {
76+
// As soon as one child evaluates to false, the AND expression is false.
77+
const is_false = node.children.some((child) => !this._evaluate(child));
78+
return !is_false;
79+
}
80+
case "OR": {
81+
// As soon as one child evaluates to true, the OR expression is true.
82+
const is_true = node.children.some((child) => this._evaluate(child));
83+
return is_true;
84+
}
5785
case "comparison":
5886
switch (node.operator) {
5987
case "=":
@@ -69,21 +97,25 @@ DependsHandler.prototype = {
6997
case ">=":
7098
return value >= node.value;
7199
case "~=":
72-
if (value === null) return false;
100+
if (value === null) {
101+
return false;
102+
}
73103
return value.indexOf(node.value) != -1;
74104
case "=~":
75-
if (value === null || !node.value) return false;
105+
if (value === null || !node.value) {
106+
return false;
107+
}
76108
return node.value.indexOf(value) != -1;
77109
}
78110
break;
79111
case "truthy":
80112
return !!value;
81113
}
82-
},
114+
}
83115

84-
evaluate: function () {
116+
evaluate() {
85117
return this._evaluate(this.ast);
86-
},
87-
};
118+
}
119+
}
88120

89121
export default DependsHandler;

0 commit comments

Comments
 (0)