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:
parent
31329da876
commit
da212ce53e
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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() {
|
||||
|
Loading…
Reference in New Issue
Block a user