diff --git a/main.ts b/main.ts index e8bf023..634b9a7 100644 --- a/main.ts +++ b/main.ts @@ -15,6 +15,7 @@ import { StyleService } from './services/style-service'; // Import UI components import { StatusBar } from './ui/status-bar'; import { StatusDropdown } from './ui/status-dropdown'; +import { StatusDropdownComponent } from './ui/status-dropdown-component'; import { StatusPaneView } from './ui/status-pane-view'; import { ExplorerIntegration } from './ui/explorer'; import { BatchStatusModal } from './ui/modals'; diff --git a/styles.css b/styles.css index 8c69d4a..26cd289 100644 --- a/styles.css +++ b/styles.css @@ -1192,4 +1192,244 @@ /* Debug outline to help identify the button (you can remove this after debugging) */ .note-status-toolbar-button-container { outline: 1px solid rgba(255, 0, 0, 0.3) !important; +} + +/* Make the unified dropdown look consistent in all contexts */ +.note-status-unified-dropdown { + width: 280px; + max-width: 90vw; + border-radius: var(--radius-m, 8px); + overflow: hidden; + box-shadow: var(--shadow-m), 0 0 15px rgba(0, 0, 0, 0.1); + border: 1px solid var(--background-modifier-border); + background: var(--background-primary); + z-index: 9999 !important; /* Ensure it's above everything */ + pointer-events: all !important; /* Ensure dropdown receives clicks */ +} + +/* Better animation for unified dropdown */ +.note-status-popover-animate-in { + animation: statusDropdownFadeIn 0.22s var(--ease-out) forwards; +} + +.note-status-popover-animate-out { + animation: statusDropdownFadeOut 0.22s var(--ease-out) forwards; +} + +@keyframes statusDropdownFadeIn { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes statusDropdownFadeOut { + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0.95); + } +} + +/* Fix for positioning when transitioning */ +.note-status-popover { + transform-origin: top left; + pointer-events: all !important; +} + +/* Make sure clickable elements in dropdown work properly */ +.note-status-option, +.note-status-option-icon, +.note-status-option-text, +.note-status-chip, +.note-status-chip-remove, +.note-status-popover-search-input { + pointer-events: all !important; +} + +/* Fix option clicking */ +.note-status-option { + cursor: pointer !important; + transition: background-color 0.15s ease; +} + +.note-status-option:hover { + background-color: var(--background-modifier-hover) !important; +} + +.note-status-option.is-selected { + background-color: var(--interactive-accent-hover) !important; + color: var(--text-on-accent) !important; +} + +/* Make options more visible when selecting */ +.note-status-option-selecting { + background-color: var(--interactive-accent) !important; + color: var(--text-on-accent) !important; + animation: pulse 0.3s var(--ease-out); +} + +@keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(0.98); } + 100% { transform: scale(1); } +} + +/* Ensure dropdown shows above modal if needed */ +.modal-container .note-status-unified-dropdown { + z-index: 10001 !important; +} + +/* Make sure scrollable containers in dropdown work properly */ +.note-status-options-container { + max-height: 250px; + overflow-y: auto; + scrollbar-width: thin; +} + +/* Fix dropdown header styling */ +.note-status-popover-header { + padding: 12px; + border-bottom: 1px solid var(--background-modifier-border); + background: var(--background-secondary-alt); + display: flex; + align-items: center; + position: sticky; + top: 0; + z-index: 2; +} + +/* Improve status badges styling */ +.note-status-chip { + display: inline-flex; + align-items: center; + padding: 4px 8px; + margin: 2px; + border-radius: 12px; + background: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + color: var(--text-normal); + font-size: 0.9em; + max-width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.note-status-chip-icon { + margin-right: 4px; +} + +/* Remove button on badges */ +.note-status-chip-remove { + display: flex; + align-items: center; + justify-content: center; + margin-left: 4px; + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--background-modifier-border); + color: var(--text-muted); + cursor: pointer !important; + transition: all 0.15s var(--ease-out); +} + +.note-status-chip-remove:hover { + background: var(--text-accent); + color: var(--text-on-accent); + transform: scale(1.1); +} + +/* Improve search input */ +.note-status-popover-search { + padding: 8px; + position: sticky; + top: 0; + background: var(--background-primary); + z-index: 3; + border-bottom: 1px solid var(--background-modifier-border); +} + +.note-status-popover-search-input { + width: 100%; + padding: 8px; + border-radius: var(--input-radius, 4px); + border: 1px solid var(--background-modifier-border); + background: var(--background-primary); + color: var(--text-normal); +} + +.note-status-popover-search-input:focus { + border-color: var(--interactive-accent); + box-shadow: 0 0 0 2px rgba(var(--interactive-accent-rgb), 0.2); + outline: none; +} + +/* Better options styling */ +.note-status-option { + display: flex; + align-items: center; + padding: 8px 12px; + margin: 2px 4px; + border-radius: var(--radius-s, 4px); + background: var(--background-primary); + color: var(--text-normal); +} + +.note-status-option.is-selected { + font-weight: 500; +} + +.note-status-option-icon { + margin-right: 8px; + font-size: 1.1em; +} + +.note-status-option-check { + margin-left: auto; + color: var(--text-accent); +} + +/* Fix toolbar button */ +.note-status-toolbar-button-container { + display: flex !important; + align-items: center !important; + margin-right: 8px !important; +} + +.note-status-toolbar-button { + display: flex !important; + align-items: center !important; + justify-content: center !important; + width: 32px !important; + height: 32px !important; + border-radius: var(--radius-s, 4px) !important; + background: transparent !important; + color: var(--text-normal) !important; + cursor: pointer !important; + transition: background 0.2s ease !important; +} + +.note-status-toolbar-button:hover { + background: var(--background-modifier-hover) !important; +} + +/* Prevent event propagation issues */ +.note-status-unified-dropdown * { + pointer-events: all !important; +} + +/* Animation for badge removal */ +.note-status-chip-removing { + transform: scale(0.8); + opacity: 0; + transition: all 0.15s ease-out; } \ No newline at end of file diff --git a/ui/context-menus.ts b/ui/context-menus.ts index deb11fa..473f997 100644 --- a/ui/context-menus.ts +++ b/ui/context-menus.ts @@ -1,6 +1,7 @@ import { App, Menu, TFile } from 'obsidian'; import { NoteStatusSettings } from '../models/types'; import { StatusService } from '../services/status-service'; +import { StatusDropdownComponent } from './status-dropdown-component'; /** * Handles context menu interactions for status changes @@ -9,11 +10,29 @@ export class StatusContextMenu { private settings: NoteStatusSettings; private statusService: StatusService; public app: App; + private dropdownComponent: StatusDropdownComponent; constructor(app: App, settings: NoteStatusSettings, statusService: StatusService) { this.app = app; this.settings = settings; this.statusService = statusService; + this.dropdownComponent = new StatusDropdownComponent(app, statusService, settings); + + // Set up the callback to handle status changes + this.dropdownComponent.setOnStatusChange((statuses) => { + // Notify UI of status changes + window.dispatchEvent(new CustomEvent('note-status:status-changed', { + detail: { statuses } + })); + + // Force a full UI refresh to ensure all elements are updated + window.dispatchEvent(new CustomEvent('note-status:force-refresh')); + + // Also trigger explorer update specifically + setTimeout(() => { + window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); + }, 50); + }); } /** @@ -21,95 +40,209 @@ export class StatusContextMenu { */ public updateSettings(settings: NoteStatusSettings): void { this.settings = settings; + this.dropdownComponent.updateSettings(settings); } /** * Shows the context menu for changing a status of one or more files */ public showForFiles(files: TFile[], position?: { x: number; y: number }): void { + if (files.length === 0) return; + + // For single file, we can show the dropdown directly without menu + if (files.length === 1) { + const targetFile = files[0]; + // Get the current statuses for this file + const currentStatuses = this.statusService.getFileStatuses(targetFile); + + // Create a dummy target element for dropdown positioning + const dummyTarget = document.createElement('div'); + if (position) { + dummyTarget.style.position = 'fixed'; + dummyTarget.style.left = `${position.x}px`; + dummyTarget.style.top = `${position.y}px`; + dummyTarget.style.width = '0'; + dummyTarget.style.height = '0'; + document.body.appendChild(dummyTarget); + } + + // Set the target file for the dropdown to update + this.dropdownComponent.setTargetFile(targetFile); + + // Update dropdown with current statuses + this.dropdownComponent.updateStatuses(currentStatuses); + + // Show the dropdown + if (position) { + this.dropdownComponent.open(dummyTarget, position); + } else { + // If no position, use screen center + const center = { + x: window.innerWidth / 2, + y: window.innerHeight / 2 + }; + this.dropdownComponent.open(dummyTarget, center); + } + + // Clean up dummy target after dropdown is shown + setTimeout(() => { + if (dummyTarget.parentNode) { + dummyTarget.parentNode.removeChild(dummyTarget); + } + }, 100); + + return; + } + + // For multiple files, show a menu first with options for batch actions const menu = new Menu(); - - // Get all available statuses - const allStatuses = this.statusService.getAllStatuses(); - - if (this.settings.useMultipleStatuses) { - // Add a replace section - menu.addItem((item) => - item - .setTitle('Replace with...') - .setIcon('note') - .onClick(() => { - const replaceMenu = new Menu(); - - allStatuses - .filter(status => status.name !== 'unknown') - .forEach(status => { - replaceMenu.addItem((subItem) => - subItem - .setTitle(`${status.name} ${status.icon}`) - .setIcon('tag') - .onClick(async () => { - await this.statusService.batchUpdateStatuses(files, [status.name], 'replace'); - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); - }) - ); - }); - - if (position) { - replaceMenu.showAtPosition(position); - } else { - replaceMenu.showAtMouseEvent(new MouseEvent('contextmenu')); + menu.addClass('note-status-batch-menu'); + + // Add title section + menu.addItem((item) => { + item.setTitle(`Update ${files.length} files`) + .setDisabled(true); + return item; + }); + + // Add "Replace with status" option + menu.addItem((item) => + item + .setTitle('Replace with status...') + .setIcon('tag') + .onClick(() => { + // Create dummy element for positioning + const dummyTarget = document.createElement('div'); + if (position) { + dummyTarget.style.position = 'fixed'; + dummyTarget.style.left = `${position.x}px`; + dummyTarget.style.top = `${position.y}px`; + document.body.appendChild(dummyTarget); + } + + // Clear target file since we're handling multiple + this.dropdownComponent.setTargetFile(null); + + // Set up callback for multiple files + this.dropdownComponent.setOnStatusChange(async (statuses) => { + if (statuses.length > 0) { + // Batch update all files with the selected statuses + await this.statusService.batchUpdateStatuses(files, statuses, 'replace'); + + // Dispatch events for UI update + window.dispatchEvent(new CustomEvent('note-status:batch-update-complete', { + detail: { + statuses, + fileCount: files.length, + mode: 'replace' + } + })); + window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); } - }) - ); - - // Add "Add status" option + }); + + // Reset to unknown status for selection + this.dropdownComponent.updateStatuses(['unknown']); + + // Show the dropdown + if (position) { + this.dropdownComponent.open(dummyTarget, position); + } else { + const center = { + x: window.innerWidth / 2, + y: window.innerHeight / 2 + }; + this.dropdownComponent.open(dummyTarget, center); + } + + // Clean up dummy target + setTimeout(() => { + if (dummyTarget.parentNode) { + dummyTarget.parentNode.removeChild(dummyTarget); + } + }, 100); + }) + ); + + // Add "Add status" option (only for multiple status mode) + if (this.settings.useMultipleStatuses) { menu.addItem((item) => item .setTitle('Add status...') .setIcon('plus') .onClick(() => { - const addMenu = new Menu(); - - allStatuses - .filter(status => status.name !== 'unknown') - .forEach(status => { - addMenu.addItem((subItem) => - subItem - .setTitle(`${status.name} ${status.icon}`) - .setIcon('plus') - .onClick(async () => { - await this.statusService.batchUpdateStatuses(files, [status.name], 'add'); - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); - }) - ); - }); - + // Create dummy element for positioning + const dummyTarget = document.createElement('div'); if (position) { - addMenu.showAtPosition(position); - } else { - addMenu.showAtMouseEvent(new MouseEvent('contextmenu')); + dummyTarget.style.position = 'fixed'; + dummyTarget.style.left = `${position.x}px`; + dummyTarget.style.top = `${position.y}px`; + document.body.appendChild(dummyTarget); } + + // Clear target file since we're handling multiple + this.dropdownComponent.setTargetFile(null); + + // Set up callback for batch add + this.dropdownComponent.setOnStatusChange(async (statuses) => { + if (statuses.length > 0) { + // We'll use the explicitly selected status for the add operation + // Get only the newly selected status + const statusToAdd = statuses[0]; // Use the first selected status + + if (statusToAdd && statusToAdd !== 'unknown') { + // Add this status to all files + for (const file of files) { + await this.statusService.addNoteStatus(statusToAdd, file); + } + + // Dispatch events for UI update + window.dispatchEvent(new CustomEvent('note-status:batch-update-complete', { + detail: { + statuses: [statusToAdd], + fileCount: files.length, + mode: 'add' + } + })); + window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); + } + } + }); + + // Reset to unknown status for selection + this.dropdownComponent.updateStatuses(['unknown']); + + // Show the dropdown + if (position) { + this.dropdownComponent.open(dummyTarget, position); + } else { + const center = { + x: window.innerWidth / 2, + y: window.innerHeight / 2 + }; + this.dropdownComponent.open(dummyTarget, center); + } + + // Clean up dummy target + setTimeout(() => { + if (dummyTarget.parentNode) { + dummyTarget.parentNode.removeChild(dummyTarget); + } + }, 100); }) ); - } else { - // Single status mode - but still using arrays internally - allStatuses - .filter(status => status.name !== 'unknown') - .forEach(status => { - menu.addItem((item) => - item - .setTitle(`${status.name} ${status.icon}`) - .setIcon('tag') - .onClick(async () => { - // Always use arrays, even in single status mode - await this.statusService.batchUpdateStatuses(files, [status.name], 'replace'); - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); - }) - ); - }); } - + + // Add option to open the batch modal for more advanced options + menu.addItem((item) => + item + .setTitle('Open batch update modal...') + .setIcon('lucide-edit') + .onClick(() => { + window.dispatchEvent(new CustomEvent('note-status:open-batch-modal')); + }) + ); + // Show the menu if (position) { menu.showAtPosition(position); @@ -122,27 +255,68 @@ export class StatusContextMenu { * Shows a context menu for a single file */ public showForFile(file: TFile, event: MouseEvent): void { - const menu = new Menu(); - - // Add status change options - menu.addItem((item) => - item.setTitle('Change Status') - .setIcon('tag') - .onClick(() => { - this.showForFiles([file]); - }) - ); - - // Add other file options - // Example: Open in new tab - menu.addItem((item) => - item.setTitle('Open in New Tab') - .setIcon('lucide-external-link') - .onClick(() => { - this.app.workspace.openLinkText(file.path, file.path, 'tab'); - }) - ); - - menu.showAtMouseEvent(event); + // Create position from event + const position = { x: event.clientX, y: event.clientY }; + + // Modify the onStatusChange callback specifically for this file + const originalCallback = this.dropdownComponent.getOnStatusChange(); + + // Set a custom callback for this specific file operation + this.dropdownComponent.setOnStatusChange(async (statuses) => { + if (statuses.length > 0) { + // Update the file with the selected status + if (this.settings.useMultipleStatuses) { + // If the status is already applied, toggle it off + const currentStatuses = this.statusService.getFileStatuses(file); + if (currentStatuses.includes(statuses[0])) { + await this.statusService.removeNoteStatus(statuses[0], file); + } else { + // Add the status + await this.statusService.addNoteStatus(statuses[0], file); + } + } else { + // Replace with the single status + await this.statusService.updateNoteStatuses([statuses[0]], file); + } + + // Force explorer icon update for this specific file + this.app.metadataCache.trigger('changed', file); + + // Directly update the explorer icon + setTimeout(() => { + const explorerIntegration = (this.app as any).plugins.plugins['note-status']?.explorerIntegration; + if (explorerIntegration) { + explorerIntegration.updateFileExplorerIcons(file); + } + }, 50); + + // Dispatch events to update other UI components + window.dispatchEvent(new CustomEvent('note-status:status-changed', { + detail: { statuses, file: file.path } + })); + + window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); + + // Force a full UI refresh to ensure all elements are updated + setTimeout(() => { + window.dispatchEvent(new CustomEvent('note-status:force-refresh')); + }, 100); + } + }); + + // Show the dropdown + this.showForFiles([file], position); + + // Restore the original callback after a delay + setTimeout(() => { + this.dropdownComponent.setOnStatusChange(originalCallback); + }, 300); + } + + /** + * Clean up resources + */ + public dispose(): void { + this.dropdownComponent.dispose(); } } \ No newline at end of file diff --git a/ui/status-dropdown-component.ts b/ui/status-dropdown-component.ts new file mode 100644 index 0000000..4821959 --- /dev/null +++ b/ui/status-dropdown-component.ts @@ -0,0 +1,463 @@ +import { setIcon, TFile } from 'obsidian'; +import { NoteStatusSettings, Status } from '../models/types'; +import { StatusService } from '../services/status-service'; + +/** + * Unified dropdown component for status selection + */ +export class StatusDropdownComponent { + private app: any; + private statusService: StatusService; + private settings: NoteStatusSettings; + private dropdownElement: HTMLElement | null = null; + private currentStatuses: string[] = ['unknown']; + private onStatusChange: (statuses: string[]) => void; + private isOpen = false; + private animationDuration = 220; + private clickOutsideHandler: (e: MouseEvent) => void; + private targetFile: TFile | null = null; + + constructor(app: any, statusService: StatusService, settings: NoteStatusSettings) { + this.app = app; + this.statusService = statusService; + this.settings = settings; + this.onStatusChange = () => {}; + + // Bind methods + this.handleClickOutside = this.handleClickOutside.bind(this); + this.handleEscapeKey = this.handleEscapeKey.bind(this); + } + + /** + * Updates the current statuses + */ + public updateStatuses(statuses: string[] | string): void { + // Normalize input to always be an array + if (typeof statuses === 'string') { + this.currentStatuses = [statuses]; + } else { + this.currentStatuses = [...statuses]; // Create a copy to ensure it's updated + } + + // Update dropdown if it's open + if (this.isOpen && this.dropdownElement) { + this.refreshDropdownContent(); + } + } + + /** + * Set the target file for status updates + */ + public setTargetFile(file: TFile | null): void { + this.targetFile = file; + } + + /** + * Updates settings reference + */ + public updateSettings(settings: NoteStatusSettings): void { + this.settings = settings; + + // Update dropdown if it's open + if (this.isOpen && this.dropdownElement) { + this.refreshDropdownContent(); + } + } + + /** + * Set callback for status changes + */ + public setOnStatusChange(callback: (statuses: string[]) => void): void { + this.onStatusChange = callback; + } + + /** + * Get the current status change callback + */ + public getOnStatusChange(): (statuses: string[]) => void { + return this.onStatusChange; + } + + /** + * Toggle the dropdown visibility + */ + public toggle(targetEl: HTMLElement, position?: { x: number, y: number }): void { + if (this.isOpen) { + this.close(); + } else { + this.open(targetEl, position); + } + } + + /** + * Open the dropdown + */ + public open(targetEl: HTMLElement, position?: { x: number, y: number }): void { + if (this.isOpen) { + this.close(); + } + + this.isOpen = true; + + // Create dropdown element + this.dropdownElement = document.createElement('div'); + this.dropdownElement.addClass('note-status-popover', 'note-status-unified-dropdown'); + document.body.appendChild(this.dropdownElement); + + // Fill dropdown content + this.refreshDropdownContent(); + + // Position the dropdown + if (position) { + this.positionAt(position.x, position.y); + } else { + this.positionRelativeTo(targetEl); + } + + // Add animation class + this.dropdownElement.addClass('note-status-popover-animate-in'); + + // Add event listeners + setTimeout(() => { + this.clickOutsideHandler = this.handleClickOutside; + document.addEventListener('click', this.clickOutsideHandler); + document.addEventListener('keydown', this.handleEscapeKey); + }, 10); + } + + /** + * Close the dropdown + */ + public close(): void { + if (!this.dropdownElement || !this.isOpen) return; + + // Add exit animation + this.dropdownElement.addClass('note-status-popover-animate-out'); + + // Clean up event listeners + document.removeEventListener('click', this.clickOutsideHandler); + document.removeEventListener('keydown', this.handleEscapeKey); + + // Remove after animation completes + setTimeout(() => { + if (this.dropdownElement) { + this.dropdownElement.remove(); + this.dropdownElement = null; + this.isOpen = false; + } + }, this.animationDuration); + } + + /** + * Refresh the dropdown content + */ + private refreshDropdownContent(): void { + if (!this.dropdownElement) return; + + // Clear content + this.dropdownElement.empty(); + + // Create header + const headerEl = this.dropdownElement.createDiv({ cls: 'note-status-popover-header' }); + + // Title with icon + const titleEl = headerEl.createDiv({ cls: 'note-status-popover-title' }); + const iconContainer = titleEl.createDiv({ cls: 'note-status-popover-icon' }); + setIcon(iconContainer, 'tag'); + titleEl.createSpan({ text: 'Note Status', cls: 'note-status-popover-label' }); + + // Current status chips + const chipsContainer = this.dropdownElement.createDiv({ cls: 'note-status-popover-chips' }); + + // Show 'No status' indicator if no statuses or only unknown status + if (this.currentStatuses.length === 0 || + (this.currentStatuses.length === 1 && this.currentStatuses[0] === 'unknown')) { + chipsContainer.createDiv({ + cls: 'note-status-empty-indicator', + text: 'No status assigned' + }); + } else { + // Add chip for each status + this.currentStatuses.forEach(status => { + if (status === 'unknown') return; // Skip unknown status + + const statusObj = this.statusService.getAllStatuses().find(s => s.name === status); + if (!statusObj) return; + + const chipEl = chipsContainer.createDiv({ + cls: `note-status-chip status-${status}` + }); + + // Status icon + chipEl.createSpan({ + text: statusObj.icon, + cls: 'note-status-chip-icon' + }); + + // Status name + chipEl.createSpan({ + text: statusObj.name, + cls: 'note-status-chip-text' + }); + + // Remove button (only show if multiple statuses allowed) + if (this.settings.useMultipleStatuses && this.currentStatuses.length > 1) { + const removeBtn = chipEl.createDiv({ + cls: 'note-status-chip-remove', + attr: { + 'aria-label': `Remove ${status} status`, + 'title': `Remove ${status} status` + } + }); + setIcon(removeBtn, 'x'); + + removeBtn.addEventListener('click', async (e) => { + e.stopPropagation(); + + // Add remove animation + chipEl.addClass('note-status-chip-removing'); + + // Only proceed with removal if we have a target file + if (this.targetFile) { + // Wait for animation to complete before actually removing + setTimeout(async () => { + // Remove this status + await this.statusService.removeNoteStatus(status, this.targetFile); + + // Get updated statuses + const updatedStatuses = this.statusService.getFileStatuses(this.targetFile); + + // Update current statuses + this.currentStatuses = updatedStatuses; + + // Refresh chips + this.refreshDropdownContent(); + + // Call status change callback + this.onStatusChange(updatedStatuses); + }, 150); + } + }); + } + }); + } + + // Create search filter + const searchContainer = this.dropdownElement.createDiv({ cls: 'note-status-popover-search' }); + const searchInput = searchContainer.createEl('input', { + type: 'text', + placeholder: 'Filter statuses...', + cls: 'note-status-popover-search-input' + }); + + // Create status options container + const statusOptionsContainer = this.dropdownElement.createDiv({ cls: 'note-status-options-container' }); + + // Get all available statuses + const allStatuses = this.statusService.getAllStatuses() + .filter(status => status.name !== 'unknown'); + + // Populate status options + const populateOptions = (filter = '') => { + statusOptionsContainer.empty(); + + const filteredStatuses = allStatuses.filter(status => !filter || + status.name.toLowerCase().includes(filter.toLowerCase()) || + status.icon.includes(filter)); + + if (filteredStatuses.length === 0) { + // Show empty state + statusOptionsContainer.createDiv({ + cls: 'note-status-empty-options', + text: filter ? `No statuses match "${filter}"` : 'No statuses found' + }); + return; + } + + filteredStatuses.forEach(status => { + const isSelected = this.currentStatuses.includes(status.name); + + const optionEl = statusOptionsContainer.createDiv({ + cls: `note-status-option ${isSelected ? 'is-selected' : ''} status-${status.name}` + }); + + // Status icon + optionEl.createSpan({ + text: status.icon, + cls: 'note-status-option-icon' + }); + + // Status name + optionEl.createSpan({ + text: status.name, + cls: 'note-status-option-text' + }); + + // Check icon for selected status + if (isSelected) { + const checkIcon = optionEl.createDiv({ cls: 'note-status-option-check' }); + setIcon(checkIcon, 'check'); + } + + // Add click handler + optionEl.addEventListener('click', async () => { + try { + // Add selection animation + optionEl.addClass('note-status-option-selecting'); + + // Apply status changes after brief delay for animation + setTimeout(async () => { + // Handle the case where we have a specific target file + if (this.targetFile) { + if (this.settings.useMultipleStatuses) { + await this.statusService.toggleNoteStatus(status.name, this.targetFile); + } else { + await this.statusService.updateNoteStatuses([status.name], this.targetFile); + // Close dropdown in single status mode + this.close(); + } + + // Get fresh status from file + const freshStatuses = this.statusService.getFileStatuses(this.targetFile); + + // Update current statuses + this.currentStatuses = [...freshStatuses]; + + // Refresh status options if dropdown still open + if (this.isOpen && this.settings.useMultipleStatuses) { + populateOptions(searchInput.value); + } + + // Call status change callback + this.onStatusChange(freshStatuses); + } else { + // This is for batch operations or when no specific file is targeted + // We'll just trigger the callback with the selected status + this.onStatusChange([status.name]); + + // Usually we want to close after selection in this case + if (!this.settings.useMultipleStatuses) { + this.close(); + } + } + }, 150); + } catch (error) { + console.error('Error updating status:', error); + } + }); + }); + }; + + // Initial population + populateOptions(); + + // Add search functionality + searchInput.addEventListener('input', () => { + populateOptions(searchInput.value); + }); + + // Focus search input after a short delay + setTimeout(() => { + searchInput.focus(); + }, 50); + } + + /** + * Position the dropdown at specific coordinates + */ + private positionAt(x: number, y: number): void { + if (!this.dropdownElement) return; + + this.dropdownElement.style.position = 'fixed'; + this.dropdownElement.style.zIndex = '999'; + this.dropdownElement.style.left = `${x}px`; + this.dropdownElement.style.top = `${y}px`; + + // Ensure dropdown doesn't go off-screen + setTimeout(() => { + if (!this.dropdownElement) return; + + const rect = this.dropdownElement.getBoundingClientRect(); + + if (rect.right > window.innerWidth) { + this.dropdownElement.style.left = `${window.innerWidth - rect.width - 10}px`; + } + + if (rect.bottom > window.innerHeight) { + this.dropdownElement.style.top = `${window.innerHeight - rect.height - 10}px`; + } + + // Set max height based on viewport + const maxHeight = window.innerHeight - rect.top - 20; + this.dropdownElement.style.maxHeight = `${maxHeight}px`; + }, 0); + } + + /** + * Position the dropdown relative to a target element + */ + private positionRelativeTo(targetEl: HTMLElement): void { + if (!this.dropdownElement) return; + + // Reset positioning + this.dropdownElement.style.position = 'fixed'; + this.dropdownElement.style.zIndex = '999'; + + // Get target element's position + const targetRect = targetEl.getBoundingClientRect(); + + // Position below the element + this.dropdownElement.style.top = `${targetRect.bottom + 5}px`; + + // Align to left edge of target by default + this.dropdownElement.style.left = `${targetRect.left}px`; + + // Check if dropdown would go off-screen to the right + setTimeout(() => { + if (!this.dropdownElement) return; + + const rect = this.dropdownElement.getBoundingClientRect(); + + if (rect.right > window.innerWidth) { + // Align to right edge instead + this.dropdownElement.style.left = 'auto'; + this.dropdownElement.style.right = `${window.innerWidth - targetRect.right}px`; + } + + if (rect.bottom > window.innerHeight) { + // Position above the element instead + this.dropdownElement.style.top = 'auto'; + this.dropdownElement.style.bottom = `${window.innerHeight - targetRect.top + 5}px`; + } + + // Set max height based on viewport + const maxHeight = window.innerHeight - rect.top - 20; + this.dropdownElement.style.maxHeight = `${maxHeight}px`; + }, 0); + } + + /** + * Handle click outside the dropdown + */ + private handleClickOutside(e: MouseEvent): void { + if (this.dropdownElement && !this.dropdownElement.contains(e.target as Node)) { + this.close(); + } + } + + /** + * Handle escape key to close dropdown + */ + private handleEscapeKey(e: KeyboardEvent): void { + if (e.key === 'Escape') { + this.close(); + } + } + + /** + * Dispose of resources + */ + public dispose(): void { + this.close(); + } +} \ No newline at end of file diff --git a/ui/status-dropdown.ts b/ui/status-dropdown.ts index 2a44283..9e6912a 100644 --- a/ui/status-dropdown.ts +++ b/ui/status-dropdown.ts @@ -1,6 +1,7 @@ -import { MarkdownView, Notice, Editor, Menu, setIcon } from 'obsidian'; -import { NoteStatusSettings, Status } from '../models/types'; +import { MarkdownView, Editor, setIcon } from 'obsidian'; +import { NoteStatusSettings } from '../models/types'; import { StatusService } from '../services/status-service'; +import { StatusDropdownComponent } from './status-dropdown-component'; /** * Enhanced status dropdown with toolbar integration @@ -9,25 +10,31 @@ export class StatusDropdown { private app: any; private settings: NoteStatusSettings; private statusService: StatusService; - private dropdownContainer?: HTMLElement; - private statusChipsContainer?: HTMLElement; private currentStatuses: string[] = ['unknown']; - private statusPopover?: HTMLElement; - private isPopoverOpen = false; - private clickOutsideHandler: any; private toolbarButtonContainer?: HTMLElement; private toolbarButton?: HTMLElement; + private dropdownComponent: StatusDropdownComponent; - // Animation timings - private readonly ANIMATION_DURATION = 220; - constructor(app: any, settings: NoteStatusSettings, statusService: StatusService) { this.app = app; this.settings = settings; this.statusService = statusService; - // Bind methods to preserve this context - this.handleClickOutside = this.handleClickOutside.bind(this); + // Initialize the dropdown component + this.dropdownComponent = new StatusDropdownComponent(app, statusService, settings); + + // Configure dropdown component callbacks + this.dropdownComponent.setOnStatusChange((statuses) => { + // Update current statuses and toolbar button + this.currentStatuses = [...statuses]; // Make sure to copy the array + this.updateToolbarButton(); + + // Dispatch events for UI update + window.dispatchEvent(new CustomEvent('note-status:status-changed', { + detail: { statuses } + })); + window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); + }); // Initialize toolbar button after layout is ready this.app.workspace.onLayoutReady(() => { @@ -39,7 +46,7 @@ export class StatusDropdown { * Initialize the toolbar button in the Obsidian ribbon */ private initToolbarButton(): void { - // Clear timeout to prevent multiple initializations + // Clear any existing button if (this.toolbarButton) { this.toolbarButton.remove(); this.toolbarButton = undefined; @@ -76,43 +83,21 @@ export class StatusDropdown { // Add click handler this.toolbarButton.addEventListener('click', (e) => { e.stopPropagation(); - this.toggleStatusPopover(this.toolbarButton); + e.preventDefault(); + this.toggleStatusPopover(); }); // Add the button to the container this.toolbarButtonContainer.appendChild(this.toolbarButton); - // Add some debug styling to check visibility - this.toolbarButtonContainer.style.border = '2px solid red'; - this.toolbarButton.style.border = '1px solid blue'; - - // Force display properties - this.toolbarButtonContainer.style.display = 'flex'; - this.toolbarButtonContainer.style.visibility = 'visible'; - this.toolbarButtonContainer.style.opacity = '1'; - // Insert at a more reliable position - just prepend to the container try { toolbarContainer.prepend(this.toolbarButtonContainer); - console.log('Note Status: Toolbar button added successfully at position:', - Array.from(toolbarContainer.children).indexOf(this.toolbarButtonContainer)); } catch (error) { console.error('Note Status: Error inserting toolbar button', error); // Fallback - just append it toolbarContainer.appendChild(this.toolbarButtonContainer); } - - // Force a redraw - setTimeout(() => { - if (this.toolbarButtonContainer) { - this.toolbarButtonContainer.style.display = 'none'; - setTimeout(() => { - if (this.toolbarButtonContainer) { - this.toolbarButtonContainer.style.display = 'flex'; - } - }, 10); - } - }, 100); }, 500); // Waiting 500ms to ensure the UI is ready } @@ -178,10 +163,8 @@ export class StatusDropdown { // Update toolbar button this.updateToolbarButton(); - // Clear and render fresh dropdown if it exists - if (this.dropdownContainer) { - this.render(); - } + // Update dropdown component + this.dropdownComponent.updateStatuses(this.currentStatuses); } /** @@ -190,472 +173,124 @@ export class StatusDropdown { public updateSettings(settings: NoteStatusSettings): void { this.settings = settings; this.updateToolbarButton(); - if (this.dropdownContainer) { - this.render(); - } - } - - /** - * Render or remove the dropdown based on settings - */ - public render(): void { - // Remove existing popover if open - if (this.isPopoverOpen) { - this.closeStatusPopover(); - } + this.dropdownComponent.updateSettings(settings); } /** * Toggle the status popover */ - private toggleStatusPopover(targetEl?: HTMLElement): void { - if (this.isPopoverOpen) { - this.closeStatusPopover(); - } else { - this.openStatusPopover(targetEl); + private toggleStatusPopover(): void { + // Get the active file first + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile) return; + + // Set the target file for the dropdown component + this.dropdownComponent.setTargetFile(activeFile); + + // Get current statuses + const currentStatuses = this.statusService.getFileStatuses(activeFile); + this.dropdownComponent.updateStatuses(currentStatuses); + + // Create a position below the toolbar button + if (this.toolbarButton) { + const rect = this.toolbarButton.getBoundingClientRect(); + const position = { + x: rect.left, + y: rect.bottom + 5 + }; + + // Force open the dropdown with explicit position + this.dropdownComponent.open(this.toolbarButton, position); } } - /** - * Open the status selection popover - * @param targetEl - The element that triggered the popover (for positioning) - */ - private openStatusPopover(targetEl?: HTMLElement): void { - if (this.isPopoverOpen) { - this.closeStatusPopover(); - return; - } - - this.isPopoverOpen = true; - - // Create popover element - this.statusPopover = document.createElement('div'); - this.statusPopover.addClass('note-status-popover', 'note-status-toolbar-popover'); - document.body.appendChild(this.statusPopover); - - // Create header - const headerEl = this.statusPopover.createDiv({ cls: 'note-status-popover-header' }); - - // Title with icon - const titleEl = headerEl.createDiv({ cls: 'note-status-popover-title' }); - const iconContainer = titleEl.createDiv({ cls: 'note-status-popover-icon' }); - setIcon(iconContainer, 'tag'); - titleEl.createSpan({ text: 'Note Status', cls: 'note-status-popover-label' }); - - // Current status chips - const chipsContainer = this.statusPopover.createDiv({ cls: 'note-status-popover-chips' }); - - // Show 'No status' indicator if no statuses or only unknown status - if (this.currentStatuses.length === 0 || - (this.currentStatuses.length === 1 && this.currentStatuses[0] === 'unknown')) { - chipsContainer.createDiv({ - cls: 'note-status-empty-indicator', - text: 'No status assigned' - }); - } else { - // Add chip for each status - this.currentStatuses.forEach(status => { - if (status === 'unknown') return; // Skip unknown status - - const statusObj = this.statusService.getAllStatuses().find(s => s.name === status); - if (!statusObj) return; - - const chipEl = chipsContainer.createDiv({ - cls: `note-status-chip status-${status}` - }); - - // Status icon - chipEl.createSpan({ - text: statusObj.icon, - cls: 'note-status-chip-icon' - }); - - // Status name - chipEl.createSpan({ - text: statusObj.name, - cls: 'note-status-chip-text' - }); - - // Remove button (only show if multiple statuses allowed) - if (this.settings.useMultipleStatuses && this.currentStatuses.length > 1) { - const removeBtn = chipEl.createDiv({ - cls: 'note-status-chip-remove', - attr: { - 'aria-label': `Remove ${status} status`, - 'title': `Remove ${status} status` - } - }); - setIcon(removeBtn, 'x'); - - removeBtn.addEventListener('click', async (e) => { - e.stopPropagation(); - - // Add remove animation - chipEl.addClass('note-status-chip-removing'); - - // Wait for animation to complete before actually removing - setTimeout(async () => { - // Remove this status - await this.statusService.removeNoteStatus(status); - - // Get updated statuses - const updatedStatuses = this.statusService.getFileStatuses(this.app.workspace.getActiveFile()); - - // Update current statuses - this.currentStatuses = updatedStatuses; - - // Update toolbar button - this.updateToolbarButton(); - - // Refresh chips - this.openStatusPopover(targetEl); - - // Dispatch event for UI update - window.dispatchEvent(new CustomEvent('note-status:status-changed', { - detail: { statuses: updatedStatuses } - })); - }, 150); - }); - } - }); - } - - // Create search filter - const searchContainer = this.statusPopover.createDiv({ cls: 'note-status-popover-search' }); - const searchInput = searchContainer.createEl('input', { - type: 'text', - placeholder: 'Filter statuses...', - cls: 'note-status-popover-search-input' - }); - - // Create status options container - const statusOptionsContainer = this.statusPopover.createDiv({ cls: 'note-status-options-container' }); - - // Get all available statuses - const allStatuses = this.statusService.getAllStatuses() - .filter(status => status.name !== 'unknown'); - - // Populate status options - const populateOptions = (filter = '') => { - statusOptionsContainer.empty(); - - const filteredStatuses = allStatuses.filter(status => !filter || - status.name.toLowerCase().includes(filter.toLowerCase()) || - status.icon.includes(filter)); - - if (filteredStatuses.length === 0) { - // Show empty state - statusOptionsContainer.createDiv({ - cls: 'note-status-empty-options', - text: filter ? `No statuses match "${filter}"` : 'No statuses found' - }); - return; - } - - filteredStatuses.forEach(status => { - const isSelected = this.currentStatuses.includes(status.name); - - const optionEl = statusOptionsContainer.createDiv({ - cls: `note-status-option ${isSelected ? 'is-selected' : ''} status-${status.name}` - }); - - // Status icon - optionEl.createSpan({ - text: status.icon, - cls: 'note-status-option-icon' - }); - - // Status name - optionEl.createSpan({ - text: status.name, - cls: 'note-status-option-text' - }); - - // Check icon for selected status - if (isSelected) { - const checkIcon = optionEl.createDiv({ cls: 'note-status-option-check' }); - setIcon(checkIcon, 'check'); - } - - // Add click handler - optionEl.addEventListener('click', async () => { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile) return; - - try { - // Add selection animation - optionEl.addClass('note-status-option-selecting'); - - // Apply status changes after brief delay for animation - setTimeout(async () => { - if (this.settings.useMultipleStatuses) { - await this.statusService.toggleNoteStatus(status.name); - } else { - await this.statusService.updateNoteStatuses([status.name]); - // Close popover in single status mode - this.closeStatusPopover(); - } - - // Get fresh status from file - const freshStatuses = this.statusService.getFileStatuses(activeFile); - - // Update current statuses and toolbar button - this.currentStatuses = [...freshStatuses]; - this.updateToolbarButton(); - - // Refresh status options if popover still open - if (this.isPopoverOpen && this.settings.useMultipleStatuses) { - populateOptions(searchInput.value); - } - - // Dispatch events for UI update - window.dispatchEvent(new CustomEvent('note-status:status-changed', { - detail: { statuses: freshStatuses } - })); - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); - }, 150); - } catch (error) { - console.error('Error updating status:', error); - new Notice('Failed to update status'); - } - }); - }); - }; - - // Initial population - populateOptions(); - - // Add search functionality - searchInput.addEventListener('input', () => { - populateOptions(searchInput.value); - }); - - // Position the popover relative to the toolbar button - this.positionToolbarPopover(targetEl); - - // Add animation class - this.statusPopover.addClass('note-status-popover-animate-in'); - - // Focus search input - setTimeout(() => { - searchInput.focus(); - }, 50); - - // Add click outside listener to close popover - setTimeout(() => { - this.clickOutsideHandler = this.handleClickOutside; - document.addEventListener('click', this.clickOutsideHandler); - - // Also add escape key listener - document.addEventListener('keydown', this.handleEscapeKey); - }, 10); - } - - /** - * Position the popover relative to the toolbar button - */ - private positionToolbarPopover(targetEl?: HTMLElement): void { - if (!this.statusPopover || !targetEl) return; - - // Reset positioning - this.statusPopover.style.position = 'fixed'; - this.statusPopover.style.zIndex = '999'; - - // Get target element's position - const targetRect = targetEl.getBoundingClientRect(); - - // Position below the button - this.statusPopover.style.top = `${targetRect.bottom + 5}px`; - this.statusPopover.style.right = `${window.innerWidth - targetRect.right}px`; - - // Set max height based on viewport - const maxHeight = window.innerHeight - targetRect.bottom - 40; - this.statusPopover.style.maxHeight = `${maxHeight}px`; - } - - /** - * Handle escape key to close popover - */ - private handleEscapeKey = (e: KeyboardEvent) => { - if (e.key === 'Escape' && this.isPopoverOpen) { - this.closeStatusPopover(); - } - }; - - /** - * Handle click outside the popover - */ - private handleClickOutside(e: MouseEvent) { - if (this.statusPopover && !this.statusPopover.contains(e.target as Node) && - this.toolbarButton && !this.toolbarButton.contains(e.target as Node)) { - this.closeStatusPopover(); - } - } - - /** - * Close the status selection popover - */ - private closeStatusPopover(): void { - if (!this.statusPopover) return; - - // Add exit animation - this.statusPopover.addClass('note-status-popover-animate-out'); - - // Clean up event listeners immediately - document.removeEventListener('click', this.clickOutsideHandler); - document.removeEventListener('keydown', this.handleEscapeKey); - - // Remove after animation completes - setTimeout(() => { - if (this.statusPopover) { - this.statusPopover.remove(); - this.statusPopover = undefined; - this.isPopoverOpen = false; - } - }, this.ANIMATION_DURATION); - } - /** * Show status dropdown in context menu */ public showInContextMenu(editor: Editor, view: MarkdownView): void { - const menu = new Menu(); - const customClass = 'note-status-context-menu'; - // Apply class manually since Menu doesn't have addClass method - (menu as any).dom?.addClass?.(customClass); - const activeFile = this.app.workspace.getActiveFile(); if (!activeFile) return; + // For editor context menu, use cursor position if possible + let position: {x: number, y: number}; + + try { + // Try to get cursor position + const cursor = editor.getCursor('head'); + const editorPosition = editor.posToCoords(cursor); + + if (editorPosition) { + position = { + x: editorPosition.left, + y: editorPosition.top + 20 // Add small offset below cursor + }; + } else { + // If can't get cursor position, use editor element position + const editorEl = view.contentEl.querySelector('.cm-editor'); + if (editorEl) { + const rect = editorEl.getBoundingClientRect(); + position = { + x: rect.left + 100, // Offset from left + y: rect.top + 100 // Offset from top + }; + } else { + // Last resort - use middle of viewport + position = { + x: window.innerWidth / 2, + y: window.innerHeight / 3 + }; + } + } + } catch (error) { + console.error('Error getting position for dropdown:', error); + // Fallback to center of screen + position = { + x: window.innerWidth / 2, + y: window.innerHeight / 3 + }; + } + + // Create a dummy target element for positioning + const dummyTarget = document.createElement('div'); + dummyTarget.style.position = 'fixed'; + dummyTarget.style.left = `${position.x}px`; + dummyTarget.style.top = `${position.y}px`; + dummyTarget.style.zIndex = '1000'; + document.body.appendChild(dummyTarget); + + // Set the active file as the target for the dropdown + this.dropdownComponent.setTargetFile(activeFile); + + // Get current statuses for this file const currentStatuses = this.statusService.getFileStatuses(activeFile); - // Add a header item - menu.addItem((item) => { - item.setTitle('Change Note Status') - .setDisabled(true); - // Apply class manually since MenuItem doesn't have setClass method - (item as any).dom?.addClass?.('note-status-menu-header'); - return item; - }); - - // Get all available statuses grouped by template - const allStatuses = this.statusService.getAllStatuses(); + // Update dropdown with current statuses + this.dropdownComponent.updateStatuses(currentStatuses); - // Group statuses by template name if they have one - const statusesByTemplate: Record = { 'Custom': [] }; + // Show dropdown at the calculated position + this.dropdownComponent.open(dummyTarget, position); - allStatuses.forEach(status => { - if (status.name !== 'unknown') { - // For now, just put all in custom group - // In a future version, we could implement proper template grouping - statusesByTemplate['Custom'].push(status); + // Clean up dummy target after dropdown is shown + setTimeout(() => { + if (dummyTarget.parentNode) { + dummyTarget.parentNode.removeChild(dummyTarget); } - }); - - // Add status options by template - Object.entries(statusesByTemplate).forEach(([templateName, statuses]) => { - if (statuses.length === 0) return; - - // Add template section - if (Object.keys(statusesByTemplate).length > 1) { - menu.addSeparator(); - - menu.addItem((item) => { - item.setTitle(templateName) - .setDisabled(true); - // Apply class manually since MenuItem doesn't have setClass method - (item as any).dom?.addClass?.('note-status-menu-section'); - return item; - }); - } - - // Add statuses from this template - statuses.forEach(status => { - const isActive = currentStatuses.includes(status.name); - - menu.addItem((item) => { - item - .setTitle(`${status.icon} ${status.name}`) - .setIcon(isActive ? 'check-circle' : 'circle'); - - // Apply class manually if active - if (isActive) { - (item as any).dom?.addClass?.('is-active'); - } - - if (this.settings.useMultipleStatuses) { - // Toggle mode - add checkmark for active statuses - item.onClick(async () => { - await this.statusService.toggleNoteStatus(status.name); - - // Get updated statuses - const updatedStatuses = this.statusService.getFileStatuses(activeFile); - - // Update current statuses and toolbar button - this.currentStatuses = updatedStatuses; - this.updateToolbarButton(); - - // Dispatch events for UI update - window.dispatchEvent(new CustomEvent('note-status:status-changed', { - detail: { statuses: updatedStatuses } - })); - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); - }); - } else { - // Single select mode - but still using arrays internally - item.onClick(async () => { - await this.statusService.updateNoteStatuses([status.name]); - - // Update current statuses and toolbar button - this.currentStatuses = [status.name]; - this.updateToolbarButton(); - - // Dispatch events for UI update - window.dispatchEvent(new CustomEvent('note-status:status-changed', { - detail: { statuses: [status.name] } - })); - window.dispatchEvent(new CustomEvent('note-status:refresh-ui')); - }); - } - - return item; - }); - }); - }); - - // Add separator and additional actions - menu.addSeparator(); - - // Add "batch update" option - menu.addItem((item) => { - item.setTitle('Batch Update Status...') - .setIcon('folders') - .onClick(() => { - // Dispatch event to open batch modal - window.dispatchEvent(new CustomEvent('note-status:open-batch-modal')); - }); - return item; - }); - - // Position menu near cursor - const cursor = editor.getCursor('to'); - editor.posToOffset(cursor); - const editorEl = view.contentEl.querySelector('.cm-content'); - - if (editorEl) { - const rect = editorEl.getBoundingClientRect(); - menu.showAtPosition({ x: rect.left + 20, y: rect.top + 40 }); - } else { - menu.showAtPosition({ x: 0, y: 0 }); - } + }, 100); + } + + /** + * Render method (kept for compatibility) + */ + public render(): void { + // This is now a no-op, as the dropdown component handles everything internally } /** * Remove dropdown when plugin is unloaded */ public unload(): void { - // Close any open popover - this.closeStatusPopover(); + // Clean up dropdown component + this.dropdownComponent.dispose(); // Remove toolbar button if (this.toolbarButtonContainer) { diff --git a/ui/status-pane-view.ts b/ui/status-pane-view.ts index 8cd32a4..69b770a 100644 --- a/ui/status-pane-view.ts +++ b/ui/status-pane-view.ts @@ -249,25 +249,27 @@ export class StatusPaneView extends View { private showFileContextMenu(e: MouseEvent, file: TFile): void { const menu = new Menu(); - + // Add status change options menu.addItem((item) => - item.setTitle('Change Status') - .setIcon('tag') - .onClick(() => { - this.plugin.showStatusContextMenu([file]); - }) + item.setTitle('Change Status') + .setIcon('tag') + .onClick(() => { + // Use the position from the event + const position = { x: e.clientX, y: e.clientY }; + this.plugin.statusContextMenu.showForFile(file, e); + }) ); - + // Add open options menu.addItem((item) => - item.setTitle('Open in New Tab') - .setIcon('lucide-external-link') - .onClick(() => { - this.app.workspace.openLinkText(file.path, file.path, 'tab'); - }) + item.setTitle('Open in New Tab') + .setIcon('lucide-external-link') + .onClick(() => { + this.app.workspace.openLinkText(file.path, file.path, 'tab'); + }) ); - + menu.showAtMouseEvent(e); }