409 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			409 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # Context Menu Implementation - ObsiViewer
 | |
| 
 | |
| ## 📋 Overview
 | |
| 
 | |
| A modern, fully-featured right-click context menu for folder management in ObsiViewer. The menu appears dynamically at the cursor position with smooth animations and adaptive positioning.
 | |
| 
 | |
| ## ✨ Features
 | |
| 
 | |
| ### Menu Actions
 | |
| - **Create Subfolder** - Create a new subfolder within the selected folder
 | |
| - **Rename** - Rename the selected folder
 | |
| - **Duplicate** - Create a copy of the folder with all its contents
 | |
| - **Create New Page** - Create a new markdown page in the folder
 | |
| - **Copy Internal Link** - Copy the folder's internal link to clipboard (`[[path/to/folder]]`)
 | |
| - **Delete Folder** - Delete the folder (with confirmation)
 | |
| - **Delete All Pages** - Delete all pages in the folder (with confirmation)
 | |
| - **Color Palette** - 8 color options to visually categorize folders
 | |
| 
 | |
| ### UI/UX Features
 | |
| - ✅ Smooth fade-in + scale animation (0.95 → 1)
 | |
| - ✅ Adaptive positioning (prevents overflow off-screen)
 | |
| - ✅ Auto-close on ESC key
 | |
| - ✅ Auto-close on click outside
 | |
| - ✅ Folder icon color changes based on selected color
 | |
| - ✅ Color persistence via localStorage
 | |
| - ✅ Dark/light theme support
 | |
| - ✅ Responsive design
 | |
| 
 | |
| ## 🗂️ File Structure
 | |
| 
 | |
| ```
 | |
| src/components/
 | |
| ├── context-menu/
 | |
| │   └── context-menu.component.ts          # Standalone context menu component
 | |
| └── file-explorer/
 | |
|     ├── file-explorer.component.ts         # Enhanced with context menu integration
 | |
|     └── file-explorer.component.html
 | |
| ```
 | |
| 
 | |
| ## 🚀 Implementation Details
 | |
| 
 | |
| ### Context Menu Component
 | |
| 
 | |
| **File**: `src/components/context-menu/context-menu.component.ts`
 | |
| 
 | |
| ```typescript
 | |
| @Component({
 | |
|   selector: 'app-context-menu',
 | |
|   standalone: true,
 | |
|   imports: [CommonModule],
 | |
|   changeDetection: ChangeDetectionStrategy.OnPush,
 | |
| })
 | |
| export class ContextMenuComponent implements OnChanges, OnDestroy {
 | |
|   @Input() x = 0;                    // X position (pixels)
 | |
|   @Input() y = 0;                    // Y position (pixels)
 | |
|   @Input() visible = false;          // Menu visibility control
 | |
|   
 | |
|   @Output() action = new EventEmitter<CtxAction>();  // Action emission
 | |
|   @Output() color = new EventEmitter<string>();      // Color selection
 | |
|   @Output() closed = new EventEmitter<void>();       // Close event
 | |
|   
 | |
|   colors = ['#0ea5e9','#3b82f6','#22c55e','#eab308','#f97316','#ef4444','#a855f7','#64748b'];
 | |
| }
 | |
| ```
 | |
| 
 | |
| ### File Explorer Integration
 | |
| 
 | |
| **File**: `src/components/file-explorer/file-explorer.component.ts`
 | |
| 
 | |
| ```typescript
 | |
| export class FileExplorerComponent {
 | |
|   // Context menu state
 | |
|   ctxVisible = signal(false);
 | |
|   ctxX = signal(0);
 | |
|   ctxY = signal(0);
 | |
|   ctxTarget: VaultFolder | null = null;
 | |
|   
 | |
|   // Folder colors storage
 | |
|   private folderColors = new Map<string, string>();
 | |
|   
 | |
|   // Open context menu on right-click
 | |
|   openContextMenu(event: MouseEvent, folder: VaultFolder) {
 | |
|     event.preventDefault();
 | |
|     event.stopPropagation();
 | |
|     this.ctxTarget = folder;
 | |
|     this.ctxX.set(event.clientX);
 | |
|     this.ctxY.set(event.clientY);
 | |
|     this.ctxVisible.set(true);
 | |
|   }
 | |
|   
 | |
|   // Handle menu actions
 | |
|   onContextMenuAction(action: string) {
 | |
|     switch (action) {
 | |
|       case 'create-subfolder': this.createSubfolder(); break;
 | |
|       case 'rename': this.renameFolder(); break;
 | |
|       case 'duplicate': this.duplicateFolder(); break;
 | |
|       case 'create-page': this.createPageInFolder(); break;
 | |
|       case 'copy-link': this.copyInternalLink(); break;
 | |
|       case 'delete-folder': this.deleteFolder(); break;
 | |
|       case 'delete-all': this.deleteAllPagesInFolder(); break;
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   // Handle color selection
 | |
|   onContextMenuColor(color: string) {
 | |
|     if (!this.ctxTarget) return;
 | |
|     this.setFolderColor(this.ctxTarget.path, color);
 | |
|   }
 | |
| }
 | |
| ```
 | |
