From baa280ea189904171baece2788b177a2ff5ed54a Mon Sep 17 00:00:00 2001 From: Quorafind Date: Thu, 6 Nov 2025 22:55:27 +0800 Subject: [PATCH] perf: improve onboarding performance when open view with Obsidian onloading --- src/components/features/task/view/content.ts | 62 ++- src/components/features/task/view/listItem.ts | 434 +++++++++++++----- src/components/features/task/view/treeItem.ts | 387 ++++++++++++---- src/pages/TaskSpecificView.ts | 3 + src/styles/task-list.css | 43 +- src/styles/tree-view.css | 25 +- 6 files changed, 710 insertions(+), 244 deletions(-) diff --git a/src/components/features/task/view/content.ts b/src/components/features/task/view/content.ts index 25160cbf..5639abf9 100644 --- a/src/components/features/task/view/content.ts +++ b/src/components/features/task/view/content.ts @@ -348,17 +348,21 @@ export class ContentComponent extends Component { } private cleanupComponents() { - // Unload and clear previous components - this.taskComponents.forEach((component) => this.removeChild(component)); + this.disposeComponentList(this.taskComponents); + this.disposeComponentList(this.treeComponents); this.taskComponents = []; - this.treeComponents.forEach((component) => this.removeChild(component)); this.treeComponents = []; - // Disconnect observer from any previous elements - this.taskListObserver.disconnect(); - // Clear the container + this.taskListObserver?.disconnect(); this.taskListEl.empty(); } + private disposeComponentList(components: T[]) { + const snapshot = [...components]; + for (const component of snapshot) { + this.removeChild(component); + } + } + private isContainerVisible(): boolean { if (!this.containerEl) return false; const inDom = document.body.contains(this.containerEl); @@ -370,7 +374,7 @@ export class ContentComponent extends Component { return rect.width > 0 && rect.height > 0; } - private debounceRefreshTaskList = debounce(this.refreshTaskList, 700); + private debounceRefreshTaskList = debounce(this.refreshTaskList, 150); private refreshTaskList() { console.log("refreshing"); @@ -417,7 +421,13 @@ export class ContentComponent extends Component { const prevScrollState = this.captureScrollState(); try { - this.cleanupComponents(); // Clear previous state and components + const previousTaskComponents = [...this.taskComponents]; + const previousTreeComponents = [...this.treeComponents]; + + this.taskComponents = []; + this.treeComponents = []; + this.taskListObserver?.disconnect(); + this.removeLoadMarker(); // Reset indices for lazy loading this.nextTaskIndex = 0; @@ -425,11 +435,15 @@ export class ContentComponent extends Component { this.rootTasks = []; if (this.filteredTasks.length === 0) { + this.taskListEl.replaceChildren(); + this.disposeComponentList(previousTaskComponents); + this.disposeComponentList(previousTreeComponents); this.addEmptyState(t("No tasks found.")); return; } - // Render based on view mode + const renderTarget = document.createDocumentFragment(); + if (this.isTreeView) { const taskMap = new Map(); // Add all non-filtered tasks to the taskMap @@ -486,11 +500,15 @@ export class ContentComponent extends Component { return (a.line ?? 0) - (b.line ?? 0); }); } - this.loadRootTaskBatch(taskMap); // Load the first batch + this.loadRootTaskBatch(taskMap, renderTarget); // Load the first batch } else { - this.loadTaskBatch(); // Load the first batch + this.loadTaskBatch(renderTarget); // Load the first batch } + this.taskListEl.replaceChildren(renderTarget); + this.disposeComponentList(previousTaskComponents); + this.disposeComponentList(previousTreeComponents); + // Add load marker if necessary this.checkAndAddLoadMarker(); // Restore scroll state after render @@ -564,7 +582,9 @@ export class ContentComponent extends Component { container.scrollTop = state.scrollTop; } - private loadTaskBatch(): number { + private loadTaskBatch( + target: DocumentFragment | HTMLElement = this.taskListEl, + ): number { const fragment = document.createDocumentFragment(); const countToLoad = this.taskPageSize; const start = this.nextTaskIndex; @@ -589,11 +609,6 @@ export class ContentComponent extends Component { }; taskComponent.onTaskUpdate = async (originalTask, updatedTask) => { if (this.params.onTaskUpdate) { - console.log( - "ContentComponent onTaskUpdate", - originalTask.content, - updatedTask.content, - ); await this.params.onTaskUpdate(originalTask, updatedTask); } }; @@ -608,12 +623,15 @@ export class ContentComponent extends Component { this.taskComponents.push(taskComponent); // Keep track of rendered components } - this.taskListEl.appendChild(fragment); + target.appendChild(fragment); this.nextTaskIndex = end; // Update index for the next batch return end; // Return the new end index } - private loadRootTaskBatch(taskMap: Map): number { + private loadRootTaskBatch( + taskMap: Map, + target: DocumentFragment | HTMLElement = this.taskListEl, + ): number { const fragment = document.createDocumentFragment(); const countToLoad = this.taskPageSize; const start = this.nextRootTaskIndex; @@ -664,7 +682,7 @@ export class ContentComponent extends Component { this.treeComponents.push(treeComponent); // Keep track of rendered components } - this.taskListEl.appendChild(fragment); + target.appendChild(fragment); this.nextRootTaskIndex = end; // Update index for the next batch return end; // Return the new end index } @@ -736,7 +754,6 @@ export class ContentComponent extends Component { } private addEmptyState(message: string) { - this.cleanupComponents(); // Ensure list is clean const emptyEl = this.taskListEl.createDiv({ cls: "task-empty-state" }); emptyEl.setText(message); } @@ -829,8 +846,9 @@ export class ContentComponent extends Component { this.params.onTaskCompleted?.(t); }; taskComponent.onTaskUpdate = async (orig, upd) => { - if (this.params.onTaskUpdate) + if (this.params.onTaskUpdate) { await this.params.onTaskUpdate(orig, upd); + } }; taskComponent.onTaskContextMenu = (e, t) => { this.params.onTaskContextMenu?.(e, t); diff --git a/src/components/features/task/view/listItem.ts b/src/components/features/task/view/listItem.ts index 440fabb1..61df7e54 100644 --- a/src/components/features/task/view/listItem.ts +++ b/src/components/features/task/view/listItem.ts @@ -10,6 +10,7 @@ import { import { Task } from "@/types/task"; import { MarkdownRendererComponent } from "@/components/ui/renderers/MarkdownRenderer"; import "@/styles/task-list.css"; +import "@/styles/task-status-indicator.css"; import { createTaskCheckbox } from "./details"; import { getRelativeTimeString } from "@/utils/date/date-formatter"; import { t } from "@/translations/helper"; @@ -20,6 +21,7 @@ import { InlineEditorManager } from "./InlineEditorManager"; import { sanitizePriorityForClass } from "@/utils/task/priority-utils"; import { TaskSelectionManager } from "@/components/features/task/selection/TaskSelectionManager"; import { showBulkOperationsMenu } from "./BulkOperationsMenu"; +import { TaskStatusIndicator } from "./TaskStatusIndicator"; export class TaskListItemComponent extends Component { public element: HTMLElement; @@ -38,11 +40,22 @@ export class TaskListItemComponent extends Component { private metadataEl: HTMLElement; + private statusIndicator: TaskStatusIndicator | null = null; + private checkboxInput: HTMLInputElement | null = null; + private settings: TaskProgressBarSettings; // Use shared editor manager instead of individual editors private static editorManager: InlineEditorManager | null = null; + private static readonly PRIORITY_CONFIG = [ + { value: 5, key: "Highest", icon: "triangle", class: "highest" }, + { value: 4, key: "High", icon: "alert-triangle", class: "high" }, + { value: 3, key: "Medium", icon: "minus", class: "medium" }, + { value: 2, key: "Low", icon: "chevron-down", class: "low" }, + { value: 1, key: "Lowest", icon: "chevrons-down", class: "lowest" }, + ] as const; + // Selection management private selectionManager: TaskSelectionManager | null = null; private isTaskSelectedState = false; @@ -52,7 +65,7 @@ export class TaskListItemComponent extends Component { private viewMode: string, private app: App, private plugin: TaskProgressBarPlugin, - selectionManager?: TaskSelectionManager, + selectionManager?: TaskSelectionManager ) { super(); @@ -70,7 +83,7 @@ export class TaskListItemComponent extends Component { if (!TaskListItemComponent.editorManager) { TaskListItemComponent.editorManager = new InlineEditorManager( this.app, - this.plugin, + this.plugin ); } } @@ -86,7 +99,7 @@ export class TaskListItemComponent extends Component { try { await this.onTaskUpdate(originalTask, updatedTask); console.log( - "listItem onTaskUpdate completed successfully", + "listItem onTaskUpdate completed successfully" ); // Don't update task reference here - let onContentEditFinished handle it } catch (error) { @@ -99,7 +112,7 @@ export class TaskListItemComponent extends Component { }, onContentEditFinished: ( targetEl: HTMLElement, - updatedTask: Task, + updatedTask: Task ) => { // Update the task reference with the saved task this.task = updatedTask; @@ -112,13 +125,13 @@ export class TaskListItemComponent extends Component { // Release the editor from the manager TaskListItemComponent.editorManager?.releaseEditor( - this.task.id, + this.task.id ); }, onMetadataEditFinished: ( targetEl: HTMLElement, updatedTask: Task, - fieldType: string, + fieldType: string ) => { // Update the task reference with the saved task this.task = updatedTask; @@ -128,7 +141,7 @@ export class TaskListItemComponent extends Component { // Release the editor from the manager TaskListItemComponent.editorManager?.releaseEditor( - this.task.id, + this.task.id ); }, useEmbeddedEditor: true, // Enable Obsidian's embedded editor @@ -136,7 +149,7 @@ export class TaskListItemComponent extends Component { return TaskListItemComponent.editorManager!.getEditor( this.task, - editorOptions, + editorOptions ); } @@ -146,7 +159,7 @@ export class TaskListItemComponent extends Component { private isCurrentlyEditing(): boolean { return ( TaskListItemComponent.editorManager?.hasActiveEditor( - this.task.id, + this.task.id ) || false ); } @@ -159,8 +172,8 @@ export class TaskListItemComponent extends Component { "task-genius:selection-changed", () => { this.updateSelectionVisualState(); - }, - ), + } + ) ); // Setup long press detection for mobile @@ -172,7 +185,7 @@ export class TaskListItemComponent extends Component { this.selectionManager?.enterSelectionMode(); this.handleMultiSelect(); }, - }, + } ); } } @@ -197,11 +210,11 @@ export class TaskListItemComponent extends Component { () => { // Refresh view after operation // The parent view should handle this via task updates - }, + } ).catch((error) => { console.error( "Failed to show bulk operations menu:", - error, + error ); }); return; @@ -217,6 +230,12 @@ export class TaskListItemComponent extends Component { } private renderTaskItem() { + if (this.statusIndicator) { + this.removeChild(this.statusIndicator); + this.statusIndicator = null; + } + this.checkboxInput = null; + this.element.empty(); if (this.task.completed) { @@ -234,25 +253,69 @@ export class TaskListItemComponent extends Component { const checkbox = createTaskCheckbox( this.task.status, this.task, - el, + el ); + this.checkboxInput = checkbox; this.registerDomEvent(checkbox, "click", (event) => { event.stopPropagation(); - if (this.onTaskCompleted) { - this.onTaskCompleted(this.task); - } + // Check if we should merge indicator with checkbox + if ( + !this.plugin.settings.enableIndicatorWithCheckbox && + this.statusIndicator + ) { + // Cycle through statuses using the status indicator + this.statusIndicator.cycle(); + } else { + // Original behavior - just complete the task + if (this.onTaskCompleted) { + this.onTaskCompleted(this.task); + } - if (this.task.status === " ") { - checkbox.checked = true; - checkbox.dataset.task = "x"; + if (this.task.status === " ") { + checkbox.checked = true; + checkbox.dataset.task = "x"; + } } }); - }, + } ); this.element.appendChild(checkboxEl); + + // Always create the status indicator (for its logic), but only render it if not merged + this.statusIndicator = new TaskStatusIndicator({ + task: this.task, + plugin: this.plugin, + canInteract: () => !this.isCurrentlyEditing(), + onStatusChange: async (previousTask, updatedTask) => { + if (this.onTaskUpdate) { + await this.onTaskUpdate(previousTask, updatedTask); + } + + this.updateTask(updatedTask); + + if (this.checkboxInput) { + this.checkboxInput.dataset.task = updatedTask.status; + this.checkboxInput.checked = updatedTask.completed; + } + + this.statusIndicator?.updateTask(updatedTask); + }, + }); + + this.addChild(this.statusIndicator); + this.statusIndicator.load(); + + // Only render the status indicator in the DOM if not merged with checkbox + if (this.plugin.settings.enableIndicatorWithCheckbox) { + const statusWrapper = this.element.createDiv({ + cls: "task-status-indicator-wrapper", + }); + this.statusIndicator.render(statusWrapper); + } + this.containerEl = this.element.createDiv({ cls: "task-item-container", }); @@ -278,45 +341,81 @@ export class TaskListItemComponent extends Component { this.renderMetadata(); - // Priority indicator if available - if (this.task.metadata.priority) { - console.log("priority", this.task.metadata.priority); + const priorityValue = this.task.metadata.priority; - // Convert priority to numeric value + if (priorityValue) { let numericPriority: number; - if (typeof this.task.metadata.priority === "string") { - switch ((this.task.metadata.priority as string).toLowerCase()) { + if (typeof priorityValue === "number") { + numericPriority = priorityValue; + } else { + switch (priorityValue) { case "low": - numericPriority = 1; - break; - case "medium": numericPriority = 2; break; - case "high": + case "medium": numericPriority = 3; break; + case "high": + numericPriority = 4; + break; + case "highest": + numericPriority = 5; + break; + case "lowest": + numericPriority = 1; + break; default: - numericPriority = - parseInt(this.task.metadata.priority) || 1; + numericPriority = parseInt(priorityValue, 10) || 1; break; } - } else { - numericPriority = this.task.metadata.priority; } - const sanitizedPriority = sanitizePriorityForClass(numericPriority); + const priorityConfig = TaskListItemComponent.PRIORITY_CONFIG.find( + (config) => config.value === numericPriority + ); const classes = ["task-priority"]; - if (sanitizedPriority) { - classes.push(`priority-${sanitizedPriority}`); + if (priorityConfig) { + classes.push(`priority-${priorityConfig.class}`); } + + if (this.plugin.settings.enableInlineEditor) { + classes.push("task-priority-clickable"); + } + const priorityEl = createDiv({ cls: classes }); - - // Priority icon based on level - let icon = "•"; - icon = "!".repeat(numericPriority); - + const icon = "!".repeat(numericPriority); priorityEl.textContent = icon; + + if (this.plugin.settings.enableInlineEditor) { + const priorityTooltip = t("Click to set priority"); + priorityEl.setAttribute("aria-label", priorityTooltip); + // priorityEl.setAttribute("title", priorityTooltip); + + this.registerDomEvent(priorityEl, "click", (e) => { + e.stopPropagation(); + if (!this.isCurrentlyEditing()) { + this.showPriorityMenu(priorityEl); + } + }); + } + this.element.appendChild(priorityEl); + } else if (this.plugin.settings.enableInlineEditor) { + const addPriorityBtn = this.element.createEl("div", { + cls: "add-priority-btn", + attr: { + "aria-label": t("Click to set priority"), + // title: t("Click to set priority"), + role: "button", + }, + }); + + this.registerDomEvent(addPriorityBtn, "click", (e) => { + e.stopPropagation(); + if (!this.isCurrentlyEditing()) { + this.showPriorityMenu(addPriorityBtn); + } + }); } // Click handler to select task @@ -352,7 +451,7 @@ export class TaskListItemComponent extends Component { if (this.task.metadata.cancelledDate) { this.renderDateMetadata( "cancelled", - this.task.metadata.cancelledDate, + this.task.metadata.cancelledDate ); } @@ -369,7 +468,7 @@ export class TaskListItemComponent extends Component { if (this.task.metadata.scheduledDate) { this.renderDateMetadata( "scheduled", - this.task.metadata.scheduledDate, + this.task.metadata.scheduledDate ); } @@ -387,7 +486,7 @@ export class TaskListItemComponent extends Component { if (this.task.metadata.completedDate) { this.renderDateMetadata( "completed", - this.task.metadata.completedDate, + this.task.metadata.completedDate ); } @@ -395,7 +494,7 @@ export class TaskListItemComponent extends Component { if (this.task.metadata.createdDate) { this.renderDateMetadata( "created", - this.task.metadata.createdDate, + this.task.metadata.createdDate ); } } @@ -443,7 +542,7 @@ export class TaskListItemComponent extends Component { | "completed" | "cancelled" | "created", - dateValue: number, + dateValue: number ) { const dateEl = this.metadataEl.createEl("div", { cls: ["task-date", `task-${type}-date`], @@ -492,7 +591,7 @@ export class TaskListItemComponent extends Component { year: "numeric", month: "long", day: "numeric", - }); + }); } if (cssClass) { @@ -512,20 +611,20 @@ export class TaskListItemComponent extends Component { type === "due" ? "dueDate" : type === "scheduled" - ? "scheduledDate" - : type === "start" - ? "startDate" - : type === "cancelled" - ? "cancelledDate" - : type === "completed" - ? "completedDate" - : null; + ? "scheduledDate" + : type === "start" + ? "startDate" + : type === "cancelled" + ? "cancelledDate" + : type === "completed" + ? "completedDate" + : null; if (fieldType) { this.getInlineEditor().showMetadataEditor( dateEl, fieldType, - dateString, + dateString ); } } @@ -571,7 +670,7 @@ export class TaskListItemComponent extends Component { this.getInlineEditor().showMetadataEditor( projectEl, "project", - this.task.metadata.project || "", + this.task.metadata.project || "" ); } }); @@ -601,7 +700,7 @@ export class TaskListItemComponent extends Component { this.getInlineEditor().showMetadataEditor( tagsContainer, "tags", - tagsString, + tagsString ); } }); @@ -623,7 +722,7 @@ export class TaskListItemComponent extends Component { this.getInlineEditor().showMetadataEditor( recurrenceEl, "recurrence", - this.task.metadata.recurrence || "", + this.task.metadata.recurrence || "" ); } }); @@ -644,7 +743,7 @@ export class TaskListItemComponent extends Component { this.getInlineEditor().showMetadataEditor( onCompletionEl, "onCompletion", - this.task.metadata.onCompletion || "", + this.task.metadata.onCompletion || "" ); } }); @@ -656,7 +755,7 @@ export class TaskListItemComponent extends Component { cls: "task-dependson", }); dependsOnEl.textContent = `⛔ ${this.task.metadata.dependsOn?.join( - ", ", + ", " )}`; // Make dependsOn clickable for editing only if inline editor is enabled @@ -667,7 +766,7 @@ export class TaskListItemComponent extends Component { this.getInlineEditor().showMetadataEditor( dependsOnEl, "dependsOn", - this.task.metadata.dependsOn?.join(", ") || "", + this.task.metadata.dependsOn?.join(", ") || "" ); } }); @@ -688,7 +787,7 @@ export class TaskListItemComponent extends Component { this.getInlineEditor().showMetadataEditor( idEl, "id", - this.task.metadata.id || "", + this.task.metadata.id || "" ); } }); @@ -785,7 +884,7 @@ export class TaskListItemComponent extends Component { if (fieldsToShow.length === 0) { menu.addItem((item) => { item.setTitle( - "All metadata fields are already set", + "All metadata fields are already set" ).setDisabled(true); }); } else { @@ -802,7 +901,7 @@ export class TaskListItemComponent extends Component { editor.showMetadataEditor( tempContainer, - field.key as any, + field.key as any ); }); }); @@ -815,6 +914,64 @@ export class TaskListItemComponent extends Component { }); } + /** + * Show priority selection menu + */ + private showPriorityMenu(buttonEl: HTMLElement): void { + const menu = new Menu(); + + TaskListItemComponent.PRIORITY_CONFIG.forEach((config) => { + menu.addItem((item) => { + item.setTitle(t(config.key)) + .setIcon(config.icon) + .onClick(async () => { + await this.updateTaskPriority(config.value); + }); + }); + }); + + menu.addItem((item) => { + item.setTitle(t("Clear priority")) + .setIcon("minus") + .onClick(async () => { + await this.updateTaskPriority(null); + }); + }); + + const rect = buttonEl.getBoundingClientRect(); + + menu.showAtPosition({ + x: rect.left, + y: rect.bottom, + }); + } + + /** + * Update task priority and refresh UI + */ + private async updateTaskPriority(priority: number | null): Promise { + const metadata = { ...this.task.metadata }; + if (priority === null) { + delete metadata.priority; + } else { + metadata.priority = priority; + } + + const updatedTask: Task = { + ...this.task, + metadata, + }; + + try { + if (this.onTaskUpdate) { + await this.onTaskUpdate(this.task, updatedTask); + } + this.updateTask(updatedTask); + } catch (error) { + console.error("Failed to update task priority:", error); + } + } + private formatDateForInput(date: Date): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); @@ -831,30 +988,22 @@ export class TaskListItemComponent extends Component { // Clear the content element this.contentEl.empty(); - // 使用 requestAnimationFrame 确保 DOM 完全清理后再渲染新内容 - requestAnimationFrame(() => { - // Create new renderer - this.markdownRenderer = new MarkdownRendererComponent( - this.app, - this.contentEl, - this.task.filePath, - ); - this.addChild(this.markdownRenderer); + // Create new renderer immediately (no need for async) + this.markdownRenderer = new MarkdownRendererComponent( + this.app, + this.contentEl, + this.task.filePath + ); + this.addChild(this.markdownRenderer); - // Render the markdown content - 使用最新的 originalMarkdown - this.markdownRenderer.render( - this.task.originalMarkdown || "\u200b", - ); + // Render the markdown content + this.markdownRenderer.render(this.task.originalMarkdown || "\u200b"); - // Re-register the click event for editing after rendering - this.registerContentClickHandler(); + // Re-register the click event for editing after rendering + this.registerContentClickHandler(); - // Update layout mode after content is rendered - // Use another requestAnimationFrame to ensure the content is fully rendered - requestAnimationFrame(() => { - this.updateLayoutMode(); - }); - }); + // Update layout mode synchronously - no waiting needed + this.updateLayoutMode(); } /** @@ -870,38 +1019,63 @@ export class TaskListItemComponent extends Component { // If disabled, always use multi-line (traditional) layout this.contentMetadataContainer.toggleClass( "multi-line-content", - true, + true ); this.contentMetadataContainer.toggleClass( "single-line-content", - false, + false ); return; } - // Get the line height of the content element - const computedStyle = window.getComputedStyle(this.contentEl); - const lineHeight = - parseFloat(computedStyle.lineHeight) || - parseFloat(computedStyle.fontSize) * 1.4; - - // Get actual content height - const contentHeight = this.contentEl.scrollHeight; - - // Check if content is multi-line (with some tolerance) - const isMultiLine = contentHeight > lineHeight * 1.2; + // Fast synchronous detection using text content analysis + const isMultiLine = this.detectMultiLineContent(); // Apply appropriate layout class using Obsidian's toggleClass method this.contentMetadataContainer.toggleClass( "multi-line-content", - isMultiLine, + isMultiLine ); this.contentMetadataContainer.toggleClass( "single-line-content", - !isMultiLine, + !isMultiLine ); } + /** + * Fast detection of multi-line content using text analysis + */ + private detectMultiLineContent(): boolean { + if (!this.contentEl) return true; + + // Method 1: Check for line breaks in text content + const textContent = this.contentEl.textContent || ""; + if (textContent.includes("\n") || textContent.includes("\r")) { + return true; + } + + // Method 2: Quick DOM measurement without layout thrashing + const computedStyle = window.getComputedStyle(this.contentEl); + const fontSize = parseFloat(computedStyle.fontSize) || 16; + const lineHeight = + parseFloat(computedStyle.lineHeight) || fontSize * 1.4; + + // Method 3: Check if scrollHeight is significantly larger than a single line + // Use a smaller threshold to avoid false positives + const contentHeight = this.contentEl.scrollHeight; + const isMultiLine = contentHeight > lineHeight * 1.1; + + // Method 4: Check for elements that typically cause multi-line layout + const hasBlockElements = this.contentEl.querySelector( + "br, div, p, ul, ol, li, blockquote" + ); + if (hasBlockElements) { + return true; + } + + return isMultiLine; + } + /** * Register click handler for content editing */ @@ -942,29 +1116,39 @@ export class TaskListItemComponent extends Component { const oldTask = this.task; this.task = task; - // Update completion status - if (oldTask.completed !== task.completed) { - if (task.completed) { - this.element.classList.add("task-completed"); - } else { - this.element.classList.remove("task-completed"); - } + // Batch DOM updates to minimize reflows + let needsContentUpdate = false; + let needsMetadataUpdate = false; + + // Check what needs updating + const contentChanged = + oldTask.originalMarkdown !== task.originalMarkdown || + oldTask.content !== task.content; + const metadataChanged = + JSON.stringify(oldTask.metadata) !== JSON.stringify(task.metadata); + const completedChanged = oldTask.completed !== task.completed; + + // Update checkbox state + if (this.checkboxInput) { + this.checkboxInput.dataset.task = task.status; + this.checkboxInput.checked = !!task.completed; } - // If content or originalMarkdown changed, update the markdown display - if ( - oldTask.originalMarkdown !== task.originalMarkdown || - oldTask.content !== task.content - ) { - // Re-render the markdown content - this.contentEl.empty(); + // Update status indicator + this.statusIndicator?.updateTask(task); + + // Update completion status + if (completedChanged) { + this.element.toggleClass("task-completed", task.completed); + } + + // If content changed, update the markdown display + if (contentChanged) { this.renderMarkdown(); } - // Check if metadata changed and update metadata display - if ( - JSON.stringify(oldTask.metadata) !== JSON.stringify(task.metadata) - ) { + // If metadata changed, update metadata display + if (metadataChanged) { this.renderMetadata(); } } @@ -1024,6 +1208,12 @@ export class TaskListItemComponent extends Component { TaskListItemComponent.editorManager.releaseEditor(this.task.id); } + if (this.statusIndicator) { + this.removeChild(this.statusIndicator); + this.statusIndicator = null; + } + this.checkboxInput = null; + this.element.detach(); } } diff --git a/src/components/features/task/view/treeItem.ts b/src/components/features/task/view/treeItem.ts index 9eab4b77..e38d144f 100644 --- a/src/components/features/task/view/treeItem.ts +++ b/src/components/features/task/view/treeItem.ts @@ -9,6 +9,7 @@ import { } from "obsidian"; import { Task } from "@/types/task"; import "@/styles/tree-view.css"; +import "@/styles/task-status-indicator.css"; import { MarkdownRendererComponent } from "@/components/ui/renderers/MarkdownRenderer"; import { createTaskCheckbox } from "./details"; import { getViewSettingOrDefault, ViewMode } from "@/common/setting-definition"; @@ -21,6 +22,7 @@ import { sanitizePriorityForClass } from "@/utils/task/priority-utils"; import { sortTasks } from "@/commands/sortTaskCommands"; import { TaskSelectionManager } from "@/components/features/task/selection/TaskSelectionManager"; import { showBulkOperationsMenu } from "./BulkOperationsMenu"; +import { TaskStatusIndicator } from "./TaskStatusIndicator"; export class TaskTreeItemComponent extends Component { public element: HTMLElement; @@ -47,10 +49,19 @@ export class TaskTreeItemComponent extends Component { private contentEl: HTMLElement; private contentMetadataContainer: HTMLElement; private taskMap: Map; + private statusIndicator: TaskStatusIndicator | null = null; // Use shared editor manager instead of individual editors private static editorManager: InlineEditorManager | null = null; + private static readonly PRIORITY_CONFIG = [ + { value: 5, key: "Highest", icon: "triangle", class: "highest" }, + { value: 4, key: "High", icon: "alert-triangle", class: "high" }, + { value: 3, key: "Medium", icon: "minus", class: "medium" }, + { value: 2, key: "Low", icon: "chevron-down", class: "low" }, + { value: 1, key: "Lowest", icon: "chevrons-down", class: "lowest" }, + ] as const; + // Selection management private selectionManager: TaskSelectionManager | null = null; private isTaskSelectedState: boolean = false; @@ -63,7 +74,7 @@ export class TaskTreeItemComponent extends Component { private childTasks: Task[] = [], taskMap: Map, private plugin: TaskProgressBarPlugin, - selectionManager?: TaskSelectionManager, + selectionManager?: TaskSelectionManager ) { super(); this.task = task; @@ -78,7 +89,7 @@ export class TaskTreeItemComponent extends Component { if (!TaskTreeItemComponent.editorManager) { TaskTreeItemComponent.editorManager = new InlineEditorManager( this.app, - this.plugin, + this.plugin ); } } @@ -93,7 +104,7 @@ export class TaskTreeItemComponent extends Component { try { await this.onTaskUpdate(originalTask, updatedTask); console.log( - "treeItem onTaskUpdate completed successfully", + "treeItem onTaskUpdate completed successfully" ); // Don't update task reference here - let onContentEditFinished handle it } catch (error) { @@ -106,7 +117,7 @@ export class TaskTreeItemComponent extends Component { }, onContentEditFinished: ( targetEl: HTMLElement, - updatedTask: Task, + updatedTask: Task ) => { // Update the task reference with the saved task this.task = updatedTask; @@ -119,13 +130,13 @@ export class TaskTreeItemComponent extends Component { // Release the editor from the manager TaskTreeItemComponent.editorManager?.releaseEditor( - this.task.id, + this.task.id ); }, onMetadataEditFinished: ( targetEl: HTMLElement, updatedTask: Task, - fieldType: string, + fieldType: string ) => { // Update the task reference with the saved task this.task = updatedTask; @@ -135,7 +146,7 @@ export class TaskTreeItemComponent extends Component { // Release the editor from the manager TaskTreeItemComponent.editorManager?.releaseEditor( - this.task.id, + this.task.id ); }, useEmbeddedEditor: true, // Enable Obsidian's embedded editor @@ -143,7 +154,7 @@ export class TaskTreeItemComponent extends Component { return TaskTreeItemComponent.editorManager!.getEditor( this.task, - editorOptions, + editorOptions ); } @@ -153,7 +164,7 @@ export class TaskTreeItemComponent extends Component { private isCurrentlyEditing(): boolean { return ( TaskTreeItemComponent.editorManager?.hasActiveEditor( - this.task.id, + this.task.id ) || false ); } @@ -174,8 +185,8 @@ export class TaskTreeItemComponent extends Component { "task-genius:selection-changed", () => { this.updateSelectionVisualState(); - }, - ), + } + ) ); // Setup long press detection for mobile @@ -187,7 +198,7 @@ export class TaskTreeItemComponent extends Component { this.selectionManager?.enterSelectionMode(); this.handleMultiSelect(); }, - }, + } ); } } @@ -208,9 +219,12 @@ export class TaskTreeItemComponent extends Component { this.selectionManager, () => { // Optionally refresh the view after bulk operation - }, + } ).catch((error) => { - console.error("Failed to show bulk operations menu:", error); + console.error( + "Failed to show bulk operations menu:", + error + ); }); return; } @@ -244,7 +258,7 @@ export class TaskTreeItemComponent extends Component { this.parentContainer.contains(e.target as Node) ) { const isCheckbox = (e.target as HTMLElement).classList.contains( - "task-checkbox", + "task-checkbox" ); if (isCheckbox) { @@ -252,7 +266,7 @@ export class TaskTreeItemComponent extends Component { this.toggleTaskCompletion(); } else if ( (e.target as HTMLElement).classList.contains( - "task-expand-toggle", + "task-expand-toggle" ) ) { e.stopPropagation(); @@ -286,6 +300,11 @@ export class TaskTreeItemComponent extends Component { private renderTaskContent() { // Clear existing content + if (this.statusIndicator) { + this.removeChild(this.statusIndicator); + this.statusIndicator = null; + } + this.parentContainer.empty(); this.parentContainer.classList.toggle("completed", this.task.completed); this.parentContainer.classList.toggle("selected", this.isSelected); @@ -308,7 +327,7 @@ export class TaskTreeItemComponent extends Component { }); setIcon( this.toggleEl, - this.isExpanded ? "chevron-down" : "chevron-right", + this.isExpanded ? "chevron-down" : "chevron-right" ); // Register toggle event @@ -327,24 +346,59 @@ export class TaskTreeItemComponent extends Component { const checkbox = createTaskCheckbox( this.task.status, this.task, - el, + el ); this.registerDomEvent(checkbox, "click", (event) => { event.stopPropagation(); - if (this.onTaskCompleted) { - this.onTaskCompleted(this.task); - } + // Check if we should merge indicator with checkbox + if ( + !this.plugin.settings.enableIndicatorWithCheckbox && + this.statusIndicator + ) { + // Cycle through statuses using the status indicator + this.statusIndicator.cycle(); + } else { + // Original behavior - just complete the task + if (this.onTaskCompleted) { + this.onTaskCompleted(this.task); + } - if (this.task.status === " ") { - checkbox.checked = true; - checkbox.dataset.task = "x"; + if (this.task.status === " ") { + checkbox.checked = true; + checkbox.dataset.task = "x"; + } } }); - }, + } ); + // Always create the status indicator (for its logic), but only render it if not merged + this.statusIndicator = new TaskStatusIndicator({ + task: this.task, + plugin: this.plugin, + canInteract: () => !this.isCurrentlyEditing(), + onStatusChange: async (previousTask, updatedTask) => { + if (this.onTaskUpdate) { + await this.onTaskUpdate(previousTask, updatedTask); + } + + this.updateTask(updatedTask); + }, + }); + + this.addChild(this.statusIndicator); + this.statusIndicator.load(); + + // Only render the status indicator in the DOM if not merged with checkbox + if (this.plugin.settings.enableIndicatorWithCheckbox) { + const statusWrapper = this.parentContainer.createDiv({ + cls: "task-status-indicator-wrapper", + }); + this.statusIndicator.render(statusWrapper); + } + const taskItemContainer = this.parentContainer.createDiv({ cls: "task-item-container", }); @@ -371,23 +425,81 @@ export class TaskTreeItemComponent extends Component { this.renderMetadata(metadataEl); - // Priority indicator if available - if (this.task.metadata.priority) { - const sanitizedPriority = sanitizePriorityForClass( - this.task.metadata.priority, - ); + const priorityValue = this.task.metadata.priority; + + if (priorityValue) { + let numericPriority: number; + if (typeof priorityValue === "number") { + numericPriority = priorityValue; + } else { + switch (priorityValue) { + case "low": + numericPriority = 2; + break; + case "medium": + numericPriority = 3; + break; + case "high": + numericPriority = 4; + break; + case "highest": + numericPriority = 5; + break; + case "lowest": + numericPriority = 1; + break; + default: + numericPriority = parseInt(priorityValue, 10) || 1; + break; + } + } + + const sanitizedPriority = sanitizePriorityForClass(numericPriority); const classes = ["task-priority"]; if (sanitizedPriority) { classes.push(`priority-${sanitizedPriority}`); } + + if (this.plugin.settings.enableInlineEditor) { + classes.push("task-priority-clickable"); + } + const priorityEl = createDiv({ cls: classes }); - - // Priority icon based on level - let icon = "•"; - icon = "!".repeat(this.task.metadata.priority); - + const icon = "!".repeat(numericPriority); priorityEl.textContent = icon; - this.parentContainer.appendChild(priorityEl); + + if (this.plugin.settings.enableInlineEditor) { + const priorityTooltip = t("Click to set priority"); + priorityEl.setAttribute("aria-label", priorityTooltip); + // priorityEl.setAttribute("title", priorityTooltip); + + this.registerDomEvent(priorityEl, "click", (e) => { + e.stopPropagation(); + if (!this.isCurrentlyEditing()) { + this.showPriorityMenu(priorityEl); + } + }); + } + + metadataEl.appendChild(priorityEl); + } else if (this.plugin.settings.enableInlineEditor) { + const addPriorityBtn = metadataEl.createDiv({ + cls: "add-priority-btn", + attr: { + "aria-label": t("Click to set priority"), + // title: t("Click to set priority"), + role: "button", + }, + }); + + // setIcon(addPriorityBtn, "star"); + + this.registerDomEvent(addPriorityBtn, "click", (e) => { + e.stopPropagation(); + if (!this.isCurrentlyEditing()) { + this.showPriorityMenu(addPriorityBtn); + } + }); } } @@ -399,7 +511,7 @@ export class TaskTreeItemComponent extends Component { this.renderDateMetadata( metadataEl, "cancelled", - this.task.metadata.cancelledDate, + this.task.metadata.cancelledDate ); } @@ -410,7 +522,7 @@ export class TaskTreeItemComponent extends Component { this.renderDateMetadata( metadataEl, "due", - this.task.metadata.dueDate, + this.task.metadata.dueDate ); } @@ -419,7 +531,7 @@ export class TaskTreeItemComponent extends Component { this.renderDateMetadata( metadataEl, "scheduled", - this.task.metadata.scheduledDate, + this.task.metadata.scheduledDate ); } @@ -428,7 +540,7 @@ export class TaskTreeItemComponent extends Component { this.renderDateMetadata( metadataEl, "start", - this.task.metadata.startDate, + this.task.metadata.startDate ); } @@ -442,7 +554,7 @@ export class TaskTreeItemComponent extends Component { this.renderDateMetadata( metadataEl, "completed", - this.task.metadata.completedDate, + this.task.metadata.completedDate ); } @@ -451,7 +563,7 @@ export class TaskTreeItemComponent extends Component { this.renderDateMetadata( metadataEl, "created", - this.task.metadata.createdDate, + this.task.metadata.createdDate ); } } @@ -500,7 +612,7 @@ export class TaskTreeItemComponent extends Component { | "completed" | "cancelled" | "created", - dateValue: number, + dateValue: number ) { const dateEl = metadataEl.createEl("div", { cls: ["task-date", `task-${type}-date`], @@ -549,7 +661,7 @@ export class TaskTreeItemComponent extends Component { year: "numeric", month: "long", day: "numeric", - }); + }); } if (cssClass) { @@ -570,20 +682,20 @@ export class TaskTreeItemComponent extends Component { type === "due" ? "dueDate" : type === "scheduled" - ? "scheduledDate" - : type === "start" - ? "startDate" - : type === "cancelled" - ? "cancelledDate" - : type === "completed" - ? "completedDate" - : null; + ? "scheduledDate" + : type === "start" + ? "startDate" + : type === "cancelled" + ? "cancelledDate" + : type === "completed" + ? "completedDate" + : null; if (fieldType) { editor.showMetadataEditor( dateEl, fieldType, - dateString, + dateString ); } } @@ -630,7 +742,7 @@ export class TaskTreeItemComponent extends Component { editor.showMetadataEditor( projectEl, "project", - this.task.metadata.project || "", + this.task.metadata.project || "" ); } }); @@ -665,7 +777,7 @@ export class TaskTreeItemComponent extends Component { editor.showMetadataEditor( tagsContainer, "tags", - tagsString, + tagsString ); } }); @@ -688,7 +800,7 @@ export class TaskTreeItemComponent extends Component { editor.showMetadataEditor( recurrenceEl, "recurrence", - this.task.metadata.recurrence || "", + this.task.metadata.recurrence || "" ); } }); @@ -710,7 +822,7 @@ export class TaskTreeItemComponent extends Component { editor.showMetadataEditor( onCompletionEl, "onCompletion", - this.task.metadata.onCompletion || "", + this.task.metadata.onCompletion || "" ); } }); @@ -722,7 +834,7 @@ export class TaskTreeItemComponent extends Component { cls: "task-dependson", }); dependsOnEl.textContent = `⛔ ${this.task.metadata.dependsOn?.join( - ", ", + ", " )}`; // Make dependsOn clickable for editing only if inline editor is enabled @@ -734,7 +846,7 @@ export class TaskTreeItemComponent extends Component { editor.showMetadataEditor( dependsOnEl, "dependsOn", - this.task.metadata.dependsOn?.join(", ") || "", + this.task.metadata.dependsOn?.join(", ") || "" ); } }); @@ -756,7 +868,7 @@ export class TaskTreeItemComponent extends Component { editor.showMetadataEditor( idEl, "id", - this.task.metadata.id || "", + this.task.metadata.id || "" ); } }); @@ -853,7 +965,7 @@ export class TaskTreeItemComponent extends Component { if (fieldsToShow.length === 0) { menu.addItem((item) => { item.setTitle( - "All metadata fields are already set", + "All metadata fields are already set" ).setDisabled(true); }); } else { @@ -870,7 +982,7 @@ export class TaskTreeItemComponent extends Component { editor.showMetadataEditor( tempContainer, - field.key as any, + field.key as any ); }); }); @@ -883,6 +995,63 @@ export class TaskTreeItemComponent extends Component { }); } + /** + * Show priority selection menu + */ + private showPriorityMenu(buttonEl: HTMLElement): void { + const menu = new Menu(); + + TaskTreeItemComponent.PRIORITY_CONFIG.forEach((config) => { + menu.addItem((item) => { + item.setTitle(t(config.key)) + .setIcon(config.icon) + .onClick(async () => { + await this.updateTaskPriority(config.value); + }); + }); + }); + + menu.addItem((item) => { + item.setTitle(t("Clear priority")) + .setIcon("minus") + .onClick(async () => { + await this.updateTaskPriority(null); + }); + }); + + const rect = buttonEl.getBoundingClientRect(); + menu.showAtPosition({ + x: rect.left, + y: rect.bottom, + }); + } + + /** + * Update task priority and refresh UI + */ + private async updateTaskPriority(priority: number | null): Promise { + const metadata = { ...this.task.metadata }; + if (priority === null) { + delete metadata.priority; + } else { + metadata.priority = priority; + } + + const updatedTask: Task = { + ...this.task, + metadata, + }; + + try { + if (this.onTaskUpdate) { + await this.onTaskUpdate(this.task, updatedTask); + } + this.updateTask(updatedTask); + } catch (error) { + console.error("Failed to update task priority:", error); + } + } + private formatDateForInput(date: Date): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); @@ -899,11 +1068,11 @@ export class TaskTreeItemComponent extends Component { // Clear the content element this.contentEl.empty(); - // Create new renderer + // Create new renderer immediately (no need for async) this.markdownRenderer = new MarkdownRendererComponent( this.app, this.contentEl, - this.task.filePath, + this.task.filePath ); this.addChild(this.markdownRenderer); @@ -913,11 +1082,8 @@ export class TaskTreeItemComponent extends Component { // Re-register the click event for editing after rendering this.registerContentClickHandler(); - // Update layout mode after content is rendered - // Use requestAnimationFrame to ensure the content is fully rendered - requestAnimationFrame(() => { - this.updateLayoutMode(); - }); + // Update layout mode synchronously - no waiting needed + this.updateLayoutMode(); } /** @@ -933,38 +1099,63 @@ export class TaskTreeItemComponent extends Component { // If disabled, always use multi-line (traditional) layout this.contentMetadataContainer.toggleClass( "multi-line-content", - true, + true ); this.contentMetadataContainer.toggleClass( "single-line-content", - false, + false ); return; } - // Get the line height of the content element - const computedStyle = window.getComputedStyle(this.contentEl); - const lineHeight = - parseFloat(computedStyle.lineHeight) || - parseFloat(computedStyle.fontSize) * 1.4; - - // Get actual content height - const contentHeight = this.contentEl.scrollHeight; - - // Check if content is multi-line (with some tolerance) - const isMultiLine = contentHeight > lineHeight * 1.2; + // Fast synchronous detection using text content analysis + const isMultiLine = this.detectMultiLineContent(); // Apply appropriate layout class using Obsidian's toggleClass method this.contentMetadataContainer.toggleClass( "multi-line-content", - isMultiLine, + isMultiLine ); this.contentMetadataContainer.toggleClass( "single-line-content", - !isMultiLine, + !isMultiLine ); } + /** + * Fast detection of multi-line content using text analysis + */ + private detectMultiLineContent(): boolean { + if (!this.contentEl) return true; + + // Method 1: Check for line breaks in text content + const textContent = this.contentEl.textContent || ""; + if (textContent.includes("\n") || textContent.includes("\r")) { + return true; + } + + // Method 2: Quick DOM measurement without layout thrashing + const computedStyle = window.getComputedStyle(this.contentEl); + const fontSize = parseFloat(computedStyle.fontSize) || 16; + const lineHeight = + parseFloat(computedStyle.lineHeight) || fontSize * 1.4; + + // Method 3: Check if scrollHeight is significantly larger than a single line + // Use a smaller threshold to avoid false positives + const contentHeight = this.contentEl.scrollHeight; + const isMultiLine = contentHeight > lineHeight * 1.1; + + // Method 4: Check for elements that typically cause multi-line layout + const hasBlockElements = this.contentEl.querySelector( + "br, div, p, ul, ol, li, blockquote" + ); + if (hasBlockElements) { + return true; + } + + return isMultiLine; + } + /** * Register click handler for content editing */ @@ -1013,7 +1204,7 @@ export class TaskTreeItemComponent extends Component { // Get view configuration to check if we should hide completed and abandoned tasks const viewConfig = getViewSettingOrDefault( this.plugin, - this.viewMode as ViewMode, + this.viewMode as ViewMode ); const abandonedStatus = this.plugin.settings.taskStatuses.abandoned.split("|"); @@ -1037,7 +1228,7 @@ export class TaskTreeItemComponent extends Component { tasksToRender = sortTasks( [...tasksToRender], childSortCriteria, - this.plugin.settings, + this.plugin.settings ); } else { // Default sorting: incomplete first, then priority (high->low), due date (earlier->later), content; tie-break by filePath->line @@ -1061,7 +1252,7 @@ export class TaskTreeItemComponent extends Component { }); const contentCmp = collator.compare( a.content ?? "", - b.content ?? "", + b.content ?? "" ); if (contentCmp !== 0) return contentCmp; const fp = (a.filePath || "").localeCompare(b.filePath || ""); @@ -1088,7 +1279,7 @@ export class TaskTreeItemComponent extends Component { grandchildren, // Pass the correctly found grandchildren this.taskMap, // Pass the map down recursively this.plugin, // Pass the plugin down - this.selectionManager || undefined, // Pass selection manager down + this.selectionManager || undefined // Pass selection manager down ); // Pass up events @@ -1178,7 +1369,7 @@ export class TaskTreeItemComponent extends Component { if (this.toggleEl instanceof HTMLElement) { setIcon( this.toggleEl, - this.isExpanded ? "chevron-down" : "chevron-right", + this.isExpanded ? "chevron-down" : "chevron-right" ); } @@ -1214,6 +1405,7 @@ export class TaskTreeItemComponent extends Component { const oldTask = this.task; this.task = task; this.renderTaskContent(); + this.statusIndicator?.updateTask(task); // Update completion status if (oldTask.completed !== task.completed) { @@ -1240,7 +1432,7 @@ export class TaskTreeItemComponent extends Component { ) { // Re-render metadata const metadataEl = this.parentContainer.querySelector( - ".task-metadata", + ".task-metadata" ) as HTMLElement; if (metadataEl) { this.renderMetadata(metadataEl); @@ -1322,26 +1514,26 @@ export class TaskTreeItemComponent extends Component { if (elementToToggle) { elementToToggle.classList.toggle( "is-selected", - this.isSelected, + this.isSelected ); // Also ensure the parent container reflects selection if separate element if (this.parentContainer) { this.parentContainer.classList.toggle( "selected", - this.isSelected, + this.isSelected ); } } else { console.warn( "Could not find element to toggle selection class for task:", - this.task.id, + this.task.id ); } } // Recursively update children this.childComponents.forEach((child) => - child.updateSelectionVisuals(selectedId), + child.updateSelectionVisuals(selectedId) ); } @@ -1353,7 +1545,7 @@ export class TaskTreeItemComponent extends Component { if (this.toggleEl instanceof HTMLElement) { setIcon( this.toggleEl, - this.isExpanded ? "chevron-down" : "chevron-right", + this.isExpanded ? "chevron-down" : "chevron-right" ); } @@ -1401,6 +1593,11 @@ export class TaskTreeItemComponent extends Component { TaskTreeItemComponent.editorManager.releaseEditor(this.task.id); } + if (this.statusIndicator) { + this.removeChild(this.statusIndicator); + this.statusIndicator = null; + } + // Clean up child components this.childComponents.forEach((component) => { component.unload(); diff --git a/src/pages/TaskSpecificView.ts b/src/pages/TaskSpecificView.ts index a03964e7..10065d4f 100644 --- a/src/pages/TaskSpecificView.ts +++ b/src/pages/TaskSpecificView.ts @@ -331,6 +331,9 @@ export class TaskSpecificView extends ItemView { onTaskCompleted: (task: Task) => { this.toggleTaskCompletion(task); }, + onTaskUpdate: async (originalTask: Task, updatedTask: Task) => { + await this.handleTaskUpdate(originalTask, updatedTask); + }, onTaskContextMenu: (event: MouseEvent, task: Task) => { this.handleTaskContextMenu(event, task); }, diff --git a/src/styles/task-list.css b/src/styles/task-list.css index e3733845..3c3c19dc 100644 --- a/src/styles/task-list.css +++ b/src/styles/task-list.css @@ -77,7 +77,9 @@ flex-wrap: wrap; align-items: flex-start; gap: var(--size-2-2); - transition: all 0.2s ease; + /* Remove transition to prevent flash during layout changes */ + /* Use GPU-accelerated properties only if transition is needed */ + will-change: auto; } /* Single line content layout - horizontal */ @@ -253,6 +255,45 @@ color: var(--text-accent); } +.task-priority-clickable { + cursor: pointer; + transition: opacity 0.2s ease; +} + +.task-priority-clickable:hover { + opacity: 0.7; +} + +.add-priority-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + margin-left: 4px; + padding: 0; + /* border: 1px dashed var(--text-muted); */ + border-radius: 3px; + background: transparent; + color: var(--text-muted); + font-size: 12px; + cursor: pointer; + transition: all 0.2s ease; +} + +.add-priority-btn:hover { + border-color: var(--text-normal); + color: var(--text-normal); + background: var(--background-modifier-hover); +} + +.task-item.tree-task-item .task-priority-clickable, +.task-item.tree-task-item .add-priority-btn { + position: absolute; + right: var(--size-4-2); + top: var(--size-4-2); +} + /* New field styles */ .task-oncompletion { display: inline-flex; diff --git a/src/styles/tree-view.css b/src/styles/tree-view.css index f939748a..cc80730c 100644 --- a/src/styles/tree-view.css +++ b/src/styles/tree-view.css @@ -115,12 +115,16 @@ align-items: center; } -.tree-task-item .task-content-metadata-container.single-line-content .task-item-content { +.tree-task-item + .task-content-metadata-container.single-line-content + .task-item-content { flex: 1; min-width: 0; /* Allow content to shrink */ } -.tree-task-item .task-content-metadata-container.single-line-content .task-metadata { +.tree-task-item + .task-content-metadata-container.single-line-content + .task-metadata { flex-shrink: 0; margin-top: 0; } @@ -131,11 +135,15 @@ align-items: flex-start; } -.tree-task-item .task-content-metadata-container.multi-line-content .task-item-content { +.tree-task-item + .task-content-metadata-container.multi-line-content + .task-item-content { width: 100%; } -.tree-task-item .task-content-metadata-container.multi-line-content .task-metadata { +.tree-task-item + .task-content-metadata-container.multi-line-content + .task-metadata { margin-top: 4px; } @@ -175,6 +183,15 @@ color: var(--text-accent); } +.task-priority-clickable { + cursor: pointer; + transition: opacity 0.2s ease; +} + +.task-priority-clickable:hover { + opacity: 0.7; +} + /* New field styles for tree view */ .tree-task-item .task-oncompletion { display: inline-flex;