lutu-gl_Obsidian-Minimal-Quiz/main.ts

227 lines
7 KiB
TypeScript
Raw Normal View History

import { App, Component, Editor, MarkdownRenderer, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
2025-02-18 02:00:07 +00:00
interface MinimalQuizSettings {
alignment: 'left' | 'center';
2025-03-12 10:27:47 +00:00
blurBackground: boolean;
2025-02-18 02:00:07 +00:00
}
const DEFAULT_SETTINGS: MinimalQuizSettings = {
alignment: 'center',
2025-03-12 10:27:47 +00:00
blurBackground: true
};
2025-02-18 02:00:07 +00:00
2025-03-02 19:18:58 +00:00
export default class MinimalQuizPlugin extends Plugin {
settings: MinimalQuizSettings;
2025-02-18 02:00:07 +00:00
async onload() {
await this.loadSettings();
2025-03-02 19:18:58 +00:00
this.addSettingTab(new MinimalQuizSettingTab(this.app, this));
2025-03-29 23:19:00 +00:00
this.addRibbonIcon('checkbox-glyph', 'Start quiz', () => {
2025-03-02 23:11:41 +00:00
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (activeView) {
const editor = activeView.editor;
this.startQuiz(editor);
} else {
2025-03-29 23:19:00 +00:00
new Notice('No active markdown editor found.');
2025-03-02 23:11:41 +00:00
}
});
this.addCommand({
id: 'show-questions-modal',
name: 'Start quiz on current file',
editorCallback: (editor: Editor, view: MarkdownView) => {
2025-03-02 23:11:41 +00:00
this.startQuiz(editor);
},
});
}
2025-03-02 23:11:41 +00:00
startQuiz(editor: Editor) {
const content = editor.getValue();
const qaMap = this.extractQuestionsAndAnswers(content);
const entries = Array.from(qaMap.entries());
if (entries.length > 0) {
const filePath = this.app.workspace.getActiveFile()?.path ?? 'unknown';
new QuestionsModal(this.app, entries, this.settings, filePath).open();
2025-03-02 23:11:41 +00:00
} else {
new Notice('No questions found.');
}
}
2025-02-18 02:00:07 +00:00
extractQuestionsAndAnswers(content: string): Map<string, string> {
const qaMap = new Map<string, string>();
const regex = /(.*\?)\n([\s\S]*?)(?=\n\n|$)/g;
let match;
while ((match = regex.exec(content)) !== null) {
const question = match[1].trim();
const answer = match[2].trim();
if (question && answer) {
qaMap.set(question, answer);
}
}
2025-02-18 02:00:07 +00:00
return qaMap;
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
2025-02-18 02:00:07 +00:00
async saveSettings() {
await this.saveData(this.settings);
}
2025-02-18 02:00:07 +00:00
}
2025-03-02 19:18:58 +00:00
class QuestionsModal extends Modal {
entries: [string, string][];
answerVisible = false;
currentIndex = 0;
settings: MinimalQuizSettings;
component: Component;
sourcePath: string;
constructor(app: App, questions: [string, string][], settings: MinimalQuizSettings, sourcePath: string) {
super(app);
this.entries = questions;
this.settings = settings;
this.component = new Component();
this.sourcePath = sourcePath;
}
onOpen() {
2025-03-12 10:27:47 +00:00
const { modalEl } = this;
// blur of the background
const bgEl = modalEl.parentElement;
if (bgEl && this.settings.blurBackground) {
bgEl.classList.remove("quiz-no-blur");
bgEl.classList.add("quiz-blur");
2025-03-12 10:27:47 +00:00
} else if (bgEl) {
bgEl.classList.remove("quiz-blur");
bgEl.classList.add("quiz-no-blur");
2025-03-12 10:27:47 +00:00
}
this.scope.register([], 'Enter', () => {
this.toggleAnswer();
});
this.scope.register([], ' ', () => {
this.toggleAnswer();
});
2025-02-18 02:00:07 +00:00
this.render();
}
2025-03-02 21:17:13 +00:00
render() {
const { contentEl } = this;
contentEl.empty();
2025-03-02 20:29:20 +00:00
2025-03-02 21:17:13 +00:00
if (this.currentIndex >= this.entries.length) {
2025-03-29 23:19:00 +00:00
new Notice('You finished the quiz - good job!');
2025-03-02 21:17:13 +00:00
this.close();
return;
}
2025-02-18 02:00:07 +00:00
2025-03-02 21:17:13 +00:00
const progressText = `${this.currentIndex + 1}/${this.entries.length}`;
const progressEl = contentEl.createEl('div', {
text: `Progress: ${progressText}`,
});
progressEl.classList.add('quiz-progress');
2025-03-02 20:29:20 +00:00
2025-03-02 21:17:13 +00:00
const [question, answer] = this.entries[this.currentIndex];
2025-03-02 20:29:20 +00:00
const questionContainer = contentEl.createEl('div');
this.renderQuestion(question, questionContainer);
2025-03-02 23:11:41 +00:00
const answerContainer = contentEl.createEl('div');
if (this.answerVisible) {
this.renderAnswer(answer, answerContainer);
}
const isLastQuestion = this.currentIndex === this.entries.length - 1;
const buttonText = this.answerVisible
2025-03-29 23:19:00 +00:00
? (isLastQuestion ? 'Finish quiz' : 'Next question')
: 'Show answer';
const button = contentEl.createEl('button', {
text: buttonText,
});
button.classList.add('quiz-button');
2025-03-02 21:17:13 +00:00
button.addEventListener('click', () => this.toggleAnswer());
contentEl.classList.remove('quiz-align-left', 'quiz-align-center');
contentEl.classList.add(
this.settings.alignment === 'center' ? 'quiz-align-center' : 'quiz-align-left'
);
2025-03-02 21:17:13 +00:00
}
renderQuestion(question: string, questionContainer: HTMLElement) {
this.component.load();
MarkdownRenderer.render(this.app, question, questionContainer, this.sourcePath, this.component);
}
2025-02-18 02:00:07 +00:00
renderAnswer(answer: string, answerContainer: HTMLElement){
this.component.load();
MarkdownRenderer.render(this.app, answer, answerContainer, this.sourcePath, this.component);
2025-03-02 23:11:41 +00:00
}
toggleAnswer() {
if (this.answerVisible) {
this.currentIndex++;
this.answerVisible = false;
} else {
this.answerVisible = true;
}
this.render();
}
onClose() {
const { contentEl } = this;
contentEl.empty();
this.component.unload();
}
}
class MinimalQuizSettingTab extends PluginSettingTab {
plugin: MinimalQuizPlugin;
constructor(app: App, plugin: MinimalQuizPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
2025-03-29 23:19:00 +00:00
.setName('Modal alignment')
.setDesc('Choose the alignment of the modal content.')
.addDropdown((dropdown) =>
dropdown
.addOption('left', 'Left')
.addOption('center', 'Center')
.setValue(this.plugin.settings.alignment)
.onChange(async (value: 'left' | 'center') => {
this.plugin.settings.alignment = value;
await this.plugin.saveSettings();
})
);
2025-03-12 10:27:47 +00:00
new Setting(containerEl)
2025-03-29 23:19:00 +00:00
.setName('Blur background')
2025-03-12 10:27:47 +00:00
.setDesc('Enable or disable background blur when the modal is open.')
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.blurBackground)
.onChange(async (value) => {
this.plugin.settings.blurBackground = value;
await this.plugin.saveSettings();
})
);
}
}