import { Plugin, setIcon, WorkspaceLeaf } from "obsidian"; import { DEFAULT_SETTINGS, TabPinSettings, TabPinSettingTab } from "./settings"; const BTN_CLASS = "tab-pin-toggle"; const PINNED_CLASS = "is-pinned"; export default class TabPinButtonPlugin extends Plugin { settings: TabPinSettings; private observer?: MutationObserver; private seenLeaves = new WeakSet(); async onload() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); this.addSettingTab(new TabPinSettingTab(this.app, this)); this.applyHoverStyle(); this.decorateAllTabHeaders(); this.registerEvent(this.app.workspace.on("layout-change", () => this.onWorkspaceChange())); this.registerEvent(this.app.workspace.on("active-leaf-change", () => this.syncAllButtons())); this.observer = new MutationObserver(() => { this.decorateAllTabHeaders(); this.syncAllButtons(); }); this.observer.observe(document.body, { childList: true, subtree: true }); this.autoPinNewLeavesPass(); } onunload() { this.observer?.disconnect(); document.querySelectorAll(`.${BTN_CLASS}`).forEach((el) => el.remove()); } async saveSettings() { await this.saveData(this.settings); } applyHoverStyle() { const root = document.documentElement; if (this.settings.showPinOnlyOnHover) { root.style.setProperty("--tab-pin-initial-opacity", "0"); root.style.setProperty("--tab-pin-hover-opacity", "1"); } else { root.style.setProperty("--tab-pin-initial-opacity", "0.7"); root.style.setProperty("--tab-pin-hover-opacity", "1"); } } private onWorkspaceChange() { this.decorateAllTabHeaders(); this.syncAllButtons(); this.autoPinNewLeavesPass(); } private decorateAllTabHeaders() { const headers = document.querySelectorAll(".workspace-tab-header"); headers.forEach((header) => { if (header.querySelector(`.${BTN_CLASS}`)) return; const inner = header.querySelector(".workspace-tab-header-inner") ?? header; const btn = inner.createSpan({ cls: BTN_CLASS }); const closeBtn = inner.querySelector(".workspace-tab-header-inner-close-button"); if (closeBtn) closeBtn.before(btn); else inner.appendChild(btn); btn.setAttribute("aria-label", "Toggle pin"); btn.setAttribute("tabindex", "0"); setIcon(btn, "pin"); btn.addEventListener("click", (ev) => { ev.stopPropagation(); ev.preventDefault(); this.activateHeader(header); const leaf = this.app.workspace.activeLeaf as any; if (!leaf) return; const pinned = !!leaf?.pinned; if (typeof leaf.setPinned === "function") leaf.setPinned(!pinned); else this.toggleViaViewState(leaf as WorkspaceLeaf, !pinned); this.syncButtonForHeader(header); }); btn.addEventListener("keydown", (e: KeyboardEvent) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); (btn as HTMLElement).click(); } }); }); } private syncAllButtons() { document.querySelectorAll(".workspace-tab-header").forEach((h) => this.syncButtonForHeader(h)); } private syncButtonForHeader(header: HTMLElement) { const btn = header.querySelector(`.${BTN_CLASS}`); if (!btn) return; const isPinned = this.isHeaderPinned(header); btn.classList.toggle(PINNED_CLASS, isPinned); btn.setAttribute("aria-pressed", isPinned ? "true" : "false"); btn.title = isPinned ? "Unpin tab" : "Pin tab"; // hide if pinned if (this.isHeaderPinned(header)) { btn.style.display = "none"; } else { btn.style.display = ""; } } private isHeaderPinned(header: HTMLElement): boolean { if (header.classList.contains("mod-pinned")) return true; if (header.classList.contains("is-active")) { const leaf = this.app.workspace.activeLeaf as any; return !!leaf?.pinned; } return false; } private activateHeader(header: HTMLElement) { header.dispatchEvent(new MouseEvent("mousedown", { bubbles: true })); header.click(); } private async toggleViaViewState(leaf: WorkspaceLeaf, toPinned: boolean) { const vs = await leaf.getViewState(); await leaf.setViewState({ ...vs, pinned: toPinned }); } private autoPinNewLeavesPass() { if (!this.settings.autoPinNewTabs) return; const leaves = this.app.workspace.getLeavesOfType?.("") || this.getAllLeavesFallback(); for (const leaf of leaves) { if (this.seenLeaves.has(leaf)) continue; this.seenLeaves.add(leaf); const anyLeaf = leaf as any; const pinned = !!anyLeaf?.pinned; if (!pinned) { if (typeof anyLeaf.setPinned === "function") anyLeaf.setPinned(true); else this.toggleViaViewState(leaf, true); } } this.syncAllButtons(); } private getAllLeavesFallback(): WorkspaceLeaf[] { const out: WorkspaceLeaf[] = []; const visit: any = (container: any) => { if (!container) return; if (Array.isArray(container.children)) container.children.forEach(visit); if (container?.view && container?.leaf) out.push(container.leaf as WorkspaceLeaf); if (container?.leaves) container.leaves.forEach((l: WorkspaceLeaf) => out.push(l)); }; // @ts-expect-error accessing private rootSplit visit((this.app.workspace as any).rootSplit); return out; } }