Skip to content

Commit 63283f3

Browse files
authored
Merge pull request #1126 from Patternslib/pat-checklist-toggle
Feature: input checkbox to toggle checked boxes true/false.
2 parents dabf3c3 + 625f7e5 commit 63283f3

File tree

4 files changed

+113
-17
lines changed

4 files changed

+113
-17
lines changed

src/pat/checklist/checklist.js

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ import "../../core/jquery-ext";
77
export const parser = new Parser("checklist");
88
parser.addArgument("select", ".select-all");
99
parser.addArgument("deselect", ".deselect-all");
10+
parser.addArgument("toggle", ".toggle-all");
1011

1112
export default Base.extend({
1213
name: "checklist",
1314
trigger: ".pat-checklist",
1415
jquery_plugin: true,
1516
all_selects: [],
1617
all_deselects: [],
18+
all_toggles: [],
1719
all_checkboxes: [],
1820
all_radios: [],
1921

@@ -24,7 +26,9 @@ export default Base.extend({
2426
},
2527

2628
_init() {
27-
this.all_checkboxes = this.el.querySelectorAll("input[type=checkbox]");
29+
this.all_checkboxes = this.el.querySelectorAll(
30+
`input[type=checkbox]:not(${this.options.toggle}`
31+
);
2832
this.all_radios = this.el.querySelectorAll("input[type=radio]");
2933

3034
this.all_selects = dom.find_scoped(this.el, this.options.select);
@@ -37,14 +41,19 @@ export default Base.extend({
3741
btn.addEventListener("click", this.deselect_all.bind(this));
3842
}
3943

44+
this.all_toggles = dom.find_scoped(this.el, this.options.toggle);
45+
for (const btn of this.all_toggles) {
46+
btn.addEventListener("click", this.toggle_all.bind(this));
47+
}
48+
4049
// update select/deselect button status
4150
this.el.addEventListener("change", this._handler_change.bind(this));
42-
this.change_buttons();
51+
this.change_buttons_and_toggles();
4352
this.change_checked();
4453
},
4554

4655
_handler_change() {
47-
utils.debounce(() => this.change_buttons(), 50)();
56+
utils.debounce(() => this.change_buttons_and_toggles(), 50)();
4857
utils.debounce(() => this.change_checked(), 50)();
4958
},
5059

@@ -65,7 +74,7 @@ export default Base.extend({
6574
let res;
6675
let parent = el.parentNode;
6776
while (parent) {
68-
res = parent.querySelectorAll(sel);
77+
res = parent.querySelectorAll(`${sel}:not(${this.options.toggle})`);
6978
if (res.length || parent === this.el) {
7079
// return if results were found or we reached the pattern top
7180
return res;
@@ -84,7 +93,7 @@ export default Base.extend({
8493
return chkbxs;
8594
},
8695

87-
change_buttons() {
96+
change_buttons_and_toggles() {
8897
let chkbxs;
8998
for (const btn of this.all_selects) {
9099
chkbxs = this.find_checkboxes(btn, "input[type=checkbox]");
@@ -98,6 +107,12 @@ export default Base.extend({
98107
.map((el) => el.matches(":checked"))
99108
.every((it) => it === false);
100109
}
110+
for (const tgl of this.all_toggles) {
111+
chkbxs = this.find_checkboxes(tgl, "input[type=checkbox]");
112+
tgl.checked = [...chkbxs]
113+
.map((el) => el.matches(":checked"))
114+
.every((it) => it === true);
115+
}
101116
},
102117

103118
select_all(e) {
@@ -121,19 +136,28 @@ export default Base.extend({
121136
}
122137
},
123138

139+
toggle_all(e) {
140+
e.preventDefault();
141+
const checked = e.target.checked;
142+
const chkbxs = this.find_checkboxes(e.target, "input[type=checkbox]");
143+
for (const box of chkbxs) {
144+
box.checked = checked;
145+
box.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
146+
}
147+
},
148+
124149
change_checked() {
125-
for (const it of [...this.all_checkboxes].concat([...this.all_radios])) {
150+
for (const it of [...this.all_checkboxes, ...this.all_radios]) {
126151
for (const label of it.labels) {
127-
label.classList.remove("unchecked");
128-
label.classList.remove("checked");
152+
label.classList.remove("checked", "unchecked");
129153
label.classList.add(it.checked ? "checked" : "unchecked");
130154
}
131155
}
132156

133157
for (const fieldset of dom.querySelectorAllAndMe(this.el, "fieldset")) {
134158
if (
135159
fieldset.querySelectorAll(
136-
"input[type=checkbox]:checked, input[type=radio]:checked"
160+
`input[type=checkbox]:checked:not(${this.options.toggle}), input[type=radio]:checked`
137161
).length
138162
) {
139163
fieldset.classList.remove("unchecked");

src/pat/checklist/checklist.test.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,4 +553,44 @@ describe("pat-checklist", () => {
553553
expect(l2.classList.contains("unchecked")).toEqual(true);
554554
expect(l3.classList.contains("checked")).toEqual(true);
555555
});
556+
557+
it("Initializes on checkboxes with toggle-all checkbox", async () => {
558+
document.body.innerHTML = `
559+
<fieldset class="pat-checklist">
560+
<label><input type="checkbox" class="toggle-all"> toggle selection</label>
561+
<label><input type="checkbox" checked>Option 1</label>
562+
<label><input type="checkbox">Option 2</label>
563+
<label><input type="checkbox">Option 3</label>
564+
</fieldset>
565+
`;
566+
Pattern.init(document.querySelector(".pat-checklist"));
567+
568+
const [f1] = document.querySelectorAll("fieldset");
569+
const [t1] = document.querySelectorAll("input.toggle-all");
570+
const [i1, i2, i3] = document.querySelectorAll("input[type=checkbox]:not(.toggle-all)");
571+
572+
expect(f1.classList.contains("checked")).toEqual(true);
573+
expect(i1.checked).toEqual(true);
574+
expect(i2.checked).toEqual(false);
575+
expect(i3.checked).toEqual(false);
576+
expect(t1.checked).toEqual(false);
577+
578+
t1.click();
579+
await utils.timeout(100);
580+
581+
expect(f1.classList.contains("checked")).toEqual(true);
582+
expect(i1.checked).toEqual(true);
583+
expect(i2.checked).toEqual(true);
584+
expect(i3.checked).toEqual(true);
585+
expect(t1.checked).toEqual(true);
586+
587+
t1.click();
588+
await utils.timeout(100);
589+
590+
expect(f1.classList.contains("unchecked")).toEqual(true);
591+
expect(i1.checked).toEqual(false);
592+
expect(i2.checked).toEqual(false);
593+
expect(i3.checked).toEqual(false);
594+
expect(t1.checked).toEqual(false);
595+
});
556596
});

src/pat/checklist/documentation.md

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ select and deselect all checkboxes in a block. This requires two changes
1010
in your markup:
1111

1212
1. add a `pat-checklist` class to the containing element
13-
2. add a select and deselect buttons
13+
2. add a select and deselect buttons or a toggle checkbox
1414

1515
Here is a simple example.
1616

@@ -26,9 +26,19 @@ Here is a simple example.
2626
<label><input type="checkbox"/> Option four</label>
2727
</fieldset>
2828

29+
An example with toggle checkbox.
30+
31+
<fieldset class="pat-checklist">
32+
<label><input type="checkbox" class="toggle-all"> Select all/none</label>
33+
<label><input type="checkbox" checked="checked"/> Option one</label>
34+
<label><input type="checkbox"/> Option two</label>
35+
<label><input type="checkbox"/> Option three</label>
36+
<label><input type="checkbox"/> Option four</label>
37+
</fieldset>
38+
2939
The selectors used to find the select-all and deselect-all buttons are
3040
configurable. The default values are `.functions .select-all` and
31-
`.functions .dselect-all`. You can configure them using shorthand
41+
`.functions .deselect-all`. You can configure them using shorthand
3242
notation:
3343

3444
<fieldset class="pat-checklist" data-pat-checklist=".selectAll .deselectAll">
@@ -42,8 +52,11 @@ is: if all checkboxes are already checked the select-all button will be
4252
disabled. And if no checkboxes are checked the deselect-all button will
4353
be disabled.
4454

45-
### JavaScript API
4655

47-
The JavaScript API is entirely optional since Patterns already
48-
automatically enables the switching behaviour for all elements with a
49-
`data-pat-checklist` attribute.
56+
### Option reference
57+
58+
| Property | Type | Default Value | Description |
59+
| ----------- | ------- | ---------------- | -------------------------------------------- |
60+
| `select` | String | `.select-all` | CSS selector for the "Select All" button. |
61+
| `deselect` | String | `.deselect-all` | CSS selector for the "Deselect All" button. |
62+
| `toggle` | String | `.toggle-all` | CSS selector for the "Toggle" button. |

src/pat/checklist/index.html

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
</head>
1313
<body>
1414
<div class="functions" id="ElemOutsidePatChecklist">
15-
<h3>Select / Deselect all for exmaple 2</h3>
15+
<h3>Select / Deselect all for example 2</h3>
1616
<div class="pat-toolbar">
1717
<div class="toggles list-functions pat-button-cluster">
1818
<button class="pat-button select-all">Select all</button>
@@ -73,9 +73,27 @@ <h3>Example 2: Buttons outside of .pat-checklist element</h3>
7373
</fieldset>
7474
</div>
7575
</form>
76+
7677
<form class="vertical row">
7778
<div class="six columns">
78-
<h3>Example 3: Hierarchy of Checkboxes</h3>
79+
<h3>Example 3: Toggle checkbox</h3>
80+
<p>
81+
Clicking the toggle checkbox to change the states.
82+
</p>
83+
<fieldset class="pat-checklist checklist">
84+
<label><input type="checkbox" class="toggle-all" /> Toggle checkboxes</label>
85+
86+
<label><input type="checkbox" checked="checked" /> Option one</label>
87+
<label><input type="checkbox" /> Option two</label>
88+
<label><input type="checkbox" /> Option three</label>
89+
<label><input type="checkbox" /> Option four</label>
90+
</fieldset>
91+
</div>
92+
</form>
93+
94+
<form class="vertical row">
95+
<div class="six columns">
96+
<h3>Example 4: Hierarchy of Checkboxes</h3>
7997
<p>
8098
Clicking select all / deselect all only affects parent of
8199
buttons and its children.
@@ -136,5 +154,6 @@ <h3>Example 3: Hierarchy of Checkboxes</h3>
136154
</div>
137155
<div class="six columns"></div>
138156
</form>
157+
139158
</body>
140159
</html>

0 commit comments

Comments
 (0)