I’ve been working on this recently, inspired by another plugin made by someone. I’ll probably turn it into a real plugin soon but idk how to do that. here’s the code if ya’ll know how to use it.
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() {}
};