How to support renaming from File header view title, like Obsidian does?

Hello!

When you create a note, Obsidian will highlight the “Untitled” in the header of the file view so the user can already type a name for that Note (this is the behavior if you disable Inline title). Also, you can always click on the title to rename the file.

I’m developing a plugin for Obsidian that extends from the file TextFileView, and I would like to reproduce this behavior given that my view doesn’t have any line title and also because it’s a perfect user flow, from my opinion.

I tried to find in the documentation and here how to reproduce this behavior, but without success. Can someone share some light on how I could build this?

Thank you!

Ok, I did a more deep research and I want to share the solution I found. The main problem was that since my extension uses a file that’s not in Markdown format, some built-in behavior is not available by default.

There are 2 things that enable this behavior:

  • Calling `await super.onOpen();` and `await super.onClose();` on their respectives extended methods in your View class, if they are extended.
  • Calling the `createNewMarkdownFile` method when opening the the View

A good example can be found in obsidian-kanban plugin, check their main.ts file in Github.

However, if your plugin doesn’t work with Markdown, you need to implement this behavior directly in your method that creates a new file:

    // The header element becomes contenteditable asynchronously after setViewState,
	// so defer one tick before selecting it.
	setTimeout(() => {
		const titleEl = leaf.view.containerEl
			.closest(".workspace-leaf")
			?.querySelector<HTMLElement>(".view-header-title");
		if (titleEl) {
			titleEl.focus();
			const range = document.createRange();
			range.selectNodeContents(titleEl);
			const sel = window.getSelection();
			sel?.removeAllRanges();
			sel?.addRange(range);
		}
	}, 0);

Would be great if this behavior gets exposed in the API so we can reuse it, as there’s a risk that this might fail in the future. But it worked for now.

I found that the key code lies in:

this.app.workspace.getLeaf().openFile(newFile, { eState: { rename: "all" } });

Complete test code to register a View for .test files:

import { Plugin, TextFileView } from "obsidian";

export default class Test extends Plugin {
    async onload() {
        this.registerView("test-view", (leaf) => new TestView(leaf));
        this.registerExtensions(["test"], "test-view");
        this.app.fileManager.registerFileParentCreator("test", () => this.app.vault.getRoot());

        this.addRibbonIcon("dice", "Test", async () => {
            const newFile = await this.app.fileManager.createNewFile(null, "New Test File", "test", "Random content for test");

            // open new file and rename
            this.app.workspace.getLeaf().openFile(newFile, { eState: { rename: "all" } });
        });
    }
}

class TestView extends TextFileView {
    getViewData(): string {
        throw new Error("Method not implemented.");
    }
    setViewData(data: string, clear: boolean): void {
        this.contentEl.innerHTML = `<h1>${data}</h1>`;
    }
    clear(): void {
    }
    getViewType(): string {
        return "test"
    }
}
1 Like

Thank you @the_tree, that’s a pretty good finding!

I tested and can confirm that this is the correct solution.