11import {
22 readToolboxVoteSnapshot ,
3- updateToolboxVoteOrder ,
3+ reorderToolboxVoteRows ,
44} from "../src/engine/api/toolbox-votes-api-client.js" ;
55
66const status = document . querySelector ( "[data-toolbox-votes-status]" ) ;
7+ const dragStatus = document . querySelector ( "[data-toolbox-votes-drag-status]" ) ;
78const body = document . querySelector ( "[data-toolbox-votes-body]" ) ;
89const selectedOrder = document . querySelector ( "[data-toolbox-votes-selected-order]" ) ;
910const selectedGroup = document . querySelector ( "[data-toolbox-votes-selected-group]" ) ;
1011const selectedPath = document . querySelector ( "[data-toolbox-votes-selected-path]" ) ;
12+ const sortButtons = Array . from ( document . querySelectorAll ( "[data-toolbox-votes-sort]" ) ) ;
13+ const sortHeaders = Array . from ( document . querySelectorAll ( "[data-toolbox-votes-sort-header]" ) ) ;
14+ const sortButtonLabels = new Map ( sortButtons . map ( ( button ) => [
15+ button . dataset . toolboxVotesSort ,
16+ button . textContent . trim ( ) ,
17+ ] ) ) ;
18+
19+ const SORT_TYPES = Object . freeze ( {
20+ down : "number" ,
21+ order : "number" ,
22+ up : "number" ,
23+ } ) ;
1124
1225let selectedToolId = "" ;
1326let snapshotRows = [ ] ;
27+ let draggedToolId = "" ;
28+ let sortState = {
29+ direction : "asc" ,
30+ key : "order" ,
31+ } ;
1432
1533function setStatus ( message ) {
1634 if ( status ) {
1735 status . textContent = message ;
1836 }
1937}
2038
39+ function sortLabel ( key ) {
40+ return sortButtonLabels . get ( key ) || key ;
41+ }
42+
43+ function isOrderSort ( ) {
44+ return sortState . key === "order" ;
45+ }
46+
47+ function wholeNumberOrder ( value ) {
48+ const order = Number ( value ) ;
49+ return Math . max ( 1 , Math . round ( Number . isFinite ( order ) ? order : 1 ) ) ;
50+ }
51+
2152function tableCell ( value ) {
2253 const cell = document . createElement ( "td" ) ;
2354 cell . textContent = value === null || value === undefined ? "" : String ( value ) ;
2455 return cell ;
2556}
2657
58+ function toolNameCell ( voteRow ) {
59+ const cell = document . createElement ( "td" ) ;
60+ const link = document . createElement ( "a" ) ;
61+ link . href = voteRow . path || "#" ;
62+ link . textContent = voteRow . toolName ;
63+ link . setAttribute ( "aria-label" , `Open ${ voteRow . toolName } ` ) ;
64+ cell . append ( link ) ;
65+ return cell ;
66+ }
67+
68+ function orderCell ( voteRow ) {
69+ const cell = tableCell ( wholeNumberOrder ( voteRow . order ) ) ;
70+ cell . dataset . toolboxVotesOrder = voteRow . toolId ;
71+ cell . title = isOrderSort ( )
72+ ? "Drag this row to update Toolbox order."
73+ : "Sort by Order to enable drag/drop ordering." ;
74+ return cell ;
75+ }
76+
77+ function sortValue ( voteRow , key ) {
78+ if ( key === "currentUserVote" ) {
79+ return voteRow . currentUserVote || "None" ;
80+ }
81+ if ( key === "order" ) {
82+ return wholeNumberOrder ( voteRow . order ) ;
83+ }
84+ return voteRow [ key ] ?? "" ;
85+ }
86+
87+ function sortedRows ( ) {
88+ const direction = sortState . direction === "asc" ? 1 : - 1 ;
89+ return [ ...snapshotRows ] . sort ( ( left , right ) => {
90+ const leftValue = sortValue ( left , sortState . key ) ;
91+ const rightValue = sortValue ( right , sortState . key ) ;
92+ if ( SORT_TYPES [ sortState . key ] === "number" ) {
93+ return ( Number ( leftValue ) - Number ( rightValue ) ) * direction ;
94+ }
95+ return String ( leftValue ) . localeCompare ( String ( rightValue ) ) * direction ;
96+ } ) ;
97+ }
98+
99+ function updateSortHeaders ( ) {
100+ sortHeaders . forEach ( ( header ) => {
101+ const selected = header . dataset . toolboxVotesSortHeader === sortState . key ;
102+ header . setAttribute ( "aria-sort" , selected ? ( sortState . direction === "asc" ? "ascending" : "descending" ) : "none" ) ;
103+ const button = header . querySelector ( "[data-toolbox-votes-sort]" ) ;
104+ if ( ! button ) {
105+ return ;
106+ }
107+ const baseLabel = sortButtonLabels . get ( button . dataset . toolboxVotesSort ) || button . textContent . trim ( ) ;
108+ button . textContent = selected ? `${ baseLabel } ${ sortState . direction === "asc" ? "↑" : "↓" } ` : baseLabel ;
109+ button . classList . toggle ( "primary" , selected ) ;
110+ button . setAttribute ( "aria-pressed" , String ( selected ) ) ;
111+ } ) ;
112+ }
113+
114+ function updateDragStatus ( ) {
115+ if ( ! dragStatus ) {
116+ return ;
117+ }
118+ if ( isOrderSort ( ) ) {
119+ dragStatus . textContent = "Drag/drop row ordering is enabled while sorted by Order." ;
120+ return ;
121+ }
122+ dragStatus . textContent = `Drag/drop row ordering is disabled while sorted by ${ sortLabel ( sortState . key ) } . Sort by Order to reorder.` ;
123+ }
124+
27125function setSelectedDetails ( voteRow ) {
28126 if ( selectedOrder ) {
29- selectedOrder . textContent = voteRow ? String ( voteRow . order ) : "None" ;
127+ selectedOrder . textContent = voteRow ? String ( wholeNumberOrder ( voteRow . order ) ) : "None" ;
30128 }
31129 if ( selectedGroup ) {
32130 selectedGroup . textContent = voteRow ?. group || "None" ;
@@ -45,31 +143,72 @@ function selectRow(toolId) {
45143 } ) ;
46144}
47145
48- function orderCell ( voteRow ) {
49- const cell = document . createElement ( "td" ) ;
50- const input = document . createElement ( "input" ) ;
51- input . type = "number" ;
52- input . min = "0" ;
53- input . step = "1" ;
54- input . value = String ( voteRow . order ) ;
55- input . dataset . toolboxVotesOrderInput = voteRow . toolId ;
56- input . setAttribute ( "aria-label" , `Order for ${ voteRow . toolName } ` ) ;
57- input . addEventListener ( "change" , ( ) => {
58- updateOrder ( voteRow , input . value ) ;
59- } ) ;
60- input . addEventListener ( "focus" , ( ) => {
61- selectRow ( voteRow . toolId ) ;
62- } ) ;
63- cell . append ( input ) ;
64- return cell ;
146+ function orderedToolIdsAfterDrop ( targetToolId , event ) {
147+ const displayToolIds = sortedRows ( ) . map ( ( row ) => row . toolId ) . filter ( ( toolId ) => toolId !== draggedToolId ) ;
148+ const targetIndex = displayToolIds . indexOf ( targetToolId ) ;
149+ if ( targetIndex === - 1 ) {
150+ return null ;
151+ }
152+ const targetRow = event . currentTarget ;
153+ const targetBox = targetRow . getBoundingClientRect ( ) ;
154+ const insertAfterTarget = event . clientY > targetBox . top + ( targetBox . height / 2 ) ;
155+ displayToolIds . splice ( targetIndex + ( insertAfterTarget ? 1 : 0 ) , 0 , draggedToolId ) ;
156+ return sortState . direction === "desc" ? displayToolIds . reverse ( ) : displayToolIds ;
157+ }
158+
159+ function handleDragStart ( event , voteRow ) {
160+ if ( ! isOrderSort ( ) ) {
161+ event . preventDefault ( ) ;
162+ updateDragStatus ( ) ;
163+ return ;
164+ }
165+ draggedToolId = voteRow . toolId ;
166+ selectRow ( voteRow . toolId ) ;
167+ event . dataTransfer . effectAllowed = "move" ;
168+ event . dataTransfer . setData ( "text/plain" , voteRow . toolId ) ;
169+ }
170+
171+ function handleDragOver ( event ) {
172+ if ( ! isOrderSort ( ) || ! draggedToolId ) {
173+ return ;
174+ }
175+ event . preventDefault ( ) ;
176+ event . dataTransfer . dropEffect = "move" ;
177+ }
178+
179+ function handleDrop ( event , targetToolId ) {
180+ if ( ! isOrderSort ( ) || ! draggedToolId || draggedToolId === targetToolId ) {
181+ draggedToolId = "" ;
182+ return ;
183+ }
184+ event . preventDefault ( ) ;
185+ const orderedToolIds = orderedToolIdsAfterDrop ( targetToolId , event ) ;
186+ if ( ! orderedToolIds ) {
187+ draggedToolId = "" ;
188+ return ;
189+ }
190+ const movedToolId = draggedToolId ;
191+ try {
192+ const snapshot = reorderToolboxVoteRows ( orderedToolIds ) ;
193+ renderSnapshot ( snapshot , "Toolbox vote order updated. Rows were renumbered with whole-number order values." ) ;
194+ selectRow ( movedToolId ) ;
195+ } catch ( error ) {
196+ const message = error instanceof Error ? error . message : String ( error || "Toolbox vote row order unavailable." ) ;
197+ setStatus ( `Toolbox vote rows could not be reordered. ${ message } ` ) ;
198+ } finally {
199+ draggedToolId = "" ;
200+ }
65201}
66202
67203function renderRows ( rows ) {
68204 if ( ! body ) {
69205 return ;
70206 }
71207 snapshotRows = rows ;
72- if ( ! rows . length ) {
208+ updateSortHeaders ( ) ;
209+ updateDragStatus ( ) ;
210+ const displayRows = sortedRows ( ) ;
211+ if ( ! displayRows . length ) {
73212 const row = document . createElement ( "tr" ) ;
74213 const cell = document . createElement ( "td" ) ;
75214 cell . colSpan = 8 ;
@@ -80,14 +219,28 @@ function renderRows(rows) {
80219 return ;
81220 }
82221
83- body . replaceChildren ( ...rows . map ( ( voteRow ) => {
222+ const allowDrag = isOrderSort ( ) ;
223+ body . replaceChildren ( ...displayRows . map ( ( voteRow ) => {
84224 const row = document . createElement ( "tr" ) ;
85225 row . dataset . toolboxVotesToolId = voteRow . toolId ;
226+ row . dataset . toolboxVotesDrag = allowDrag ? "enabled" : "disabled" ;
227+ row . setAttribute ( "aria-disabled" , String ( ! allowDrag ) ) ;
228+ row . setAttribute ( "draggable" , String ( allowDrag ) ) ;
229+ row . title = allowDrag
230+ ? "Drag this row to reorder Toolbox tools."
231+ : `Drag/drop disabled while sorted by ${ sortLabel ( sortState . key ) } .` ;
86232 row . addEventListener ( "click" , ( ) => {
87233 selectRow ( voteRow . toolId ) ;
88234 } ) ;
235+ row . addEventListener ( "dragstart" , ( event ) => {
236+ handleDragStart ( event , voteRow ) ;
237+ } ) ;
238+ row . addEventListener ( "dragover" , handleDragOver ) ;
239+ row . addEventListener ( "drop" , ( event ) => {
240+ handleDrop ( event , voteRow . toolId ) ;
241+ } ) ;
89242 row . append (
90- tableCell ( voteRow . toolName ) ,
243+ toolNameCell ( voteRow ) ,
91244 orderCell ( voteRow ) ,
92245 tableCell ( voteRow . group ) ,
93246 tableCell ( voteRow . path ) ,
@@ -99,25 +252,15 @@ function renderRows(rows) {
99252 return row ;
100253 } ) ) ;
101254
102- selectRow ( selectedToolId || rows [ 0 ] . toolId ) ;
255+ const selectedRow = snapshotRows . find ( ( row ) => row . toolId === selectedToolId ) ;
256+ selectRow ( selectedRow ? selectedToolId : displayRows [ 0 ] . toolId ) ;
103257}
104258
105259function renderSnapshot ( snapshot , message ) {
106260 renderRows ( snapshot . rows || [ ] ) ;
107261 setStatus ( message || `Showing Toolbox vote totals for ${ snapshot . currentUserName || "current session" } .` ) ;
108262}
109263
110- function updateOrder ( voteRow , value ) {
111- try {
112- const snapshot = updateToolboxVoteOrder ( voteRow . toolId , Number ( value ) ) ;
113- renderSnapshot ( snapshot , `${ voteRow . toolName } order updated to ${ Number ( value ) } .` ) ;
114- selectRow ( voteRow . toolId ) ;
115- } catch ( error ) {
116- const message = error instanceof Error ? error . message : String ( error || "Toolbox vote order unavailable." ) ;
117- setStatus ( `Toolbox vote order could not be updated. ${ message } ` ) ;
118- }
119- }
120-
121264function renderToolboxVotes ( ) {
122265 if ( window . GameFoundrySessionGuard ?. blocked ) {
123266 return ;
@@ -131,4 +274,22 @@ function renderToolboxVotes() {
131274 }
132275}
133276
277+ sortButtons . forEach ( ( button ) => {
278+ button . addEventListener ( "click" , ( ) => {
279+ const nextKey = button . dataset . toolboxVotesSort ;
280+ if ( sortState . key === nextKey ) {
281+ sortState = {
282+ direction : sortState . direction === "asc" ? "desc" : "asc" ,
283+ key : nextKey ,
284+ } ;
285+ } else {
286+ sortState = {
287+ direction : "asc" ,
288+ key : nextKey ,
289+ } ;
290+ }
291+ renderRows ( snapshotRows ) ;
292+ } ) ;
293+ } ) ;
294+
134295renderToolboxVotes ( ) ;
0 commit comments