diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 7984cc2b23..4fd1a8d35e 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -31,6 +31,7 @@ Changelog * Maintenance: Allow `ViewSet` subclasses to customise `url_prefix` and `url_namespace` logic (Matt Westcott) * Maintenance: Simplify `SnippetViewSet` registration code (Sage Abdullah) * Maintenance: Rename groups `IndexView.results_template_name` to `results.html` (Sage Abdullah) + * Maintenance: Migrate form submission listing checkbox toggling to the shared `w-bulk` Stimulus implementation (LB (Ben) Johnston) 5.1.1 (14.08.2023) diff --git a/client/src/controllers/BulkController.test.js b/client/src/controllers/BulkController.test.js index df956401c2..435e793374 100644 --- a/client/src/controllers/BulkController.test.js +++ b/client/src/controllers/BulkController.test.js @@ -4,7 +4,7 @@ import { BulkController } from './BulkController'; describe('BulkController', () => { beforeEach(() => { document.body.innerHTML = ` - <div data-controller="w-bulk"> + <div id="bulk-container" data-controller="w-bulk"> <input id="select-all" type="checkbox" data-w-bulk-target="all" data-action="w-bulk#toggleAll"> <div id="checkboxes"> <input type="checkbox" data-w-bulk-target="item" disabled data-action="w-bulk#toggle"> @@ -115,4 +115,41 @@ describe('BulkController', () => { expect(itemCheckbox.checked).toBe(true); }); }); + + it('should allow for action targets to have classes toggled when any checkboxes are clicked', async () => { + const container = document.getElementById('bulk-container'); + + // create innerActions container that will be conditionally hidden with test classes + container.setAttribute( + 'data-w-bulk-action-inactive-class', + 'hidden w-invisible', + ); + const innerActions = document.createElement('div'); + innerActions.id = 'inner-actions'; + innerActions.className = 'keep-me hidden w-invisible'; + innerActions.setAttribute('data-w-bulk-target', 'action'); + container.prepend(innerActions); + + const innerActionsElement = document.getElementById('inner-actions'); + + expect( + document + .getElementById('checkboxes') + .querySelectorAll(':checked:not(:disabled)').length, + ).toEqual(0); + + expect(innerActionsElement.className).toEqual('keep-me hidden w-invisible'); + + const firstCheckbox = document + .getElementById('checkboxes') + .querySelector("[type='checkbox']:not([disabled])"); + + firstCheckbox.click(); + + expect(innerActionsElement.className).toEqual('keep-me'); + + firstCheckbox.click(); + + expect(innerActionsElement.className).toEqual('keep-me hidden w-invisible'); + }); }); diff --git a/client/src/controllers/BulkController.ts b/client/src/controllers/BulkController.ts index 2f29eab3c6..398f79a6ca 100644 --- a/client/src/controllers/BulkController.ts +++ b/client/src/controllers/BulkController.ts @@ -1,21 +1,40 @@ import { Controller } from '@hotwired/stimulus'; + /** * Adds the ability to collectively toggle a set of (non-disabled) checkboxes. * - * @example + * @example - Basic usage * <div data-controller="w-bulk"> * <input type="checkbox" data-action="w-bulk#toggleAll" data-w-bulk-target="all"> * <div> - * <input type="checkbox" data-action="w-bulk#change" data-w-bulk-target="item" disabled> - * <input type="checkbox" data-action="w-bulk#change" data-w-bulk-target="item"> - * <input type="checkbox" data-action="w-bulk#change" data-w-bulk-target="item"> + * <input type="checkbox" data-action="w-bulk#toggle" data-w-bulk-target="item" disabled> + * <input type="checkbox" data-action="w-bulk#toggle" data-w-bulk-target="item"> + * <input type="checkbox" data-action="w-bulk#toggle" data-w-bulk-target="item"> * </div> * <button data-action="w-bulk#toggleAll" data-w-bulk-force-param="false">Clear all</button> * <button data-action="w-bulk#toggleAll" data-w-bulk-force-param="true">Select all</button> * </div> + * + * @example - Showing and hiding an actions container + * <div data-controller="w-bulk" data-w-bulk-action-inactive-class="w-invisible"> + * <div class="w-invisible" data-w-bulk-target="action" id="inner-actions"> + * <button type="button">Some action</button> + * </div> + * <input data-action="w-bulk#toggleAll" data-w-bulk-target="all" type="checkbox"/> + * <div id="checkboxes"> + * <input data-action="w-bulk#toggle" data-w-bulk-target="item" disabled="" type="checkbox" /> + * <input data-action="w-bulk#toggle" data-w-bulk-target="item" type="checkbox"/> + * <input data-action="w-bulk#toggle" data-w-bulk-target="item" type="checkbox" /> + * </div> + * </div> + */ export class BulkController extends Controller<HTMLElement> { - static targets = ['all', 'item']; + static classes = ['actionInactive']; + static targets = ['action', 'all', 'item']; + + /** Target(s) that will have the `actionInactive` classes removed if any actions are checked */ + declare readonly actionTargets: HTMLElement[]; /** All select-all checkbox targets */ declare readonly allTargets: HTMLInputElement[]; @@ -23,6 +42,9 @@ export class BulkController extends Controller<HTMLElement> { /** All item checkbox targets */ declare readonly itemTargets: HTMLInputElement[]; + /** Classes to remove on the actions target if any actions are checked */ + declare readonly actionInactiveClasses: string[]; + get activeItems() { return this.itemTargets.filter(({ disabled }) => !disabled); } @@ -36,13 +58,27 @@ export class BulkController extends Controller<HTMLElement> { /** * When something is toggled, ensure the select all targets are kept in sync. + * Update the classes on the action targets to reflect the current state. */ toggle() { - const isAllChecked = !this.activeItems.some((item) => !item.checked); + const activeItems = this.activeItems; + const totalCheckedItems = activeItems.filter((item) => item.checked).length; + const isAnyChecked = totalCheckedItems > 0; + const isAllChecked = totalCheckedItems === activeItems.length; + this.allTargets.forEach((target) => { // eslint-disable-next-line no-param-reassign target.checked = isAllChecked; }); + + const actionInactiveClasses = this.actionInactiveClasses; + if (!actionInactiveClasses.length) return; + + this.actionTargets.forEach((element) => { + actionInactiveClasses.forEach((actionInactiveClass) => { + element.classList.toggle(actionInactiveClass, !isAnyChecked); + }); + }); } /** diff --git a/docs/releases/5.2.md b/docs/releases/5.2.md index fd7518c476..5827f239da 100644 --- a/docs/releases/5.2.md +++ b/docs/releases/5.2.md @@ -50,6 +50,7 @@ depth: 1 * Allow `ViewSet` subclasses to customise `url_prefix` and `url_namespace` logic (Matt Westcott) * Simplify `SnippetViewSet` registration code (Sage Abdullah) * Rename groups `IndexView.results_template_name` to `results.html` (Sage Abdullah) + * Migrate form submission listing checkbox toggling to the shared `w-bulk` Stimulus implementation (LB (Ben) Johnston) ## Upgrade considerations - changes affecting all projects diff --git a/wagtail/contrib/forms/templates/wagtailforms/list_submissions.html b/wagtail/contrib/forms/templates/wagtailforms/list_submissions.html index 8ddcfca0e3..28d3e30a61 100644 --- a/wagtail/contrib/forms/templates/wagtailforms/list_submissions.html +++ b/wagtail/contrib/forms/templates/wagtailforms/list_submissions.html @@ -1,17 +1,17 @@ {% load i18n %} <div class="overflow"> - <table class="listing"> + <table class="listing" data-controller="w-bulk" data-w-bulk-action-inactive-class="w-invisible"> <col /> <col /> <col /> <thead> <tr> <th colspan="{{ data_headings|length|add:1 }}"> - <button class="button no" id="delete-submissions" style="visibility: hidden">{% trans "Delete selected submissions" %}</button> + <button class="button no w-invisible" data-w-bulk-target="action">{% trans "Delete selected submissions" %}</button> </th> </tr> <tr> - <th><input type="checkbox" id="select-all" /></th> + <th><input type="checkbox" data-action="w-bulk#toggleAll" data-w-bulk-target="all" /></th> {% for heading in data_headings %} <th id="{{ heading.name }}" class="{% if heading.order %}ordered icon {% if heading.order == 'ascending' %}icon-arrow-up-after{% else %}icon-arrow-down-after{% endif %}{% endif %}"> {% if heading.order %}<a href="?order_by={% if heading.order == 'ascending' %}-{% endif %}{{ heading.name }}">{{ heading.label }}</a>{% else %}{{ heading.label }}{% endif %} @@ -23,7 +23,7 @@ {% for row in data_rows %} <tr> <td> - <input type="checkbox" name="selected-submissions" class="select-submission" value="{{ row.model_id }}" /> + <input type="checkbox" name="selected-submissions" class="select-submission" value="{{ row.model_id }}" data-action="w-bulk#toggle" data-w-bulk-target="item" /> </td> {% for cell in row.fields %} <td> diff --git a/wagtail/contrib/forms/templates/wagtailforms/submissions_index.html b/wagtail/contrib/forms/templates/wagtailforms/submissions_index.html index 663e56f67c..335e039d0c 100644 --- a/wagtail/contrib/forms/templates/wagtailforms/submissions_index.html +++ b/wagtail/contrib/forms/templates/wagtailforms/submissions_index.html @@ -21,62 +21,6 @@ timepicker: false, format: 'Y-m-d', }); - - var selectAllCheckbox = document.getElementById('select-all'); - var deleteButton = document.getElementById('delete-submissions'); - - function updateActions() { - var submissionCheckboxes = $('input[type=checkbox].select-submission'); - var someSubmissionsSelected = submissionCheckboxes.is(':checked'); - var everySubmissionSelected = !submissionCheckboxes.is(':not(:checked)'); - - // Select all box state - if (everySubmissionSelected) { - // Every submission has been selected - selectAllCheckbox.checked = true; - selectAllCheckbox.indeterminate = false; - } else if (someSubmissionsSelected) { - // At least one, but not all submissions have been selected - selectAllCheckbox.checked = false; - selectAllCheckbox.indeterminate = true; - } else { - // No submissions have been selected - selectAllCheckbox.checked = false; - selectAllCheckbox.indeterminate = false; - } - - // Delete button state - if (someSubmissionsSelected) { - deleteButton.classList.remove('disabled') - deleteButton.style.visibility = "visible"; - } else { - deleteButton.classList.add('disabled') - deleteButton.style.visibility = "hidden"; - } - } - - - // Event handlers - - $(selectAllCheckbox).on('change', function() { - let checked = this.checked; - - // Update checkbox states - $('input[type=checkbox].select-submission').each(function() { - this.checked = checked; - }); - - updateActions(); - }); - - $('input[type=checkbox].select-submission').on('change', function() { - updateActions(); - }); - - // initial call to updateActions to bring delete button state in sync with checkboxes - // in the case that some checkboxes are pre-checked (which will be the case in some - // browsers when using the back button) - updateActions(); }); </script> {% endblock %}