diff --git a/django/contrib/admin/static/admin/js/actions.js b/django/contrib/admin/static/admin/js/actions.js index 239aa5dc39..5aee34ea5e 100644 --- a/django/contrib/admin/static/admin/js/actions.js +++ b/django/contrib/admin/static/admin/js/actions.js @@ -88,6 +88,16 @@ window.Actions = function(actionCheckboxes, options) { options = Object.assign({}, defaults, options); let list_editable_changed = false; + let lastChecked = null; + let shiftPressed = false; + + document.addEventListener('keydown', (event) => { + shiftPressed = event.shiftKey; + }); + + document.addEventListener('keyup', (event) => { + shiftPressed = event.shiftKey; + }); document.getElementById(options.allToggleId).addEventListener('click', function(event) { checker(actionCheckboxes, options, this.checked); @@ -113,12 +123,28 @@ }); }); + function affectedCheckboxes(target, withModifier) { + const multiSelect = (lastChecked && withModifier && lastChecked !== target); + if (!multiSelect) { + return [target]; + } + const checkboxes = Array.from(actionCheckboxes); + const targetIndex = checkboxes.findIndex(el => el === target); + const lastCheckedIndex = checkboxes.findIndex(el => el === lastChecked); + const startIndex = Math.min(targetIndex, lastCheckedIndex); + const endIndex = Math.max(targetIndex, lastCheckedIndex); + const filtered = checkboxes.filter((el, index) => (startIndex <= index) && (index <= endIndex)); + return filtered; + }; + Array.from(document.getElementById('result_list').tBodies).forEach(function(el) { el.addEventListener('change', function(event) { const target = event.target; if (target.classList.contains('action-select')) { - target.closest('tr').classList.toggle(options.selectedClass, target.checked); + const checkboxes = affectedCheckboxes(target, shiftPressed); + checker(checkboxes, options, target.checked); updateCounter(actionCheckboxes, options); + lastChecked = target; } else { list_editable_changed = true; } diff --git a/docs/releases/3.2.1.txt b/docs/releases/3.2.1.txt index 6b0b1576cf..76291f57c1 100644 --- a/docs/releases/3.2.1.txt +++ b/docs/releases/3.2.1.txt @@ -44,3 +44,6 @@ Bugfixes * Fixed a regression in Django 3.2 that caused a crash when decoding a cookie value, used by ``django.contrib.messages.storage.cookie.CookieStorage``, in the pre-Django 3.2 format (:ticket:`32643`). + +* Fixed a regression in Django 3.2 that stopped the shift-key modifier + selecting multiple rows in the admin changelist (:ticket:`32647`). diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py index 28acc401c1..8da1c2f799 100644 --- a/tests/admin_changelist/tests.py +++ b/tests/admin_changelist/tests.py @@ -1381,6 +1381,28 @@ class SeleniumTests(AdminSeleniumTestCase): self.assertIs(all_selector.get_property('checked'), False) self.assertEqual(row.get_attribute('class'), '') + def test_modifier_allows_multiple_section(self): + """ + Selecting a row and then selecting another row whilst holding shift + should select all rows in-between. + """ + from selenium.webdriver.common.action_chains import ActionChains + from selenium.webdriver.common.keys import Keys + + Parent.objects.bulk_create([Parent(name='parent%d' % i) for i in range(5)]) + self.admin_login(username='super', password='secret') + self.selenium.get(self.live_server_url + reverse('admin:admin_changelist_parent_changelist')) + checkboxes = self.selenium.find_elements_by_css_selector('tr input.action-select') + self.assertEqual(len(checkboxes), 5) + for c in checkboxes: + self.assertIs(c.get_property('checked'), False) + # Check first row. Hold-shift and check next-to-last row. + checkboxes[0].click() + ActionChains(self.selenium).key_down(Keys.SHIFT).click(checkboxes[-2]).key_up(Keys.SHIFT).perform() + for c in checkboxes[:-2]: + self.assertIs(c.get_property('checked'), True) + self.assertIs(checkboxes[-1].get_property('checked'), False) + def test_select_all_across_pages(self): Parent.objects.bulk_create([Parent(name='parent%d' % i) for i in range(101)]) self.admin_login(username='super', password='secret')