Loading core/modules/user/css/user.admin.css +7 −0 Original line number Diff line number Diff line Loading @@ -20,3 +20,10 @@ padding-bottom: 0.5em; font-size: 0.85em; } .permissions tbody tr:has(input[type="checkbox"].js-rid-authenticated:not(:checked)) .js-fake-checkbox { display: none; } .permissions tbody tr:has(input[type="checkbox"].js-rid-authenticated:checked) .js-real-checkbox { display: none; } core/modules/user/tests/src/FunctionalJavascript/UserPermissionsTest.php +13 −12 Original line number Diff line number Diff line Loading @@ -50,7 +50,7 @@ protected function setUp(): void { } /** * Tests the dummy checkboxes added to the permissions page. * Tests the fake checkboxes added to the permissions page. */ public function testPermissionCheckboxes(): void { $this->drupalLogin($this->adminUser); Loading @@ -58,24 +58,25 @@ public function testPermissionCheckboxes(): void { $page = $this->getSession()->getPage(); $wrapper = $page->find('css', '.form-item-' . $this->rid . '-administer-modules'); $real_checkbox = $wrapper->find('css', '.real-checkbox'); $dummy_checkbox = $wrapper->find('css', '.dummy-checkbox'); $fake_checkbox = $wrapper->find('css', '.fake-checkbox'); // The real per-role checkbox is visible and unchecked, the dummy copy is // invisible. $this->assertTrue($real_checkbox->isVisible()); $this->assertFalse($real_checkbox->isChecked()); $this->assertFalse($dummy_checkbox->isVisible()); // The real per-role checkbox is visible and unchecked, the fake copy does // not exist yet. $this->assertNull($fake_checkbox); // Enable the permission for all authenticated users. $page->findField('authenticated[administer modules]')->click(); // The real and dummy checkboxes switch visibility and the dummy is now both // The checkboxes have been initialized. $real_checkbox = $wrapper->find('css', '.real-checkbox'); $fake_checkbox = $wrapper->find('css', '.fake-checkbox'); // The real and fake checkboxes switch visibility and the fake is now both // checked and disabled. $this->assertFalse($real_checkbox->isVisible()); $this->assertTrue($dummy_checkbox->isVisible()); $this->assertTrue($dummy_checkbox->isChecked()); $this->assertTrue($dummy_checkbox->hasAttribute('disabled')); $this->assertTrue($fake_checkbox->isVisible()); $this->assertTrue($fake_checkbox->isChecked()); $this->assertTrue($fake_checkbox->hasAttribute('disabled')); } } core/modules/user/user.permissions.js +74 −73 Original line number Diff line number Diff line Loading @@ -13,80 +13,81 @@ * Attaches functionality to the permissions table. */ Drupal.behaviors.permissions = { attach(context) { once('permissions', 'table#permissions').forEach((table) => { // On a site with many roles and permissions, this behavior initially // has to perform thousands of DOM manipulations to inject checkboxes // and hide them. By detaching the table from the DOM, all operations // can be performed without triggering internal layout and re-rendering // processes in the browser. const $table = $(table); let $ancestor; let method; if ($table.prev().length) { $ancestor = $table.prev(); method = 'after'; } else { $ancestor = $table.parent(); method = 'append'; } $table.detach(); // Create dummy checkboxes. We use dummy checkboxes instead of reusing attach() { const [table] = once('permissions', 'table#permissions'); if (!table) { return; } // Create fake checkboxes. We use fake checkboxes instead of reusing // the existing checkboxes here because new checkboxes don't alter the // submitted form. If we'd automatically check existing checkboxes, the // permission table would be polluted with redundant entries. This is // deliberate, but desirable when we automatically check them. const $dummy = $(Drupal.theme('checkbox')) const $fakeCheckbox = $(Drupal.theme('checkbox')) .removeClass('form-checkbox') .addClass('dummy-checkbox js-dummy-checkbox') .attr('disabled', 'disabled') .attr('checked', 'checked') .attr( 'title', Drupal.t( .addClass('fake-checkbox js-fake-checkbox') .attr({ disabled: 'disabled', checked: 'checked', title: Drupal.t( 'This permission is inherited from the authenticated user role.', ), ) .hide(); $table .find('input[type="checkbox"]') .not('.js-rid-anonymous, .js-rid-authenticated') .addClass('real-checkbox js-real-checkbox') .after($dummy); // Initialize the authenticated user checkbox. $table .find('input[type=checkbox].js-rid-authenticated') .on('click.permissions', this.toggle) // .triggerHandler() cannot be used here, as it only affects the first // element. .each(this.toggle); // Re-insert the table into the DOM. $ancestor[method]($table); }); }, const $wrapper = $('<div></div>').append($fakeCheckbox); const fakeCheckboxHtml = $wrapper.html(); /** * Toggles all dummy checkboxes based on the checkboxes' state. * Process each table row to create fake checkboxes. * * If the "authenticated user" checkbox is checked, the checked and disabled * checkboxes are shown, the real checkboxes otherwise. * @param {object} object * @param {HTMLElement} object.target */ toggle() { const authCheckbox = this; const $row = $(this).closest('tr'); // jQuery performs too many layout calculations for .hide() and .show(), // leading to a major page rendering lag on sites with many roles and // permissions. Therefore, we toggle visibility directly. $row.find('.js-real-checkbox').each(function () { this.style.display = authCheckbox.checked ? 'none' : ''; function tableRowProcessing({ target }) { once('permission-checkbox', target).forEach((checkbox) => { checkbox .closest('tr') .querySelectorAll( 'input[type="checkbox"]:not(.js-rid-anonymous, .js-rid-authenticated)', ) .forEach((check) => { check.classList.add('real-checkbox', 'js-real-checkbox'); check.insertAdjacentHTML('beforebegin', fakeCheckboxHtml); }); $row.find('.js-dummy-checkbox').each(function () { this.style.display = authCheckbox.checked ? '' : 'none'; }); } // An IntersectionObserver object is associated with each of the table // rows to activate checkboxes interactively as users scroll the page // up or down. This prevents processing all checkboxes on page load. const checkedCheckboxObserver = new IntersectionObserver( (entries, thisObserver) => { entries .filter((entry) => entry.isIntersecting) .forEach((entry) => { tableRowProcessing(entry); thisObserver.unobserve(entry.target); }); }, { rootMargin: '50%', }, ); // Select rows with checked authenticated role and attach an observer // to each. table .querySelectorAll( 'tbody tr input[type="checkbox"].js-rid-authenticated:checked', ) .forEach((checkbox) => checkedCheckboxObserver.observe(checkbox)); // Create checkboxes only when necessary on click. $(table).on( 'click.permissions', 'input[type="checkbox"].js-rid-authenticated', tableRowProcessing, ); }, }; Loading Loading
core/modules/user/css/user.admin.css +7 −0 Original line number Diff line number Diff line Loading @@ -20,3 +20,10 @@ padding-bottom: 0.5em; font-size: 0.85em; } .permissions tbody tr:has(input[type="checkbox"].js-rid-authenticated:not(:checked)) .js-fake-checkbox { display: none; } .permissions tbody tr:has(input[type="checkbox"].js-rid-authenticated:checked) .js-real-checkbox { display: none; }
core/modules/user/tests/src/FunctionalJavascript/UserPermissionsTest.php +13 −12 Original line number Diff line number Diff line Loading @@ -50,7 +50,7 @@ protected function setUp(): void { } /** * Tests the dummy checkboxes added to the permissions page. * Tests the fake checkboxes added to the permissions page. */ public function testPermissionCheckboxes(): void { $this->drupalLogin($this->adminUser); Loading @@ -58,24 +58,25 @@ public function testPermissionCheckboxes(): void { $page = $this->getSession()->getPage(); $wrapper = $page->find('css', '.form-item-' . $this->rid . '-administer-modules'); $real_checkbox = $wrapper->find('css', '.real-checkbox'); $dummy_checkbox = $wrapper->find('css', '.dummy-checkbox'); $fake_checkbox = $wrapper->find('css', '.fake-checkbox'); // The real per-role checkbox is visible and unchecked, the dummy copy is // invisible. $this->assertTrue($real_checkbox->isVisible()); $this->assertFalse($real_checkbox->isChecked()); $this->assertFalse($dummy_checkbox->isVisible()); // The real per-role checkbox is visible and unchecked, the fake copy does // not exist yet. $this->assertNull($fake_checkbox); // Enable the permission for all authenticated users. $page->findField('authenticated[administer modules]')->click(); // The real and dummy checkboxes switch visibility and the dummy is now both // The checkboxes have been initialized. $real_checkbox = $wrapper->find('css', '.real-checkbox'); $fake_checkbox = $wrapper->find('css', '.fake-checkbox'); // The real and fake checkboxes switch visibility and the fake is now both // checked and disabled. $this->assertFalse($real_checkbox->isVisible()); $this->assertTrue($dummy_checkbox->isVisible()); $this->assertTrue($dummy_checkbox->isChecked()); $this->assertTrue($dummy_checkbox->hasAttribute('disabled')); $this->assertTrue($fake_checkbox->isVisible()); $this->assertTrue($fake_checkbox->isChecked()); $this->assertTrue($fake_checkbox->hasAttribute('disabled')); } }
core/modules/user/user.permissions.js +74 −73 Original line number Diff line number Diff line Loading @@ -13,80 +13,81 @@ * Attaches functionality to the permissions table. */ Drupal.behaviors.permissions = { attach(context) { once('permissions', 'table#permissions').forEach((table) => { // On a site with many roles and permissions, this behavior initially // has to perform thousands of DOM manipulations to inject checkboxes // and hide them. By detaching the table from the DOM, all operations // can be performed without triggering internal layout and re-rendering // processes in the browser. const $table = $(table); let $ancestor; let method; if ($table.prev().length) { $ancestor = $table.prev(); method = 'after'; } else { $ancestor = $table.parent(); method = 'append'; } $table.detach(); // Create dummy checkboxes. We use dummy checkboxes instead of reusing attach() { const [table] = once('permissions', 'table#permissions'); if (!table) { return; } // Create fake checkboxes. We use fake checkboxes instead of reusing // the existing checkboxes here because new checkboxes don't alter the // submitted form. If we'd automatically check existing checkboxes, the // permission table would be polluted with redundant entries. This is // deliberate, but desirable when we automatically check them. const $dummy = $(Drupal.theme('checkbox')) const $fakeCheckbox = $(Drupal.theme('checkbox')) .removeClass('form-checkbox') .addClass('dummy-checkbox js-dummy-checkbox') .attr('disabled', 'disabled') .attr('checked', 'checked') .attr( 'title', Drupal.t( .addClass('fake-checkbox js-fake-checkbox') .attr({ disabled: 'disabled', checked: 'checked', title: Drupal.t( 'This permission is inherited from the authenticated user role.', ), ) .hide(); $table .find('input[type="checkbox"]') .not('.js-rid-anonymous, .js-rid-authenticated') .addClass('real-checkbox js-real-checkbox') .after($dummy); // Initialize the authenticated user checkbox. $table .find('input[type=checkbox].js-rid-authenticated') .on('click.permissions', this.toggle) // .triggerHandler() cannot be used here, as it only affects the first // element. .each(this.toggle); // Re-insert the table into the DOM. $ancestor[method]($table); }); }, const $wrapper = $('<div></div>').append($fakeCheckbox); const fakeCheckboxHtml = $wrapper.html(); /** * Toggles all dummy checkboxes based on the checkboxes' state. * Process each table row to create fake checkboxes. * * If the "authenticated user" checkbox is checked, the checked and disabled * checkboxes are shown, the real checkboxes otherwise. * @param {object} object * @param {HTMLElement} object.target */ toggle() { const authCheckbox = this; const $row = $(this).closest('tr'); // jQuery performs too many layout calculations for .hide() and .show(), // leading to a major page rendering lag on sites with many roles and // permissions. Therefore, we toggle visibility directly. $row.find('.js-real-checkbox').each(function () { this.style.display = authCheckbox.checked ? 'none' : ''; function tableRowProcessing({ target }) { once('permission-checkbox', target).forEach((checkbox) => { checkbox .closest('tr') .querySelectorAll( 'input[type="checkbox"]:not(.js-rid-anonymous, .js-rid-authenticated)', ) .forEach((check) => { check.classList.add('real-checkbox', 'js-real-checkbox'); check.insertAdjacentHTML('beforebegin', fakeCheckboxHtml); }); $row.find('.js-dummy-checkbox').each(function () { this.style.display = authCheckbox.checked ? '' : 'none'; }); } // An IntersectionObserver object is associated with each of the table // rows to activate checkboxes interactively as users scroll the page // up or down. This prevents processing all checkboxes on page load. const checkedCheckboxObserver = new IntersectionObserver( (entries, thisObserver) => { entries .filter((entry) => entry.isIntersecting) .forEach((entry) => { tableRowProcessing(entry); thisObserver.unobserve(entry.target); }); }, { rootMargin: '50%', }, ); // Select rows with checked authenticated role and attach an observer // to each. table .querySelectorAll( 'tbody tr input[type="checkbox"].js-rid-authenticated:checked', ) .forEach((checkbox) => checkedCheckboxObserver.observe(checkbox)); // Create checkboxes only when necessary on click. $(table).on( 'click.permissions', 'input[type="checkbox"].js-rid-authenticated', tableRowProcessing, ); }, }; Loading