import { App } from 'obsidian'; import { ReferenceData, appendHTML } from './types'; import { fetchReference } from './referenceService'; export class ReferencePopover { private app: App; private popoverEl: HTMLElement | null = null; private showGeneration = 0; private static instance: ReferencePopover | null = null; private constructor(app: App) { this.app = app; } public static getInstance(app: App): ReferencePopover { if (!ReferencePopover.instance) { ReferencePopover.instance = new ReferencePopover(app); } return ReferencePopover.instance; } public async show(targetEl: HTMLElement, verseRef: string) { this.hide(); const generation = ++this.showGeneration; this.popoverEl = activeDocument.createDiv({ cls: 'ref-popover' }); this.popoverEl.createEl('p', { text: 'Loading…', cls: 'ref-popover-loading' }); activeDocument.body.appendChild(this.popoverEl); this.positionPopover(targetEl); const data = await fetchReference(verseRef); if (generation !== this.showGeneration) return; // stale, another show() ran this.updateContent(data); activeDocument.addEventListener('click', this.handleOutsideClick); this.popoverEl.addEventListener('click', (e) => e.stopPropagation()); } private updateContent(data: ReferenceData | null) { if (!this.popoverEl) return; this.popoverEl.empty(); if (!data || data.results.length === 0) { this.popoverEl.createEl('p', { text: 'No results found.' }); return; } for (const html of data.results) { appendHTML(this.popoverEl.createDiv({ cls: 'ref-popover-result' }), html); } } private positionPopover(targetEl: HTMLElement) { if (!this.popoverEl) return; const targetRect = targetEl.getBoundingClientRect(); const popoverRect = this.popoverEl.getBoundingClientRect(); let top = targetRect.bottom + window.scrollY + 5; let left = targetRect.left + window.scrollX; if (left + popoverRect.width > window.innerWidth) { left = window.innerWidth - popoverRect.width - 20; } if (top + popoverRect.height > window.innerHeight && targetRect.top > popoverRect.height) { top = targetRect.top + window.scrollY - popoverRect.height - 5; } this.popoverEl.setCssProps({ '--ref-popover-top': `${top}px`, '--ref-popover-left': `${left}px`, }); } public hide() { if (this.popoverEl) { this.popoverEl.remove(); this.popoverEl = null; activeDocument.removeEventListener('click', this.handleOutsideClick); } } private handleOutsideClick = (event: MouseEvent) => { if (this.popoverEl && !this.popoverEl.contains(event.target as Node)) { this.hide(); } } }