@@ -105,7 +105,7 @@ export default class FileTree {
105105 const name = entry . name || Path . basename ( entry . url ) ;
106106
107107 if ( entry . isDirectory ) {
108- return this . createFolderElement ( name , entry . url ) ;
108+ return this . createFolderElement ( name , entry . url , recycledEl ) ;
109109 } else {
110110 return this . createFileElement ( name , entry . url , recycledEl ) ;
111111 }
@@ -115,12 +115,39 @@ export default class FileTree {
115115 * Create folder element (collapsible)
116116 * @param {string } name
117117 * @param {string } url
118+ * @param {HTMLElement } [recycledEl] - Optional recycled element for reuse
118119 * @returns {HTMLElement }
119120 */
120- createFolderElement ( name , url ) {
121+ createFolderElement ( name , url , recycledEl ) {
122+ // Try to recycle existing folder element
123+ if ( recycledEl && recycledEl . classList . contains ( "collapsible" ) ) {
124+ const $title = recycledEl . $title ;
125+ if ( $title ) {
126+ $title . dataset . url = url ;
127+ $title . dataset . name = name ;
128+ const textEl = $title . querySelector ( ".text" ) ;
129+ if ( textEl ) textEl . textContent = name ;
130+
131+ // Collapse if expanded and clear children
132+ if ( ! recycledEl . classList . contains ( "hidden" ) ) {
133+ recycledEl . classList . add ( "hidden" ) ;
134+ const childTree = this . childTrees . get ( recycledEl . _folderUrl ) ;
135+ if ( childTree ) {
136+ childTree . destroy ( ) ;
137+ this . childTrees . delete ( recycledEl . _folderUrl ) ;
138+ }
139+ recycledEl . $ul . innerHTML = "" ;
140+ }
141+
142+ recycledEl . _folderUrl = url ;
143+ return recycledEl ;
144+ }
145+ }
146+
121147 const $wrapper = tag ( "div" , {
122148 className : "list collapsible hidden" ,
123149 } ) ;
150+ $wrapper . _folderUrl = url ;
124151
125152 const $indicator = tag ( "span" , { className : "icon folder" } ) ;
126153
@@ -189,10 +216,11 @@ export default class FileTree {
189216 queueMicrotask ( ( ) => toggle ( ) ) ;
190217 }
191218
192- // Add properties for external access
219+ // Add properties for external access (keep unclasped for collapsableList compatibility)
193220 Object . defineProperties ( $wrapper , {
194221 collapsed : { get : ( ) => $wrapper . classList . contains ( "hidden" ) } ,
195- unclasped : { get : ( ) => ! $wrapper . classList . contains ( "hidden" ) } ,
222+ expanded : { get : ( ) => ! $wrapper . classList . contains ( "hidden" ) } ,
223+ unclasped : { get : ( ) => ! $wrapper . classList . contains ( "hidden" ) } , // Legacy compatibility
196224 $title : { get : ( ) => $title } ,
197225 $ul : { get : ( ) => $content } ,
198226 expand : {
@@ -302,38 +330,70 @@ export default class FileTree {
302330 */
303331 appendEntry ( name , url , isDirectory ) {
304332 const entry = { name, url, isDirectory, isFile : ! isDirectory } ;
305- const $el = this . createEntryElement ( entry ) ;
306333
334+ // Insert in sorted position
307335 if ( isDirectory ) {
308- // Insert at beginning (before files)
309- const firstFile = this . container . querySelector ( '[data-type="file"]' ) ;
310- if ( firstFile ) {
311- this . container . insertBefore ( $el , firstFile ) ;
336+ // Find first file or end of dirs
337+ const insertIndex = this . entries . findIndex ( ( e ) => ! e . isDirectory ) ;
338+ if ( insertIndex === - 1 ) {
339+ this . entries . push ( entry ) ;
312340 } else {
313- this . container . appendChild ( $el ) ;
341+ this . entries . splice ( insertIndex , 0 , entry ) ;
314342 }
315343 } else {
316- // Append at end
317- this . container . appendChild ( $el ) ;
344+ this . entries . push ( entry ) ;
318345 }
319346
320- this . entries . push ( entry ) ;
347+ // Re-sort entries
348+ this . entries = helpers . sortDir ( this . entries , {
349+ sortByName : true ,
350+ showHiddenFiles : true ,
351+ } ) ;
352+
353+ // Update rendering based on mode
354+ if ( this . virtualList ) {
355+ // Virtual list mode: update items
356+ this . virtualList . setItems ( this . entries ) ;
357+ } else {
358+ // Fragment mode: re-render
359+ this . container . innerHTML = "" ;
360+ this . renderWithFragment ( ) ;
361+ }
321362 }
322363
323364 /**
324365 * Remove an entry from the tree
325366 * @param {string } url
326367 */
327368 removeEntry ( url ) {
328- const $el = this . findElement ( url ) ;
329- if ( $el ) {
330- // For folders, remove the wrapper div
331- if ( $el . dataset . type === "dir" ) {
332- $el . closest ( ".list.collapsible" ) ?. remove ( ) ;
333- } else {
334- $el . remove ( ) ;
369+ // Update data first
370+ const index = this . entries . findIndex ( ( e ) => e . url === url ) ;
371+ if ( index === - 1 ) return ;
372+
373+ // Clean up child tree if folder
374+ const entry = this . entries [ index ] ;
375+ if ( entry . isDirectory && this . childTrees . has ( url ) ) {
376+ this . childTrees . get ( url ) . destroy ( ) ;
377+ this . childTrees . delete ( url ) ;
378+ }
379+
380+ // Remove from entries
381+ this . entries . splice ( index , 1 ) ;
382+
383+ // Update rendering based on mode
384+ if ( this . virtualList ) {
385+ // Virtual list mode: update items
386+ this . virtualList . setItems ( this . entries ) ;
387+ } else {
388+ // Fragment mode: remove element directly
389+ const $el = this . findElement ( url ) ;
390+ if ( $el ) {
391+ if ( $el . dataset . type === "dir" ) {
392+ $el . closest ( ".list.collapsible" ) ?. remove ( ) ;
393+ } else {
394+ $el . remove ( ) ;
395+ }
335396 }
336- this . entries = this . entries . filter ( ( e ) => e . url !== url ) ;
337397 }
338398 }
339399}
0 commit comments