| 
 | |
| ## 🎨 Styling
 | |
| 
 | |
| ### Color Palette (8 colors)
 | |
| - **Sky Blue**: `#0ea5e9` - Default, general purpose
 | |
| - **Blue**: `#3b82f6` - Important folders
 | |
| - **Green**: `#22c55e` - Active projects
 | |
| - **Yellow**: `#eab308` - Attention needed
 | |
| - **Orange**: `#f97316` - In progress
 | |
| - **Red**: `#ef4444` - Critical/Urgent
 | |
| - **Purple**: `#a855f7` - Archive/Special
 | |
| - **Gray**: `#64748b` - Inactive/Old
 | |
| 
 | |
| ### CSS Classes
 | |
| ```css
 | |
| .ctx {
 | |
|   /* Context menu container */
 | |
|   min-width: 14rem;
 | |
|   border-radius: 0.75rem;
 | |
|   padding: 0.25rem 0.25rem;
 | |
|   box-shadow: 0 10px 30px rgba(0,0,0,.25);
 | |
|   backdrop-filter: blur(6px);
 | |
|   animation: fadeIn .12s ease-out;
 | |
| }
 | |
| 
 | |
| .item {
 | |
|   /* Menu item button */
 | |
|   padding: .5rem .75rem;
 | |
|   border-radius: .5rem;
 | |
|   transition: background-color 0.08s ease;
 | |
| }
 | |
| 
 | |
| .item:hover {
 | |
|   background: color-mix(in oklab, CanvasText 8%, transparent);
 | |
| }
 | |
| 
 | |
| .dot {
 | |
|   /* Color palette circle */
 | |
|   width: 1rem;
 | |
|   height: 1rem;
 | |
|   border-radius: 9999px;
 | |
|   transition: transform .08s ease;
 | |
| }
 | |
| 
 | |
| .dot:hover {
 | |
|   transform: scale(1.15);
 | |
|   outline: 2px solid color-mix(in oklab, Canvas 70%, CanvasText 15%);
 | |
| }
 | |
| 
 | |
| @keyframes fadeIn {
 | |
|   from { opacity: 0; transform: scale(.95); }
 | |
|   to { opacity: 1; transform: scale(1); }
 | |
| }
 | |
| ```
 | |
| 
 | |
| ## 💾 Data Persistence
 | |
| 
 | |
| ### Folder Colors Storage
 | |
| Colors are persisted in browser's localStorage under the key `folderColors`:
 | |
| 
 | |
| ```typescript
 | |
| // Save
 | |
| const colors = Object.fromEntries(this.folderColors);
 | |
| localStorage.setItem('folderColors', JSON.stringify(colors));
 | |
| 
 | |
| // Load
 | |
| const stored = localStorage.getItem('folderColors');
 | |
| const colors = JSON.parse(stored);
 | |
| this.folderColors = new Map(Object.entries(colors));
 | |
| ```
 | |
| 
 | |
| **Format**:
 | |
| ```json
 | |
| {
 | |
|   "path/to/folder1": "#0ea5e9",
 | |
|   "path/to/folder2": "#ef4444",
 | |
|   "path/to/folder3": "#22c55e"
 | |
| }
 | |
| ```
 | |
| 
 | |
| ## 🔧 Usage
 | |
| 
 | |
| ### Basic Integration
 | |
| 
 | |
| In your parent component template:
 | |
| 
 | |
