1function _array_like_to_array(arr, len) { 2 if (len == null || len > arr.length) len = arr.length; 3 for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i]; 4 return arr2; 5} 6function _array_without_holes(arr) { 7 if (Array.isArray(arr)) return _array_like_to_array(arr); 8} 9function _assert_this_initialized(self) { 10 if (self === void 0) { 11 throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 12 } 13 return self; 14} 15function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { 16 try { 17 var info = gen[key](arg); 18 var value = info.value; 19 } catch (error) { 20 reject(error); 21 return; 22 } 23 if (info.done) { 24 resolve(value); 25 } else { 26 Promise.resolve(value).then(_next, _throw); 27 } 28} 29function _async_to_generator(fn) { 30 return function() { 31 var self = this, args = arguments; 32 return new Promise(function(resolve, reject) { 33 var gen = fn.apply(self, args); 34 function _next(value) { 35 asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); 36 } 37 function _throw(err) { 38 asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); 39 } 40 _next(undefined); 41 }); 42 }; 43} 44function _class_call_check(instance, Constructor) { 45 if (!(instance instanceof Constructor)) { 46 throw new TypeError("Cannot call a class as a function"); 47 } 48} 49function _defineProperties(target, props) { 50 for(var i = 0; i < props.length; i++){ 51 var descriptor = props[i]; 52 descriptor.enumerable = descriptor.enumerable || false; 53 descriptor.configurable = true; 54 if ("value" in descriptor) descriptor.writable = true; 55 Object.defineProperty(target, descriptor.key, descriptor); 56 } 57} 58function _create_class(Constructor, protoProps, staticProps) { 59 if (protoProps) _defineProperties(Constructor.prototype, protoProps); 60 if (staticProps) _defineProperties(Constructor, staticProps); 61 return Constructor; 62} 63function _define_property(obj, key, value) { 64 if (key in obj) { 65 Object.defineProperty(obj, key, { 66 value: value, 67 enumerable: true, 68 configurable: true, 69 writable: true 70 }); 71 } else { 72 obj[key] = value; 73 } 74 return obj; 75} 76function _get_prototype_of(o) { 77 _get_prototype_of = Object.setPrototypeOf ? Object.getPrototypeOf : function getPrototypeOf(o) { 78 return o.__proto__ || Object.getPrototypeOf(o); 79 }; 80 return _get_prototype_of(o); 81} 82function _inherits(subClass, superClass) { 83 if (typeof superClass !== "function" && superClass !== null) { 84 throw new TypeError("Super expression must either be null or a function"); 85 } 86 subClass.prototype = Object.create(superClass && superClass.prototype, { 87 constructor: { 88 value: subClass, 89 writable: true, 90 configurable: true 91 } 92 }); 93 if (superClass) _set_prototype_of(subClass, superClass); 94} 95function _instanceof(left, right) { 96 if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { 97 return !!right[Symbol.hasInstance](left); 98 } else { 99 return left instanceof right; 100 } 101} 102function _iterable_to_array(iter) { 103 if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); 104} 105function _non_iterable_spread() { 106 throw new TypeError("Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); 107} 108function _possible_constructor_return(self, call) { 109 if (call && (_type_of(call) === "object" || typeof call === "function")) { 110 return call; 111 } 112 return _assert_this_initialized(self); 113} 114function _set_prototype_of(o, p) { 115 _set_prototype_of = Object.setPrototypeOf || function setPrototypeOf(o, p) { 116 o.__proto__ = p; 117 return o; 118 }; 119 return _set_prototype_of(o, p); 120} 121function _to_consumable_array(arr) { 122 return _array_without_holes(arr) || _iterable_to_array(arr) || _unsupported_iterable_to_array(arr) || _non_iterable_spread(); 123} 124function _type_of(obj) { 125 "@swc/helpers - typeof"; 126 return obj && typeof Symbol !== "undefined" && obj.constructor === Symbol ? "symbol" : typeof obj; 127} 128function _unsupported_iterable_to_array(o, minLen) { 129 if (!o) return; 130 if (typeof o === "string") return _array_like_to_array(o, minLen); 131 var n = Object.prototype.toString.call(o).slice(8, -1); 132 if (n === "Object" && o.constructor) n = o.constructor.name; 133 if (n === "Map" || n === "Set") return Array.from(n); 134 if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _array_like_to_array(o, minLen); 135} 136function _is_native_reflect_construct() { 137 if (typeof Reflect === "undefined" || !Reflect.construct) return false;
138 if (Reflect.construct.sham) return false; 139 if (typeof Proxy === "function") return true; 140 try { 141 Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function() {})); 142 return true; 143 } catch (e) { 144 return false; 145 } 146} 147function _create_super(Derived) { 148 var hasNativeReflectConstruct = _is_native_reflect_construct(); 149 return function _createSuperInternal() { 150 var Super = _get_prototype_of(Derived), result; 151 if (hasNativeReflectConstruct) { 152 var NewTarget = _get_prototype_of(this).constructor; 153 result = Reflect.construct(Super, arguments, NewTarget); 154 } else { 155 result = Super.apply(this, arguments); 156 } 157 return _possible_constructor_return(this, result); 158 }; 159} 160function _ts_generator(thisArg, body) { 161 var f, y, t, g, _ = { 162 label: 0, 163 sent: function() { 164 if (t[0] & 1) throw t[1]; 165 return t[1]; 166 }, 167 trys: [], 168 ops: [] 169 }; 170 return(g = { 171 next: verb(0), 172 "throw": verb(1), 173 "return": verb(2) 174 }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { 175 return this; 176 }), g); 177 function verb(n) { 178 return function(v) { 179 return step([ 180 n, 181 v 182 ]); 183 }; 184 } 185 function step(op) { 186 if (f) throw new TypeError("Generator is already executing."); 187 while(_)try { 188 if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 189 if (y = 0, t) op = [ 190 op[0] & 2, 191 t.value 192 ]; 193 switch(op[0]){ 194 case 0: 195 case 1: 196 t = op; 197 break; 198 case 4: 199 _.label++; 200 return { 201 value: op[1], 202 done: false 203 }; 204 case 5: 205 _.label++; 206 y = op[1]; 207 op = [ 208 0 209 ]; 210 continue; 211 case 7: 212 op = _.ops.pop(); 213 _.trys.pop(); 214 continue; 215 default: 216 if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { 217 _ = 0; 218 continue; 219 } 220 if (op[0] === 3 && (!t || op[1] > t[0] && op[1] < t[3])) { 221 _.label = op[1]; 222 break; 223 } 224 if (op[0] === 6 && _.label < t[1]) { 225 _.label = t[1]; 226 t = op; 227 break; 228 } 229 if (t && _.label < t[2]) { 230 _.label = t[2]; 231 _.ops.push(op); 232 break; 233 } 234 if (t[2]) _.ops.pop(); 235 _.trys.pop(); 236 continue; 237 } 238 op = body.call(thisArg, _); 239 } catch (e) { 240 op = [ 241 6, 242 e 243 ]; 244 y = 0; 245 } finally{ 246 f = t = 0; 247 } 248 if (op[0] & 5) throw op[1]; 249 return { 250 value: op[0] ? op[1] : void 0, 251 done: true 252 }; 253 } 254} 255import { Controller } from "@hotwired/stimulus"; 256import { DirectoryNode, FileSystemNode } from "../../lib/file-manager/models/FileSystemNode.js"; 257import { FileSystemService } from "../../lib/file-manager/services/FileSystemService.js"; 258import { positionContextMenu, escapeHtml, getParentPath, setupInPlaceRename, decodeNodePath } from "../../lib/file-manager/utils/index.js"; 259import { loadSvgIntoElement } from "../../lib/file-manager/utils/svg-loader.js"; 260import { getActionAvailability } from "../../lib/file-manager/models/ActionAvailability.js"; 261/** 262 * Name a new folder is pre-filled with, matching the browser pane 263 */ var NEW_FOLDER_NAME = "New Folder"; 264var TreeController = /*#__PURE__*/ function(Controller) { 265 "use strict"; 266 _inherits(TreeController, Controller); 267 var _super = _create_super(TreeController); 268 function TreeController() { 269 _class_call_check(this, TreeController); 270 var _this; 271 _this = _super.apply(this, arguments); 272 _define_property(_assert_this_initialized(_this), "service", void 0); 273 _define_property(_assert_this_initialized(_this), "contextMenuPath", null); 274 _define_property(_assert_this_initialized(_this), "rootNode", null);
275 _define_property(_assert_this_initialized(_this), "nodeMap", new Map()) // O(1) path-to-node lookup 276 ; 277 _define_property(_assert_this_initialized(_this), "selectedPath", null) // null = no selection, '' = root selected 278 ; 279 _define_property(_assert_this_initialized(_this), "isTrashSelected", false) // Whether the trash node is currently selected 280 ; 281 _define_property(_assert_this_initialized(_this), "subdirectoryEventUnsubscribe", null); 282 _define_property(_assert_this_initialized(_this), "isRenamingInPlace", false); 283 _define_property(_assert_this_initialized(_this), "renameInputElement", null); 284 _define_property(_assert_this_initialized(_this), "renderPending", false); 285 // Clipboard state for copy/cut operations 286 _define_property(_assert_this_initialized(_this), "clipboardPaths", []); 287 _define_property(_assert_this_initialized(_this), "clipboardIdentifiers", []) // Store identifiers for cross-directory paste 288 ; 289 _define_property(_assert_this_initialized(_this), "clipboardMode", null); 290 // ==================== Drag & Drop from Browser Pane ==================== 291 // Drag state for tree items 292 _define_property(_assert_this_initialized(_this), "isDragging", false); 293 _define_property(_assert_this_initialized(_this), "draggedPath", null); 294 return _this; 295 } 296 _create_class(TreeController, [ 297 { 298 key: "connect", 299 value: function connect() { 300 var _this = this; 301 this.service = new FileSystemService({ 302 rootIdentifier: this.rootIdentifierValue 303 }); 304 // Close context menu when clicking elsewhere 305 document.addEventListener("click", this.handleDocumentClick.bind(this)); 306 // Subscribe to subdirectory detection events to update toggle visibility 307 this.subdirectoryEventUnsubscribe = this.service.on("directory:hasSubdirectory", function(event) { 308 // A directory now knows it has subdirectories, update the item controller 309 _this.updateItemCanExpand(event.identifier, true); 310 }); 311 // Load initial directory using the root identifier
312 // Skip if skipAutoLoad is true (manager will control initial load) 313 if (!this.skipAutoLoadValue) { 314 this.loadRootDirectory(); 315 } 316 } 317 }, 318 { 319 key: "disconnect", 320 value: function disconnect() { 321 this.service.cancelAll(); 322 document.removeEventListener("click", this.handleDocumentClick.bind(this)); 323 // Unsubscribe from events 324 if (this.subdirectoryEventUnsubscribe) { 325 this.subdirectoryEventUnsubscribe(); 326 this.subdirectoryEventUnsubscribe = null; 327 } 328 } 329 }, 330 { 331 key: "handleDocumentClick", 332 value: /** 333 * Handle document click to close context menu 334 */ function handleDocumentClick() { 335 this.hideContextMenu(); 336 } 337 }, 338 { 339 key: "loadRootDirectory", 340 value: /** 341 * Load the root directory and render the tree 342 */ function loadRootDirectory() { 343 var _this = this; 344 return _async_to_generator(function() { 345 var error; 346 return _ts_generator(this, function(_state) { 347 switch(_state.label){ 348 case 0: 349 _this.showLoading(); 350 _this.hideError(); 351 _state.label = 1; 352 case 1: 353 _state.trys.push([ 354 1, 355 3, 356 , 357 4 358 ]); 359 return [ 360 4, 361 _this.service.loadDirectory(_this.rootIdentifierValue) 362 ]; 363 case 2: 364 _this.rootNode = _state.sent(); 365 _this.rootNode.isExpanded = true; 366 _this.nodeMap.clear(); 367 _this.registerNode(_this.rootNode); 368 _this.render(); 369 _this.hideLoading(); 370 return [ 371 3, 372 4 373 ]; 374 case 3: 375 error = _state.sent(); 376 // Ignore AbortError - it just means a newer request superseded this one 377 if (_instanceof(error, Error) && error.name === "AbortError") { 378 return [ 379 2 380 ]; 381 } 382 _this.hideLoading(); 383 _this.showError(error); 384 return [ 385 3, 386 4 387 ]; 388 case 4: 389 return [ 390 2 391 ]; 392 } 393 }); 394 })(); 395 } 396 }, 397 { 398 /** 399 * Initialize the tree with an existing directory node (no fetch required). 400 * Used when the browser has already loaded the directory data. 401 * 402 * If the directory has ancestors (i.e., it's not the root), this method 403 * builds a tree structure from the root ancestor down to the current directory, 404 * expanding all ancestors so the user can navigate up to parent directories. 405 * After the initial render, it asynchronously loads sibling directories for 406 * each ancestor so the full tree structure is visible. 407 */ key: "initializeWithDirectory", 408 value: function initializeWithDirectory(directory) { 409 this.hideLoading(); 410 this.hideError(); 411 this.nodeMap.clear(); 412 // Get the ancestor chain from root to parent 413 var ancestors = directory.ancestors; 414 if (ancestors.length === 0) { 415 // Directory is at root level - use it directly as the root 416 this.rootNode = directory; 417 this.rootNode.isExpanded = true; 418 this.selectedPath = directory.path; 419 this.registerNode(this.rootNode); 420 } else { 421 // Build tree structure from ancestors 422 // ancestors[0] is the root, ancestors[last] is the immediate parent 423 this.rootNode = ancestors[0]; 424 this.rootNode.isExpanded = true; 425 // Link ancestors together via children arrays 426 for(var i = 0; i < ancestors.length - 1; i++){ 427 var parent = ancestors[i]; 428 var child = ancestors[i + 1]; 429 parent.children = [ 430 child 431 ]; 432 parent.isExpanded = true; 433 parent.hasSubdirectory = true; 434 } 435 // Link the last ancestor (immediate parent) to the current directory 436 var immediateParent = ancestors[ancestors.length - 1]; 437 immediateParent.children = [ 438 directory 439 ]; 440 immediateParent.isExpanded = true; 441 immediateParent.hasSubdirectory = true; 442 // Expand the current directory and mark it as selected 443 directory.isExpanded = true; 444 this.selectedPath = directory.path; 445 // Register all nodes starting from root 446 this.registerNode(this.rootNode); 447 } 448 this.render(); 449 // After initial render, load siblings for expanded ancestors 450 if (ancestors.length > 0) { 451 this.loadAncestorSiblings(ancestors); 452 } 453 } 454 }, 455 { 456 key: "loadAncestorSiblings", 457 value: /** 458 * Load sibling directories for each ancestor in the chain.
459 * Called after initializeWithDirectory to populate the tree with siblings 460 * that aren't in the direct ancestor path. Loads all ancestors in parallel. 461 */ function loadAncestorSiblings(ancestors) { 462 var _this = this; 463 return _async_to_generator(function() { 464 var loadPromises; 465 return _ts_generator(this, function(_state) { 466 switch(_state.label){ 467 case 0: 468 if (ancestors.length === 0) return [ 469 2 470 ]; 471 loadPromises = ancestors.map(function() { 472 var _ref = _async_to_generator(function(ancestor) { 473 var loaded, existingByPath, _iteratorNormalCompletion, _didIteratorError, _iteratorError, _iterator, _step, child, mergedChildren, _iteratorNormalCompletion1, _didIteratorError1, _iteratorError1, _iterator1, _step1, loadedChild, existing, subdirs, error; 474 return _ts_generator(this, function(_state) { 475 switch(_state.label){ 476 case 0: 477 _state.trys.push([ 478 0, 479 2, 480 , 481 3 482 ]); 483 return [ 484 4, 485 _this.service.loadDirectory(ancestor.id) 486 ]; 487 case 1: 488 loaded = _state.sent(); 489 if (!loaded.children) return [ 490 2 491 ]; 492 // Build a set of existing child paths to preserve expanded/selected state 493 existingByPath = new Map(); 494 if (ancestor.children) { 495 _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined; 496 try { 497 for(_iterator = ancestor.children[Symbol.iterator](); !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){ 498 child = _step.value; 499 existingByPath.set(child.path, child); 500 } 501 } catch (err) { 502 _didIteratorError = true; 503 _iteratorError = err; 504 } finally{ 505 try { 506 if (!_iteratorNormalCompletion && _iterator.return != null) { 507 _iterator.return(); 508 } 509 } finally{ 510 if (_didIteratorError) { 511 throw _iteratorError; 512 } 513 } 514 } 515 } 516 // Merge: keep existing children (preserves expanded state), add new ones 517 mergedChildren = []; 518 _iteratorNormalCompletion1 = true, _didIteratorError1 = false, _iteratorError1 = undefined; 519 try { 520 for(_iterator1 = loaded.children[Symbol.iterator](); !(_iteratorNormalCompletion1 = (_step1 = _iterator1.next()).done); _iteratorNormalCompletion1 = true){ 521 loadedChild
521= _step1.value; 522 existing = existingByPath.get(loadedChild.path); 523 if (existing) { 524 mergedChildren.push(existing); 525 } else { 526 mergedChildren.push(loadedChild); 527 if (loadedChild.isDirectory()) { 528 _this.registerNode(loadedChild); 529 } 530 } 531 } 532 } catch (err) { 533 _didIteratorError1 = true; 534 _iteratorError1 = err; 535 } finally{ 536 try { 537 if (!_iteratorNormalCompletion1 && _iterator1.return != null) { 538 _iterator1.return(); 539 } 540 } finally{ 541 if (_didIteratorError1) { 542 throw _iteratorError1; 543 } 544 } 545 } 546 ancestor.children = mergedChildren; 547 subdirs = ancestor.getSubdirectories(); 548 ancestor.hasSubdirectory = subdirs.length > 0; 549 return [ 550 3, 551 3 552 ]; 553 case 2: 554 error = _state.sent(); 555 // Silently ignore - the skeleton tree is still usable 556 if (_instanceof(error, Error) && error.name === "AbortError") return [ 557 2 558 ]; 559 console.warn("Failed to load siblings for ancestor:", ancestor.path, error); 560 return [ 561 3, 562 3 563 ]; 564 case 3: 565 return [ 566 2 567 ]; 568 } 569 }); 570 }); 571 return function(ancestor) { 572 return _ref.apply(this, arguments); 573 }; 574 }()); 575 return [ 576 4, 577 Promise.all(loadPromises) 578 ]; 579 case 1: 580 _state.sent(); 581 _this.render(); 582 return [ 583 2 584 ]; 585 } 586 }); 587 })(); 588 } 589 }, 590 { 591 /** 592 * Sync the tree to show and select a directory that was loaded in the browser. 593 * This method integrates the directory and its ancestors into the existing tree 594 * without requiring a full re-render from root. It's designed to be called 595 * after browser navigation to keep the tree in sync. 596 * 597 * @param directory The directory node loaded by the browser 598 */ key: "syncWithDirectory", 599 value: function syncWithDirectory(directory) { 600 // If we don't have a root node yet, initialize with this directory 601 if (!this.rootNode) { 602 this.initializeWithDirectory(directory); 603 return; 604 } 605 // Check if this directory is already in our tree 606 var existingNode = this.nodeMap.get(directory.path); 607 if (existingNode) { 608 // Directory already exists in tree 609 // Update children from the browser's loaded directory 610 this.updateNodeChildren(existingNode, directory); 611 // Expand to it and select it 612 this.expandToPath(directory.path); 613 this.setSelection(directory.path); 614 this.render(); 615 return; 616 } 617 // Directory not in tree - we need to integrate it 618 // Walk up the ancestor chain to find where we connect to existing tree 619 var ancestors = directory.ancestors; 620 // Find the deepest ancestor that's already in our tree 621 var connectionPoint = null; 622 var connectionIndex = -1; 623 for(var i = ancestors.length - 1; i >= 0; i--){ 624 var ancestor = ancestors[i]; 625 var existing = this.nodeMap.get(ancestor.path); 626 if (existing) { 627 connectionPoint = existing; 628 connectionIndex = i; 629 break; 630 } 631 } 632 if (connectionPoint) { 633 var _this = this, _loop = function(i1) { 634 var _parent_children; 635 var childAncestor = ancestors[i1]; 636 // Check if this child is already in parent's children 637 var existingChild = (_parent_children = parent.children) === null || _parent_children === void 0 ? void 0 : _parent_children.find(function(c) { 638 return c.path === childAncestor.path; 639 }); 640 if (existingChild && existingChild.isDirectory()) { 641 // Use existing child node 642 parent = existingChild; 643 parent.isExpanded = true; 644 parent.hasSubdirectory = true; 645 _this.registerNode(parent); 646 } else { 647 // Need to add this ancestor as a child 648 if (!parent.children) { 649 parent.children = []; 650 } 651 parent.children.push(childAncestor); 652 childAncestor.isExpanded = true; 653 childAncestor.hasSubdirectory = true; 654 _this.registerNode(childAncestor); 655 parent = childAncestor; 656 } 657 }; 658 var _parent_children; 659 // Found a connection point - build the chain from there 660 var parent = connectionPoint; 661 parent.isExpanded = true; 662 parent.hasSubdirectory = true; 663 // Add intermediate ancestors that weren't in the tree 664 for(var i1 = connectionIndex + 1; i1 < ancestors.length; i1++)_loop(i1); 665 // Now add the directory itself under the last ancestor 666 var existingDir = (_parent_children = parent.children) === null || _parent_children === void 0 ? void 0 : _parent_children.find(function(c) { 667 return c.path === directory.path; 668 }); 669 if (!existingDir) { 670 if (!parent.children) { 671 parent.children = []; 672 } 673 parent.children.push(directory); 674 } else if (existingDir.isDirectory()) { 675 // Directory already exists - update its children from loaded data 676 this.updateNodeChildren(existingDir, directory); 677 } 678 directory.isExpanded = true; 679 // Update hasSubdirectory based on actual subdirectories 680 var subdirs = directory.getSubdirectories(); 681 directory.hasSubdirectory = subdirs.length > 0; 682 this.registerNode(directory); 683 var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined; 684 try { 685 // Also register subdirectories so they can be found in nodeMap 686 for(var _iterator = subdirs[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){ 687 var subdir = _step.value; 688 this.registerNode(subdir); 689 } 690 } catch (err) {
691 _didIteratorError = true; 692 _iteratorError = err; 693 } finally{ 694 try { 695 if (!_iteratorNormalCompletion && _iterator.return != null) { 696 _iterator.return(); 697 } 698 } finally{ 699 if (_didIteratorError) { 700 throw _iteratorError; 701 } 702 } 703 } 704 } else { 705 // No connection point found - this shouldn't happen if root is loaded, 706 // but fall back to re-initializing with this directory 707 this.initializeWithDirectory(directory); 708 return; 709 } 710 // Update selection 711 this.selectedPath = directory.path; 712 // Re-render the tree to show the new structure 713 this.render(); 714 } 715 }, 716 { 717 key: "expandToPath", 718 value: /** 719 * Expand all ancestors to make a path visible in the tree. 720 * Does not select the path, just ensures it's visible. 721 */ function expandToPath(path) { 722 // Walk up the path and expand each parent 723 var parts = path.split("/"); 724 var currentPath = ""; 725 for(var i = 0; i < parts.length; i++){ 726 if (i > 0 || parts[0] !== "") { 727 currentPath = currentPath ? currentPath + "/" + parts[i] : parts[i]; 728 } 729 var node = this.nodeMap.get(currentPath); 730 if (node && !node.isExpanded && node.canExpand) { 731 node.isExpanded = true; 732 } 733 } 734 // Also ensure root is expanded 735 if (this.rootNode) { 736 this.rootNode.isExpanded = true; 737 } 738 this.render(); 739 } 740 }, 741 { 742 key: "updateNodeChildren", 743 value: /** 744 * Update a tree node's metadata from a browser-loaded directory. 745 * This syncs the tree's representation with the browser's loaded data. 746 * 747 * IMPORTANT: This method must NOT modify existingNode.children because the 748 * existingNode may be shared with the browser controller. Modifying children 749 * to only contain directories would corrupt the browser's file listing. 750 * Instead, we only update metadata and register subdirectories in our nodeMap. 751 * The tree rendering uses getSubdirectories() to filter directories for display. 752 */ function updateNodeChildren(existingNode, loadedDirectory) { 753 // Get subdirectories from the loaded directory 754 var loadedSubdirs = loadedDirectory.getSubdirectories(); 755 // Update hasSubdirectory based on whether there are subdirectories 756 existingNode.hasSubdirectory = loadedSubdirs.length > 0; 757 existingNode.isExpanded = true; 758 var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined; 759 try { 760 // Register all subdirectories in our nodeMap so they can be found 761 // We do NOT modify existingNode.children - it's shared with browser 762 for(var _iterator = loadedSubdirs[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){ 763 var subdir = _step.value; 764 if (!this.nodeMap.has(subdir.path)) { 765 this.registerNode(subdir); 766 } 767 } 768 } catch (err) { 769 _didIteratorError = true; 770 _iteratorError = err; 771 } finally{ 772 try { 773 if (!_iteratorNormalCompletion && _iterator.return != null) { 774 _iterator.return(); 775 } 776 } finally{ 777 if (_didIteratorError) { 778 throw _iteratorError; 779 } 780 } 781 } 782 } 783 }, 784 { 785 key: "updateItemCanExpand", 786 value: /** 787 * Update a specific item when its node's canExpand changes 788 */ function updateItemCanExpand(path, _canExpand) { 789 var itemElement = this.findItemElement(path); 790 if (itemElement) { 791 var controller = this.application.getControllerForElementAndIdentifier(itemElement, "file-manager--tree-item"); 792 if (controller === null || controller === void 0 ? void 0 : controller.rerender) { 793 controller.rerender(); 794 } 795 } 796 } 797 }, 798 { 799 key: "findItemElement", 800 value: /** 801 * Find an item element by path 802 */ function findItemElement(path) { 803 return this.treeTarget.querySelector('[data-file-manager--tree-item-path-value="'.concat(CSS.escape(path), '"]')); 804 } 805 }, 806 { 807 key: "handleItemToggle", 808 value: /** 809 * Handle item toggle event from tree-item controller 810 */ function handleItemToggle(event) { 811 var _this = this; 812 return _async_to_generator(function() { 813 var path; 814 return _ts_generator(this, function(_state) { 815 switch(_state.label){ 816 case 0: 817 path = event.detail.path; 818 return [ 819 4, 820 _this.toggleDirectory(path) 821 ]; 822 case 1: 823 _state.sent(); 824 return [ 825 2 826 ]; 827 } 828 }); 829 })(); 830 } 831 }, 832 { 833 /** 834 * Handle item select event from tree-item controller 835 */ key: "handleItemSelect", 836 value: function handleItemSelect(event) { 837 var path = event.detail.path; 838 this.selectDirectory(path); 839 } 840 }, 841 { 842 key: "handleItemDblSelect", 843 value: /** 844 * Handle item double-select event from tree-item controller 845 */ function handleItemDblSelect(event) { 846 var _this = this; 847 return _async_to_generator(function() { 848 var path; 849 return _ts_generator(this, function(_state) { 850 switch(_state.label){ 851 case 0: 852 path = event.detail.path; 853 return [ 854 4, 855 _this.handleDoubleClick(path) 856 ]; 857 case 1: 858 _state.sent(); 859 return [ 860 2 861 ]; 862 } 863 }); 864 })(); 865 } 866 }, 867 { 868 /** 869 * Handle item context menu event from tree-item controller
870 */ key: "handleItemContextMenu", 871 value: function handleItemContextMenu(event) { 872 var _event_detail = event.detail, path = _event_detail.path, x = _event_detail.x, y = _event_detail.y; 873 this.selectedPath = path; 874 this.contextMenuPath = path; 875 this.updateSelectedItem(); 876 this.showContextMenu(x, y); 877 } 878 }, 879 { 880 /** 881 * Handle keyboard shortcuts on the tree pane 882 */ key: "handleKeydown", 883 value: function handleKeydown(event) { 884 // Don't handle keyboard shortcuts while in-place editing 885 if (this.isRenamingInPlace) return; 886 // Enter: Toggle expand/collapse of selected directory 887 if (event.key === "Enter") { 888 event.preventDefault(); 889 // selectedPath can be '' for root, so check if we have any selection 890 // by verifying a directory was actually selected (not the initial state) 891 if (this.selectedPath !== null) { 892 this.handleDoubleClick(this.selectedPath); 893 } 894 return; 895 } 896 // Delete: Move selected directory to trash (Shift+Delete: permanent delete) 897 if (event.key === "Delete") { 898 event.preventDefault(); 899 if (this.selectedPath !== null && this.selectedPath !== "") { 900 this.deleteDirectory(this.selectedPath, event.shiftKey); 901 } 902 return; 903 } 904 // F2: Rename selected directory 905 if (event.key === "F2") { 906 event.preventDefault(); 907 if (this.selectedPath !== null && this.selectedPath !== "") { 908 this.startInPlaceRename(this.selectedPath); 909 } 910 return; 911 } 912 // Ctrl+C: Copy selected directory 913 if ((event.ctrlKey || event.metaKey) && event.key === "c") { 914 event.preventDefault(); 915 if (this.selectedPath !== null && this.selectedPath !== "") { 916 this.copyDirectory(this.selectedPath); 917 } 918 return; 919 } 920 // Ctrl+X: Cut selected directory 921 if ((event.ctrlKey || event.metaKey) && event.key === "x") { 922 event.preventDefault(); 923 if (this.selectedPath !== null && this.selectedPath !== "") { 924 this.cutDirectory(this.selectedPath); 925 } 926 return; 927 } 928 // Ctrl+V: Paste into selected directory 929 if ((event.ctrlKey || event.metaKey) && event.key === "v") { 930 event.preventDefault(); 931 var _this_selectedPath; 932 var targetPath = (_this_selectedPath = this.selectedPath) !== null && _this_selectedPath !== void 0 ? _this_selectedPath : ""; 933 this.pasteIntoDirectory(targetPath); 934 return; 935 } 936 } 937 }, 938 { 939 key: "toggleDirectory", 940 value: /** 941 * Toggle a directory's expanded state 942 */ function toggleDirectory(path) { 943 var _this = this; 944 return _async_to_generator(function() { 945 var node, itemElement, controller, _controller_rerender, _controller_rerender1, loadedNode, error, _controller_rerender2, _controller_rerender3; 946 return _ts_generator(this, function(_state) { 947 switch(_state.label){ 948 case 0: 949 node = _this.findNodeByPath(path); 950 if (!node || !node.isDirectory()) return [ 951 2 952 ]; 953 if (!node.canExpand) return [ 954 2 955 ]; 956 itemElement = _this.findItemElement(path); 957 controller = itemElement ? _this.application.getControllerForElementAndIdentifier(itemElement, "file-manager--tree-item") : null; 958 if (!node.isExpanded) return [ 959 3, 960 1 961 ]; 962 // Collapse 963 node.isExpanded = false;
964 controller === null || controller === void 0 ? void 0 : (_controller_rerender = controller.rerender) === null || _controller_rerender === void 0 ? void 0 : _controller_rerender.call(controller); 965 _this.removeChildrenFromDOM(path); 966 return [ 967 3, 968 7 969 ]; 970 case 1: 971 if (!!node.hasLoadedChildren) return [ 972 3, 973 6 974 ]; 975 node.isLoading = true; 976 controller === null || controller === void 0 ? void 0 : (_controller_rerender1 = controller.rerender) === null || _controller_rerender1 === void 0 ? void 0 : _controller_rerender1.call(controller); 977 _state.label = 2; 978 case 2: 979 _state.trys.push([ 980 2, 981 4, 982 5, 983 6 984 ]); 985 return [ 986 4, 987 _this.service.loadDirectory(node.id) 988 ]; 989 case 3: 990 loadedNode = _state.sent(); 991 node.children = loadedNode.children; 992 // Register newly loaded children in nodeMap 993 _this.registerNode(node); 994 return [ 995 3, 996 6 997 ]; 998 case 4: 999 error = _state.sent(); 1000 console.error("Failed to load directory:", error); 1001 return [ 1002 3, 1003 6 1004 ]; 1005 case 5: 1006 node.isLoading = false; 1007 controller === null || controller === void 0 ? void 0 : (_controller_rerender2 = controller.rerender) === null || _controller_rerender2 === void 0 ? void 0 : _controller_rerender2.call(controller); 1008 return [ 1009 7 1010 ]; 1011 case 6: 1012 // After loading, only expand if there are subdirectories 1013 if (node.canExpand && node.getSubdirectories().length > 0) { 1014 ; 1015 node.isExpanded = true; 1016 controller === null || controller === void 0 ? void 0 : (_controller_rerender3 = controller.rerender) === null || _controller_rerender3 === void 0 ? void 0 : _controller_rerender3.call(controller); 1017 _this.appendChildrenToDOM(node, itemElement); 1018 } 1019 _state.label = 7; 1020 case 7: 1021 return [ 1022 2 1023 ]; 1024 } 1025 }); 1026 })(); 1027 } 1028 }, 1029 { 1030 /** 1031 * Select a directory (single click) - also dispatches navigation event 1032 */ key: "selectDirectory", 1033 value: function selectDirectory(path) { 1034 var previousPath = this.selectedPath; 1035 this.selectedPath = path; 1036 // Update previous and current selection 1037 if (previousPath !== path) { 1038 this.updateItemSelected(previousPath, false); 1039 } 1040 this.updateItemSelected(path, true); 1041 // Find the node to get its identifier 1042 var node = this.findNodeByPath(path); 1043 var identifier = (node === null || node === void 0 ? void 0 : node.id) || (path === "" ? this.rootIdentifierValue : undefined); 1044 // Dispatch select event to navigate browser pane 1045 this.dispatch("select", { 1046 detail: { 1047 path: path, 1048 identifier: identifier 1049 }, 1050 bubbles: true 1051 }); 1052 } 1053 }, 1054 { 1055 /** 1056 * Set selection visually without dispatching navigation event. 1057 * Used when the browser has already navigated and we just need to sync the tree UI. 1058 */ key: "setSelection", 1059 value: function setSelection(path) { 1060 var previousPath = this.selectedPath; 1061 this.selectedPath = path; 1062 // Deselect trash when a regular directory is selected 1063 if (this.isTrashSelected) { 1064 this.isTrashSelected = false;
1065 // Re-render to update trash item visual state 1066 var trashItem = this.treeTarget.querySelector(".file-tree__item--trash .file-tree__item-content"); 1067 if (trashItem) { 1068 trashItem.classList.remove("file-tree__item-content--selected"); 1069 } 1070 } 1071 // Update previous and current selection visually 1072 if (previousPath !== path) { 1073 this.updateItemSelected(previousPath, false); 1074 } 1075 this.updateItemSelected(path, true); 1076 } 1077 }, 1078 { 1079 key: "updateItemSelected", 1080 value: /** 1081 * Update a specific item's selected value 1082 */ function updateItemSelected(path, selected) { 1083 if (path === null) return; 1084 var itemElement = this.findItemElement(path); 1085 if (itemElement) { 1086 var controller = this.application.getControllerForElementAndIdentifier(itemElement, "file-manager--tree-item"); 1087 if (controller) { 1088 controller.selectedValue = selected; 1089 } 1090 } 1091 } 1092 }, 1093 { 1094 key: "updateSelectedItem", 1095 value: /** 1096 * Update selected item (for context menu) 1097 */ function updateSelectedItem() { 1098 var _this = this; 1099 // Clear all selections and set the new one 1100 var allItems = this.treeTarget.querySelectorAll('[data-controller="file-manager--tree-item"]'); 1101 allItems.forEach(function(el) { 1102 var controller = _this.application.getControllerForElementAndIdentifier(el, "file-manager--tree-item"); 1103 if (controller) { 1104 controller.selectedValue = false; 1105 } 1106 }); 1107 if (this.selectedPath !== null) { 1108 this.updateItemSelected(this.selectedPath, true); 1109 } 1110 } 1111 }, 1112 { 1113 key: "handleDoubleClick", 1114 value: /** 1115 * Handle double click - toggle expand/collapse AND navigate 1116 */ function handleDoubleClick(path) { 1117 var _this = this; 1118 return _async_to_generator(function() { 1119 var node; 1120 return _ts_generator(this, function(_state) { 1121 switch(_state.label){ 1122 case 0: 1123 _this.selectedPath = path; 1124 _this.updateSelectedItem(); 1125 node = _this.findNodeByPath(path); 1126 if (!node || !node.isDirectory()) return [ 1127 2 1128 ]; 1129 if (!node.canExpand) return [ 1130 3, 1131 2 1132 ]; 1133 return [ 1134 4, 1135 _this.toggleDirectory(path) 1136 ]; 1137 case 1: 1138 _state.sent(); 1139 _state.label = 2; 1140 case 2: 1141 // Also navigate browser pane 1142 _this.dispatch("select", { 1143 detail: { 1144 path: path, 1145 identifier: node.id 1146 }, 1147 bubbles: true 1148 }); 1149 return [ 1150 2 1151 ]; 1152 } 1153 }); 1154 })(); 1155 } 1156 }, 1157 { 1158 /** 1159 * Get a node by its path. Used by tree-item controllers to access their node data. 1160 */ key: "getNode", 1161 value: function getNode(path) { 1162 var _this_nodeMap_get; 1163 return (_this_nodeMap_get = this.nodeMap.get(path)) !== null && _this_nodeMap_get !== void 0 ? _this_nodeMap_get : null; 1164 } 1165 }, 1166 { 1167 key: "findNodeByPath", 1168 value: /** 1169 * Find a node by its path in the tree (uses nodeMap for O(1) lookup) 1170 */ function findNodeByPath(path) { 1171 var _this_nodeMap_get; 1172 return (_this_nodeMap_get = this.nodeMap.get(path)) !== null && _this_nodeMap_get !== void 0 ? _this_nodeMap_get : null; 1173 } 1174 }, 1175 { 1176 key: "registerNode", 1177 value: /** 1178 * Register a node in the nodeMap (and all its children recursively) 1179 */ function registerNode(node) { 1180 this.nodeMap.set(node.path, node); 1181 if (node.children) { 1182 var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined; 1183 try {
1184 for(var _iterator = node.children[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){ 1185 var child = _step.value; 1186 if (child.isDirectory()) { 1187 this.registerNode(child); 1188 } 1189 } 1190 } catch (err) { 1191 _didIteratorError = true; 1192 _iteratorError = err; 1193 } finally{ 1194 try { 1195 if (!_iteratorNormalCompletion && _iterator.return != null) { 1196 _iterator.return(); 1197 } 1198 } finally{ 1199 if (_didIteratorError) { 1200 throw _iteratorError; 1201 } 1202 } 1203 } 1204 } 1205 } 1206 }, 1207 { 1208 key: "unregisterNode", 1209 value: /** 1210 * Unregister a node from the nodeMap (and all its children recursively) 1211 */ function unregisterNode(node) { 1212 this.nodeMap.delete(node.path); 1213 if (node.children) { 1214 var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined; 1215 try { 1216 for(var _iterator = node.children[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){ 1217 var child = _step.value; 1218 if (child.isDirectory()) { 1219 this.unregisterNode(child); 1220 } 1221 } 1222 } catch (err) { 1223 _didIteratorError = true; 1224 _iteratorError = err; 1225 } finally{ 1226 try { 1227 if (!_iteratorNormalCompletion && _iterator.return != null) { 1228 _iterator.return(); 1229 } 1230 } finally{ 1231 if (_didIteratorError) { 1232 throw _iteratorError; 1233 } 1234 } 1235 } 1236 } 1237 } 1238 }, 1239 { 1240 key: "render", 1241 value: /** 1242 * Render the tree (full re-render) 1243 * 1244 * A re-render rebuilds every row, which would throw away the in-place 1245 * input the user is typing a name into - and those renders arrive 1246 * unbidden, from the browser pane syncing a directory it has just 1247 * finished loading. Hold them until the input is done instead. 1248 */ function render() { 1249 if (this.isRenamingInPlace) { 1250 this.renderPending = true; 1251 return; 1252 } 1253 this.renderPending = false; 1254 if (!this.rootNode) { 1255 this.treeTarget.innerHTML = ""; 1256 return; 1257 } 1258 var html = this.renderDirectoryItem(this.rootNode, 0); 1259 // Add trash node at the bottom of the tree 1260 if (this.trashRootIdentifierValue) { 1261 html += this.renderTrashItem(); 1262 } 1263 this.treeTarget.innerHTML = html; 1264 // Load SVG icons for trash item 1265 if (this.trashRootIdentifierValue) { 1266 this.loadSvgPlaceholders(this.treeTarget); 1267 } 1268 } 1269 }, 1270 { 1271 key: "flushPendingRender", 1272 value: /** 1273 * Run a re-render that was held back while an in-place input was open 1274 */ function flushPendingRender() { 1275 if (this.renderPending) { 1276 this.render(); 1277 } 1278 } 1279 }, 1280 { 1281 key: "renderDirectoryItem", 1282 value: /**
1283 * Render a directory item as an li element with optional children ul 1284 */ function renderDirectoryItem(node, depth) { 1285 var isSelected = node.path === this.selectedPath; 1286 var html = '\n <li class="file-tree__item"\n draggable="true"\n data-controller="file-manager--tree-item"\n data-file-manager--tree-item-path-value="'.concat(escapeHtml(node.path), '"\n data-file-manager--tree-item-depth-value="').concat(depth, '"\n data-file-manager--tree-item-selected-value="').concat(isSelected, '"\n data-action="contextmenu->file-manager--tree-item#contextMenu dragstart->file-manager--tree#handleItemDragStart dragend->file-manager--tree#handleItemDragEnd dragover->file-manager--tree#handleItemDragOver dragleave->file-manager--tree#handleItemDragLeave drop->file-manager--tree#handleItemDrop">\n '); 1287 // Render children if expanded 1288 if (node.isExpanded && node.children) { 1289 var subdirectories = node.getSubdirectories().sort(function(a, b) { 1290 return a.name.localeCompare(b.name); 1291 }); 1292 if (subdirectories.length > 0) { 1293 html += '<ul class="file-tree__children">'; 1294 var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined; 1295 try { 1296 for(var _iterator = subdirectories[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){ 1297 var child = _step.value; 1298 html += this.renderDirectoryItem(child, depth + 1); 1299 } 1300 } catch (err) { 1301 _didIteratorError = true; 1302 _iteratorError = err; 1303 } finally{ 1304 try { 1305 if (!_iteratorNormalCompletion && _iterator.return != null) { 1306 _iterator.return(); 1307 } 1308 } finally{ 1309 if (_didIteratorError) { 1310 throw _iteratorError; 1311 } 1312 } 1313 } 1314 html += "</ul>"; 1315 } 1316 } 1317 html += "</li>"; 1318 return html; 1319 } 1320 }, 1321 { 1322 key: "renderTrashItem", 1323 value: /** 1324 * Render the trash item as a special non-expandable tree entry 1325 */ function renderTrashItem() { 1326 var isSelected = this.isTrashSelected; 1327 return '\n <li class="file-tree__item file-tree__item--trash"\n data-action="contextmenu->file-manager--tree#handleTrashContextMenu">\n <div class="file-tree__item-content'.concat(isSelected ? " file-tree__item-content--selected" : "", '" style="padding-left: 0px">\n <span class="file-tree__toggle file-tree__toggle--disabled">\n <span class="file-tree__toggle-placeholder"></span>\n </span>\n <a href="#" class="file-tree__link"\n data-action="click->file-manager--tree#handleTrashClick">\n <span class="context-menu__svg-icon file-tree__folder-icon" data-svg-url="').concat(escapeHtml(this.iconTrashXValue), '" aria-hidden="true"></span>\n <span class="file-tree__name">').concat(escapeHtml(this.transTrashValue), "</span>\n </a>\n </div>\n </li>\n "); 1328 } 1329 }, 1330 { 1331 key: "appendChildrenToDOM", 1332 value: /** 1333 * Append children to DOM without full re-render 1334 */ function appendChildrenToDOM(node, itemElement) { 1335 if (!itemElement) return; 1336 var subdirectories = node.getSubdirectories().sort(function(a, b) { 1337 return a.name.localeCompare(b.name); 1338 }); 1339 if (subdirectories.length === 0) return; 1340 var childrenUl = this.getOrCreateChildrenList(itemElement); 1341 // Get depth from current item 1342 // Note: Double-dash in attribute name creates hyphenated dataset property 1343 var currentDepth = parseInt(itemElement.getAttribute("data-file-manager--tree-item-depth-value") || "0", 10); 1344 // Render children 1345 var childrenHtml = ""; 1346 var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined; 1347 try { 1348 for(var _iterator = subdirectories[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){ 1349 var child = _step.value; 1350 childrenHtml += this.renderDirectoryItem(child, currentDepth + 1); 1351 } 1352 } catch (err) {
1353 _didIteratorError = true; 1354 _iteratorError = err; 1355 } finally{ 1356 try { 1357 if (!_iteratorNormalCompletion && _iterator.return != null) { 1358 _iterator.return(); 1359 } 1360 } finally{ 1361 if (_didIteratorError) { 1362 throw _iteratorError; 1363 } 1364 } 1365 } 1366 childrenUl.innerHTML = childrenHtml; 1367 } 1368 }, 1369 { 1370 key: "removeChildrenFromDOM", 1371 value: /** 1372 * Remove children from DOM without full re-render 1373 */ function removeChildrenFromDOM(path) { 1374 var itemElement = this.findItemElement(path); 1375 if (!itemElement) return; 1376 var childrenUl = itemElement.querySelector(":scope > .file-tree__children"); 1377 if (childrenUl) { 1378 childrenUl.remove(); 1379 } 1380 } 1381 }, 1382 { 1383 key: "showLoading", 1384 value: /** 1385 * Show loading indicator 1386 */ function showLoading() { 1387 if (this.hasLoadingTarget) { 1388 this.loadingTarget.classList.remove("hidden"); 1389 this.treeTarget.classList.add("hidden"); 1390 } 1391 } 1392 }, 1393 { 1394 key: "hideLoading", 1395 value: /** 1396 * Hide loading indicator 1397 */ function hideLoading() { 1398 if (this.hasLoadingTarget) { 1399 this.loadingTarget.classList.add("hidden"); 1400 this.treeTarget.classList.remove("hidden"); 1401 } 1402 } 1403 }, 1404 { 1405 key: "showError", 1406 value: /** 1407 * Show error message 1408 */ function showError(error) { 1409 if (this.hasErrorTarget) { 1410 var message = this.transErrorValue.replace("%message%", error.message); 1411 this.errorTarget.textContent = message; 1412 this.errorTarget.classList.remove("hidden"); 1413 this.treeTarget.classList.add("hidden"); 1414 } 1415 } 1416 }, 1417 { 1418 key: "hideError", 1419 value: /** 1420 * Hide error message 1421 */ function hideError() { 1422 if (this.hasErrorTarget) { 1423 this.errorTarget.classList.add("hidden"); 1424 this.treeTarget.classList.remove("hidden"); 1425 } 1426 } 1427 }, 1428 { 1429 key: "navigateTo", 1430 value: /** 1431 * Programmatically select and navigate to a path by identifier. 1432 * Ensures root is loaded first if needed, then expands ancestors and selects the target. 1433 */ function navigateTo(identifier) { 1434 var _this = this; 1435 return _async_to_generator(function() { 1436 var node, ancestors, _iteratorNormalCompletion, _didIteratorError, _iteratorError, _iterator, _step, ancestor, cachedAncestor, err, error; 1437 return _ts_generator(this, function(_state) { 1438 switch(_state.label){ 1439 case 0: 1440 _state.trys.push([ 1441 0, 1442 13, 1443 , 1444 14 1445 ]); 1446 if (!!_this.rootNode) return [ 1447 3, 1448 2 1449 ]; 1450 return [ 1451 4, 1452 _this.loadRootDirectory() 1453 ]; 1454 case 1: 1455 _state.sent(); 1456 _state.label = 2; 1457 case 2: 1458 // If we're navigating to root, just select it 1459 if (identifier === _this.rootIdentifierValue) { 1460 _this.selectedPath = ""; 1461 _this.render(); 1462 return [ 1463 2 1464 ]; 1465 } 1466 return [ 1467 4, 1468 _this.service.getNode(identifier) 1469 ]; 1470 case 3: 1471 node = _state.sent(); 1472 if (!node || !node.isDirectory()) return [ 1473 2 1474 ]; 1475 // Expand all ancestor directories 1476 ancestors = node.ancestors; 1477 _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined; 1478 _state.label = 4; 1479 case 4: 1480 _state.trys.push([ 1481 4, 1482 10, 1483 11, 1484 12 1485 ]); 1486 _iterator = ancestors[Symbol.iterator](); 1487 _state.label = 5; 1488 case 5: 1489 if (!!(_iteratorNormalCompletion = (_step = _iterator.next()).done)) return [ 1490 3, 1491 9 1492 ]; 1493 ancestor = _step.value; 1494 // Find the matching node in our
1494cache and expand it 1495 cachedAncestor = _this.service.getCached(ancestor.id); 1496 if (!(cachedAncestor && cachedAncestor.isDirectory())) return [ 1497 3, 1498 8 1499 ]; 1500 if (!!cachedAncestor.hasLoadedChildren) return [ 1501 3, 1502 7 1503 ]; 1504 return [ 1505 4, 1506 _this.service.loadDirectory(ancestor.id) 1507 ]; 1508 case 6: 1509 _state.sent(); 1510 // Register newly loaded children 1511 _this.registerNode(cachedAncestor); 1512 _state.label = 7; 1513 case 7: 1514 cachedAncestor.isExpanded = true; 1515 _state.label = 8; 1516 case 8: 1517 _iteratorNormalCompletion = true; 1518 return [ 1519 3, 1520 5 1521 ]; 1522 case 9: 1523 return [ 1524 3, 1525 12 1526 ]; 1527 case 10: 1528 err = _state.sent(); 1529 _didIteratorError = true; 1530 _iteratorError = err; 1531 return [ 1532 3, 1533 12 1534 ]; 1535 case 11: 1536 try { 1537 if (!_iteratorNormalCompletion && _iterator.return != null) { 1538 _iterator.return(); 1539 } 1540 } finally{ 1541 if (_didIteratorError) { 1542 throw _iteratorError; 1543 } 1544 } 1545 return [ 1546 7 1547 ]; 1548 case 12: 1549 // Also ensure the root is expanded 1550 if (_this.rootNode) { 1551 _this.rootNode.isExpanded = true; 1552 } 1553 _this.selectedPath = node.path; 1554 _this.render(); 1555 return [ 1556 3, 1557 14 1558 ]; 1559 case 13: 1560 error = _state.sent(); 1561 console.error("Failed to navigate to path:", error); 1562 return [ 1563 3, 1564 14 1565 ]; 1566 case 14: 1567 return [ 1568 2 1569 ]; 1570 } 1571 }); 1572 })(); 1573 } 1574 }, 1575 { 1576 key: "refresh", 1577 value: /** 1578 * Refresh the tree from the root 1579 */ function refresh() { 1580 var _this = this; 1581 return _async_to_generator(function() { 1582 return _ts_generator(this, function(_state) { 1583 switch(_state.label){ 1584 case 0: 1585 _this.service.clearCache(); 1586 return [ 1587 4, 1588 _this.loadRootDirectory() 1589 ]; 1590 case 1: 1591 _state.sent(); 1592 return [ 1593 2 1594 ]; 1595 } 1596 }); 1597 })(); 1598 } 1599 }, 1600 { 1601 /** 1602 * Get the FileSystemService instance (for external access) 1603 */ key: "getService", 1604 value: function getService() { 1605 return this.service; 1606 } 1607 }, 1608 { 1609 /** 1610 * Get the currently selected path 1611 * Returns null if no selection, '' for root, or the path string 1612 */ key: "getSelectedPath", 1613 value: function getSelectedPath() { 1614 return this.selectedPath; 1615 } 1616 }, 1617 { 1618 /** 1619 * Clear the current selection 1620 */ key: "clearSelection", 1621 value: function clearSelection() { 1622 if (this.selectedPath === null) return; 1623 // Remove highlight from previously selected item 1624 var previousSelected = this.element.querySelector(".file-tree__item--selected"); 1625 if (previousSelected) { 1626 previousSelected.classList.remove("file-tree__item--selected"); 1627 } 1628 this.selectedPath = null; 1629 this.dispatchSelectionChange(); 1630 } 1631 }, 1632 { 1633 // ==================== Context Menu ==================== 1634 /** 1635 * Handle context menu on empty area of tree 1636 */ key: "treeContextMenu",
1637 value: function treeContextMenu(event) { 1638 event.preventDefault(); 1639 event.stopPropagation(); 1640 // Only show if clicking on empty area (not on a tree item) 1641 var target = event.target; 1642 if (target.closest(".file-tree__item")) { 1643 return; 1644 } 1645 // Use null to indicate no directory selected (empty area) 1646 this.contextMenuPath = null; 1647 this.showContextMenu(event.clientX, event.clientY); 1648 } 1649 }, 1650 { 1651 key: "buildSelectionContext", 1652 value: /** 1653 * Build selection context for action availability calculation (tree pane) 1654 */ function buildSelectionContext() { 1655 var hasDirectorySelected = this.contextMenuPath !== null; 1656 var isRootSelected = this.contextMenuPath === ""; 1657 return { 1658 selectedCount: hasDirectorySelected ? 1 : 0, 1659 isFileOnly: false, 1660 isSingleSelection: hasDirectorySelected, 1661 activePane: "tree", 1662 isRootSelected: isRootSelected, 1663 hasClipboard: this.hasClipboard() 1664 }; 1665 } 1666 }, 1667 { 1668 key: "showContextMenu", 1669 value: /** 1670 * Show context menu at specified position 1671 */ function showContextMenu(x, y) { 1672 if (!this.hasContextMenuTarget) return; 1673 // Use shared ActionAvailability for consistent behavior with toolbar 1674 var context = this.buildSelectionContext(); 1675 var availability = getActionAvailability(context); 1676 var hasDirectorySelected = context.selectedCount > 0; 1677 var html = '<ul class="file-browser__context-menu-list">'; 1678 // If a directory is selected (contextMenuPath is not null) 1679 if (hasDirectorySelected) { 1680 // Open is always available when a directory is selected 1681 if (availability.open) { 1682 html += '\n <li class="file-browser__context-menu-item">\n <button type="button" data-action="click->file-manager--tree#contextMenuOpen">\n <span class="context-menu__svg-icon" data-svg-url="'.concat(escapeHtml(this.iconPlayerPlayValue), '" aria-hidden="true"></span>\n ').concat(escapeHtml(this.transOpenValue), '\n </button>\n </li>\n <li class="file-browser__context-menu-separator"></li>\n '); 1683 } 1684 // New Folder - creates inside the right-clicked directory 1685 if (availability.newFolder) { 1686 html += '\n <li class="file-browser__context-menu-item">\n <button type="button" data-action="click->file-manager--tree#contextMenuNewFolder">\n <span class="context-menu__svg-icon" data-svg-url="'.concat(escapeHtml(this.iconFolderPlusValue), '" aria-hidden="true"></span>\n ').concat(escapeHtml(this.transNewFolderValue), '\n </button>\n </li>\n <li class="file-browser__context-menu-separator"></li>\n '); 1687 } 1688 // Cut, Copy - only for non-root directories (based on availability) 1689 if (availability.cut) { 1690 html += '\n <li class="file-browser__context-menu-item">\n <button type="button" data-action="click->file-manager--tree#contextMenuCut">\n <span class="context-menu__svg-icon" data-svg-url="'.concat(escapeHtml(this.iconCutValue), '" aria-hidden="true"></span>\n ').concat(escapeHtml(this.transCutValue), "\n </button>\n </li>\n "); 1691 } 1692 if (availability.copy) { 1693 html += '\n <li class="file-browser__context-menu-item">\n <button type="button" data-action="click->
1693file-manager--tree#contextMenuCopy">\n <span class="context-menu__svg-icon" data-svg-url="'.concat(escapeHtml(this.iconCopyValue), '" aria-hidden="true"></span>\n ').concat(escapeHtml(this.transCopyValue), "\n </button>\n </li>\n "); 1694 } 1695 // Paste - available when clipboard has items 1696 html += '\n <li class="file-browser__context-menu-item">\n <button type="button" data-action="click->file-manager--tree#contextMenuPaste"'.concat(availability.paste ? "" : " disabled", '>\n <span class="context-menu__svg-icon" data-svg-url="').concat(escapeHtml(this.iconClipboardCopyValue), '" aria-hidden="true"></span>\n ').concat(escapeHtml(this.transPasteValue), '\n </button>\n </li>\n <li class="file-browser__context-menu-separator"></li>\n '); 1697 // Rename, Delete - only for non-root directories (based on availability) 1698 if (availability.rename) { 1699 html += '\n <li class="file-browser__context-menu-item">\n <button type="button" data-action="click->file-manager--tree#contextMenuRename">\n <span class="context-menu__svg-icon" data-svg-url="'.concat(escapeHtml(this.iconPencilValue), '" aria-hidden="true"></span>\n ').concat(escapeHtml(this.transRenameValue), "\n </button>\n </li>\n "); 1700 } 1701 if (availability.delete) { 1702 html += '\n <li class="file-browser__context-menu-item file-browser__context-menu-item--danger">\n <button type="button" data-action="click->file-manager--tree#contextMenuDelete">\n <span class="context-menu__svg-icon" data-svg-url="'.concat(escapeHtml(this.iconTrashValue), '" aria-hidden="true"></span>\n ').concat(escapeHtml(this.transDeleteValue), '\n </button>\n </li>\n <li class="file-browser__context-menu-separator"></li>\n '); 1703 } 1704 } else { 1705 // No directory selected (empty area) - New Folder and Paste act on 1706 // the current selection, falling back to the root 1707 if (availability.newFolder) { 1708 html += '\n <li class="file-browser__context-menu-item">\n <button type="button" data-action="click->file-manager--tree#contextMenuNewFolder">\n <span class="context-menu__svg-icon" data-svg-url="'.concat(escapeHtml(this.iconFolderPlusValue), '" aria-hidden="true"></span>\n ').concat(escapeHtml(this.transNewFolderValue), '\n </button>\n </li>\n <li class="file-browser__context-menu-separator"></li>\n '); 1709 } 1710 html += '\n <li class="file-browser__context-menu-item">\n <button type="button" data-action="click->file-manager--tree#contextMenuPaste"'.concat(availability.paste ? "" : " disabled", '>\n <span class="context-menu__svg-icon" data-svg-url="').concat(escapeHtml(this.iconClipboardCopyValue), '" aria-hidden="true"></span>\n ').concat(escapeHtml(this.transPasteValue), '\n </button>\n </li>\n <li class="file-browser__context-menu-separator"></li>\n '); 1711 } 1712 // Refresh - always available 1713 html += '\n <li class="file-browser__context-menu-item">\n <button type="button" data-action="click->
1713file-manager--tree#contextMenuRefresh">\n <span class="context-menu__svg-icon" data-svg-url="'.concat(escapeHtml(this.iconRefreshValue), '" aria-hidden="true"></span>\n ').concat(escapeHtml(this.transRefreshValue), "\n </button>\n </li>\n "); 1714 html += "</ul>"; 1715 this.contextMenuTarget.innerHTML = html; 1716 this.loadSvgPlaceholders(this.contextMenuTarget); 1717 positionContextMenu(this.contextMenuTarget, x, y); 1718 } 1719 }, 1720 { 1721 key: "loadSvgPlaceholders", 1722 value: /** 1723 * Load SVG content into placeholder elements 1724 */ function loadSvgPlaceholders(container) { 1725 var placeholders = container.querySelectorAll("[data-svg-url]"); 1726 placeholders.forEach(function(placeholder) { 1727 var url = placeholder.dataset.svgUrl; 1728 if (url) { 1729 loadSvgIntoElement(placeholder, url); 1730 } 1731 }); 1732 } 1733 }, 1734 { 1735 key: "hideContextMenu", 1736 value: /** 1737 * Hide context menu 1738 */ function hideContextMenu() { 1739 if (this.hasContextMenuTarget) { 1740 this.contextMenuTarget.classList.add("hidden"); 1741 } 1742 } 1743 }, 1744 { 1745 /** 1746 * Context menu: Open 1747 */ key: "contextMenuOpen", 1748 value: function contextMenuOpen() { 1749 this.hideContextMenu(); 1750 if (this.contextMenuPath === null) return; 1751 // Dispatch select event to open the directory 1752 this.dispatch("select", { 1753 detail: { 1754 path: this.contextMenuPath 1755 }, 1756 bubbles: true 1757 }); 1758 } 1759 }, 1760 { 1761 /** 1762 * Context menu: Cut - store selected path for move operation 1763 */ key: "contextMenuCut", 1764 value: function contextMenuCut() { 1765 this.hideContextMenu(); 1766 if (this.contextMenuPath === null || this.contextMenuPath === "") return; 1767 this.cutDirectory(this.contextMenuPath); 1768 } 1769 }, 1770 { 1771 /** 1772 * Context menu: Copy - store selected path for copy operation 1773 */ key: "contextMenuCopy", 1774 value: function contextMenuCopy() { 1775 this.hideContextMenu(); 1776 if (this.contextMenuPath === null || this.contextMenuPath === "") return; 1777 this.copyDirectory(this.contextMenuPath); 1778 } 1779 }, 1780 { 1781 key: "contextMenuPaste", 1782 value: /** 1783 * Context menu: Paste - perform copy/move operation 1784 */ function contextMenuPaste() { 1785 var _this = this; 1786 return _async_to_generator(function() { 1787 var _this_contextMenuPath, _ref, targetPath; 1788 return _ts_generator(this, function(_state) { 1789 switch(_state.label){ 1790 case 0: 1791 _this.hideContextMenu(); 1792 // Paste into the selected directory, or root if no selection 1793 targetPath = (_ref = (_this_contextMenuPath = _this.contextMenuPath) !== null && _this_contextMenuPath !== void 0 ? _this_contextMenuPath : _this.selectedPath) !== null && _ref !== void 0 ? _ref : ""; 1794 return [ 1795 4, 1796 _this.pasteIntoDirectory(targetPath) 1797 ]; 1798 case 1: 1799 _state.sent(); 1800 return [ 1801 2 1802 ]; 1803 } 1804 }); 1805 })(); 1806 } 1807 }, 1808 { 1809 key: "contextMenuDelete", 1810 value: /** 1811 * Context menu: Delete - move selected directory to trash (or permanently if Shift is held) 1812 */ function contextMenuDelete(event) { 1813 var _this = this; 1814 return _async_to_generator(function() { 1815 var permanent; 1816 return _ts_generator(this, function(_state) { 1817 switch(_state.label){ 1818 case 0: 1819 permanent = _instanceof(event, MouseEvent) || _instanceof(event, KeyboardEvent) ? event.shiftKey : false;
1820 _this.hideContextMenu(); 1821 if (_this.contextMenuPath === null || _this.contextMenuPath === "") return [ 1822 2 1823 ]; 1824 return [ 1825 4, 1826 _this.deleteDirectory(_this.contextMenuPath, permanent) 1827 ]; 1828 case 1: 1829 _state.sent(); 1830 return [ 1831 2 1832 ]; 1833 } 1834 }); 1835 })(); 1836 } 1837 }, 1838 { 1839 // ==================== Clipboard Operations ==================== 1840 /** 1841 * Cut a directory (store for move) 1842 */ key: "cutDirectory", 1843 value: function cutDirectory(path) { 1844 this.clipboardPaths = [ 1845 path 1846 ]; 1847 // Store identifier at cut time so paste works across directories 1848 var node = this.findNodeByPath(path); 1849 this.clipboardIdentifiers = node ? [ 1850 node.id 1851 ] : []; 1852 this.clipboardMode = "cut"; 1853 // Dispatch event to notify manager/browser 1854 this.dispatch("clipboardChange", { 1855 detail: { 1856 mode: "cut", 1857 paths: this.clipboardPaths, 1858 identifiers: this.clipboardIdentifiers, 1859 hasClipboard: true 1860 }, 1861 bubbles: true 1862 }); 1863 } 1864 }, 1865 { 1866 /** 1867 * Copy a directory (store for copy) 1868 */ key: "copyDirectory", 1869 value: function copyDirectory(path) { 1870 this.clipboardPaths = [ 1871 path 1872 ]; 1873 // Store identifier at copy time so paste works across directories 1874 var node = this.findNodeByPath(path); 1875 this.clipboardIdentifiers = node ? [ 1876 node.id 1877 ] : []; 1878 this.clipboardMode = "copy"; 1879 // Dispatch event to notify manager/browser 1880 this.dispatch("clipboardChange", { 1881 detail: { 1882 mode: "copy", 1883 paths: this.clipboardPaths, 1884 identifiers: this.clipboardIdentifiers, 1885 hasClipboard: true 1886 }, 1887 bubbles: true 1888 }); 1889 } 1890 }, 1891 { 1892 key: "pasteIntoDirectory", 1893 value: /** 1894 * Paste clipboard contents into target directory 1895 */ function pasteIntoDirectory(targetPath) { 1896 var _this = this; 1897 return _async_to_generator(function() { 1898 var mode, targetNode, sourceIdentifiers, result, result1, error; 1899 return _ts_generator(this, function(_state) { 1900 switch(_state.label){ 1901 case 0: 1902 if (!_this.hasClipboard()) return [ 1903 2 1904 ]; 1905 mode = _this.clipboardMode; 1906 // Get target node to get its identifier 1907 targetNode = _this.findNodeByPath(targetPath); 1908 if (!targetNode) return [ 1909 2 1910 ]; 1911 // Use stored identifiers (captured at copy/cut time) for cross-directory paste 1912 sourceIdentifiers = _this.clipboardIdentifiers; 1913 if (sourceIdentifiers.length === 0) return [ 1914 2 1915 ]; 1916 _state.label = 1; 1917 case 1: 1918 _state.trys.push([ 1919 1, 1920 8, 1921 , 1922 9 1923 ]); 1924 if (!(mode === "copy")) return [ 1925 3, 1926 4 1927 ]; 1928 return [ 1929 4, 1930 _this.service.copyMultiple(sourceIdentifiers, targetNode.id) 1931 ]; 1932 case 2: 1933 result = _state.sent(); 1934 return [ 1935 4, 1936 _this.handlePasteResult(result, "copy", targetPath) 1937 ]; 1938 case 3: 1939 _state.sent(); 1940 return [ 1941 3, 1942 7 1943 ]; 1944 case 4: 1945 if (!(mode === "cut")) return [ 1946 3, 1947 7 1948 ]; 1949 return [ 1950 4, 1951 _this.service.moveMultiple(sourceIdentifiers, targetNode.id) 1952 ]; 1953 case 5: 1954 result1 = _state.sent(); 1955 return [ 1956 4, 1957 _this.handlePasteResult(result1, "move", targetPath) 1958 ]; 1959 case 6: 1960 _state.sent(); 1961 // Clear clipboard after move 1962 _this.clearClipboard(); 1963 _state.label = 7; 1964 case 7: 1965 return [ 1966 3, 1967 9 1968 ]; 1969 case 8: 1970 error = _state.sent(); 1971 _this.dispatch("operationError", { 1972 detail: { 1973 type: mode === "copy" ? "copy" : "move", 1974 message: "Failed to ".concat(mode, " items"), 1975 error: error.message 1976 }, 1977 bubbles: true 1978 }); 1979 return [ 1980 3, 1981 9 1982 ]; 1983 case 9: 1984 return [ 1985 2 1986 ]; 1987 } 1988 }); 1989 })(); 1990 } 1991 }, 1992 { 1993 key: "handlePasteResult", 1994 value: /** 1995 * Handle paste result and update tree 1996 */ function handlePasteResult(result, operation, targetPath) { 1997 var _this = this; 1998 return _async_to_generator(function() { 1999 var successfulNodes, errors, _iteratorNormalCompletion, _didIteratorError, _iteratorError, _iterator, _step, r, _iteratorNormalCompletion1, _didIteratorError1, _iteratorError1, _iterator1, _step1, node, _iteratorNormalCompletion2, _didIteratorError2, _iteratorError2, _iterator2, _step2, r1, sourcePath; 2000 return _ts_generator(this, function(_state) { 2001 successfulNodes = []; 2002 errors = []; 2003 _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined; 2004 try { 2005 for(_iterator = result.results[Symbol.iterator](); !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){ 2006 r = _step.value;
2007 if (r.success && r.node) { 2008 successfulNodes.push(r.node); 2009 } else if (!r.success && r.error) { 2010 errors.push(r.error); 2011 } 2012 } 2013 } catch (err) { 2014 _didIteratorError = true; 2015 _iteratorError = err; 2016 } finally{ 2017 try { 2018 if (!_iteratorNormalCompletion && _iterator.return != null) { 2019 _iterator.return(); 2020 } 2021 } finally{ 2022 if (_didIteratorError) { 2023 throw _iteratorError; 2024 } 2025 } 2026 } 2027 _iteratorNormalCompletion1 = true, _didIteratorError1 = false, _iteratorError1 = undefined; 2028 try { 2029 // Update tree: add successfully pasted directories 2030 for(_iterator1 = successfulNodes[Symbol.iterator](); !(_iteratorNormalCompletion1 = (_step1 = _iterator1.next()).done); _iteratorNormalCompletion1 = true){ 2031 node = _step1.value; 2032 if (node["@type"] === "elpan:directory") { 2033 _this.addDirectoryToTree(targetPath, node.name); 2034 } 2035 } 2036 } catch (err) { 2037 _didIteratorError1 = true; 2038 _iteratorError1 = err; 2039 } finally{ 2040 try { 2041 if (!_iteratorNormalCompletion1 && _iterator1.return != null) { 2042 _iterator1.return(); 2043 } 2044 } finally{ 2045 if (_didIteratorError1) { 2046 throw _iteratorError1; 2047 } 2048 } 2049 } 2050 // For move operations, also remove source directories 2051 if (operation === "move") { 2052 _iteratorNormalCompletion2 = true, _didIteratorError2 = false, _iteratorError2 = undefined; 2053 try { 2054 for(_iterator2 = result.results[Symbol.iterator](); !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true){ 2055 r1 = _step2.value; 2056 if (r1.success && r1.sourceIdentifier) { 2057 sourcePath = decodeNodePath(r1.sourceIdentifier); 2058 if (sourcePath) { 2059 _this.removeDirectoryByPath(sourcePath); 2060 } 2061 } 2062 } 2063 } catch (err) { 2064 _didIteratorError2 = true; 2065 _iteratorError2 = err; 2066 } finally{ 2067 try { 2068 if (!_iteratorNormalCompletion2 && _iterator2.return != null) { 2069 _iterator2.return(); 2070 } 2071 } finally{ 2072 if (_didIteratorError2) { 2073 throw _iteratorError2; 2074 } 2075 } 2076 } 2077 } 2078 // Show errors if any 2079 if (errors.length > 0) { 2080 _this.dispatch("operationError", { 2081 detail: { 2082 type: operation, 2083 message: "Some items failed to ".concat(operation), 2084 error: errors.join("\n") 2085 }, 2086 bubbles: true 2087 }); 2088 } 2089 // Notify browser about the paste operation 2090 _this.dispatch("pasteComplete", { 2091 detail: { 2092 operation: operation, 2093 targetPath: targetPath, 2094 results: result.results 2095 }, 2096 bubbles: true 2097 }); 2098 return [ 2099 2 2100 ]; 2101 }); 2102 })(); 2103 } 2104 }, 2105 { 2106 key: "deleteDirectory", 2107 value: /** 2108 * Delete a directory (move to trash) 2109 */ function deleteDirectory(path) { 2110 var permanent = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : false;
2111 var _this = this; 2112 return _async_to_generator(function() { 2113 var node, confirmMessage, result, _tmp, removedIdentifiers, errorMessages, error; 2114 return _ts_generator(this, function(_state) { 2115 switch(_state.label){ 2116 case 0: 2117 node = _this.findNodeByPath(path); 2118 if (!node) return [ 2119 2 2120 ]; 2121 // Confirm deletion 2122 confirmMessage = permanent ? 'Permanently delete "'.concat(node.name, '"? This cannot be undone.') : 'Move "'.concat(node.name, '" to trash?'); 2123 if (!window.confirm(confirmMessage)) { 2124 return [ 2125 2 2126 ]; 2127 } 2128 _state.label = 1; 2129 case 1: 2130 _state.trys.push([ 2131 1, 2132 6, 2133 , 2134 7 2135 ]); 2136 if (!permanent) return [ 2137 3, 2138 3 2139 ]; 2140 return [ 2141 4, 2142 _this.service.permanentDeleteMultiple([ 2143 node.id 2144 ]) 2145 ]; 2146 case 2: 2147 _tmp = _state.sent(); 2148 return [ 2149 3, 2150 5 2151 ]; 2152 case 3: 2153 return [ 2154 4, 2155 _this.service.trashMultiple([ 2156 node.id 2157 ]) 2158 ]; 2159 case 4: 2160 _tmp = _state.sent(); 2161 _state.label = 5; 2162 case 5: 2163 result = _tmp; 2164 // Check for success or already-gone items 2165 removedIdentifiers = _to_consumable_array(result.success).concat(_to_consumable_array(result.notFound)); 2166 if (removedIdentifiers.includes(node.id)) { 2167 // Remove from tree 2168 _this.removeDirectoryByPath(path); 2169 // Dispatch event to notify browser 2170 _this.dispatch("directoryDeleted", { 2171 detail: { 2172 paths: [ 2173 path 2174 ] 2175 }, 2176 bubbles: true 2177 }); 2178 } 2179 // Show errors if any 2180 if (result.errors.length > 0) { 2181 errorMessages = result.errors.map(function(e) { 2182 return "".concat(e.identifier, ": ").concat(e.error); 2183 }).join("\n"); 2184 _this.showError(new Error("Failed to delete:\n".concat(errorMessages))); 2185 } 2186 return [ 2187 3, 2188 7 2189 ]; 2190 case 6: 2191 error = _state.sent(); 2192 _this.showError(error); 2193 return [ 2194 3, 2195 7 2196 ]; 2197 case 7: 2198 return [ 2199 2 2200 ]; 2201 } 2202 }); 2203 })(); 2204 } 2205 }, 2206 { 2207 /** 2208 * Check if clipboard has items 2209 */ key: "hasClipboard", 2210 value: function hasClipboard() { 2211 return this.clipboardIdentifiers.length > 0 && this.clipboardMode !== null; 2212 } 2213 }, 2214 { 2215 /** 2216 * Clear the clipboard 2217 */ key: "clearClipboard", 2218 value: function clearClipboard() { 2219 this.clipboardPaths = []; 2220 this.clipboardIdentifiers = []; 2221 this.clipboardMode = null; 2222 this.dispatch("clipboardChange", { 2223 detail: { 2224 mode: null, 2225 paths: [], 2226 identifiers: [], 2227 hasClipboard: false 2228 }, 2229 bubbles: true 2230 }); 2231 } 2232 }, 2233 { 2234 /** 2235 * Set clipboard from external source (e.g., browser pane) 2236 */ key: "setClipboard", 2237 value: function setClipboard(paths, mode) { 2238 this.clipboardPaths = paths; 2239 // Note: identifiers should be set by the caller if cross-directory paste is needed 2240 this.clipboardMode = mode; 2241 } 2242 }, 2243 { 2244 key: "contextMenuRename", 2245 value: /** 2246 * Context menu: Rename the selected directory 2247 */ function contextMenuRename() { 2248 var _this = this; 2249 return _async_to_generator(function() { 2250 return _ts_generator(this, function(_state) { 2251 switch(_state.label){ 2252 case 0: 2253 _this.hideContextMenu(); 2254 // Only rename if we have a context menu path (not root) 2255 if (_this.contextMenuPath === null || _this.contextMenuPath === "") { 2256 return [ 2257 2 2258 ]; 2259 } 2260 return [ 2261 4, 2262 _this.startInPlaceRename(_this.contextMenuPath) 2263 ]; 2264 case 1: 2265 _state.sent(); 2266 return [ 2267 2 2268 ]; 2269 } 2270 }); 2271 })(); 2272 } 2273 }, 2274 { 2275 key: "startInPlaceRename", 2276 value: /** 2277 * Start in-place rename for the given path 2278 */ function startInPlaceRename(path) { 2279 var _this = this; 2280 return _async_to_generator(function() { 2281 var _nameElement_parentElement, node, treeItem, nameElement, originalName, input, cancelled, handleBlur, handleKeydown, handleClick; 2282 return _ts_generator(this, function(_state) { 2283 // If already renaming, don't start another 2284 if (_this.isRenamingInPlace) return [ 2285 2 2286 ]; 2287 node = _this.findNodeByPath(path); 2288 if (!node) return [ 2289 2 2290 ]; 2291 // Find the tree item element for this path 2292 treeItem = _this.treeTarget.querySelector('[data-file-manager--tree-item-path-value="'.concat(CSS.escape(path), '"]')); 2293 if (!treeItem) return [ 2294 2 2295 ]; 2296 // Find the name element within the item 2297 nameElement = treeItem.querySelector(".file-tree__name"); 2298 if (!nameElement) return [ 2299 2 2300 ]; 2301 originalName = node.name; 2302 // Create input element for in-place editing 2303 input = document.createElement("input"); 2304 input.type = "text"; 2305 input.className = "file-tree__rename-input"; 2306 input.value = originalName; 2307 // Hide name element and insert input 2308 nameElement.style.display = "none"; 2309 (_nameElement_parentElement = nameElement.parentElement) === null || _nameElement_parentElement === void 0 ? void 0 : _nameElement_parentElement.insertBefore(input, nameElement.nextSibling); 2310 // Set up the input state 2311 _this.isRenamingInPlace = true; 2312 _this.renameInputElement = input; 2313 // Track whether rename was cancelled to prevent blur handler from running 2314 cancelled = false;
2315 // Handle input events 2316 handleBlur = function() { 2317 if (cancelled) return; 2318 _this.commitTreeRename(path, originalName, input.value.trim(), nameElement, input); 2319 }; 2320 handleKeydown = function(e) { 2321 if (e.key === "Enter") { 2322 e.preventDefault(); 2323 e.stopPropagation(); 2324 input.blur(); // This will trigger handleBlur 2325 } else if (e.key === "Escape") { 2326 e.preventDefault(); 2327 e.stopPropagation(); 2328 cancelled = true; 2329 input.removeEventListener("blur", handleBlur); 2330 _this.cancelTreeRename(nameElement, input); 2331 } 2332 // Stop propagation to prevent tree keyboard navigation 2333 e.stopPropagation(); 2334 }; 2335 // Prevent clicks from bubbling to the tree item 2336 handleClick = function(e) { 2337 e.stopPropagation(); 2338 e.preventDefault(); 2339 }; 2340 input.addEventListener("blur", handleBlur); 2341 input.addEventListener("keydown", handleKeydown); 2342 input.addEventListener("click", handleClick); 2343 // Focus and select all text (directories don't have extensions) 2344 input.focus(); 2345 input.select(); 2346 return [ 2347 2 2348 ]; 2349 }); 2350 })(); 2351 } 2352 }, 2353 { 2354 key: "commitTreeRename", 2355 value: /** 2356 * Commit the tree rename operation 2357 */ function commitTreeRename(path, originalName, newName, nameElement, input) { 2358 var _this = this; 2359 return _async_to_generator(function() { 2360 var cleanup, node, result, _result_error, error; 2361 return _ts_generator(this, function(_state) { 2362 switch(_state.label){ 2363 case 0: 2364 // Clean up the input 2365 cleanup = function() { 2366 input.remove(); 2367 nameElement.style.display = ""; 2368 _this.isRenamingInPlace = false; 2369 _this.renameInputElement = null; 2370 _this.flushPendingRender(); 2371 }; 2372 // Check if user cancelled or entered the same name 2373 if (newName === "" || newName === originalName) { 2374 cleanup(); 2375 return [ 2376 2 2377 ]; 2378 } 2379 // Get the node to get its identifier 2380 node = _this.findNodeByPath(path); 2381 if (!node) { 2382 cleanup(); 2383 return [ 2384 2 2385 ]; 2386 } 2387 _state.label = 1; 2388 case 1: 2389 _state.trys.push([ 2390 1, 2391 4, 2392 , 2393 5 2394 ]); 2395 return [ 2396 4, 2397 _this.service.rename(node.id, newName) 2398 ]; 2399 case 2: 2400 result = _state.sent(); 2401 if (!result.success) { 2402 ; 2403 _this.showError(new Error((_result_error = result.error) !== null && _result_error !== void 0 ? _result_error : "Rename failed")); 2404 cleanup(); 2405 return [ 2406 2 2407 ]; 2408 } 2409 // Reset rename state before refresh 2410 _this.isRenamingInPlace = false;
2411 _this.renameInputElement = null; 2412 // Refresh the tree to reflect the change 2413 return [ 2414 4, 2415 _this.refresh() 2416 ]; 2417 case 3: 2418 _state.sent(); 2419 // Update selected path if needed 2420 if (_this.selectedPath === path && result.newPath) { 2421 _this.selectedPath = result.newPath; 2422 } 2423 // Dispatch event to notify browser about the rename 2424 _this.dispatch("directoryRenamed", { 2425 detail: { 2426 oldPath: path, 2427 newPath: result.newPath, 2428 newIdentifier: result.newIdentifier 2429 }, 2430 bubbles: true 2431 }); 2432 return [ 2433 3, 2434 5 2435 ]; 2436 case 4: 2437 error = _state.sent(); 2438 _this.showError(error); 2439 cleanup(); 2440 return [ 2441 3, 2442 5 2443 ]; 2444 case 5: 2445 return [ 2446 2 2447 ]; 2448 } 2449 }); 2450 })(); 2451 } 2452 }, 2453 { 2454 key: "cancelTreeRename", 2455 value: /** 2456 * Cancel the tree rename operation 2457 */ function cancelTreeRename(nameElement, input) { 2458 input.remove(); 2459 nameElement.style.display = ""; 2460 this.isRenamingInPlace = false; 2461 this.renameInputElement = null; 2462 this.flushPendingRender(); 2463 // Restore focus to the tree panel to keep keyboard shortcuts working 2464 this.element.focus(); 2465 } 2466 }, 2467 { 2468 key: "contextMenuNewFolder", 2469 value: // ==================== New Folder ==================== 2470 /** 2471 * Context menu: New Folder 2472 */ function contextMenuNewFolder() { 2473 var _this = this; 2474 return _async_to_generator(function() { 2475 var _this_contextMenuPath, _ref; 2476 return _ts_generator(this, function(_state) { 2477 switch(_state.label){ 2478 case 0: 2479 _this.hideContextMenu(); 2480 return [ 2481 4, 2482 _this.createNewFolder((_ref = (_this_contextMenuPath = _this.contextMenuPath) !== null && _this_contextMenuPath !== void 0 ? _this_contextMenuPath : _this.selectedPath) !== null && _ref !== void 0 ? _ref : "") 2483 ]; 2484 case 1: 2485 _state.sent(); 2486 return [ 2487 2 2488 ]; 2489 } 2490 }); 2491 })(); 2492 } 2493 }, 2494 { 2495 key: "createNewFolder", 2496 value: /** 2497 * Create a directory under `targetPath`, defaulting to the current tree 2498 * selection, and name it in place in the tree pane. 2499 * 2500 * The row carrying the input is a throwaway DOM node rather than a node in 2501 * the tree model: the tree shares its DirectoryNode objects with the 2502 * browser pane, so a placeholder in `children` would show up as a phantom 2503 * directory in the file listing too. 2504 */ function createNewFolder(targetPath) { 2505 var _this = this; 2506 return _async_to_generator(function() { 2507 var _ref, parentPath, parentNode, parentItem, depth, row, nameElement, finish, input; 2508 return _ts_generator(this, function(_state) { 2509 switch(_state.label){ 2510 case 0: 2511 if (_this.isRenamingInPlace) return [ 2512 2 2513 ]; 2514 parentPath = (_ref = targetPath !== null && targetPath !== void 0 ? targetPath : _this.selectedPath) !== null && _ref !== void 0 ? _ref : ""; 2515 parentNode = _this.findNodeByPath(parentPath); 2516 if (!parentNode) return [ 2517 2 2518 ]; 2519 if (!!parentNode.isExpanded) return [ 2520 3, 2521 2 2522 ]; 2523 return [ 2524 4, 2525 _this.toggleDirectory(parentPath) 2526 ]; 2527 case 1: 2528 _state.sent(); 2529 _state.label = 2; 2530 case 2: 2531 parentItem = _this.findItemElement(parentPath); 2532 if (!parentItem) return [ 2533 2 2534 ]; 2535 depth = parseInt(parentItem.getAttribute("data-file-manager--tree-item-depth-value") || "0", 10) + 1; 2536 row = _this.buildNewFolderRow(depth); 2537 if (!row) return [ 2538 2 2539 ]; 2540 _this.getOrCreateChildrenList(parentItem).appendChild(row); 2541 row.scrollIntoView({ 2542 block: "nearest" 2543 }); 2544 nameElement = row.querySelector(".file-tree__name"); 2545 if (!nameElement) { 2546 row.remove(); 2547 return [ 2548 2 2549 ]; 2550 } 2551 finish = function() { 2552 _this.isRenamingInPlace = false;
2553 _this.renameInputElement = null; 2554 row.remove(); 2555 _this.flushPendingRender(); 2556 }; 2557 input = setupInPlaceRename(nameElement, { 2558 originalName: NEW_FOLDER_NAME, 2559 isFile: false, 2560 onCommit: function() { 2561 var _ref = _async_to_generator(function(newName) { 2562 return _ts_generator(this, function(_state) { 2563 switch(_state.label){ 2564 case 0: 2565 finish(); 2566 return [ 2567 4, 2568 _this.commitNewFolder(parentNode, parentPath, newName) 2569 ]; 2570 case 1: 2571 _state.sent(); 2572 return [ 2573 2 2574 ]; 2575 } 2576 }); 2577 }); 2578 return function(newName) { 2579 return _ref.apply(this, arguments); 2580 }; 2581 }(), 2582 onCancel: function() { 2583 finish(); 2584 // Restore focus to the tree panel to keep keyboard shortcuts working 2585 _this.element.focus(); 2586 } 2587 }).input; 2588 _this.isRenamingInPlace = true; 2589 _this.renameInputElement = input; 2590 return [ 2591 2 2592 ]; 2593 } 2594 }); 2595 })(); 2596 } 2597 }, 2598 { 2599 key: "commitNewFolder", 2600 value: /** 2601 * Create the directory the in-place input was named for 2602 */ function commitNewFolder(parentNode, parentPath, name) { 2603 var _this = this; 2604 return _async_to_generator(function() { 2605 var fail, result, _result_error, error; 2606 return _ts_generator(this, function(_state) { 2607 switch(_state.label){ 2608 case 0: 2609 fail = function(error) { 2610 _this.dispatch("operationError", { 2611 detail: { 2612 type: "mkdir", 2613 message: name, 2614 error: error 2615 }, 2616 bubbles: true 2617 }); 2618 }; 2619 if (name.includes("/") || name.includes("\\")) { 2620 fail("Folder name cannot contain path separators"); 2621 return [ 2622 2 2623 ]; 2624 } 2625 _state.label = 1; 2626 case 1: 2627 _state.trys.push([ 2628 1, 2629 4, 2630 , 2631 5 2632 ]); 2633 return [ 2634 4, 2635 _this.service.createDirectory(parentNode.id, name) 2636 ]; 2637 case 2: 2638 result = _state.sent(); 2639 if (!result.success || !result.node) { 2640 ; 2641 fail((_result_error = result.error) !== null && _result_error !== void 0 ? _result_error : "Failed to create folder"); 2642 return [ 2643 2 2644 ]; 2645 } 2646 return [ 2647 4, 2648 _this.insertCreatedDirectory(parentNode, result.node) 2649 ]; 2650 case 3: 2651 _state.sent(); 2652 // Let the manager refresh the browser pane if it is showing the 2653 // directory the new folder landed in 2654 _this.dispatch("directoryCreated", { 2655 detail: { 2656 identifier: result.identifier, 2657 path: result.path, 2658 parentPath: parentPath, 2659 name: name 2660 }, 2661 bubbles: true 2662 }); 2663 return [ 2664 3, 2665 5 2666 ]; 2667 case 4: 2668 error = _state.sent(); 2669 fail(error.message); 2670 return [ 2671 3, 2672 5 2673 ]; 2674 case 5: 2675 return [ 2676 2 2677 ]; 2678 } 2679 }); 2680 })(); 2681 } 2682 }, 2683 { 2684 key: "insertCreatedDirectory", 2685 value: /** 2686 * Add a freshly created directory to the tree under its parent 2687 */ function insertCreatedDirectory(parentNode, data) { 2688 var _this = this; 2689 return _async_to_generator(function() { 2690 var node, loaded, error, _parentNode_children, children; 2691 return _ts_generator(this, function(_state) { 2692 switch(_state.label){ 2693 case 0: 2694 node = FileSystemNode.fromData(data); 2695 if (!node.isDirectory()) return [ 2696 2 2697 ]; 2698 parentNode.hasSubdirectory = true; 2699 if (!!parentNode.hasLoadedChildren) return [ 2700 3, 2701 4 2702 ]; 2703 _state.label = 1; 2704 case 1: 2705 _state.trys.push([ 2706 1, 2707 3, 2708 , 2709 4 2710 ]); 2711 return [ 2712 4, 2713 _this.service.loadDirectory(parentNode.id) 2714 ]; 2715 case 2: 2716 loaded = _state.sent(); 2717 parentNode.children = loaded.children; 2718 _this.registerNode(parentNode); 2719 return [ 2720 3, 2721 4 2722 ]; 2723 case 3: 2724 error = _state.sent(); 2725 if (_instanceof(error, Error) && error.
2725name === "AbortError") return [ 2726 2 2727 ]; 2728 // The directory was created; the tree just cannot show it yet 2729 _this.updateItemCanExpand(parentNode.path, true); 2730 return [ 2731 2 2732 ]; 2733 case 4: 2734 children = (_parentNode_children = parentNode.children) !== null && _parentNode_children !== void 0 ? _parentNode_children : []; 2735 if (!children.some(function(child) { 2736 return child.path === node.path; 2737 })) { 2738 parentNode.children = _to_consumable_array(children).concat([ 2739 node 2740 ]); 2741 } 2742 parentNode.isExpanded = true; 2743 _this.registerNode(node); 2744 _this.render(); 2745 return [ 2746 2 2747 ]; 2748 } 2749 }); 2750 })(); 2751 } 2752 }, 2753 { 2754 key: "buildNewFolderRow", 2755 value: /** 2756 * Build the throwaway tree row that hosts the new folder's name input. 2757 * Reuses the tree item templates so it lines up with the real rows. 2758 */ function buildNewFolderRow(depth) { 2759 var _content_querySelector; 2760 var content = this.cloneItemTemplate("item-content"); 2761 if (!content) return null; 2762 var wrapper = content.querySelector(".file-tree__item-content"); 2763 if (wrapper) { 2764 wrapper.style.paddingLeft = "".concat(depth * 16, "px"); 2765 } 2766 // Non-expandable toggle, so the name lines up with its siblings 2767 var toggleSlot = content.querySelector('[data-slot="toggle"]'); 2768 var toggle = this.cloneItemTemplate("toggle-disabled"); 2769 if (toggleSlot && toggle) { 2770 var chevronSlot = toggle.querySelector('[data-slot="chevron"]'); 2771 var chevron = this.cloneItemTemplate("chevron-placeholder"); 2772 if (chevronSlot && chevron) { 2773 chevronSlot.replaceWith(chevron); 2774 } 2775 toggleSlot.replaceWith(toggle); 2776 } 2777 var iconSlot = content.querySelector('[data-slot="icon"]'); 2778 var icon = this.cloneItemTemplate("folder-icon"); 2779 if (iconSlot && icon) { 2780 iconSlot.replaceWith(icon); 2781 } 2782 var nameSlot = content.querySelector('[data-slot="name"]'); 2783 if (nameSlot) { 2784 nameSlot.textContent = NEW_FOLDER_NAME; 2785 } 2786 (_content_querySelector = content.querySelector('[data-slot="loading"]')) === null || _content_querySelector === void 0 ? void 0 : _content_querySelector.remove(); 2787 // The row is backed by no node, so strip everything that would have 2788 // Stimulus treat it as one 2789 var link = content.querySelector(".file-tree__link"); 2790 if (link) { 2791 link.removeAttribute("data-controller"); 2792 link.removeAttribute("data-action"); 2793 link.removeAttribute("href"); 2794 } 2795 var item = document.createElement("li"); 2796 item.className = "file-tree__item file-tree__item--new"; 2797 item.appendChild(content); 2798 return item; 2799 } 2800 }, 2801 { 2802 key: "cloneItemTemplate", 2803 value: /** 2804 * Clone one of the tree item templates 2805 */ function cloneItemTemplate(name) { 2806 var template = this.element.querySelector('template[data-tree-item-template="'.concat(name, '"]')); 2807 return template ? template.content.cloneNode(true) : null; 2808 } 2809 }, 2810 { 2811 key: "getOrCreateChildrenList", 2812 value: /** 2813 * Get the children list of a tree item, creating it if the item has none 2814 */ function getOrCreateChildrenList(item) { 2815 var list = item.querySelector(":scope > .file-tree__children"); 2816 if (!list) { 2817 list = document.createElement("ul"); 2818 list.className = "file-tree__children"; 2819 item.appendChild(list); 2820 } 2821 return list; 2822 } 2823 }, 2824 { 2825 /** 2826 * Context menu: Refresh
2827 * Dispatches requestRefresh event to manager for coordinated refresh 2828 */ key: "contextMenuRefresh", 2829 value: function contextMenuRefresh() { 2830 this.hideContextMenu(); 2831 this.dispatch("requestRefresh", { 2832 detail: { 2833 source: "tree" 2834 }, 2835 bubbles: true 2836 }); 2837 } 2838 }, 2839 { 2840 // ==================== Trash Operations ==================== 2841 /** 2842 * Handle click on the trash tree item. 2843 * Dispatches a trashSelect event so the manager can navigate the browser to trash. 2844 */ key: "handleTrashClick", 2845 value: function handleTrashClick(event) { 2846 event.preventDefault(); 2847 // Deselect any currently selected directory 2848 if (this.selectedPath !== null) { 2849 this.setSelection(null); 2850 } 2851 this.isTrashSelected = true; 2852 this.render(); 2853 this.dispatch("trashSelect", { 2854 detail: { 2855 identifier: this.trashRootIdentifierValue 2856 }, 2857 bubbles: true 2858 }); 2859 } 2860 }, 2861 { 2862 /** 2863 * Handle context menu on the trash tree item 2864 */ key: "handleTrashContextMenu", 2865 value: function handleTrashContextMenu(event) { 2866 event.preventDefault(); 2867 event.stopPropagation(); 2868 this.showTrashContextMenu(event.clientX, event.clientY); 2869 } 2870 }, 2871 { 2872 key: "showTrashContextMenu", 2873 value: /** 2874 * Show the trash-specific context menu 2875 */ function showTrashContextMenu(x, y) { 2876 if (!this.hasContextMenuTarget) return; 2877 var html = '<ul class="file-browser__context-menu-list">'; 2878 html += '\n <li class="file-browser__context-menu-item file-browser__context-menu-item--danger">\n <button type="button" data-action="click->file-manager--tree#contextMenuEmptyTrash">\n <span class="context-menu__svg-icon" data-svg-url="'.concat(escapeHtml(this.iconTrashXValue), '" aria-hidden="true"></span>\n ').concat(escapeHtml(this.transEmptyTrashValue), "\n </button>\n </li>\n "); 2879 html += "</ul>"; 2880 this.contextMenuTarget.innerHTML = html; 2881 this.loadSvgPlaceholders(this.contextMenuTarget); 2882 positionContextMenu(this.contextMenuTarget, x, y); 2883 } 2884 }, 2885 { 2886 /** 2887 * Context menu: Empty Trash 2888 */ key: "contextMenuEmptyTrash", 2889 value: function contextMenuEmptyTrash() { 2890 this.hideContextMenu(); 2891 this.dispatch("emptyTrash", { 2892 detail: { 2893 identifier: this.trashRootIdentifierValue 2894 }, 2895 bubbles: true 2896 }); 2897 } 2898 }, 2899 { 2900 /** 2901 * Select the trash node programmatically 2902 */ key: "selectTrash", 2903 value: function selectTrash() { 2904 if (this.selectedPath !== null) { 2905 this.setSelection(null); 2906 } 2907 this.isTrashSelected = true; 2908 this.render(); 2909 } 2910 }, 2911 { 2912 /** 2913 * Deselect the trash node (called when navigating away from trash) 2914 */ key: "deselectTrash", 2915 value: function deselectTrash() { 2916 this.isTrashSelected = false; 2917 this.render(); 2918 } 2919 }, 2920 { 2921 /** 2922 * Handle drag start on a tree item 2923 */ key: "handleItemDragStart", 2924 value: function handleItemDragStart(event) { 2925 var item = event.target.closest(".file-tree__item"); 2926 if (!item) return; 2927 var path = item.getAttribute("data-file-manager--tree-item-path-value"); 2928 if (!path) return; 2929 this.isDragging = true; 2930 this.draggedPath = path; 2931 // Look up the node to get its identifier 2932 var node = this.findNodeByPath(path); 2933 var identifier = (node === null || node === void 0 ? void 0 : node.id) || (path === "" ? this.rootIdentifierValue : undefined); 2934 // Set the drag data with internal marker 2935 if (event.dataTransfer) { 2936 event.dataTransfer.effectAllowed = "move"; 2937 // Store identifier for cross-pane operations (if we have it) 2938 if (identifier) { 2939 event.dataTransfer.setData("text/plain", JSON.stringify([ 2940 identifier 2941 ])); 2942 } else { 2943 // Fallback to path if identifier not found (should not happen normally) 2944 event.dataTransfer.setData("text/plain", JSON.stringify([ 2945 path 2946 ])); 2947 } 2948 // Store path for local operations 2949 event.dataTransfer.setData("application/x-file-manager-paths", JSON.stringify([ 2950 path 2951 ])); 2952 // Set a marker to identify internal drags (prevents upload dropzone from activating) 2953 event.dataTransfer.setData("application/x-file-manager-internal", "true"); 2954 } 2955 // Add dragging class to the item 2956 item.classList.add("file-tree__item--dragging"); 2957 } 2958 }, 2959 { 2960 /** 2961 * Handle drag end on a tree item 2962 */ key: "handleItemDragEnd", 2963 value: function handleItemDragEnd(event) { 2964 this.isDragging = false;
2965 this.draggedPath = null; 2966 // Remove dragging class from all items 2967 var items = this.element.querySelectorAll(".file-tree__item--dragging"); 2968 items.forEach(function(item) { 2969 return item.classList.remove("file-tree__item--dragging"); 2970 }); 2971 this.clearDropTargetStyling(); 2972 } 2973 }, 2974 { 2975 /** 2976 * Handle drag over on a tree item - show drop target indicator 2977 */ key: "handleItemDragOver", 2978 value: function handleItemDragOver(event) { 2979 var _event_dataTransfer; 2980 // Only accept internal file manager drags 2981 if (!((_event_dataTransfer = event.dataTransfer) === null || _event_dataTransfer === void 0 ? void 0 : _event_dataTransfer.types.includes("application/x-file-manager-internal"))) { 2982 return; 2983 } 2984 event.preventDefault(); 2985 event.stopPropagation(); 2986 var item = event.target.closest(".file-tree__item"); 2987 if (!item) return; 2988 // Clear previous drop targets 2989 this.clearDropTargetStyling(); 2990 item.classList.add("file-tree__item--drop-target"); 2991 if (event.dataTransfer) { 2992 event.dataTransfer.dropEffect = "move"; 2993 } 2994 } 2995 }, 2996 { 2997 /** 2998 * Handle drag leave from a tree item 2999 */ key: "handleItemDragLeave", 3000 value: function handleItemDragLeave(event) { 3001 var item = event.target.closest(".file-tree__item"); 3002 if (item) { 3003 item.classList.remove("file-tree__item--drop-target"); 3004 } 3005 } 3006 }, 3007 { 3008 key: "handleItemDrop", 3009 value: /** 3010 * Handle drop on a tree item - dispatch event for manager to handle 3011 */ function handleItemDrop(event) { 3012 var _this = this; 3013 return _async_to_generator(function() { 3014 var _event_dataTransfer, item, targetPath, sourceIdentifiers, _event_dataTransfer1, data, _event_dataTransfer2, targetNode, targetIdentifier, sourcePaths, _event_dataTransfer3, pathsData; 3015 return _ts_generator(this, function(_state) { 3016 // Only accept internal file manager drags 3017 if (!((_event_dataTransfer = event.dataTransfer) === null || _event_dataTransfer === void 0 ? void 0 : _event_dataTransfer.types.includes("application/x-file-manager-internal"))) { 3018 return [ 3019 2 3020 ]; 3021 } 3022 event.preventDefault(); 3023 event.stopPropagation(); 3024 item = event.target.closest(".file-tree__item"); 3025 if (!item) return [ 3026 2 3027 ]; 3028 targetPath = item.getAttribute("data-file-manager--tree-item-path-value"); 3029 if (targetPath === null) return [ 3030 2 3031 ]; 3032 // Clear drop styling 3033 _this.clearDropTargetStyling(); 3034 item.classList.remove("file-tree__item--drop-target"); 3035 // Get the dragged identifiers from dataTransfer 3036 sourceIdentifiers = []; 3037 try { 3038 ; 3039 // First try the dedicated identifiers type 3040 data = (_event_dataTransfer1 = event.dataTransfer) === null || _event_dataTransfer1 === void 0 ? void 0 : _event_dataTransfer1.getData("application/x-file-manager-identifiers"); 3041 if (data) { 3042 sourceIdentifiers = JSON.parse(data); 3043 } else { 3044 ; 3045 // Fallback to text/plain for backwards compatibility 3046 data = (_event_dataTransfer2 = event.dataTransfer) === null || _event_dataTransfer2 === void 0 ? void 0 : _event_dataTransfer2.getData("text/plain"); 3047 if (data) { 3048 sourceIdentifiers = JSON.parse(data); 3049 } 3050 } 3051 } catch (e) { 3052 return [ 3053 2 3054 ]; 3055 } 3056 if (sourceIdentifiers.length === 0) return [ 3057 2 3058 ]; 3059 // Look up the target node to get its identifier 3060 targetNode = _this.findNodeByPath(targetPath); 3061 targetIdentifier = (targetNode === null || targetNode === void 0 ? void 0 : targetNode.id) || (targetPath === "" ? _this.rootIdentifierValue : undefined); 3062 if (!targetIdentifier) return [ 3063 2 3064 ]; 3065 // Get source paths for local operations (tree updates) 3066 sourcePaths = []; 3067 try { 3068 ; 3069 pathsData = (_event_dataTransfer3 = event.dataTransfer) === null || _event_dataTransfer3 === void 0 ? void 0 : _event_dataTransfer3.getData("application/x-file-manager-paths"); 3070 if (pathsData) { 3071 sourcePaths = JSON.parse(pathsData); 3072 } 3073 } catch (e) { 3074 // Paths are optional, fall back to decoding from identifiers if needed 3075 } 3076 // Don't allow dropping onto a dragged item 3077 if (sourcePaths.includes(targetPath)) return [ 3078 2 3079 ]; 3080 // Dispatch event for manager controller to handle the move 3081 _this.dispatch("dropMove", { 3082 detail: { 3083 sourceIdentifiers: sourceIdentifiers, 3084 targetIdentifier: targetIdentifier, 3085 sourcePaths: sourcePaths, 3086 targetPath: targetPath 3087 }, 3088 bubbles: true 3089 }); 3090 return [ 3091 2 3092 ]; 3093 }); 3094 })(); 3095 } 3096 }, 3097 { 3098 key: "clearDropTargetStyling", 3099 value: /** 3100 * Clear drop target styling from all tree items 3101 */ function clearDropTargetStyling() { 3102 var dropTargets = this.element.querySelectorAll(".file-tree__item--drop-target");
3103 dropTargets.forEach(function(item) { 3104 return item.classList.remove("file-tree__item--drop-target"); 3105 }); 3106 } 3107 }, 3108 { 3109 // ==================== Incremental Tree Updates ==================== 3110 /** 3111 * Remove a directory from the tree by path (used after move operations) 3112 * @param path The path of the directory to remove 3113 */ key: "removeDirectoryByPath", 3114 value: function removeDirectoryByPath(path) { 3115 if (!this.rootNode) { 3116 return; 3117 } 3118 // Find the parent of the node to remove 3119 var parentPath = getParentPath(path); 3120 // If parentPath is empty, the directory is at root level - use rootNode 3121 var parentNode = parentPath === "" ? this.rootNode : this.findNodeByPath(parentPath); 3122 if (parentNode && parentNode.children) { 3123 // Remove the node from parent's children 3124 parentNode.children = parentNode.children.filter(function(child) { 3125 return child.path !== path; 3126 }); 3127 // Update hasSubdirectory flag if no more subdirectories 3128 var hasSubdirs = parentNode.children.some(function(child) { 3129 return child.isDirectory(); 3130 }); 3131 parentNode.hasSubdirectory = hasSubdirs; 3132 // Re-render the tree 3133 this.render(); 3134 } 3135 // Also remove the DOM element directly for immediate feedback 3136 var itemElement = this.findItemElement(path); 3137 if (itemElement) { 3138 itemElement.remove(); 3139 } 3140 } 3141 }, 3142 { 3143 /** 3144 * Add a directory to the tree (used after move/copy operations) 3145 * @param parentPath The path where to add the new directory 3146 * @param name The name of the new directory 3147 */ key: "addDirectoryToTree", 3148 value: function addDirectoryToTree(parentPath, name) { 3149 if (!this.rootNode) return; 3150 var parentNode = this.findNodeByPath(parentPath); 3151 if (!parentNode) return; 3152 // Construct the new path 3153 var newPath = parentPath ? "".concat(parentPath, "/").concat(name) : name; 3154 // If parent hasn't loaded children yet, just update canExpand flag 3155 // The directory will appear when user expands the parent (loads from server) 3156 if (!parentNode.hasLoadedChildren) { 3157 // Ensure the parent shows as expandable 3158 parentNode.hasSubdirectory = true; 3159 // Update the toggle visibility in the DOM 3160 this.updateItemCanExpand(parentPath, true); 3161 return; 3162 } 3163 // Check if directory already exists 3164 if (parentNode.children) { 3165 var exists = parentNode.children.some(function(child) { 3166 return child.path === newPath; 3167 }); 3168 if (exists) return; // Already exists, no need to add 3169 } 3170 // Create new directory node 3171 var newNode = new DirectoryNode({ 3172 path: newPath, 3173 name: name, 3174 type: "directory", 3175 size: null, 3176 modified: new Date().toISOString(), 3177 permissions: null, 3178 owner: null, 3179 hasSubdirectories: false 3180 }); 3181 // Add to parent's children (parent has loaded children, safe to add) 3182 if (!parentNode.children) { 3183 parentNode.children = []; 3184 } 3185 parentNode.children.push(newNode); 3186 // Update hasSubdirectory flag 3187 parentNode.hasSubdirectory = true; 3188 // Re-render if parent is expanded 3189 if (parentNode.isExpanded) { 3190 this.render(); 3191 } 3192 } 3193 } 3194 ]); 3195 return TreeController; 3196}(Controller); 3197_define_property(TreeController, "targets", [ 3198 "tree", 3199 "loading", 3200 "error", 3201 "contextMenu" 3202]); 3203_define_property(TreeController, "values", { 3204 rootIdentifier: String, 3205 trashRootIdentifier: { 3206 type: String, 3207 default: "" 3208 }, 3209 // Skip auto-loading the root directory on connect (let manager control initial load) 3210 skipAutoLoad: { 3211 type: Boolean, 3212 default: false 3213 }, 3214 // Translation values 3215 transError: { 3216 type: String, 3217 default: "Error: %message%" 3218 }, 3219 transOpen: { 3220 type: String, 3221 default: "Open" 3222 }, 3223 transNewFolder: { 3224 type: String, 3225 default: "New Folder" 3226 }, 3227 transRename: { 3228 type: String, 3229 default: "Rename" 3230 }, 3231 transDelete: { 3232 type: String, 3233 default: "Delete" 3234 }, 3235 transRefresh: { 3236 type: String, 3237 default: "Refresh" 3238 }, 3239 transCopy: { 3240 type: String, 3241 default: "Copy" 3242 }, 3243 transCut: { 3244 type: String, 3245 default: "Cut" 3246 }, 3247 transPaste: { 3248 type: String, 3249 default: "Paste" 3250 }, 3251 transTrash: { 3252 type: String, 3253 default: "Trash" 3254 }, 3255 transRestore: { 3256 type: String, 3257 default: "Restore" 3258 }, 3259 transDeletePermanently: { 3260 type: String, 3261 default: "Delete Permanently" 3262 }, 3263 transEmptyTrash: { 3264 type: String, 3265 default: "Empty Trash" 3266 }, 3267 // Icon URLs 3268 iconPlayerPlay: { 3269 type: String, 3270 default: "" 3271 }, 3272 iconFolderPlus: { 3273 type: String, 3274 default: "" 3275 }, 3276 iconCut: { 3277 type: String, 3278 default: "" 3279 }, 3280 iconCopy: { 3281 type: String, 3282 default: "" 3283 },
3284 iconClipboardCopy: { 3285 type: String, 3286 default: "" 3287 }, 3288 iconPencil: { 3289 type: String, 3290 default: "" 3291 }, 3292 iconTrash: { 3293 type: String, 3294 default: "" 3295 }, 3296 iconRestore: { 3297 type: String, 3298 default: "" 3299 }, 3300 iconTrashX: { 3301 type: String, 3302 default: "" 3303 }, 3304 iconRefresh: { 3305 type: String, 3306 default: "" 3307 } 3308}); 3309/** 3310 * FileTree Controller 3311 * 3312 * Displays a collapsible tree view of directories in the left pane. 3313 * Uses semantic ul > li structure with tree-item sub-controllers. 3314 * Dispatches 'directory:select' events when a directory is clicked. 3315 * 3316 * Controller identifier: file-manager--tree 3317 * 3318 * @example 3319 * <div data-controller="file-manager--tree" 3320 * data-file-manager--tree-filesystem-type-value="account" 3321 * data-file-manager--tree-filesystem-identifier-value="abc123" 3322 * data-file-manager--tree-root-path-value="" 3323 * data-action="keydown->file-manager--tree#handleKeydown 3324 * file-manager--tree-item:select->file-manager--tree#handleItemSelect 3325 * file-manager--tree-item:dblselect->file-manager--tree#handleItemDblSelect 3326 * file-manager--tree-item:toggle->file-manager--tree#handleItemToggle 3327 * file-manager--tree-item:contextmenu->file-manager--tree#handleItemContextMenu"> 3328 * <ul data-file-manager--tree-target="tree" class="file-tree__list"></ul> 3329 * <div data-file-manager--tree-target="loading" class="hidden">Loading...</div> 3330 * <div data-file-manager--tree-target="error" class="hidden"></div> 3331 * </div> 3332 */ export { TreeController as default };
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.