0
0
mirror of https://github.com/wagtail/wagtail.git synced 2024-11-25 05:02:57 +01:00

Add support for groups within BulkController toggles

- Allows one controlled w-bulk element to contain groups of toggles that work together
- Useful for tables where the DOM structure means that columns will need to be toggled / selected as one
- Basic support for 'multi' groups to avoid issues with space separated items
This commit is contained in:
LB Johnston 2023-09-05 22:11:35 +10:00 committed by LB (Ben Johnston)
parent 31329da876
commit da212ce53e
2 changed files with 276 additions and 25 deletions

View File

@ -2,6 +2,23 @@ import { Application } from '@hotwired/stimulus';
import { BulkController } from './BulkController';
describe('BulkController', () => {
const shiftClick = async (element) => {
document.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'Shift',
shiftKey: true,
}),
);
element.click();
document.dispatchEvent(
new KeyboardEvent('keyup', {
key: 'Shift',
shiftKey: true,
}),
);
await Promise.resolve();
};
const setup = async (
html = `
<div id="bulk-container" data-controller="w-bulk" data-action="custom:event@document->w-bulk#toggleAll">
@ -224,23 +241,6 @@ describe('BulkController', () => {
const getClickedIds = () =>
Array.from(document.querySelectorAll(':checked')).map(({ id }) => id);
const shiftClick = async (element) => {
document.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'Shift',
shiftKey: true,
}),
);
element.click();
document.dispatchEvent(
new KeyboardEvent('keyup', {
key: 'Shift',
shiftKey: true,
}),
);
await Promise.resolve();
};
// initial shift usage should have no impact
await shiftClick(document.getElementById('c0'));
expect(getClickedIds()).toHaveLength(1);
@ -306,4 +306,200 @@ describe('BulkController', () => {
// it should include the previously disabled element, tracking against the DOM, not indexes
expect(getClickedIds()).toEqual(['c1', 'cx', 'c2', 'c3']);
});
describe('support for groups of checkboxes being used', () => {
const html = `
<table id="table" data-controller="w-bulk" data-action="custom:event->w-bulk#toggleAll">
<caption>
Misc items
<input id="misc-any" data-action="w-bulk#toggle" data-w-bulk-target="item" data-w-bulk-group-param="add change delete" type="checkbox"/>
<input id="misc-add-change" data-action="w-bulk#toggle" data-w-bulk-target="item" data-w-bulk-group-param="add change" type="checkbox"/>
<input id="misc-change-delete" data-action="w-bulk#toggle" data-w-bulk-target="item" data-w-bulk-group-param="change delete" type="checkbox"/>
<input id="misc-add-change-delete" data-action="w-bulk#toggle" data-w-bulk-target="item" data-w-bulk-group-param="add change delete" type="checkbox"/>
</caption>
<thead>
<tr><th>Name</th><th>Add</th><th>Change</th><th>Delete</th></tr>
</thead>
<tbody>
${[...Array(5).keys()]
.map(
(i) => `
<tr id="row-${i}">
<td>Item ${i + 1}</td>
<td><input id="row-${i}-add" data-action="w-bulk#toggle" data-w-bulk-target="item" data-w-bulk-group-param="add" type="checkbox"/></td>
<td><input id="row-${i}-change" data-action="w-bulk#toggle" data-w-bulk-target="item" data-w-bulk-group-param="change" type="checkbox"/></td>
<td><input id="row-${i}-delete" data-action="w-bulk#toggle" data-w-bulk-target="item" data-w-bulk-group-param="delete" type="checkbox"/></td>
</tr>`,
)
.join('\n')}
</tbody>
<tfoot>
<th scope="row">
Check all (Add & Change)
<input id="select-all" data-action="w-bulk#toggleAll" data-w-bulk-target="all" type="checkbox"/>
<input id="select-all-add-change" data-action="w-bulk#toggleAll" data-w-bulk-target="all" data-w-bulk-group-param="add change" type="checkbox"/>
</th>
<td>
Check all (Add)
<input id="select-all-add" data-action="w-bulk#toggleAll" data-w-bulk-target="all" data-w-bulk-group-param="add" type="checkbox"/>
</td>
<td>
Check all (Change)
<input id="select-all-change" data-action="w-bulk#toggleAll" data-w-bulk-target="all" data-w-bulk-group-param="change" type="checkbox"/>
</td>
<td>
Check all (Delete)
<input id="select-all-delete" data-action="w-bulk#toggleAll" data-w-bulk-target="all" data-w-bulk-group-param="delete" type="checkbox"/>
</td>
</tfoot>
</table>
`;
it('should allow for the toggleAll method to be used to select all, irrespective of groupings', async () => {
const totalCheckboxes = 24;
await setup(html);
const allCheckbox = document.getElementById('select-all');
expect(allCheckbox.checked).toBe(false);
expect(document.querySelectorAll('[type="checkbox"')).toHaveLength(
totalCheckboxes,
);
expect(document.querySelectorAll(':checked')).toHaveLength(0);
allCheckbox.click();
expect(allCheckbox.checked).toBe(true);
expect(document.querySelectorAll(':checked')).toHaveLength(
totalCheckboxes,
);
allCheckbox.click();
expect(document.querySelectorAll(':checked')).toHaveLength(0);
});
it('should allow for the toggleAll method to be used for single group toggling', async () => {
await setup(html);
expect(document.querySelectorAll(':checked')).toHaveLength(0);
document.getElementById('select-all-delete').click();
const checked = document.querySelectorAll(':checked');
expect(checked).toHaveLength(9);
expect(checked).toEqual(
document.querySelectorAll('[data-w-bulk-group-param~="delete"]'),
);
const otherCheckbox = document.getElementById('row-3-add');
otherCheckbox.click();
expect(document.querySelectorAll(':checked')).toHaveLength(10);
document.getElementById('select-all-delete').click();
expect(document.querySelectorAll(':checked')).toHaveLength(1);
expect(otherCheckbox.checked).toEqual(true);
});
it('should allow for the toggleAll method to be used for multi group toggling', async () => {
await setup(html);
expect(document.querySelectorAll(':checked')).toHaveLength(0);
document.getElementById('select-all-add-change').click();
const checked = document.querySelectorAll(':checked');
expect(checked).toHaveLength(17);
expect([...checked].map(({ id }) => id)).toEqual(
expect.arrayContaining([
'misc-any',
'misc-add-change',
'misc-change-delete',
'misc-add-change-delete',
'row-0-add',
'row-0-change',
// ... others not needing explicit call out
]),
);
// specific group select all checkboxes should now be checked automatically
expect(document.getElementById('select-all-add').checked).toEqual(true);
expect(document.getElementById('select-all-change').checked).toEqual(
true,
);
});
it('should support shift+click within the groups', async () => {
await setup(html);
expect(document.querySelectorAll(':checked')).toHaveLength(0);
document.getElementById('row-0-change').click();
await shiftClick(document.getElementById('row-2-change'));
// only checkboxes in
expect(document.getElementById('row-1-change').checked).toEqual(true);
expect(document.querySelectorAll(':checked')).toHaveLength(3);
// now shift again to the last checkbox
await shiftClick(document.getElementById('row-4-change'));
expect(document.getElementById('row-3-change').checked).toEqual(true);
expect(document.querySelectorAll(':checked')).toHaveLength(5);
});
it('should support the group being passed in via a CustomEvent', async () => {
await setup(html);
const table = document.getElementById('table');
expect(document.querySelectorAll(':checked')).toHaveLength(0);
table.dispatchEvent(
new CustomEvent('custom:event', { detail: { group: 'delete' } }),
);
await Promise.resolve();
const checked = document.querySelectorAll(':checked');
expect(checked).toHaveLength(9);
expect([...checked].map(({ id }) => id)).toEqual([
'misc-any',
'misc-change-delete',
'misc-add-change-delete',
'row-0-delete',
'row-1-delete',
'row-2-delete',
'row-3-delete',
'row-4-delete',
'select-all-delete',
]);
// now check one of the non-delete checkboxes
document.getElementById('row-0-add').click();
expect(document.querySelectorAll(':checked')).toHaveLength(10);
// use force to toggle only the delete checkboxes off
table.dispatchEvent(
new CustomEvent('custom:event', {
detail: { group: 'delete', force: false },
}),
);
expect(document.querySelectorAll(':checked')).toHaveLength(1);
// run a second time to confirm there should be no difference due to force
table.dispatchEvent(
new CustomEvent('custom:event', {
detail: { group: 'delete', force: false },
}),
);
expect(document.querySelectorAll(':checked')).toHaveLength(1);
});
});
});

View File

@ -1,6 +1,11 @@
import { Controller } from '@hotwired/stimulus';
type ToggleAllOptions = {
type ToggleOptions = {
/** Only toggle those within the provided group(s), a space separated set of strings. */
group?: string;
};
type ToggleAllOptions = ToggleOptions & {
/** Override check all behaviour to either force check or uncheck all */
force?: boolean;
};
@ -33,6 +38,43 @@ type ToggleAllOptions = {
* </div>
* </div>
*
* @example - Using groups to allow toggles to be controlled separately or together
* <table data-controller="w-bulk">
* <thead>
* <tr>
* <th>Name</th>
* <th>Add</th>
* <th>Change</th>
* </tr>
* </thead>
* <tbody>
* <tr>
* <td>Item 1</td>
* <td><input data-action="w-bulk#toggle" data-w-bulk-target="item" data-w-bulk-group-param="add" type="checkbox"/></td>
* <td><input data-action="w-bulk#toggle" data-w-bulk-target="item" data-w-bulk-group-param="change" type="checkbox"/></td>
* </tr>
* <tr>
* <td>Item 2</td>
* <td><input data-action="w-bulk#toggle" data-w-bulk-target="item" data-w-bulk-group-param="add" type="checkbox"/></td>
* <td><input data-action="w-bulk#toggle" data-w-bulk-target="item" data-w-bulk-group-param="change" type="checkbox"/></td>
* </tr>
* </tbody>
* <tfoot>
* <th scope="row">
* Check all (Add & Change)
* <input data-action="w-bulk#toggleAll" data-w-bulk-target="all" type="checkbox"/>
* </th>
* <td>
* Check all (Add)
* <input data-action="w-bulk#toggleAll" data-w-bulk-target="all" data-w-bulk-group-param="add" type="checkbox"/>
* </td>
* <td>
* Check all (Change)
* <input data-action="w-bulk#toggleAll" data-w-bulk-target="all" data-w-bulk-group-param="change" type="checkbox"/>
* </td>
* </tfoot>
* </table>
*
*/
export class BulkController extends Controller<HTMLElement> {
static classes = ['actionInactive'];
@ -71,9 +113,21 @@ export class BulkController extends Controller<HTMLElement> {
* Returns all valid targets (i.e. not disabled).
*/
getValidTargets(
group: string | null = null,
targets: HTMLInputElement[] = this.itemTargets,
paramAttr = `data-${this.identifier}-group-param`,
): HTMLInputElement[] {
return targets.filter(({ disabled }) => !disabled);
const activeTargets = targets.filter(({ disabled }) => !disabled);
if (!group) return activeTargets;
const groups = group.split(' ');
return activeTargets.filter((target) => {
const targetGroups = new Set(
(target.getAttribute(paramAttr) || '').split(' '),
);
return groups.some(targetGroups.has.bind(targetGroups));
});
}
/**
@ -99,8 +153,9 @@ export class BulkController extends Controller<HTMLElement> {
* If the shift key is pressed, toggle all the items between the last clicked
* item and the current item.
*/
toggle(event?: Event) {
const activeItems = this.getValidTargets();
toggle(event?: CustomEvent<ToggleOptions> & { params?: ToggleOptions }) {
const { group = null } = { ...event?.detail, ...event?.params };
const activeItems = this.getValidTargets(group);
const lastChanged = this.lastChanged;
if (this.shiftActive && lastChanged instanceof HTMLElement) {
@ -134,7 +189,7 @@ export class BulkController extends Controller<HTMLElement> {
const isAnyChecked = totalCheckedItems > 0;
const isAllChecked = totalCheckedItems === activeItems.length;
this.getValidTargets(this.allTargets).forEach((target) => {
this.getValidTargets(group, this.allTargets).forEach((target) => {
// eslint-disable-next-line no-param-reassign
target.checked = isAllChecked;
});
@ -157,7 +212,7 @@ export class BulkController extends Controller<HTMLElement> {
toggleAll(
event: CustomEvent<ToggleAllOptions> & { params?: ToggleAllOptions },
) {
const { force = null } = {
const { force = null, group = null } = {
...event.detail,
...event.params,
};
@ -177,7 +232,7 @@ export class BulkController extends Controller<HTMLElement> {
isChecked = !checkbox?.checked;
}
this.getValidTargets().forEach((target) => {
this.getValidTargets(group).forEach((target) => {
if (target.checked !== isChecked) {
// eslint-disable-next-line no-param-reassign
target.checked = isChecked;
@ -185,7 +240,7 @@ export class BulkController extends Controller<HTMLElement> {
}
});
this.toggle();
this.toggle(event);
}
disconnect() {