Clean up textarea measurement mirrors across plugin reloads

This commit is contained in:
murashit 2026-07-10 13:10:58 +09:00
parent ee0cfa9891
commit 0cb39fb6dc
3 changed files with 61 additions and 3 deletions

View file

@ -8,6 +8,7 @@ import { CodexTurnDiffView } from "./features/turn-diff/view.obsidian";
import { CodexPanelRuntime } from "./plugin-runtime";
import { type CodexPanelSettings, DEFAULT_SETTINGS, getVaultPath, normalizeSettings, settingsMatchStoredSettings } from "./settings/model";
import { CodexPanelSettingTab } from "./settings/tab.obsidian";
import { disposeTextareaHeightMirrors } from "./shared/dom/textarea-autogrow.measure";
export default class CodexPanelPlugin extends Plugin {
settings: CodexPanelSettings = DEFAULT_SETTINGS;
@ -19,6 +20,7 @@ export default class CodexPanelPlugin extends Plugin {
});
override async onload(): Promise<void> {
disposeTextareaHeightMirrors();
this.runtime.reset();
this.vaultPath = getVaultPath(this.app);
await this.loadSettings();
@ -77,6 +79,7 @@ export default class CodexPanelPlugin extends Plugin {
override onunload(): void {
this.runtime.cancelWorkspacePanelReconcile();
disposeTextareaHeightMirrors();
}
async loadSettings(): Promise<void> {

View file

@ -4,6 +4,7 @@ export interface TextareaHeightOptions {
}
const textareaHeightMirrors = new WeakMap<Document, HTMLTextAreaElement>();
const textareaHeightMirrorDocuments = new Set<Document>();
const TEXTAREA_HEIGHT_MIRROR_CLASS = "codex-panel-textarea-height-mirror";
export function syncTextareaHeight(textarea: HTMLTextAreaElement | null, options: TextareaHeightOptions): void {
@ -21,6 +22,14 @@ export function syncTextareaHeight(textarea: HTMLTextAreaElement | null, options
textarea.setCssProps(sizingProps);
}
export function disposeTextareaHeightMirrors(): void {
for (const doc of textareaHeightMirrorDocuments) {
for (const mirror of doc.querySelectorAll(`.${TEXTAREA_HEIGHT_MIRROR_CLASS}`)) mirror.remove();
textareaHeightMirrors.delete(doc);
}
textareaHeightMirrorDocuments.clear();
}
function measureTextareaNaturalScrollHeight(textarea: HTMLTextAreaElement, style: CSSStyleDeclaration): number {
const mirror = textareaHeightMirror(textarea.ownerDocument);
mirror.value = textarea.value;
@ -30,14 +39,17 @@ function measureTextareaNaturalScrollHeight(textarea: HTMLTextAreaElement, style
function textareaHeightMirror(doc: Document): HTMLTextAreaElement {
const existing = textareaHeightMirrors.get(doc);
if (existing) return existing;
const mirror = doc.createElement("textarea");
if (existing?.isConnected) return existing;
const staleMirrors = [...doc.querySelectorAll<HTMLTextAreaElement>(`.${TEXTAREA_HEIGHT_MIRROR_CLASS}`)];
const mirror = staleMirrors.shift() ?? doc.createElement("textarea");
for (const duplicate of staleMirrors) duplicate.remove();
mirror.tabIndex = -1;
mirror.setAttribute("aria-hidden", "true");
mirror.readOnly = true;
mirror.addClass(TEXTAREA_HEIGHT_MIRROR_CLASS);
doc.body.appendChild(mirror);
if (!mirror.isConnected) doc.body.appendChild(mirror);
textareaHeightMirrors.set(doc, mirror);
textareaHeightMirrorDocuments.add(doc);
return mirror;
}

View file

@ -0,0 +1,43 @@
// @vitest-environment jsdom
import { afterEach, describe, expect, it } from "vitest";
import { disposeTextareaHeightMirrors, syncTextareaHeight } from "../../../src/shared/dom/textarea-autogrow.measure";
import { installObsidianDomShims } from "../../support/dom";
installObsidianDomShims();
const MIRROR_SELECTOR = ".codex-panel-textarea-height-mirror";
afterEach(() => {
disposeTextareaHeightMirrors();
document.body.replaceChildren();
});
describe("textarea auto-grow measurement", () => {
it("adopts one existing mirror and removes reload duplicates", () => {
document.body.append(mirror(), mirror());
const textarea = document.createElement("textarea");
document.body.appendChild(textarea);
syncTextareaHeight(textarea, { minHeightFallback: 32, maxHeightFallback: 120 });
expect(document.querySelectorAll(MIRROR_SELECTOR)).toHaveLength(1);
});
it("removes tracked mirrors when the plugin lifecycle ends", () => {
const textarea = document.createElement("textarea");
document.body.appendChild(textarea);
syncTextareaHeight(textarea, { minHeightFallback: 32, maxHeightFallback: 120 });
disposeTextareaHeightMirrors();
expect(document.querySelector(MIRROR_SELECTOR)).toBeNull();
});
});
function mirror(): HTMLTextAreaElement {
const element = document.createElement("textarea");
element.className = MIRROR_SELECTOR.slice(1);
return element;
}