I made a slightly different version of your plugin for anyone whose interested. It automatically triggers the header renaming when navigated to by cursor or caret. only pressing enter will accept the rename. it definitely has its quirks.
const DEBOUNCE_DELAY = 30;
const DOM_EVENTS = ["click", "keyup"];
class EditorModeManager {
constructor(app) {
this.app = app;
this.activeMode = null;
}
create() {
const markdownEmbed = this.app.embedRegistry.embedByExtension.md({
app: this.app,
containerEl: createDiv(),
});
markdownEmbed.load();
markdownEmbed.editable = !0;
markdownEmbed.showEditor();
const EMode = Object.getPrototypeOf(
Object.getPrototypeOf(markdownEmbed.editMode),
).constructor;
markdownEmbed.unload();
// the let here enables us toGet a self referencing mardown editor
let eMode;
return (eMode = new EMode(this.app, createDiv(), {
app: this.app,
scroll: 0,
editMode: null,
get editor() {
return eMode.editor;
},
get file() {},
getMode: () => "source",
showSearch: () => {},
toggleMode: () => {},
onMarkdownScroll: () => {},
}));
}
}
class HeadingEditor {
constructor(app) {
this.app = app;
this.hEditorManager = new EditorModeManager(app);
this.hEditor = null;
this.activeModal = null;
this.modalEls = null;
}
get lineEl() {
return this.app.workspace.activeEditor?.contentEl?.querySelector(
".cm-editor.cm-focused .cm-active.HyperMD-header.cm-line",
);
}
cleanup() {
if (this.hEditor) {
this.hEditor.unload();
this.hEditor = null;
}
if (this.activeModal) {
this.activeModal.remove();
this.activeModal = null;
}
}
async captureModalElements() {
await this.app.commands.executeCommandById("editor:rename-heading");
this.activeModal = document.querySelector(".modal-container");
this.modalEls = {
parentEl: this.activeModal,
renameEl: this.activeModal.querySelector(".rename-textarea"),
submitBtn: this.activeModal.querySelector("button.mod-cta"),
cancelBtn: this.activeModal.querySelector("button.mod-cancel"),
};
this.modalEls.parentEl.empty();
return this.modalEls;
}
setupModalContainer() {
const rect = this.lineEl.getBoundingClientRect();
this.hEditor.containerEl.setCssProps({
position: "absolute",
left: `${rect.left}px`,
top: `${rect.top}px`,
width: `${rect.width}px`,
height: `${rect.height}px`,
background: "var(--background-secondary)",
});
this.hEditor.set(this.modalEls.renameEl.value);
this.hEditor.focus();
this.hEditor.editor.setCursor(0, this.hEditor.editor.getValue().length);
}
isHeader(target) {
return (
target === this.lineEl ||
(target.classList.contains("cm-header") &&
!target.classList.contains("cm-formatting"))
);
}
handleSubmit(key) {
const cursor = this.mEditor.getCursor();
const leadingSpace = this.mEditor.getLine(cursor.line).indexOf(" ");
const [newHeading, nextLine] = this.hEditor.editor
.getValue()
.split("\n")
.filter((p) => p);
if (key !== "Enter") {
this.modalEls.cancelBtn.click();
} else {
this.modalEls.renameEl.value = newHeading;
this.modalEls.submitBtn.click();
setTimeout(() => {
if (nextLine) {
this.mEditor.replaceRange(`\n${nextLine}`, {
line: cursor.line,
ch: leadingSpace + newHeading.length + 1,
});
}
}, 50);
}
setTimeout(() => {
this.mEditor.setCursor({
line: cursor.line + (key === "ArrowUp" ? -1 : 1),
ch: 0,
});
}, 50);
this.cleanup();
}
async handleHeaderEdit(event) {
const { view } = event;
if (!view?.document) return;
if (
this.isHeader(event.target) ||
(event.type === "keyup" && this.lineEl)
) {
if (!this.hEditor) {
this.hEditor = this.hEditorManager.create();
this.hEditor.load();
}
this.mEditor = this.app.workspace.activeEditor.editor;
const modalEls = await this.captureModalElements();
modalEls.parentEl.onclick = (evt) => {
if (!this.hEditor.containerEl.contains(evt.target)) {
evt.preventDefault();
evt.stopPropagation();
this.handleSubmit("Escape");
} else {
evt.stopPropagation();
}
};
modalEls.parentEl.createDiv({
cls: "kh-bg",
attr: { style: "width: 100%; height: 100%;" },
});
modalEls.parentEl.append(this.hEditor.containerEl);
this.hEditor.containerEl.onkeydown = (evt) => {
if (evt.ctrlKey) this.app.workspace.activeLeaf = null;
const actionKeys = ["ArrowUp", "ArrowDown", "Enter", "Escape"];
if (actionKeys.includes(evt.key)) {
evt.preventDefault();
if (evt.key === "Escape") {
this.cleanup();
} else {
this.handleSubmit(evt.key);
}
}
};
this.setupModalContainer();
}
}
}
const ob = require("obsidian");
module.exports = class HeadingEditorPlugin extends ob.Plugin {
onload() {
const headingEditor = new HeadingEditor(this.app);
const debouncedHandler = ob.debounce(
(evt) => headingEditor.handleHeaderEdit(evt),
DEBOUNCE_DELAY,
true,
);
this.registerEvent(
this.app.workspace.on("active-leaf-change", (leaf) => {
const { view } = leaf;
if (!view?.contentEl) return;
headingEditor.cleanup();
DOM_EVENTS.forEach((eventType) => {
this.registerDomEvent(
view.contentEl.ownerDocument,
eventType,
debouncedHandler,
);
});
}),
);
this.register(() => headingEditor.cleanup());
}
onunload() {}
};