@@ -225,6 +267,165 @@ export class BibleView {
}
}
});
+
+ document.getElementById('bookmark-btn')?.addEventListener('click', (e) => {
+ e.stopPropagation();
+ this.bookmarksOpen = !this.bookmarksOpen;
+ const panel = document.getElementById('bookmark-panel');
+ if (panel) {
+ if (this.bookmarksOpen) {
+ panel.classList.remove('hidden');
+ panel.classList.add('flex', 'flex-col');
+ } else {
+ panel.classList.add('hidden');
+ panel.classList.remove('flex', 'flex-col');
+ }
+ }
+ if (this.bookmarksOpen) this.renderBookmarkPanel();
+ });
+
+ if (!bookmarksOutsideClickInstalled) {
+ bookmarksOutsideClickInstalled = true;
+ document.addEventListener('click', (e) => {
+ const target = e.target as HTMLElement;
+ if (target.closest('#bookmark-panel') || target.closest('#bookmark-btn')) return;
+ const panel = document.getElementById('bookmark-panel');
+ if (panel && !panel.classList.contains('hidden')) {
+ panel.classList.add('hidden');
+ panel.classList.remove('flex', 'flex-col');
+ }
+ });
+ }
+
+ this.renderBookmarkPanel();
+ }
+
+ private renderBookmarkPanel() {
+ const panel = document.getElementById('bookmark-panel');
+ if (!panel) return;
+ const t = this.t.bible;
+ const rows = [...this.bookmarks].sort((a, b) => b.created - a.created).map(b => `
+
+
+
+
+
+
+
+ `).join('');
+
+ panel.innerHTML = `
+
+ ${rows ? `
${rows}` : `
${t.bookmarkEmpty}
`}
+ `;
+
+ document.getElementById('bookmark-add')?.addEventListener('click', () => {
+ this.addBookmarkFromCurrentPosition();
+ });
+ panel.querySelectorAll('.bookmark-jump').forEach(el => {
+ el.addEventListener('click', () => {
+ const id = parseInt((el as HTMLElement).getAttribute('data-id')!, 10);
+ const bm = this.bookmarks.find(x => x.id === id);
+ if (bm) this.jumpToBookmark(bm);
+ });
+ });
+ panel.querySelectorAll('.bookmark-rename').forEach(el => {
+ el.addEventListener('click', () => {
+ const id = parseInt((el as HTMLElement).getAttribute('data-id')!, 10);
+ this.renameBookmark(id);
+ });
+ });
+ panel.querySelectorAll('.bookmark-delete').forEach(el => {
+ el.addEventListener('click', () => {
+ const id = parseInt((el as HTMLElement).getAttribute('data-id')!, 10);
+ this.deleteBookmark(id);
+ });
+ });
+ }
+
+ private versionShort(versionId: string): string {
+ const v = this.versions.find(x => x.id === versionId);
+ const name = v?.name ?? '';
+ if (!name) return versionId.split('/').pop() || versionId;
+ const m = name.match(/\(([^)]+)\)\s*$/);
+ return m ? m[1] : name;
+ }
+
+ private captureCurrentVerse(): number | undefined {
+ const textEl = document.getElementById('bible-text');
+ if (!textEl) return undefined;
+ const containerTop = textEl.getBoundingClientRect().top;
+ let current: number | null = null;
+ textEl.querySelectorAll
('[id^="v"]').forEach(el => {
+ const m = el.id.match(/^v(\d+)-(\d+)$/);
+ if (!m) return;
+ if (el.getBoundingClientRect().top <= containerTop + 8) {
+ current = parseInt(m[2], 10);
+ }
+ });
+ return current ?? undefined;
+ }
+
+ private addBookmarkFromCurrentPosition() {
+ const chapter = parseInt(this.currentPassage, 10) || 1;
+ const verse = this.captureCurrentVerse();
+ const book = this.getBooksForVersion().find(b => b.id === this.currentBook);
+ const bookLabel = book?.name || this.currentBook;
+ const ref = verse ? `${bookLabel} ${chapter}:${verse}` : `${bookLabel} ${chapter}`;
+ const name = `${ref} (${this.versionShort(this.currentVersion)})`;
+ const bm: BibleBookmark = {
+ id: Date.now(),
+ name,
+ versionId: this.currentVersion,
+ bookId: this.currentBook,
+ chapter,
+ verse,
+ created: Date.now(),
+ };
+ this.bookmarks.push(bm);
+ saveBookmarks(this.bookmarks);
+ this.renderBookmarkPanel();
+ }
+
+ private renameBookmark(id: number) {
+ const bm = this.bookmarks.find(x => x.id === id);
+ if (!bm) return;
+ const name = window.prompt(this.t.bible.bookmarkRename, bm.name);
+ if (name === null) return;
+ const trimmed = name.trim();
+ if (trimmed) bm.name = trimmed;
+ saveBookmarks(this.bookmarks);
+ this.renderBookmarkPanel();
+ }
+
+ private deleteBookmark(id: number) {
+ this.bookmarks = this.bookmarks.filter(x => x.id !== id);
+ saveBookmarks(this.bookmarks);
+ this.renderBookmarkPanel();
+ }
+
+ private jumpToBookmark(bm: BibleBookmark) {
+ if (this.versions.some(v => v.id === bm.versionId)) {
+ this.currentVersion = bm.versionId;
+ const vs = document.getElementById('version-select') as HTMLSelectElement | null;
+ if (vs) vs.value = bm.versionId;
+ }
+ this.currentBook = bm.bookId;
+ this.updateBookSelect();
+ this.currentPassage = String(bm.chapter);
+ this.updateChapterSelect();
+ this.pendingScroll = bm.verse && bm.verse > 0 ? { chapter: bm.chapter, verse: bm.verse } : null;
+ this.loadReading();
+ this.closeBookmarkPanel();
+ }
+
+ private closeBookmarkPanel() {
+ this.bookmarksOpen = false;
+ const panel = document.getElementById('bookmark-panel');
+ if (panel) {
+ panel.classList.add('hidden');
+ panel.classList.remove('flex', 'flex-col');
+ }
}
private getBooksForVersion(): { id: string; name: string; chapters: number }[] {
@@ -291,6 +492,15 @@ export class BibleView {
textEl.className = `flex-1 overflow-auto p-2 max-w-3xl lg:max-w-4xl xl:max-w-5xl mx-auto liturgical-text ${fnClass}`;
textEl.innerHTML = html || `${this.t.bible.noTextFound}
`;
+
+ if (this.pendingScroll) {
+ const target = this.pendingScroll;
+ this.pendingScroll = null;
+ requestAnimationFrame(() => {
+ const el = document.getElementById(`v${target.chapter}-${target.verse}`);
+ if (el) el.scrollIntoView({ block: 'start' });
+ });
+ }
} catch {
textEl.innerHTML = `