@@ -3,30 +3,92 @@ const SENTINEL_ALL = "__all__";
33let actionCheckboxes = [ ] ;
44let actionCheckboxHeader = null ;
55let actionIdsInput = null ;
6+ let actionNameInput = null ;
67let actionForm = null ;
8+ let selectionHeader = null ;
9+ let selectionSummary = null ;
10+ let selectAllButton = null ;
11+ let actionMenuItems = [ ] ;
12+
13+ // All-pages selection is an explicit mode, entered only via the "Select all N"
14+ // button. Any deselection exits it. When active, action_ids is the __all__
15+ // sentinel the backend expands to every object across every page.
16+ let selectAllPages = false ;
17+ let lastActionCheckboxChecked = null ;
718
819function refreshActionCache ( ) {
920 actionCheckboxes = [ ...document . querySelectorAll ( "[data-action-checkbox]" ) ] ;
1021 actionCheckboxHeader = document . querySelector ( "[data-action-checkbox-all]" ) ;
11- actionIdsInput = document . querySelector ( '[name="action_ids"]' ) ;
1222 actionForm = document . querySelector ( "[data-actions-form]" ) ;
23+ actionIdsInput = actionForm ?. querySelector ( '[name="action_ids"]' ) ?? null ;
24+ actionNameInput = actionForm ?. querySelector ( '[name="action_name"]' ) ?? null ;
25+ selectionHeader = actionForm ?. closest ( "header" ) ?? null ;
26+ selectionSummary = document . querySelector ( "[data-selection-summary]" ) ;
27+ selectAllButton = document . querySelector ( "[data-action-select-all]" ) ;
28+ actionMenuItems = [ ...document . querySelectorAll ( "[data-action-name]" ) ] ;
29+ updateSelectionUI ( ) ;
1330}
1431
1532document . addEventListener ( "DOMContentLoaded" , refreshActionCache ) ;
16- document . addEventListener ( "htmx:afterSwap" , refreshActionCache ) ;
33+ document . addEventListener ( "htmx:afterSwap" , ( ) => {
34+ selectAllPages = false ;
35+ lastActionCheckboxChecked = null ;
36+ refreshActionCache ( ) ;
37+ } ) ;
1738
18- let lastActionCheckboxChecked = null ;
39+ function checkedCount ( ) {
40+ return actionCheckboxes . filter ( ( cb ) => cb . checked ) . length ;
41+ }
1942
20- function updateActionIds ( ) {
21- if ( ! actionIdsInput ) return ;
22- if ( actionCheckboxHeader ?. checked ) {
23- actionIdsInput . value = SENTINEL_ALL ;
24- } else {
25- const ids = [ ] ;
26- actionCheckboxes . forEach ( ( cb ) => {
27- if ( cb . checked ) ids . push ( cb . getAttribute ( "name" ) ) ;
28- } ) ;
29- actionIdsInput . value = ids . join ( "," ) ;
43+ function totalCount ( ) {
44+ return parseInt ( actionForm ?. dataset . totalCount ?? "0" , 10 ) ;
45+ }
46+
47+ function updateSelectionUI ( ) {
48+ const totalOnPage = actionCheckboxes . length ;
49+ const checked = checkedCount ( ) ;
50+ const total = totalCount ( ) ;
51+ const fullPageSelected = totalOnPage > 0 && checked === totalOnPage ;
52+
53+ // Header checkbox reflects the current page: checked when the whole page is
54+ // selected, indeterminate when only some rows are.
55+ if ( actionCheckboxHeader ) {
56+ actionCheckboxHeader . checked = fullPageSelected ;
57+ actionCheckboxHeader . indeterminate = checked > 0 && checked < totalOnPage ;
58+ }
59+
60+ // Presence-based flag that swaps the results count for the selection bar.
61+ if ( selectionHeader ) {
62+ if ( checked > 0 ) selectionHeader . setAttribute ( "data-selecting" , "" ) ;
63+ else selectionHeader . removeAttribute ( "data-selecting" ) ;
64+ }
65+
66+ if ( selectionSummary ) {
67+ selectionSummary . textContent = selectAllPages ? `All ${ total } selected` : `${ checked } selected` ;
68+ }
69+
70+ // The Actions dropdown is always visible, but its items only act when
71+ // something is selected — disable them otherwise (CSS dims + blocks them,
72+ // components.js drops them from keyboard nav, and the menu heading hints why).
73+ actionMenuItems . forEach ( ( item ) => {
74+ if ( checked > 0 ) item . removeAttribute ( "aria-disabled" ) ;
75+ else item . setAttribute ( "aria-disabled" , "true" ) ;
76+ } ) ;
77+
78+ // Offer "Select all N" only once the whole page is selected, we're not
79+ // already in all-pages mode, and there's more than this page to select.
80+ if ( selectAllButton ) {
81+ const showSelectAll = fullPageSelected && ! selectAllPages && total > totalOnPage ;
82+ selectAllButton . classList . toggle ( "hidden" , ! showSelectAll ) ;
83+ }
84+
85+ if ( actionIdsInput ) {
86+ if ( selectAllPages ) {
87+ actionIdsInput . value = SENTINEL_ALL ;
88+ } else {
89+ const ids = actionCheckboxes . filter ( ( cb ) => cb . checked ) . map ( ( cb ) => cb . getAttribute ( "name" ) ) ;
90+ actionIdsInput . value = ids . join ( "," ) ;
91+ }
3092 }
3193}
3294
@@ -35,44 +97,54 @@ document.addEventListener("change", (e) => {
3597 if ( ! ( target instanceof HTMLInputElement ) ) return ;
3698
3799 if ( target . matches ( "[data-action-checkbox]" ) ) {
38- if ( actionCheckboxHeader ?. checked ) {
39- const checkedCount = actionCheckboxes . filter ( ( cb ) => cb . checked ) . length ;
40- if ( checkedCount !== actionCheckboxes . length ) actionCheckboxHeader . checked = false ;
41- }
42- updateActionIds ( ) ;
100+ if ( ! target . checked ) selectAllPages = false ;
101+ updateSelectionUI ( ) ;
43102 return ;
44103 }
45104
46105 if ( target . matches ( "[data-action-checkbox-all]" ) ) {
47106 actionCheckboxes . forEach ( ( cb ) => {
48107 cb . checked = target . checked ;
49108 } ) ;
50- updateActionIds ( ) ;
109+ if ( ! target . checked ) selectAllPages = false ;
110+ updateSelectionUI ( ) ;
51111 }
52112} ) ;
53113
54- document . addEventListener ( "change" , ( e ) => {
55- const select = e . target ;
56- if ( ! select . matches ( '[name="action_name"]' ) ) return ;
57- if ( ! select . value ) return ;
58-
59- const actionName = select . value ;
60- const ids = actionIdsInput ?. value ?? "" ;
61- const count = ids && ids !== SENTINEL_ALL ? ids . split ( "," ) . length : 0 ;
62- const target =
63- ids === SENTINEL_ALL
64- ? "ALL items across all pages"
65- : count > 0
66- ? `${ count } selected ${ count === 1 ? "item" : "items" } `
67- : null ;
68- const confirmMessage = target
69- ? `Are you sure you want to perform "${ actionName } " on ${ target } ?`
70- : `Are you sure you want to perform "${ actionName } "?` ;
71-
72- if ( confirm ( confirmMessage ) ) {
73- actionForm ?. submit ( ) ;
74- } else {
75- select . value = "" ;
114+ document . addEventListener ( "click" , ( e ) => {
115+ const target = e . target ;
116+ if ( ! ( target instanceof Element ) ) return ;
117+
118+ if ( target . closest ( "[data-action-select-all]" ) ) {
119+ selectAllPages = true ;
120+ updateSelectionUI ( ) ;
121+ return ;
122+ }
123+
124+ if ( target . closest ( "[data-action-clear]" ) ) {
125+ actionCheckboxes . forEach ( ( cb ) => {
126+ cb . checked = false ;
127+ } ) ;
128+ selectAllPages = false ;
129+ updateSelectionUI ( ) ;
130+ return ;
131+ }
132+
133+ const actionItem = target . closest ( "[data-action-name]" ) ;
134+ if ( actionItem ) {
135+ if ( actionItem . getAttribute ( "aria-disabled" ) === "true" ) return ;
136+ const actionName = actionItem . getAttribute ( "data-action-name" ) ;
137+ const checked = checkedCount ( ) ;
138+ const confirmMessage = selectAllPages
139+ ? `Are you sure you want to perform "${ actionName } " on all ${ totalCount ( ) } items across all pages?`
140+ : `Are you sure you want to perform "${ actionName } " on ${ checked } selected item${ checked === 1 ? "" : "s" } ?` ;
141+
142+ if ( confirm ( confirmMessage ) ) {
143+ if ( actionNameInput ) actionNameInput . value = actionName ;
144+ actionForm ?. submit ( ) ;
145+ }
146+ // The dropdown-menu component closes itself on menuitem click, so a
147+ // cancelled confirm just leaves the menu closed with the selection intact.
76148 }
77149} ) ;
78150
@@ -89,7 +161,8 @@ document.addEventListener("click", (e) => {
89161 const min = Math . min ( thisIndex , lastIndex ) ;
90162 const max = Math . max ( thisIndex , lastIndex ) ;
91163 for ( let i = min ; i <= max ; i ++ ) actionCheckboxes [ i ] . checked = target . checked ;
92- updateActionIds ( ) ;
164+ if ( ! target . checked ) selectAllPages = false ;
165+ updateSelectionUI ( ) ;
93166 } else {
94167 lastActionCheckboxChecked = target ;
95168 }
0 commit comments