diff --git a/.horde.yml b/.horde.yml index 0e67fd66..fb9c4ed8 100644 --- a/.horde.yml +++ b/.horde.yml @@ -77,7 +77,7 @@ dependencies: horde/mime_viewer: ^3 horde/notification: ^3 horde/pack: ^2 - horde/perms: ^3 + horde/perms: ^3.1 horde/prefs: ^3 horde/rpc: ^3 horde/secret: ^3 diff --git a/js/perms-tristate.js b/js/perms-tristate.js new file mode 100644 index 00000000..ac293845 --- /dev/null +++ b/js/perms-tristate.js @@ -0,0 +1,293 @@ +/** + * perms-tristate.js. Client-side niceties for the negative-permissions + * administration dialog. + * + * Nothing here is required for the form to submit. The tri-state radios + * are plain elements. The server reads the POST + * body directly and folds the verdicts into grant / deny bitmasks in + * Horde\Core\Perms\Ui::foldTriState(). + * + * What this script adds: + * 1. Arrow-key navigation between cells in a tri-state grid. + * 2. Per-column bulk actions (grant / neutral / deny all). + * 3. Optional live "N rules will change" preview under the submit + * button, so admins see the impact of a bulk action before it + * hits the server. + * + * The script uses only native browser APIs. No jQuery, no Prototype. + * It attaches through DOMContentLoaded and cleans up nothing on + * unload. A single admin form has a page lifetime, so leak-free + * teardown is not a concern. + */ + +(() => { + 'use strict'; + + /* Cell verdict values must match Horde\Core\Perms\Ui::foldTriState. */ + const VERDICTS = ['grant', 'neutral', 'deny']; + + document.addEventListener('DOMContentLoaded', () => { + const forms = document.querySelectorAll('form.perms-form'); + forms.forEach(wireForm); + wireTreeToggles(); + }); + + /** + * Wires one perms form. Idempotent per form because the DOM only + * gets one DOMContentLoaded pass. + */ + function wireForm(form) { + wireKeyboardNav(form); + wireBulkActions(form); + wireLivePreview(form); + } + + /** + * Wires the client-side expand/collapse buttons in the perms + * admin tree. The server emits every node with its children + * inline in the DOM (Horde\Core\Perms\Ui::renderTree passes + * static=true to the ResponsiveRenderer) so the toggle is a pure + * display: none flip on the sibling