@@ -2,26 +2,31 @@ import { LitElement, html, css } from 'lit';
22
33class  QuiDirectoryTree  extends  LitElement  { 
44  static  properties  =  { 
5-     directory : {  type : Array  } ,  // Directory data 
6-     selectedPath : {  type : String  }  // Currently selected path 
5+     directory : {  type : Array  } , 
6+     selectedPath : {  type : String  } , 
7+     folderSelectable : {  type : Boolean  } , 
8+     contextMenuItems : {  type : Array  }  // Context menu items for files 
79  } ; 
810
911  constructor ( )  { 
1012    super ( ) ; 
1113    this . directory  =  [ ] ; 
1214    this . selectedPath  =  '' ; 
13-     this . _collapsedPaths  =  new  Set ( ) ;  // Track collapsed nodes 
15+     this . folderSelectable  =  false ; 
16+     this . contextMenuItems  =  [ ] ;  // Default: no context menu 
17+     this . _collapsedPaths  =  new  Set ( ) ; 
1418  } 
1519
1620  static  styles  =  css ` 
1721    :host { 
18-       --tree-node-font-family: 'Arial', sans-serif; 
19-       --tree-node-font-size: 14px; 
20-       --tree-icon-size: 16px; 
21-       --tree-node-bg-hover: #f0f0f0; 
22-       --tree-node-bg-selected: #d0e8ff; 
23-       --tree-node-color: black; 
24-       --tree-node-selected-color: black; 
22+ 
23+       --tree-node-font-family: var(--lumo-font-family, 'Arial', sans-serif); 
24+       --tree-node-font-size: var(--lumo-font-size-m, 14px); 
25+       --tree-icon-size: var(--lumo-icon-size-s, 16px); 
26+       --tree-node-bg-hover: var(--lumo-contrast-5pct, #f0f0f0); 
27+       --tree-node-bg-selected: var(--lumo-primary-color-50pct, #d0e8ff); 
28+       --tree-node-color: var(--lumo-body-text-color, black); 
29+       --tree-node-selected-color: var(--lumo-body-text-color, black); 
2530      --folder-icon-closed: 📁; 
2631      --folder-icon-open: 📂; 
2732      --file-icon: 📄; 
@@ -34,7 +39,6 @@ class QuiDirectoryTree extends LitElement {
3439      font-size: var(--tree-node-font-size); 
3540    } 
3641    .node { 
37-       cursor: pointer; 
3842      padding: 5px; 
3943      border-radius: 5px; 
4044      display: flex; 
@@ -52,31 +56,64 @@ class QuiDirectoryTree extends LitElement {
5256    .icon { 
5357      font-size: var(--tree-icon-size); 
5458    } 
59+ 
60+     .label { 
61+       cursor: pointer; 
62+     } 
63+ 
64+     .label.disabled { 
65+       cursor: default; 
66+     } 
67+ 
68+     .context-menu { 
69+       position: absolute; 
70+       background: var(--lumo-base-color, white); 
71+       border: 1px solid var(--lumo-base-color, white); 
72+       box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); 
73+       z-index: 1000; 
74+     } 
75+     .context-menu-item { 
76+       padding: 5px 10px; 
77+       cursor: pointer; 
78+     } 
79+     .context-menu-item:hover { 
80+       background-color: var(--tree-node-bg-hover); 
81+     } 
5582  ` ; 
5683
5784  render ( )  { 
58-     return  html `< ul  class ="tree "> ${ this . _renderTree ( this . directory ,  '' ) } </ ul > ` ; 
85+     return  html ` 
86+       < ul  class ="tree "> ${ this . _renderTree ( this . directory ,  '' ) } </ ul >  
87+       ${ this . _renderContextMenu ( ) }  
88+     ` ; 
5989  } 
6090
6191  _renderTree ( nodes ,  currentPath )  { 
6292    return  nodes . map ( ( node )  =>  { 
6393      const  path  =  currentPath  ? `${ currentPath } ${ node . name }   : node . name ; 
6494      const  isCollapsed  =  this . _collapsedPaths . has ( path ) ; 
95+       const  isFolder  =  node . type  ===  'folder' ; 
96+       const  isSelectable  =  isFolder  ? this . folderSelectable  : true ; 
6597
6698      return  html ` 
6799        < li >  
68-           < div   class =" node  ${ this . selectedPath   ===   path  ?  'selected'  :  '' } " >  
69-             < span  
70-                class =" icon " 
71-                @click =" ${ ( e )   =>   this . _toggleCollapse ( e ,   path ) } "  
72-             >  
73-               ${ node . type   ===   'folder'  
100+           < div  
101+             class =" node  ${ this . selectedPath   ===   path  ?  'selected'  :  '' } "  
102+             @contextmenu =" ${ ( e )   =>   this . _onContextMenu ( e ,   path ,   node ) }  
103+           >  
104+             < span   class =" icon "  @click =" ${ ( e )   =>   this . _toggleCollapse ( e ,   path ) } " >  
105+               ${ isFolder  
74106                ? isCollapsed  
75107                  ? this . _getIcon ( 'folder-icon-closed' )  
76108                  : this . _getIcon ( 'folder-icon-open' )  
77109                : this . _getIcon ( 'file-icon' ) }  
78110            </ span >  
79-             < span  @click ="${ ( e )  =>  this . _onNodeClick ( e ,  path ,  node ) } > ${ node . name } </ span >  
111+             < span  
112+               class ="label  ${ ! isSelectable  ? 'disabled'  : '' }  
113+               @click ="${ isSelectable  ? ( e )  =>  this . _onNodeClick ( e ,  path ,  node )  : null }  
114+             >  
115+               ${ node . name }  
116+             </ span >  
80117          </ div >  
81118          ${ node . children  &&  ! isCollapsed  
82119            ? html `< ul  class ="tree "> ${ this . _renderTree ( node . children ,  path ) } </ ul > `  
@@ -86,6 +123,26 @@ class QuiDirectoryTree extends LitElement {
86123    } ) ; 
87124  } 
88125
126+   _renderContextMenu ( )  { 
127+     if  ( ! this . _contextMenuData )  return  '' ; 
128+     const  {  x,  y,  filePath,  node }  =  this . _contextMenuData ; 
129+ 
130+     return  html ` 
131+       < div  class ="context-menu " style ="top: ${ y } ${ x }  ">  
132+         ${ this . contextMenuItems . map (  
133+           ( item )  =>  html `  
134+             < div  
135+               class ="context-menu-item " 
136+               @click ="${ ( )  =>  this . _onContextMenuItemClick ( item ,  filePath ,  node ) }  
137+             >  
138+               ${ item . title }  
139+             </ div >  
140+           ` 
141+         ) }  
142+       </ div >  
143+     ` ; 
144+   } 
145+ 
89146  _getIcon ( variableName )  { 
90147    return  getComputedStyle ( this ) . getPropertyValue ( `--${ variableName }  ) . trim ( )  ||  '📄' ; 
91148  } 
@@ -102,7 +159,10 @@ class QuiDirectoryTree extends LitElement {
102159
103160  _onNodeClick ( event ,  path ,  node )  { 
104161    event . stopPropagation ( ) ; 
162+     if  ( node . type  ===  'folder'  &&  ! this . folderSelectable )  return ; 
163+ 
105164    this . selectedPath  =  path ; 
165+     this . _contextMenuData  =  null ;  // Hide context menu on selection 
106166    this . dispatchEvent ( 
107167      new  CustomEvent ( 'file-select' ,  { 
108168        detail : { 
@@ -116,6 +176,25 @@ class QuiDirectoryTree extends LitElement {
116176    ) ; 
117177  } 
118178
179+   _onContextMenu ( event ,  filePath ,  node )  { 
180+     event . preventDefault ( ) ; 
181+     if  ( node . type  !==  'file'  ||  this . contextMenuItems . length  ===  0 )  return ; 
182+ 
183+     this . _contextMenuData  =  { 
184+       x : event . clientX , 
185+       y : event . clientY , 
186+       filePath, 
187+       node
188+     } ; 
189+     this . requestUpdate ( ) ; 
190+   } 
191+ 
192+   _onContextMenuItemClick ( item ,  filePath ,  node )  { 
193+     this . _contextMenuData  =  null ; 
194+     this . requestUpdate ( ) ; 
195+     if  ( item . callback )  item . callback ( filePath ,  node ) ; 
196+   } 
197+ 
119198  selectFile ( filePath )  { 
120199    this . _expandToPath ( filePath ) ; 
121200    this . selectedPath  =  filePath ; 
@@ -132,7 +211,7 @@ class QuiDirectoryTree extends LitElement {
132211  } 
133212
134213  expandAll ( )  { 
135-     this . _collapsedPaths . clear ( ) ;   // Remove all paths from the collapsed set 
214+     this . _collapsedPaths . clear ( ) ; 
136215    this . requestUpdate ( ) ; 
137216  } 
138217
0 commit comments