mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
perf: improve onboarding performance when open view with Obsidian onloading
This commit is contained in:
parent
c5677e6ba7
commit
baa280ea18
6 changed files with 710 additions and 244 deletions
|
|
@ -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<T extends Component>(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<string, Task>();
|
||||
// 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<string, Task>): number {
|
||||
private loadRootTaskBatch(
|
||||
taskMap: Map<string, Task>,
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, Task>;
|
||||
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<string, Task>,
|
||||
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<void> {
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue