11import type Sortable from 'sortablejs' ;
2- import { SortableJSUrl } from '../../ExternalLibs' ;
2+ import { ExternalLibraryLoader , SortableCdn } from '../../ExternalLibs' ;
33
4- declare global {
5- interface Window {
6- Sortable ?: typeof Sortable ;
7- }
8- }
9-
10- let loadPromise : Promise < typeof Sortable > | null = null ;
11-
12- /**
13- * Dynamically loads SortableJS from CDN if not already loaded
14- */
15- async function ensureSortableJSLoaded ( ) : Promise < typeof Sortable > {
16- // Check if SortableJS is already available
17- if ( window . Sortable ) {
18- return window . Sortable ;
19- }
20-
21- if ( loadPromise ) {
22- return loadPromise ;
23- }
24-
25- // Load SortableJS from CDN
26- loadPromise = new Promise ( ( resolve , reject ) => {
27- const script = document . createElement ( 'script' ) ;
28- script . src = SortableJSUrl ;
29- script . onload = ( ) => {
30- if ( window . Sortable ) {
31- resolve ( window . Sortable ) ;
32- } else {
33- reject ( new Error ( 'SortableJS library failed to load' ) ) ;
34- }
35- } ;
36- script . onerror = ( ) => {
37- loadPromise = null ;
38- reject ( new Error ( 'Failed to load SortableJS from CDN' ) ) ;
39- } ;
40- document . head . appendChild ( script ) ;
41- } ) ;
42-
43- return loadPromise ;
44- }
4+ const sortableLoader = new ExternalLibraryLoader < typeof Sortable > ( SortableCdn . name , SortableCdn . url ) ;
455
466export namespace Microsoft . FluentUI . Blazor . Components . SortableList {
47- export async function init (
7+ export async function Initialize (
488 list : HTMLElement ,
499 group : string ,
5010 pull : any ,
@@ -56,11 +16,10 @@ export namespace Microsoft.FluentUI.Blazor.Components.SortableList {
5616 component : any
5717 ) : Promise < any > {
5818
59- await ensureSortableJSLoaded ( ) ;
19+ const Sortable = await sortableLoader . load ( ) ;
6020
6121 const controller = new AbortController ( ) ;
6222 const { signal } = controller ;
63- let grabMode : boolean = false ;
6423
6524 if ( group ) {
6625 list . setAttribute ( 'data-sortable-group' , group ) ;
@@ -106,44 +65,110 @@ export namespace Microsoft.FluentUI.Blazor.Components.SortableList {
10665 } ) ;
10766
10867 list . addEventListener ( 'keydown' , ( event : KeyboardEvent ) => {
109- const item = document . activeElement ;
110- if ( item == null ) {
68+ const item = document . activeElement as HTMLElement ;
69+ if ( item == null || ! item . classList . contains ( 'sortable-item' ) ) {
11170 return ;
11271 }
11372
114- if ( ! item . classList . contains ( 'sortable-item' ) ) return ;
73+ const isGrabbed = item . getAttribute ( 'aria-grabbed' ) === 'true' ;
11574
11675 switch ( event . key ) {
11776 case 'Enter' :
11877 case ' ' :
119- grabMode = ! grabMode ;
120- item . setAttribute ( 'aria-grabbed' , grabMode . toString ( ) ) ;
78+ item . setAttribute ( 'aria-grabbed' , ( ! isGrabbed ) . toString ( ) ) ;
12179 event . preventDefault ( ) ;
12280 break ;
12381
12482 case 'ArrowUp' :
125- if ( grabMode && item . previousElementSibling ) {
126- item . parentNode ! . insertBefore ( item , item . previousElementSibling ) ;
127- ( item as HTMLElement ) . focus ( ) ;
128- } else if ( item . previousElementSibling ) {
129- ( item . previousElementSibling as HTMLElement ) . focus ( ) ;
83+ if ( item . previousElementSibling ) {
84+ if ( isGrabbed ) {
85+ if ( ! sortable . options . sort ) {
86+ item . setAttribute ( 'aria-grabbed' , 'false' ) ;
87+ event . preventDefault ( ) ;
88+ break ;
89+ }
90+ const oldIndex = Array . from ( item . parentNode ! . children ) . indexOf ( item ) ;
91+ const newIndex = oldIndex - 1 ;
92+
93+ item . parentNode ! . insertBefore ( item , item . previousElementSibling ) ;
94+
95+ const updateEvent = new CustomEvent ( 'update' ) as any ;
96+ updateEvent . item = item ;
97+ updateEvent . from = list ;
98+ updateEvent . to = list ;
99+ updateEvent . oldIndex = oldIndex ;
100+ updateEvent . newIndex = newIndex ;
101+ updateEvent . oldDraggableIndex = oldIndex ;
102+ updateEvent . newDraggableIndex = newIndex ;
103+
104+ sortable . options . onUpdate ?.( updateEvent ) ;
105+
106+ setTimeout ( ( ) => {
107+ const refreshedList = document . getElementById ( list . id ) ;
108+ const movedItem = refreshedList ?. children [ newIndex ] as HTMLElement ;
109+ if ( movedItem ) {
110+ movedItem . focus ( ) ;
111+ movedItem . setAttribute ( 'aria-grabbed' , 'true' ) ;
112+ }
113+ const oldPositionItem = refreshedList ?. children [ oldIndex ] as HTMLElement ;
114+ if ( oldPositionItem ) {
115+ oldPositionItem . setAttribute ( 'aria-grabbed' , 'false' ) ;
116+ }
117+ } , 50 ) ;
118+ } else {
119+ ( item . previousElementSibling as HTMLElement ) . focus ( ) ;
120+ }
130121 }
131122 event . preventDefault ( ) ;
132123 break ;
133124
134125 case 'ArrowDown' :
135- if ( grabMode && item . nextElementSibling ) {
136- item . parentNode ! . insertBefore ( item . nextElementSibling , item ) ;
137- ( item as HTMLElement ) . focus ( ) ;
138- } else if ( item . nextElementSibling ) {
139- ( item . nextElementSibling as HTMLElement ) . focus ( ) ;
126+ if ( item . nextElementSibling ) {
127+ if ( isGrabbed ) {
128+ if ( ! sortable . options . sort ) {
129+ item . setAttribute ( 'aria-grabbed' , 'false' ) ;
130+ event . preventDefault ( ) ;
131+ break ;
132+ }
133+ const oldIndex = Array . from ( item . parentNode ! . children ) . indexOf ( item ) ;
134+ const newIndex = oldIndex + 1 ;
135+
136+ item . parentNode ! . insertBefore ( item . nextElementSibling , item ) ;
137+
138+ const updateEvent = new CustomEvent ( 'update' ) as any ;
139+ updateEvent . item = item ;
140+ updateEvent . from = list ;
141+ updateEvent . to = list ;
142+ updateEvent . oldIndex = oldIndex ;
143+ updateEvent . newIndex = newIndex ;
144+ updateEvent . oldDraggableIndex = oldIndex ;
145+ updateEvent . newDraggableIndex = newIndex ;
146+
147+ sortable . options . onUpdate ?.( updateEvent ) ;
148+
149+ setTimeout ( ( ) => {
150+ const refreshedList = document . getElementById ( list . id ) ;
151+ const movedItem = refreshedList ?. children [ newIndex ] as HTMLElement ;
152+ if ( movedItem ) {
153+ movedItem . focus ( ) ;
154+ movedItem . setAttribute ( 'aria-grabbed' , 'true' ) ;
155+ }
156+ const oldPositionItem = refreshedList ?. children [ oldIndex ] as HTMLElement ;
157+ if ( oldPositionItem ) {
158+ oldPositionItem . setAttribute ( 'aria-grabbed' , 'false' ) ;
159+ }
160+ } , 50 ) ;
161+ } else {
162+ ( item . nextElementSibling as HTMLElement ) . focus ( ) ;
163+ }
140164 }
141165 event . preventDefault ( ) ;
142166 break ;
143167
144168 case 'ArrowLeft' :
145169 case 'ArrowRight' :
146- if ( grabMode && group ) {
170+ if ( group ) {
171+
147172 const allLists = Array . from ( document . querySelectorAll ( `[data-sortable-group="${ group } "]` ) ) ;
148173 const currentIndex = allLists . indexOf ( list ) ;
149174 let nextIndex = currentIndex ;
@@ -155,39 +180,78 @@ export namespace Microsoft.FluentUI.Blazor.Components.SortableList {
155180 }
156181
157182 if ( nextIndex !== currentIndex ) {
158- const targetList = allLists [ nextIndex ] as HTMLElement ;
159- const targetListId = targetList . id ;
160- const oldIndex = Array . from ( item . parentNode ! . children ) . indexOf ( item ) ;
161- const newIndex = 0 ;
183+ if ( isGrabbed ) {
162184
163- sortable . options . onRemove ! ( {
164- item : item ,
165- from : list ,
166- to : targetList ,
167- oldIndex : oldIndex ,
168- newIndex : newIndex ,
169- oldDraggableIndex : oldIndex ,
170- newDraggableIndex : newIndex
171- } as any ) ;
185+ const targetList = allLists [ nextIndex ] as HTMLElement ;
186+ const targetListId = targetList . id ;
187+ const oldIndex = Array . from ( item . parentNode ! . children ) . indexOf ( item ) ;
188+ const newIndex = 0 ;
172189
173- item . setAttribute ( 'aria-grabbed' , 'false' ) ;
174- grabMode = false ;
190+ const pullMode = pull || true
191+ //const putMode = put ? true : null ;
175192
176- setTimeout ( ( ) => {
177- const newList = document . getElementById ( targetListId ) ;
178- const movedItem = newList ?. children [ newIndex ] as HTMLElement ;
179- movedItem ?. focus ( ) ;
180- } , 50 ) ;
193+ if ( pullMode === false ) {
194+ break ;
195+ }
196+
197+ // Move in DOM immediately for feedback
198+ targetList . insertBefore ( item , targetList . firstChild ) ;
199+
200+ // Notify via sortable.onRemove
201+ const removeEvent = new CustomEvent ( 'remove' ) as any ;
202+ removeEvent . item = item ;
203+ removeEvent . from = list ;
204+ removeEvent . to = targetList ;
205+ removeEvent . oldIndex = oldIndex ;
206+ removeEvent . newIndex = newIndex ;
207+ removeEvent . oldDraggableIndex = oldIndex ;
208+ removeEvent . newDraggableIndex = newIndex ;
209+ removeEvent . pullMode = pullMode ;
210+ if ( pullMode === 'clone' ) {
211+ removeEvent . clone = item . cloneNode ( true ) ;
212+ }
213+
214+ sortable . options . onRemove ?.( removeEvent ) ;
215+
216+ setTimeout ( ( ) => {
217+ const refreshedTargetList = document . getElementById ( targetListId ) ;
218+ const movedItem = refreshedTargetList ?. children [ newIndex ] as HTMLElement ;
219+ if ( movedItem ) {
220+ movedItem . focus ( ) ;
221+ movedItem . setAttribute ( 'aria-grabbed' , 'false' ) ;
222+ }
223+ const refreshedSourceList = document . getElementById ( list . id ) ;
224+ const oldPositionItem = refreshedSourceList ?. children [ oldIndex ] as HTMLElement ;
225+ if ( oldPositionItem ) {
226+ oldPositionItem . setAttribute ( 'aria-grabbed' , 'false' ) ;
227+ }
228+ } , 50 ) ;
229+ }
230+ else {
231+ const targetList = allLists [ nextIndex ] as HTMLElement ;
232+ const itemIndex = Array . from ( item . parentNode ! . children ) . indexOf ( item ) ;
233+ let targetItem = targetList . children [ itemIndex ] as HTMLElement ;
234+
235+ if ( ! targetItem && targetList . children . length > 0 ) {
236+ const firstDist = Math . abs ( itemIndex - 0 ) ;
237+ const lastDist = Math . abs ( itemIndex - ( targetList . children . length - 1 ) ) ;
238+ targetItem = ( firstDist < lastDist ? targetList . firstElementChild : targetList . lastElementChild ) as HTMLElement ;
239+ }
240+
241+ if ( targetItem ) {
242+ targetItem . focus ( ) ;
243+ }
244+ }
181245 }
182246 }
183247 event . preventDefault ( ) ;
184248 break ;
185249
186250 case 'Tab' :
187- if ( item . getAttribute ( 'aria-grabbed' ) === 'true' ) {
251+ if ( isGrabbed ) {
188252 item . setAttribute ( 'aria-grabbed' , 'false' ) ;
189- grabMode = false ;
190253 }
254+ break ;
191255 }
192256 } , { signal } ) ;
193257
0 commit comments