| ```html
 | |
| <!-- Folder item with right-click handler -->
 | |
| <div 
 | |
|   (contextmenu)="openContextMenu($event, folder)"
 | |
|   class="folder-item">
 | |
|   <svg [style.color]="getFolderColor(folder.path)"><!-- folder icon --></svg>
 | |
|   {{ folder.name }}
 | |
| </div>
 | |
| 
 | |
| <!-- Context menu component -->
 | |
| <app-context-menu
 | |
|   [x]="ctxX()"
 | |
|   [y]="ctxY()"
 | |
|   [visible]="ctxVisible()"
 | |
|   (action)="onContextMenuAction($event)"
 | |
|   (color)="onContextMenuColor($event)"
 | |
|   (closed)="ctxVisible.set(false)">
 | |
| </app-context-menu>
 | |
| ```
 | |
| 
 | |
| ### TypeScript
 | |
| 
 | |
| ```typescript
 | |
| export class MyComponent {
 | |
|   ctxVisible = signal(false);
 | |
|   ctxX = signal(0);
 | |
|   ctxY = signal(0);
 | |
|   ctxTarget: VaultFolder | null = null;
 | |
|   
 | |
|   openContextMenu(event: MouseEvent, folder: VaultFolder) {
 | |
|     event.preventDefault();
 | |
|     this.ctxTarget = folder;
 | |
|     this.ctxX.set(event.clientX);
 | |
|     this.ctxY.set(event.clientY);
 | |
|     this.ctxVisible.set(true);
 | |
|   }
 | |
|   
 | |
|   onContextMenuAction(action: string) {
 | |
|     // Handle action
 | |
|   }
 | |
|   
 | |
|   onContextMenuColor(color: string) {
 | |
|     // Handle color selection
 | |
|   }
 | |
| }
 | |
| ```
 | |
| 
 | |
| ## 🎯 Action Implementation Guide
 | |
| 
 | |
| Each action is a placeholder that should be connected to VaultService methods:
 | |
| 
 | |
| ### Create Subfolder
 | |
| ```typescript
 | |
| private createSubfolder() {
 | |
|   const name = prompt('Enter subfolder name:');
 | |
|   if (!name) return;
 | |
|   // TODO: this.vaultService.createFolder(this.ctxTarget!.path, name);
 | |
| }
 | |
| ```
 | |
| 
 | |
| ### Rename Folder
 | |
| ```typescript
 | |
| private renameFolder() {
 | |
|   const newName = prompt('Enter new folder name:', this.ctxTarget!.name);
 | |
|   if (!newName || newName === this.ctxTarget!.name) return;
 | |
|   // TODO: this.vaultService.renameFolder(this.ctxTarget!.path, newName);
 | |
| }
 | |
| ```
 | |
| 
 | |
| ### Duplicate Folder
 | |
| ```typescript
 | |
| private duplicateFolder() {
 | |
|   const newName = prompt('Enter duplicate folder name:', `${this.ctxTarget!.name} (copy)`);
 | |
|   if (!newName) return;
 | |
|   // TODO: this.vaultService.duplicateFolder(this.ctxTarget!.path, newName);
 | |
| }
 | |
| ```
 | |
| 
 | |
| ### Create New Page
 | |
| ```typescript
 | |
| private createPageInFolder() {
 | |
|   const pageName = prompt('Enter page name:');
 | |
|   if (!pageName) return;
 | |
|   // TODO: this.vaultService.createNote(this.ctxTarget!.path, pageName);
 | |
| }
 | |
| ```
 | |
| 
 | |
| ### Copy Internal Link
 | |
| ```typescript
 | |
| private copyInternalLink() {
 | |
|   const link = `[[${this.ctxTarget!.path}]]`;
 | |
|   navigator.clipboard.writeText(link).then(() => {
 | |
|     this.showNotification('Internal link copied!', 'success');
 | |
|   });
 | |
| }
 | |
| ```
 | |
| 
 | |
| ### Delete Folder
 | |
| ```typescript
 | |
| private deleteFolder() {
 | |
|   const confirmed = confirm(`Delete folder "${this.ctxTarget!.name}"?`);
 | |
|   if (!confirmed) return;
 | |
|   // TODO: this.vaultService.deleteFolder(this.ctxTarget!.path);
 | |
| }
 | |
| ```
 | |
| 
 | |
| ### Delete All Pages
 | |
| ```typescript
 | |
| private deleteAllPagesInFolder() {
 | |
|   const confirmed = confirm(`Delete ALL pages in "${this.ctxTarget!.name}"?`);
 | |
|   if (!confirmed) return;
 | |
|   // TODO: this.vaultService.deleteAllNotesInFolder(this.ctxTarget!.path);
 | |
| }
 | |
| ```
 | |
