mirror of
https://github.com/skylernorgaard/obsidian-bible-journal-plugin.git
synced 2026-07-22 08:30:44 +00:00
944 lines
28 KiB
TypeScript
944 lines
28 KiB
TypeScript
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(),
|
||
});
|
||
}
|
||
}
|
||
}
|