Use Obsidian window-aware DOM APIs

This commit is contained in:
murashit 2026-05-17 21:48:06 +09:00
parent f41bab3f69
commit 9fbaafac69
7 changed files with 62 additions and 31 deletions

View file

@ -47,12 +47,10 @@ export default class CodexPanelPlugin extends Plugin {
}
async activateView(): Promise<CodexPanelView> {
const existing = this.app.workspace.getLeavesOfType(VIEW_TYPE_CODEX_PANEL)[0];
const leaf = existing ?? this.app.workspace.getRightLeaf(false);
if (!leaf) throw new Error("Could not create a right sidebar leaf.");
await leaf.setViewState({ type: VIEW_TYPE_CODEX_PANEL, active: true });
await this.app.workspace.revealLeaf(leaf);
const leaf = await this.app.workspace.ensureSideLeaf(VIEW_TYPE_CODEX_PANEL, "right", {
active: true,
reveal: true,
});
return leaf.view as CodexPanelView;
}

View file

@ -52,7 +52,7 @@ export class PanelMessageRenderer {
workspaceRoot: state.activeThreadCwd ?? this.options.vaultPath,
openDetails: state.openDetails,
onDetailsToggle: () => {
window.requestAnimationFrame(() => {
messagesEl.win.requestAnimationFrame(() => {
state.messagesPinnedToBottom = isNearScrollBottom(messagesEl);
});
},
@ -72,7 +72,7 @@ export class PanelMessageRenderer {
});
syncMessageRenderBlocks(messagesEl, blocks, this.options.blockSignatures);
window.requestAnimationFrame(() => {
messagesEl.win.requestAnimationFrame(() => {
if (shouldScrollToBottom) {
messagesEl.scrollTop = bottomScrollTop(messagesEl);
} else {
@ -99,7 +99,7 @@ export class PanelMessageRenderer {
if (!this.options.state.messagesPinnedToBottom) return;
const messagesEl = parent.closest<HTMLElement>(".codex-panel__messages");
if (!messagesEl) return;
window.requestAnimationFrame(() => {
messagesEl.win.requestAnimationFrame(() => {
if (!this.options.state.messagesPinnedToBottom) return;
messagesEl.scrollTop = bottomScrollTop(messagesEl);
this.options.state.messagesPinnedToBottom = isNearScrollBottom(messagesEl);

View file

@ -202,14 +202,14 @@ export class CodexPanelView extends ItemView {
async onOpen(): Promise<void> {
this.composerController.registerNoteIndexInvalidation((eventRef) => this.registerEvent(eventRef));
this.registerDomEvent(activeDocument, "pointerdown", (event) => this.closeToolbarPanelOnOutsidePointer(event));
this.registerDomEvent(this.containerEl.doc, "pointerdown", (event) => this.closeToolbarPanelOnOutsidePointer(event));
this.render();
await this.ensureConnected();
}
async onClose(): Promise<void> {
if (this.scheduledRenderTimer !== null) {
window.clearTimeout(this.scheduledRenderTimer);
this.containerEl.win.clearTimeout(this.scheduledRenderTimer);
this.scheduledRenderTimer = null;
}
this.connection.disconnect();
@ -800,7 +800,8 @@ export class CodexPanelView extends ItemView {
if (!this.hasOpenToolbarPanel()) return;
const target = event.target;
if (target instanceof Element) {
const viewWindow = this.containerEl.doc.defaultView;
if (viewWindow && target instanceof viewWindow.Element) {
const insideToolbarPanel = target.closest(".codex-panel__toolbar-primary, .codex-panel__toolbar-panel");
if (insideToolbarPanel && this.containerEl.contains(insideToolbarPanel)) return;
}
@ -823,7 +824,7 @@ export class CodexPanelView extends ItemView {
private scheduleRender(): void {
if (this.scheduledRenderTimer !== null) return;
this.scheduledRenderTimer = window.setTimeout(() => {
this.scheduledRenderTimer = this.containerEl.win.setTimeout(() => {
this.scheduledRenderTimer = null;
this.render();
}, 50);

View file

@ -104,19 +104,15 @@ export function syncComposerHeight(composer: HTMLTextAreaElement | null): void {
if (!composer) return;
const style = getComputedStyle(composer);
const minHeight = parseCssPixels(style.minHeight, 76);
const maxHeight = composerMaxHeight(style.maxHeight);
setCssProps(composer, { height: "auto" });
const maxHeight = composerMaxHeight(style.maxHeight, composer.win);
const resetHeightProps: Record<string, string> = { height: "auto" };
composer.setCssProps(resetHeightProps);
const nextHeight = Math.min(Math.max(composer.scrollHeight, minHeight), maxHeight);
setCssProps(composer, {
const sizingProps: Record<string, string> = {
height: `${nextHeight}px`,
"overflow-y": composer.scrollHeight > maxHeight ? "auto" : "hidden",
});
}
function setCssProps(element: HTMLElement, props: Record<string, string>): void {
for (const [key, value] of Object.entries(props)) {
element.style.setProperty(key, value);
}
};
composer.setCssProps(sizingProps);
}
function parseCssPixels(value: string, fallback: number): number {
@ -124,26 +120,26 @@ function parseCssPixels(value: string, fallback: number): number {
return Number.isFinite(parsed) ? parsed : fallback;
}
function composerMaxHeight(value: string): number {
return parseCssLengthExpression(value) ?? Math.min(208, window.innerHeight * 0.4);
function composerMaxHeight(value: string, win: Window): number {
return parseCssLengthExpression(value, win) ?? Math.min(208, win.innerHeight * 0.4);
}
function parseCssLengthExpression(value: string): number | null {
function parseCssLengthExpression(value: string, win: Window): number | null {
const trimmed = value.trim();
if (!trimmed || trimmed === "none") return null;
if (/^min\(/i.test(trimmed)) {
const values = Array.from(trimmed.matchAll(/(-?\d+(?:\.\d+)?)\s*(px|vh)/gi), (match) =>
cssLengthToPixels(Number.parseFloat(match[1]), match[2]),
cssLengthToPixels(Number.parseFloat(match[1]), match[2], win),
).filter((candidate): candidate is number => Number.isFinite(candidate));
return values.length > 0 ? Math.min(...values) : null;
}
const length = trimmed.match(/^(-?\d+(?:\.\d+)?)\s*(px|vh)$/i);
if (!length) return null;
return cssLengthToPixels(Number.parseFloat(length[1]), length[2]);
return cssLengthToPixels(Number.parseFloat(length[1]), length[2], win);
}
function cssLengthToPixels(value: number, unit: string): number {
if (unit.toLowerCase() === "vh") return (window.innerHeight * value) / 100;
function cssLengthToPixels(value: number, unit: string, win: Window): number {
if (unit.toLowerCase() === "vh") return (win.innerHeight * value) / 100;
return value;
}

View file

@ -363,7 +363,7 @@ function renderThreadRenameRow(parent: HTMLElement, thread: ToolbarThreadRow, ac
actions.cancelRenameThread(thread.threadId);
}
};
window.setTimeout(() => {
input.win.setTimeout(() => {
if (input.ownerDocument.activeElement !== input) {
input.focus();
input.select();

View file

@ -226,6 +226,25 @@ export function setIcon(element: HTMLElement, icon: string): void {
function ensureElementHelpers(): void {
if (typeof HTMLElement === "undefined") return;
const NodeCtor = globalThis.Node ?? document.defaultView?.Node;
if (NodeCtor && !Object.getOwnPropertyDescriptor(NodeCtor.prototype, "doc")) {
Object.defineProperty(NodeCtor.prototype, "doc", {
configurable: true,
get(this: Node): Document {
return this.ownerDocument ?? document;
},
});
}
if (NodeCtor && !Object.getOwnPropertyDescriptor(NodeCtor.prototype, "win")) {
Object.defineProperty(NodeCtor.prototype, "win", {
configurable: true,
get(this: Node): Window {
return this.ownerDocument?.defaultView ?? window;
},
});
}
if (!HTMLElement.prototype.addClass) {
HTMLElement.prototype.addClass = function addClass(className: string): void {
this.classList.add(className);

View file

@ -21,6 +21,23 @@ declare global {
export function installObsidianDomShims(): void {
beforeEach(() => {
const NodeCtor = globalThis.Node ?? document.defaultView?.Node;
if (NodeCtor && !Object.getOwnPropertyDescriptor(NodeCtor.prototype, "doc")) {
Object.defineProperty(NodeCtor.prototype, "doc", {
configurable: true,
get(this: Node): Document {
return this.ownerDocument ?? document;
},
});
}
if (NodeCtor && !Object.getOwnPropertyDescriptor(NodeCtor.prototype, "win")) {
Object.defineProperty(NodeCtor.prototype, "win", {
configurable: true,
get(this: Node): Window {
return this.ownerDocument?.defaultView ?? window;
},
});
}
HTMLElement.prototype.addClass = function addClass(this: HTMLElement, className: string): void {
this.classList.add(...className.split(/\s+/).filter(Boolean));
};