skylernorgaard_obsidian-bib.../studyNotesPanel.ts
2026-07-16 11:44:26 -04:00

944 lines
28 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { App, MarkdownView, TFile, normalizePath, setIcon } from 'obsidian';
import type StudyBiblePlugin from './main';
import { ChapterPicker, listBibleChapterFiles } from './chapterPicker';
import { renderBacklinkList, attachSidebarNoteRowHandlers, type NoteLinkLabelStyle } from './noteList';
import { attachSidebarNoteTap, attachSidebarTap } from './sidebarTap';
import * as Utils from './utils';
import {
openStructureFile,
findMainAreaLeafForFile,
} from './openStudyNote';
import { PrepSidebarTagSearch } from './prepSidebarTags';
import { markIgnoreSwipe } from './platformMode';
type PrepSidebarPane = 'verse' | 'tags';
const SCOPES: Utils.NoteScope[] = ['chapter', 'section', 'book'];
const SCOPE_LABELS: Record<Utils.NoteScope, string> = {
chapter: 'Chapter',
section: 'Section',
book: 'Book',
};
export interface StudyNotesPanelOptions {
sidebarMode?: boolean;
onVisibilityChange?: (visible: boolean) => void;
}
export class StudyNotesPanel {
private app: App;
private plugin: StudyBiblePlugin;
private containerEl: HTMLElement;
private headerEl!: HTMLDivElement;
private chapterTitleEl: HTMLElement | null = null;
private toggleEl!: HTMLDivElement;
private listEl!: HTMLDivElement;
private scope: Utils.NoteScope = 'chapter';
private visible = true;
private sidebarMode: boolean;
private activeFile: TFile | null = null;
private notes: Utils.RelevantNotesResult | null = null;
private onVisibilityChange?: (visible: boolean) => void;
private chapterPicker: ChapterPicker;
private prepPillsEl: HTMLDivElement | null = null;
private versePaneEl: HTMLDivElement | null = null;
private tagsPaneEl: HTMLDivElement | null = null;
private prepTagSearch: PrepSidebarTagSearch | null = null;
private navContextSignature: string | null = null;
constructor(
app: App,
plugin: StudyBiblePlugin,
parentEl: HTMLElement,
options?: StudyNotesPanelOptions
) {
this.app = app;
this.plugin = plugin;
this.chapterPicker = new ChapterPicker(app);
this.sidebarMode = options?.sidebarMode ?? false;
this.onVisibilityChange = options?.onVisibilityChange;
this.containerEl = parentEl.createDiv({ cls: 'study-bible-notes-panel' });
if (this.sidebarMode) {
this.containerEl.addClass('study-bible-notes-panel-sidebar');
markIgnoreSwipe(this.containerEl);
}
}
isVisible(): boolean {
return this.visible;
}
setVisible(visible: boolean): void {
if (this.sidebarMode) {
return;
}
this.visible = visible;
this.containerEl.toggleClass('is-hidden', !visible);
this.onVisibilityChange?.(visible);
}
async render(activeFile: TFile): Promise<void> {
const { bibleBooksFolder } = this.plugin.settings;
if (this.sidebarMode && activeFile.path.startsWith(bibleBooksFolder)) {
Utils.setLastBibleChapter(activeFile);
}
this.containerEl.removeClass('is-chapter-context', 'is-prep-anchored', 'is-reader-entry');
this.activeFile = activeFile;
this.notes = await Utils.collectRelevantNotes(
this.app,
activeFile,
this.plugin.settings.bibleNotesFolder,
this.plugin.settings.bibleBooksFolder
);
this.buildShell();
await this.renderList();
}
/** Desktop prep mode: full chapter notes while editing a non-bible file. */
async renderAnchoredChapter(activeFile: TFile): Promise<void> {
Utils.setLastBibleChapter(activeFile);
this.containerEl.removeClass('is-chapter-context', 'is-reader-entry');
this.containerEl.addClass('is-prep-anchored');
const chapterChanged = this.activeFile?.path !== activeFile.path;
const navContextChanged =
this.navContextSignature !== this.buildNavContextSignature(activeFile);
const onTagsPane =
this.plugin.getWorkspaceMode() === 'prep' &&
this.plugin.getPrepSidebarPane() === 'tags';
this.activeFile = activeFile;
this.notes = await Utils.collectRelevantNotes(
this.app,
activeFile,
this.plugin.settings.bibleNotesFolder,
this.plugin.settings.bibleBooksFolder
);
if (onTagsPane && this.isPrepShellBuilt()) {
if (chapterChanged) {
this.syncPrepChapterPickerLabel();
this.listEl.empty();
void this.renderList();
}
if (chapterChanged || navContextChanged) {
this.syncPrepChapterNavRow();
this.navContextSignature = this.buildNavContextSignature(activeFile);
}
this.setPrepPane(this.plugin.getPrepSidebarPane());
return;
}
if (!this.isPrepShellBuilt() || chapterChanged) {
this.buildPrepShell();
} else if (navContextChanged) {
this.syncPrepChapterNavRow();
this.listEl.empty();
} else {
this.listEl.empty();
}
this.navContextSignature = this.buildNavContextSignature(activeFile);
await this.renderList();
}
isPrepShellBuilt(): boolean {
return !!this.prepPillsEl && this.containerEl.hasClass('is-prep-anchored');
}
/** Desktop prep mode: picker only, before any chapter is chosen. */
showPrepPicker(): void {
this.activeFile = null;
this.notes = null;
this.buildPrepShell({ placeholder: true });
this.listEl.createEl('p', {
cls: 'study-bible-notes-empty study-bible-notes-placeholder',
text: 'Pick a chapter to see your observations.',
});
}
/** Mobile reading mode: chapter picker when Bible Reader has no chapter loaded. */
showReaderEntryPicker(lastChapter?: TFile | null): void {
this.activeFile = null;
this.notes = null;
this.destroyPrepTagSearch();
this.containerEl.empty();
this.containerEl.addClass('study-bible-notes-panel-sidebar', 'is-reader-entry');
this.containerEl.removeClass('is-chapter-context', 'is-prep-anchored', 'is-reader-entry');
this.buildSidebarHeaderWithPicker(false, {
placeholder: true,
headerCls: 'study-bible-sidebar-header',
onPick: (file) => {
void this.plugin.routeChapterToReader(file);
},
});
this.listEl = this.containerEl.createDiv({ cls: 'study-bible-notes-list' });
if (lastChapter) {
const continueBlock = this.listEl.createDiv({
cls: 'study-bible-reader-entry-continue',
});
continueBlock.createDiv({
cls: 'study-bible-chapter-context-label',
text: 'Continue',
});
const button = continueBlock.createEl('button', {
type: 'button',
cls: 'study-bible-backlink study-bible-chapter-context-link',
text: this.formatChapterTitle(lastChapter),
});
attachSidebarTap(button, () => {
void this.plugin.routeChapterToReader(lastChapter);
});
}
this.listEl.createEl('p', {
cls: 'study-bible-notes-empty study-bible-notes-placeholder',
text: 'Choose a chapter to start reading.',
});
this.appendModeFooter();
}
showEmpty(message: string): void {
this.activeFile = null;
this.notes = null;
this.destroyPrepTagSearch();
this.containerEl.empty();
this.containerEl.addClass('study-bible-notes-panel-sidebar');
this.containerEl.removeClass('is-chapter-context', 'is-prep-anchored', 'is-reader-entry');
this.listEl = this.containerEl.createDiv({ cls: 'study-bible-notes-list' });
this.listEl.createEl('p', {
cls: 'study-bible-notes-empty study-bible-notes-placeholder',
text: message,
});
this.appendModeFooter();
}
/** Desktop: minimal link back when the active file is not bible-related. */
showReturnToLastChapter(lastChapter: TFile): void {
this.activeFile = null;
this.notes = null;
this.destroyPrepTagSearch();
this.containerEl.empty();
this.containerEl.addClass('study-bible-notes-panel-sidebar');
this.containerEl.addClass('is-chapter-context');
this.headerEl = this.containerEl.createDiv({
cls: 'study-bible-sidebar-header is-context-only',
});
this.chapterTitleEl = null;
const nav = this.headerEl.createDiv({ cls: 'study-bible-chapter-context' });
const block = nav.createDiv({ cls: 'study-bible-chapter-context-block' });
block.createDiv({
cls: 'study-bible-chapter-context-label',
text: 'Last chapter',
});
const button = block.createEl('button', {
type: 'button',
cls: 'study-bible-backlink study-bible-chapter-context-link',
text: this.formatChapterTitle(lastChapter),
});
attachSidebarTap(button, () => {
void this.plugin.routeChapterToReader(lastChapter);
});
}
renderChapterContext(
contextFile: TFile,
currentChapterFile?: TFile | null
): void {
this.activeFile = null;
this.notes = null;
this.destroyPrepTagSearch();
this.containerEl.empty();
this.containerEl.addClass('study-bible-notes-panel-sidebar');
this.containerEl.addClass('is-chapter-context');
this.containerEl.removeClass('is-prep-anchored', 'is-reader-entry');
this.headerEl = this.containerEl.createDiv({ cls: 'study-bible-sidebar-header is-context-only' });
this.chapterTitleEl = null;
Utils.renderChapterNavHeaders(
this.app,
this.headerEl,
contextFile,
this.plugin.settings.bibleNotesFolder,
{
routeChapter: (file) => {
const contextLeaf = findMainAreaLeafForFile(this.app, contextFile);
void this.plugin.routeChapterToReader(file, contextLeaf ?? undefined);
},
bibleBooksFolder: this.plugin.settings.bibleBooksFolder,
currentChapterFile,
}
);
this.listEl = this.containerEl.createDiv({
cls: 'study-bible-notes-list study-bible-notes-footer-spacer',
});
this.appendModeFooter();
}
private formatChapterTitle(file: TFile): string {
const parsed = Utils.parseBookChapter(file.basename);
if (parsed.chapter > 0 && !parsed.hasRange) {
return `${Utils.bookToDisplayName(parsed.book)} ${parsed.chapter}`;
}
return file.basename.replace(/\.md$/i, '');
}
private formatSidebarTitle(file: TFile): string {
return `${this.formatChapterTitle(file)} notes`;
}
private formatSidebarTitleFromNotes(): string {
if (!this.notes) {
return '';
}
return `${Utils.bookToDisplayName(this.notes.activeBook)} ${this.notes.activeChapter} notes`;
}
private buildSidebarHeader(showScopeToggle: boolean): void {
this.headerEl = this.containerEl.createDiv({ cls: 'study-bible-sidebar-header' });
this.chapterTitleEl = this.headerEl.createEl('h2', { cls: 'study-bible-sidebar-chapter-title' });
this.updateChapterTitle();
this.appendScopeToggle(showScopeToggle);
}
private buildNavContextSignature(anchorFile: TFile | null): string {
const state = this.plugin.resolveMainAreaSidebarState();
const away = anchorFile ? this.isAwayFromPrepAnchor(anchorFile) : false;
return [
anchorFile?.path ?? '',
state.contextChapter?.path ?? '',
state.readerChapter?.path ?? '',
state.auxiliaryFile?.path ?? '',
String(away),
].join('|');
}
/** True when the editor has wandered from the anchored prep chapter (note open, etc.). */
private isAwayFromPrepAnchor(anchorFile: TFile): boolean {
const state = this.plugin.resolveMainAreaSidebarState();
if (state.auxiliaryFile) {
return true;
}
if (state.readerChapter) {
return state.readerChapter.path !== anchorFile.path;
}
return false;
}
/** Editor chapter for prep nav — uses the same reader/auxiliary detection as reading mode. */
private resolvePrepNavContextChapter(): TFile | null {
const state = this.plugin.resolveMainAreaSidebarState();
return state.contextChapter ?? state.readerChapter;
}
private syncPrepChapterNavRow(): void {
if (!this.headerEl || !this.activeFile) {
return;
}
this.headerEl.querySelector('.study-bible-chapter-nav-row')?.remove();
this.insertChapterNavRow(this.headerEl, this.activeFile);
}
private insertChapterNavRow(
host: HTMLElement,
anchorFile: TFile
): void {
if (!this.isAwayFromPrepAnchor(anchorFile)) {
return;
}
const contextChapter = this.resolvePrepNavContextChapter();
const picker = host.querySelector('.study-bible-chapter-picker');
Utils.renderChapterNavRow(host, {
anchorChapterFile: anchorFile,
currentChapterFile: contextChapter,
insertAfter: picker,
onBackToAnchor: () => {
void this.plugin.routeChapterToReader(anchorFile);
},
onGoToContext: (file) => {
void this.plugin.routeChapterToReader(file).then(() => {
this.plugin.setAnchoredPrepChapter(file);
});
},
});
}
private buildSidebarHeaderWithPicker(
showScopeToggle: boolean,
options?: {
placeholder?: boolean;
headerCls?: string;
onPick?: (file: TFile) => void;
},
parentEl?: HTMLElement
): void {
const host = parentEl ?? this.containerEl;
this.headerEl = host.createDiv({
cls: options?.headerCls ?? 'study-bible-sidebar-header is-prep-anchored',
});
this.chapterTitleEl = null;
const titleBtn = this.headerEl.createEl('button', {
cls: 'study-bible-chapter-picker study-bible-sidebar-chapter-picker',
type: 'button',
});
const label =
this.activeFile && !options?.placeholder
? this.formatSidebarTitle(this.activeFile)
: 'Pick a chapter';
titleBtn.createSpan({ cls: 'study-bible-chapter-picker-label', text: label });
titleBtn.toggleClass('is-placeholder', Boolean(options?.placeholder));
titleBtn.createSpan({ cls: 'study-bible-chapter-picker-chevron', text: '▾' });
titleBtn.addEventListener('click', (event) => {
event.stopPropagation();
this.openSidebarChapterPicker(titleBtn, options?.onPick);
});
if (!options?.placeholder && this.activeFile) {
this.insertChapterNavRow(this.headerEl, this.activeFile);
}
if (showScopeToggle && !options?.placeholder) {
this.appendScopeToggle(true, this.headerEl);
}
}
private syncPrepChapterPickerLabel(): void {
const labelEl = this.headerEl?.querySelector('.study-bible-chapter-picker-label');
if (!(labelEl instanceof HTMLElement)) {
return;
}
if (this.activeFile) {
labelEl.setText(this.formatSidebarTitle(this.activeFile));
}
this.headerEl
?.querySelector('.study-bible-sidebar-chapter-picker')
?.toggleClass('is-placeholder', !this.activeFile);
}
private openSidebarChapterPicker(
anchorEl: HTMLElement,
onPick?: (file: TFile) => void
): void {
this.chapterPicker.open({
files: listBibleChapterFiles(this.app, this.plugin.settings.bibleBooksFolder),
onPick:
onPick ??
((file) => {
this.plugin.setAnchoredPrepChapter(file);
}),
currentFile: this.activeFile ?? undefined,
anchorEl,
containerEl: this.containerEl,
});
}
private appendScopeToggle(showScopeToggle: boolean, host?: HTMLElement): void {
if (!showScopeToggle) {
return;
}
const container = host ?? this.headerEl;
if (!container) {
return;
}
this.toggleEl = container.createDiv({ cls: 'study-bible-scope-toggle' });
SCOPES.forEach((scope) => {
const btn = this.toggleEl.createEl('button', {
cls: 'study-bible-scope-btn',
text: SCOPE_LABELS[scope],
attr: { type: 'button' },
});
btn.toggleClass('is-active', scope === this.scope);
btn.addEventListener('click', (event) => {
event.stopPropagation();
this.scope = scope;
this.toggleEl.querySelectorAll('.study-bible-scope-btn').forEach((el) => {
el.toggleClass('is-active', el === btn);
});
void this.renderList();
});
});
}
private updateChapterTitle(): void {
if (!this.chapterTitleEl) {
return;
}
const title =
this.activeFile != null
? this.formatSidebarTitle(this.activeFile)
: this.formatSidebarTitleFromNotes();
this.chapterTitleEl.setText(title);
this.chapterTitleEl.toggleClass('is-empty', title.length === 0);
}
private destroyPrepTagSearch(): void {
this.prepTagSearch?.destroy();
this.prepTagSearch = null;
}
private buildPrepShell(options?: { placeholder?: boolean }): void {
this.destroyPrepTagSearch();
this.containerEl.empty();
this.containerEl.addClass('study-bible-notes-panel-sidebar', 'is-prep-anchored');
this.containerEl.removeClass('is-chapter-context', 'is-reader-entry');
this.prepPillsEl = this.containerEl.createDiv({ cls: 'study-bible-prep-pills' });
this.appendPrepPaneButton('verse', 'Verse notes');
this.appendPrepPaneButton('tags', 'Tag search');
const hasChapter = !options?.placeholder && this.activeFile != null;
this.versePaneEl = this.containerEl.createDiv({ cls: 'study-bible-prep-pane' });
this.versePaneEl.dataset.pane = 'verse';
this.buildSidebarHeaderWithPicker(hasChapter, options, this.versePaneEl);
this.listEl = this.versePaneEl.createDiv({ cls: 'study-bible-notes-list' });
this.tagsPaneEl = this.containerEl.createDiv({
cls: 'study-bible-prep-pane study-bible-prep-tags-pane',
});
this.tagsPaneEl.dataset.pane = 'tags';
this.prepTagSearch = new PrepSidebarTagSearch(this.app, this.plugin);
this.prepTagSearch.render(this.tagsPaneEl);
this.setPrepPane(this.plugin.getPrepSidebarPane());
this.appendModeFooter();
}
private appendPrepPaneButton(pane: PrepSidebarPane, label: string): void {
if (!this.prepPillsEl) {
return;
}
const btn = this.prepPillsEl.createEl('button', {
cls: 'study-bible-prep-pill',
text: label,
attr: { type: 'button' },
});
btn.dataset.pane = pane;
btn.toggleClass('is-active', pane === this.plugin.getPrepSidebarPane());
btn.addEventListener('click', (event) => {
event.stopPropagation();
this.setPrepPane(pane);
});
}
private setPrepPane(pane: PrepSidebarPane): void {
this.plugin.setPrepSidebarPane(pane);
this.prepPillsEl?.querySelectorAll('.study-bible-prep-pill').forEach((el) => {
el.toggleClass('is-active', el instanceof HTMLElement && el.dataset.pane === pane);
});
this.versePaneEl?.toggleClass('is-active', pane === 'verse');
this.tagsPaneEl?.toggleClass('is-active', pane === 'tags');
if (pane === 'verse' && this.isPrepShellBuilt() && this.activeFile) {
const shouldShowNav = this.isAwayFromPrepAnchor(this.activeFile);
const hasNav = !!this.headerEl?.querySelector('.study-bible-chapter-nav-row');
if (shouldShowNav !== hasNav) {
this.syncPrepChapterNavRow();
}
void this.renderList();
}
}
private buildShell(options?: { chapterPicker?: boolean }): void {
if (!options?.chapterPicker) {
this.destroyPrepTagSearch();
this.prepPillsEl = null;
this.versePaneEl = null;
this.tagsPaneEl = null;
}
this.containerEl.empty();
if (options?.chapterPicker) {
this.buildPrepShell();
return;
}
this.buildSidebarHeader(this.sidebarMode);
if (!this.sidebarMode) {
this.toggleEl = this.containerEl.createDiv({ cls: 'study-bible-scope-toggle' });
}
if (!this.sidebarMode) {
SCOPES.forEach((scope) => {
const btn = this.toggleEl.createEl('button', {
cls: 'study-bible-scope-btn',
text: SCOPE_LABELS[scope],
attr: { type: 'button' },
});
btn.toggleClass('is-active', scope === this.scope);
btn.addEventListener('click', () => {
this.scope = scope;
this.toggleEl.querySelectorAll('.study-bible-scope-btn').forEach((el) => {
el.toggleClass('is-active', el === btn);
});
void this.renderList();
});
});
}
this.listEl = this.containerEl.createDiv({ cls: 'study-bible-notes-list' });
this.appendModeFooter();
}
private appendModeFooter(): void {
if (!this.sidebarMode) {
return;
}
const isExpanded = this.plugin.getWorkspaceMode() === 'prep';
const footer = this.containerEl.createDiv({ cls: 'study-bible-mode-footer' });
const row = footer.createDiv({ cls: 'study-bible-expansion-switch-row' });
const copy = row.createDiv({ cls: 'study-bible-expansion-copy' });
copy.createEl('strong', {
cls: 'study-bible-mode-label',
text: isExpanded ? 'Expanded view' : 'Simplified view',
});
copy.createEl('span', {
cls: 'study-bible-mode-desc',
text: isExpanded
? 'Tag search and lesson-building tools'
: 'Easy note addition and file navigation',
});
copy.createEl('span', {
cls: 'study-bible-mode-hint',
text: isExpanded ? 'Toggle to simplified view' : 'Toggle to expanded view',
});
const switchBtn = row.createEl('button', {
cls: 'study-bible-mode-switch',
attr: {
type: 'button',
role: 'switch',
'aria-checked': String(isExpanded),
'aria-label': isExpanded
? 'Switch to simplified view'
: 'Switch to expanded view',
},
});
switchBtn.toggleClass('is-expanded', isExpanded);
switchBtn.createSpan({ cls: 'study-bible-mode-switch-thumb', attr: { 'aria-hidden': 'true' } });
attachSidebarTap(switchBtn, () => {
this.plugin.toggleWorkspaceMode();
});
}
private renderScopeEmptyHint(scope: Utils.NoteScope): void {
if (!this.notes) {
return;
}
const { lead, format } = Utils.getScopeEmptyInstructions(
scope,
this.notes.activeBook,
this.notes.activeChapter
);
const el = this.listEl.createDiv({
cls: 'study-bible-notes-empty study-bible-notes-placeholder',
});
el.createEl('p', { cls: 'study-bible-notes-placeholder-line', text: lead });
el.createEl('p', {
cls: 'study-bible-notes-placeholder-line',
text: `Note format: "${format}"`,
});
}
private async renderList(): Promise<void> {
this.listEl.empty();
if (!this.notes) {
this.renderScopeEmptyHint('chapter');
return;
}
if (this.sidebarMode) {
await this.renderSidebarList();
return;
}
if (this.scope === 'chapter') {
await this.renderChapterNotes();
} else if (this.scope === 'section') {
await this.renderFlatNotes(this.notes.rangeFiles, 'section');
} else {
await this.renderFlatNotes(this.notes.bookFiles, 'book');
}
}
private async renderSidebarList(): Promise<void> {
if (!this.notes) {
return;
}
if (this.scope === 'chapter') {
await this.renderSidebarVerseNotes();
return;
}
const files =
this.scope === 'section' ? this.notes.rangeFiles : this.notes.bookFiles;
const emptyScope: Utils.NoteScope = this.scope === 'section' ? 'section' : 'book';
if (files.length === 0) {
this.renderScopeEmptyHint(emptyScope);
return;
}
const labelStyle = emptyScope === 'section' ? 'sidebar-section' : 'sidebar-book';
this.renderBacklinkList(this.listEl, files, labelStyle);
}
private scheduleSidebarRefreshAfterOpen(): void {
this.plugin.scheduleStudyNotesSidebarRefresh();
}
private renderBacklinkList(
container: HTMLElement,
files: TFile[],
labelStyle: NoteLinkLabelStyle = 'raw'
): void {
renderBacklinkList(this.app, container, files, {
notesFolder: this.plugin.settings.bibleNotesFolder,
labelStyle,
getReaderChapter: () => this.plugin.getCurrentReaderChapter(),
afterOpen: () => this.scheduleSidebarRefreshAfterOpen(),
});
}
private async renderSidebarVerseNotes(): Promise<void> {
if (!this.notes) {
return;
}
const { verseGroups, leftoverVerseFiles, verseFiles } = this.notes;
const hasNotes =
verseGroups.length > 0 || leftoverVerseFiles.length > 0 || verseFiles.length > 0;
if (!hasNotes) {
this.renderScopeEmptyHint('chapter');
return;
}
if (verseGroups.length === 0) {
this.appendStandaloneSectionsEdit(this.listEl);
}
if (verseGroups.length > 0) {
verseGroups.forEach((groupEntry) => {
this.renderStructureSubgroup(this.listEl, groupEntry.heading, groupEntry.files, true);
});
}
if (leftoverVerseFiles.length > 0) {
if (verseGroups.length > 0) {
this.renderStructureSubgroup(this.listEl, 'Other verse notes', leftoverVerseFiles, false);
} else {
this.renderBacklinkList(this.listEl, leftoverVerseFiles, 'sidebar-verse');
}
}
}
private renderStructureSubgroup(
container: HTMLElement,
heading: string,
files: TFile[],
showSectionsEdit = false
): void {
const sub = container.createDiv({ cls: 'study-bible-sidebar-subgroup' });
const header = sub.createDiv({ cls: 'study-bible-sidebar-subgroup-header' });
header.createEl('div', { cls: 'study-bible-sidebar-subgroup-title', text: heading });
if (showSectionsEdit) {
this.appendSectionsEditBtn(header);
}
this.renderBacklinkList(sub, files, 'sidebar-verse');
}
private async renderChapterNotes(): Promise<void> {
if (!this.notes) {
return;
}
const { verseGroups, leftoverVerseFiles, verseFiles } = this.notes;
const hasNotes =
verseGroups.length > 0 || leftoverVerseFiles.length > 0 || verseFiles.length > 0;
if (this.scope === 'chapter' && hasNotes && verseGroups.length === 0) {
this.appendStandaloneSectionsEdit(this.listEl);
}
if (verseGroups.length > 0) {
for (const group of verseGroups) {
await this.renderCollapsibleGroup(group.heading, group.files, false, true);
}
}
if (leftoverVerseFiles.length > 0) {
const heading = verseGroups.length > 0 ? 'Other verse notes' : undefined;
if (heading) {
await this.renderCollapsibleGroup(heading, leftoverVerseFiles, true, false);
} else {
for (const file of leftoverVerseFiles) {
await this.renderNoteRow(this.listEl, file);
}
}
}
}
private async renderCollapsibleGroup(
heading: string,
files: TFile[],
collapsed = false,
showSectionsEdit = false
): Promise<void> {
const group = this.listEl.createDiv({ cls: 'study-bible-note-group' });
group.toggleClass('is-collapsed', collapsed);
if (showSectionsEdit) {
group.addClass('is-structure-group');
}
const header = group.createDiv({ cls: 'study-bible-note-group-header' });
header.createSpan({ cls: 'study-bible-note-group-chevron', text: collapsed ? '' : '▾' });
header.createSpan({ cls: 'study-bible-note-group-title', text: heading });
if (showSectionsEdit) {
this.appendSectionsEditBtn(header);
}
const body = group.createDiv({ cls: 'study-bible-note-group-body' });
header.addEventListener('click', () => {
const isCollapsed = !group.hasClass('is-collapsed');
group.toggleClass('is-collapsed', isCollapsed);
const chevron = header.querySelector('.study-bible-note-group-chevron');
if (chevron) {
chevron.textContent = isCollapsed ? '' : '▾';
}
});
for (const file of files) {
await this.renderNoteRow(body, file);
}
}
private async renderFlatNotes(files: TFile[], emptyScope: Utils.NoteScope): Promise<void> {
if (files.length === 0) {
this.renderScopeEmptyHint(emptyScope);
return;
}
for (const file of files) {
await this.renderNoteRow(this.listEl, file);
}
}
private formatNoteRowLabel(filename: string): string {
if (this.scope === 'section') {
return Utils.formatSidebarSectionNoteLabel(filename);
}
if (this.scope === 'book') {
return Utils.formatSidebarBookNoteLabel(filename);
}
return Utils.formatSidebarVerseNoteLabel(filename);
}
private async renderNoteRow(container: HTMLElement, file: TFile): Promise<void> {
const row = container.createDiv({ cls: 'study-bible-note-row study-bible-note-card' });
attachSidebarNoteRowHandlers(this.app, row, file, {
notesFolder: this.plugin.settings.bibleNotesFolder,
getReaderChapter: () => this.plugin.getCurrentReaderChapter(),
afterOpen: () => this.scheduleSidebarRefreshAfterOpen(),
});
const main = row.createDiv({ cls: 'study-bible-note-row-main' });
const fullName = Utils.noteBasename(file.basename);
const titleEl = main.createSpan({
cls: 'study-bible-note-title',
text: this.formatNoteRowLabel(file.basename),
});
titleEl.setAttr('title', fullName);
main.createSpan({
cls: 'study-bible-note-date',
text: Utils.formatNoteDate(file),
});
}
private appendSectionsEditBtn(container: HTMLElement): HTMLButtonElement {
const hasStructure = this.notes?.hasStructureFile ?? false;
const btn = container.createEl('button', {
type: 'button',
cls: 'study-bible-icon-btn study-bible-sections-edit-btn',
attr: {
'aria-label': hasStructure ? 'Edit verse sections' : 'Define verse sections',
title: hasStructure ? 'Edit verse sections' : 'Define verse sections',
},
});
setIcon(btn, 'pencil');
btn.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
});
attachSidebarNoteTap(
btn,
() => {
void this.openDefineSections();
},
{ guardOpen: true }
);
return btn;
}
private appendStandaloneSectionsEdit(container: HTMLElement): void {
const row = container.createDiv({ cls: 'study-bible-sections-edit-row' });
this.appendSectionsEditBtn(row);
}
private async openDefineSections(): Promise<void> {
if (!this.notes) {
return;
}
const { activeBook, activeChapter } = this.notes;
const path = normalizePath(
`${this.plugin.settings.bibleBooksFolder}/${Utils.bookToDisplayName(activeBook)} ${activeChapter} - Structure.md`
);
let file = this.app.vault.getAbstractFileByPath(path);
if (!(file instanceof TFile)) {
await this.app.vault.create(path, '- v1-4\n- v5-14 - Section title\n');
file = this.app.vault.getAbstractFileByPath(path);
}
if (file instanceof TFile) {
await openStructureFile(this.app, file, {
resolveReturnChapter: () =>
Utils.getLastBibleChapter() ?? this.plugin.getCurrentReaderChapter(),
});
}
}
}