feat(task-view): add modifier key navigation to open tasks in source file

Enable Cmd/Ctrl+click on task content to directly open and navigate to the task's location in its source file. This provides quick access to edit tasks in their original context while preserving inline editing as the default behavior.

- Add Keymap import for modifier key detection
- Implement openTaskInFile() method using eState for precise line navigation
- Update click handlers to check for modifier keys before showing inline editor
- Works in both list and tree view components
This commit is contained in:
Quorafind 2025-09-05 22:10:11 +08:00
parent 937d7b7723
commit 8700e54b72
2 changed files with 80 additions and 30 deletions

View file

@ -1,4 +1,4 @@
import { App, Component, Menu, setIcon } from "obsidian";
import { App, Component, Menu, setIcon, Keymap } from "obsidian";
import { Task } from "@/types/task";
import { MarkdownRendererComponent } from "@/components/ui/renderers/MarkdownRenderer";
import "@/styles/task-list.css";
@ -752,7 +752,9 @@ export class TaskListItemComponent extends Component {
this.addChild(this.markdownRenderer);
// Render the markdown content - 使用最新的 originalMarkdown
this.markdownRenderer.render(this.task.originalMarkdown || "\u200b");
this.markdownRenderer.render(
this.task.originalMarkdown || "\u200b"
);
// Re-register the click event for editing after rendering
this.registerContentClickHandler();
@ -776,39 +778,57 @@ export class TaskListItemComponent extends Component {
// Check if dynamic metadata positioning is enabled
if (!this.plugin.settings.enableDynamicMetadataPositioning) {
// If disabled, always use multi-line (traditional) layout
this.contentMetadataContainer.toggleClass("multi-line-content", true);
this.contentMetadataContainer.toggleClass("single-line-content", false);
this.contentMetadataContainer.toggleClass(
"multi-line-content",
true
);
this.contentMetadataContainer.toggleClass(
"single-line-content",
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;
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;
// Apply appropriate layout class using Obsidian's toggleClass method
this.contentMetadataContainer.toggleClass("multi-line-content", isMultiLine);
this.contentMetadataContainer.toggleClass("single-line-content", !isMultiLine);
this.contentMetadataContainer.toggleClass(
"multi-line-content",
isMultiLine
);
this.contentMetadataContainer.toggleClass(
"single-line-content",
!isMultiLine
);
}
/**
* Register click handler for content editing
*/
private registerContentClickHandler() {
// Only enable inline editing if the setting is enabled
if (!this.plugin.settings.enableInlineEditor) {
return;
}
// Make content clickable for editing
this.registerDomEvent(this.contentEl, "click", (e) => {
// Make content clickable for editing or navigation
this.registerDomEvent(this.contentEl, "click", async (e) => {
e.stopPropagation();
if (!this.isCurrentlyEditing()) {
// Check if modifier key is pressed (Cmd/Ctrl)
if (Keymap.isModEvent(e)) {
// Open task in file
await this.openTaskInFile();
} else if (
this.plugin.settings.enableInlineEditor &&
!this.isCurrentlyEditing()
) {
// Show inline editor only if enabled
this.getInlineEditor().showContentEditor(this.contentEl);
}
});
@ -837,18 +857,35 @@ export class TaskListItemComponent extends Component {
}
// If content or originalMarkdown changed, update the markdown display
if (oldTask.originalMarkdown !== task.originalMarkdown || oldTask.content !== task.content) {
if (
oldTask.originalMarkdown !== task.originalMarkdown ||
oldTask.content !== task.content
) {
// Re-render the markdown content
this.contentEl.empty();
this.renderMarkdown();
}
// Check if metadata changed and update metadata display
if (JSON.stringify(oldTask.metadata) !== JSON.stringify(task.metadata)) {
if (
JSON.stringify(oldTask.metadata) !== JSON.stringify(task.metadata)
) {
this.renderMetadata();
}
}
private async openTaskInFile() {
const file = this.app.vault.getFileByPath(this.task.filePath);
if (file) {
const leaf = this.app.workspace.getLeaf(false);
await leaf.openFile(file, {
eState: {
line: this.task.line,
},
});
}
}
public setSelected(selected: boolean) {
if (selected) {
this.element.classList.add("selected");

View file

@ -1,4 +1,4 @@
import { App, Component, setIcon, Menu } from "obsidian";
import { App, Component, setIcon, Menu, Keymap } from "obsidian";
import { Task } from "@/types/task";
import "@/styles/tree-view.css";
import { MarkdownRendererComponent } from "@/components/ui/renderers/MarkdownRenderer";
@ -885,15 +885,16 @@ export class TaskTreeItemComponent extends Component {
* Register click handler for content editing
*/
private registerContentClickHandler() {
// Only enable inline editing if the setting is enabled
if (!this.plugin.settings.enableInlineEditor) {
return;
}
// Make content clickable for editing - only create editor when clicked
this.registerDomEvent(this.contentEl, "click", (e) => {
// Make content clickable for editing or navigation
this.registerDomEvent(this.contentEl, "click", async (e) => {
e.stopPropagation();
if (!this.isCurrentlyEditing()) {
// Check if modifier key is pressed (Cmd/Ctrl)
if (Keymap.isModEvent(e)) {
// Open task in file
await this.openTaskInFile();
} else if (this.plugin.settings.enableInlineEditor && !this.isCurrentlyEditing()) {
// Show inline editor only if enabled
const editor = this.getInlineEditor();
editor.showContentEditor(this.contentEl);
}
@ -1102,6 +1103,18 @@ export class TaskTreeItemComponent extends Component {
}
}
private async openTaskInFile() {
const file = this.app.vault.getFileByPath(this.task.filePath);
if (file) {
const leaf = this.app.workspace.getLeaf(false);
await leaf.openFile(file, {
eState: {
line: this.task.line,
},
});
}
}
public setSelected(selected: boolean) {
this.isSelected = selected;
this.element.classList.toggle("selected", selected);