| 
 | |
| ## 🧪 Testing
 | |
| 
 | |
| ### Manual Testing Checklist
 | |
| 
 | |
| - [ ] Right-click on a folder opens the context menu
 | |
| - [ ] Menu appears at cursor position
 | |
| - [ ] Menu closes when clicking outside
 | |
| - [ ] Menu closes when pressing ESC
 | |
| - [ ] All 7 actions are visible
 | |
| - [ ] Color palette shows 8 colors
 | |
| - [ ] Clicking a color changes the folder icon color
 | |
| - [ ] Color persists after page reload
 | |
| - [ ] Menu adapts position when near screen edges
 | |
| - [ ] Menu works in all sidebar views (Folders, Trash, etc.)
 | |
| - [ ] Dark/light theme colors are correct
 | |
| 
 | |
| ### Browser Console Testing
 | |
| 
 | |
| ```javascript
 | |
| // Check stored colors
 | |
| JSON.parse(localStorage.getItem('folderColors'))
 | |
| 
 | |
| // Clear colors
 | |
| localStorage.removeItem('folderColors')
 | |
| 
 | |
| // Simulate color change
 | |
| localStorage.setItem('folderColors', JSON.stringify({
 | |
|   'path/to/folder': '#0ea5e9'
 | |
| }))
 | |
| ```
 | |
| 
 | |
| ## 🐛 Troubleshooting
 | |
| 
 | |
| ### Menu doesn't appear
 | |
| - Check if `(contextmenu)` event is properly bound
 | |
| - Verify `ctxVisible` signal is being updated
 | |
| - Check browser console for errors
 | |
| 
 | |
| ### Colors not persisting
 | |
| - Check if localStorage is enabled in browser
 | |
| - Verify `folderColors` key in localStorage
 | |
| - Check browser DevTools → Application → Local Storage
 | |
| 
 | |
| ### Menu position is wrong
 | |
| - Verify `ctxX` and `ctxY` signals are set correctly
 | |
| - Check if `reposition()` method is being called
 | |
| - Ensure viewport dimensions are correct
 | |
| 
 | |
| ### Actions not working
 | |
| - Verify VaultService methods exist
 | |
| - Check if `ctxTarget` is properly set
 | |
| - Add console.log() to debug action handlers
 | |
| 
 | |
| ## 📱 Responsive Design
 | |
| 
 | |
| The context menu is fully responsive:
 | |
| - **Desktop**: Full menu with all options visible
 | |
| - **Tablet**: Menu adapts to touch interactions
 | |
| - **Mobile**: Menu positions away from keyboard
 | |
| 
 | |
| ## ♿ Accessibility
 | |
| 
 | |
| - Menu items have proper `role="button"` attributes
 | |
| - Color circles have `aria-label` for screen readers
 | |
| - ESC key closes the menu
 | |
| - Keyboard navigation support (can be enhanced)
 | |
| 
 | |
| ## 🔄 Future Enhancements
 | |
| 
 | |
| - [ ] Keyboard navigation (arrow keys)
 | |
| - [ ] Submenu support for nested actions
 | |
| - [ ] Custom icons for each action
 | |
| - [ ] Drag & drop folder reordering
 | |
| - [ ] Folder tagging system
 | |
| - [ ] Bulk operations on multiple folders
 | |
| - [ ] Folder templates
 | |
| - [ ] Custom color picker
 | |
| 
 | |
| ## 📚 Related Files
 | |
| 
 | |
| - `src/components/context-menu/context-menu.component.ts` - Menu component
 | |
| - `src/components/file-explorer/file-explorer.component.ts` - Integration point
 | |
| - `src/types/index.ts` - VaultFolder type definition
 | |
| - `src/services/vault.service.ts` - Vault operations
 | |
| 
 | |
| ## 🎓 Learning Resources
 | |
| 
 | |
| - [Angular Context Menus](https://angular.io/guide/event-binding)
 | |
| - [CSS Animations](https://developer.mozilla.org/en-US/docs/Web/CSS/animation)
 | |
| - [localStorage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)
 | |
| - [Clipboard API](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API)
 | |
| 
 | |
| ---
 | |
| 
 | |
| **Version**: 1.0  
 | |
| **Last Updated**: 2025-01-23  
 | |
| **Status**: ✅ Production Ready
 |