diff --git a/CHANGELOG.md b/CHANGELOG.md index cb4f94f7..1fcfbd40 120000 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1 @@ -docs/docs/changelog.md \ No newline at end of file +docs/docs/changelog.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 54cb9a6d..f6af8f1f 120000 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1 +1 @@ -docs/docs/en/contributing.md \ No newline at end of file +docs/docs/en/contributing.md diff --git a/src/escape-html.ts b/src/escape-html.ts new file mode 100644 index 00000000..2fb46d9c --- /dev/null +++ b/src/escape-html.ts @@ -0,0 +1,13 @@ +/** + * Escapes HTML special characters in a string. + * @param s - The input string. + * @returns - The escaped string. + */ +export function escapeHtml(s: string): string { + return s + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} diff --git a/src/gui/card-ui.tsx b/src/gui/card-ui.tsx index 2ff5fbab..7ec05c92 100644 --- a/src/gui/card-ui.tsx +++ b/src/gui/card-ui.tsx @@ -6,6 +6,7 @@ import { ReviewResponse } from "src/algorithms/base/repetition-item"; import { textInterval } from "src/algorithms/osr/note-scheduling"; import { Card } from "src/card"; import { Deck } from "src/deck"; +import { escapeHtml } from "src/escape-html"; import { FlashcardReviewMode, IFlashcardReviewSequencer as IFlashcardReviewSequencer, @@ -70,6 +71,9 @@ export class CardUI { public answerButton: HTMLButtonElement; public lastPressed: number; + private clozeInputs: NodeListOf; + private clozeAnswers: NodeListOf; + private chosenDeck: Deck | null; private totalCardsInSession: number = 0; private totalDecksInSession: number = 0; @@ -217,6 +221,9 @@ export class CardUI { // Update response buttons this._resetResponseButtons(); + + // Setup cloze input listeners + this._setupClozeInputListeners(); } private get _currentCard(): Card { @@ -556,6 +563,35 @@ export class CardUI { } } + private _setupClozeInputListeners(): void { + this.clozeInputs = document.querySelectorAll(".cloze-input"); + + this.clozeInputs.forEach((input) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + input.addEventListener("change", (e) => {}); + }); + } + + private _evaluateClozeAnswers(): void { + this.clozeAnswers = document.querySelectorAll(".cloze-answer"); + + if (this.clozeAnswers.length === this.clozeInputs.length) { + for (let i = 0; i < this.clozeAnswers.length; i++) { + const clozeInput = this.clozeInputs[i] as HTMLInputElement; + const clozeAnswer = this.clozeAnswers[i] as HTMLElement; + + const inputText = clozeInput.value.trim(); + const answerText = clozeAnswer.innerText.trim(); + + const answerElement = + inputText === answerText + ? `${escapeHtml(inputText)}` + : `[${escapeHtml(inputText)}${answerText}]`; + clozeAnswer.innerHTML = answerElement; + } + } + } + private _showAnswer(): void { const timeNow = now(); if ( @@ -589,6 +625,9 @@ export class CardUI { this._currentQuestion.questionText.textDirection, ); + // Evaluate cloze answers + this._evaluateClozeAnswers(); + // Show response buttons this.answerButton.addClass("sr-is-hidden"); this.hardButton.removeClass("sr-is-hidden"); @@ -619,9 +658,10 @@ export class CardUI { } private _keydownHandler = (e: KeyboardEvent) => { - // Prevents any input, if the edit modal is open or if the view is not in focus + // Prevents any input, if the edit modal is open, input area is in focus, or if the view is not in focus if ( document.activeElement.nodeName === "TEXTAREA" || + document.activeElement.nodeName === "INPUT" || this.mode === FlashcardMode.Closed || !this.plugin.getSRInFocusState() ) { diff --git a/src/gui/settings.tsx b/src/gui/settings.tsx index 20dc2092..d09bd970 100644 --- a/src/gui/settings.tsx +++ b/src/gui/settings.tsx @@ -197,6 +197,24 @@ export class SRSettingTab extends PluginSettingTab { ); containerEl.createEl("h3", { text: t("GROUP_FLASHCARD_SEPARATORS") }); + const convertClozePatternsToInputsEl = new Setting(containerEl).setName( + t("CONVERT_CLOZE_PATTERNS_TO_INPUTS"), + ); + convertClozePatternsToInputsEl.descEl.insertAdjacentHTML( + "beforeend", + t("CONVERT_CLOZE_PATTERNS_TO_INPUTS_DESC"), + ); + convertClozePatternsToInputsEl.addToggle((toggle) => + toggle + .setValue(this.plugin.data.settings.convertClozePatternsToInputs) + .onChange(async (value) => { + this.plugin.data.settings.convertClozePatternsToInputs = value; + await this.plugin.savePluginData(); + + this.display(); + }), + ); + const convertHighlightsToClozesEl = new Setting(containerEl).setName( t("CONVERT_HIGHLIGHTS_TO_CLOZES"), ); diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 5f9f0aed..73fc162f 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -118,6 +118,9 @@ export default { "Randomly (once all cards in previous deck reviewed)", REVIEW_DECK_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck", DISABLE_CLOZE_CARDS: "Disable cloze cards?", + CONVERT_CLOZE_PATTERNS_TO_INPUTS: "Convert cloze patterns to input fields", + CONVERT_CLOZE_PATTERNS_TO_INPUTS_DESC: + "Replace cloze patterns with input fields when reviewing cloze cards.", CONVERT_HIGHLIGHTS_TO_CLOZES: "Convert ==highlights== to clozes", CONVERT_HIGHLIGHTS_TO_CLOZES_DESC: 'Add/remove the ${defaultPattern} from your "Cloze Patterns"', diff --git a/src/question-type.ts b/src/question-type.ts index b11c2174..17b05edf 100644 --- a/src/question-type.ts +++ b/src/question-type.ts @@ -96,7 +96,11 @@ class QuestionTypeCloze implements IQuestionTypeHandler { expand(questionText: string, settings: SRSettings): CardFrontBack[] { const clozecrafter = new ClozeCrafter(settings.clozePatterns); const clozeNote = clozecrafter.createClozeNote(questionText); - const clozeFormatter = new QuestionTypeClozeFormatter(); + + // Determine which question formatter to use based on settings (Cloze patterns as inputs or not). + const clozeFormatter = settings.convertClozePatternsToInputs + ? new QuestionTypeClozeInputFormatter() + : new QuestionTypeClozeFormatter(); let front: string, back: string; const result: CardFrontBack[] = []; @@ -124,6 +128,20 @@ export class QuestionTypeClozeFormatter implements IClozeFormatter { } } +export class QuestionTypeClozeInputFormatter implements IClozeFormatter { + asking(answer?: string, hint?: string): string { + return `${!hint ? "" : `[${hint}]`}`; + } + + showingAnswer(answer: string, _hint?: string): string { + return `${answer}`; + } + + hiding(answer?: string, hint?: string): string { + return `${!hint ? "[...]" : `[${hint}]`}`; + } +} + export class QuestionTypeFactory { static create(questionType: CardType): IQuestionTypeHandler { let handler: IQuestionTypeHandler; diff --git a/src/settings.ts b/src/settings.ts index d2c8a3da..41999a9a 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -13,6 +13,7 @@ export interface SRSettings { randomizeCardOrder: boolean; flashcardCardOrder: string; flashcardDeckOrder: string; + convertClozePatternsToInputs: boolean; convertHighlightsToClozes: boolean; convertBoldTextToClozes: boolean; convertCurlyBracketsToClozes: boolean; @@ -73,6 +74,7 @@ export const DEFAULT_SETTINGS: SRSettings = { randomizeCardOrder: null, flashcardCardOrder: "DueFirstRandom", flashcardDeckOrder: "PrevDeckComplete_Sequential", + convertClozePatternsToInputs: false, convertHighlightsToClozes: true, convertBoldTextToClozes: false, convertCurlyBracketsToClozes: false,