diff --git a/client/src/controllers/BulkController.test.js b/client/src/controllers/BulkController.test.js
index 06324852db..1bef481c6d 100644
--- a/client/src/controllers/BulkController.test.js
+++ b/client/src/controllers/BulkController.test.js
@@ -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 = `
@@ -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 = `
+
+ `;
+
+ 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);
+ });
+ });
});
diff --git a/client/src/controllers/BulkController.ts b/client/src/controllers/BulkController.ts
index bf8615545a..2a9fe616b5 100644
--- a/client/src/controllers/BulkController.ts
+++ b/client/src/controllers/BulkController.ts
@@ -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 = {
*
*
*
+ * @example - Using groups to allow toggles to be controlled separately or together
+ *