Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
13 changes: 13 additions & 0 deletions src/escape-html.ts
Original file line number Diff line number Diff line change
@@ -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, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
42 changes: 41 additions & 1 deletion src/gui/card-ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -70,6 +71,9 @@ export class CardUI {
public answerButton: HTMLButtonElement;
public lastPressed: number;

private clozeInputs: NodeListOf<Element>;
private clozeAnswers: NodeListOf<Element>;

private chosenDeck: Deck | null;
private totalCardsInSession: number = 0;
private totalDecksInSession: number = 0;
Expand Down Expand Up @@ -217,6 +221,9 @@ export class CardUI {

// Update response buttons
this._resetResponseButtons();

// Setup cloze input listeners
this._setupClozeInputListeners();
}

private get _currentCard(): Card {
Expand Down Expand Up @@ -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
? `<span style="color: green">${escapeHtml(inputText)}</span>`
: `[<span style="color: red; text-decoration: line-through;">${escapeHtml(inputText)}</span><span style="color: green">${answerText}</span>]`;
clozeAnswer.innerHTML = answerElement;
}
}
}

private _showAnswer(): void {
const timeNow = now();
if (
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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()
) {
Expand Down
18 changes: 18 additions & 0 deletions src/gui/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
);
Expand Down
3 changes: 3 additions & 0 deletions src/lang/locale/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code>${defaultPattern}</code> from your "Cloze Patterns"',
Expand Down
20 changes: 19 additions & 1 deletion src/question-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -124,6 +128,20 @@ export class QuestionTypeClozeFormatter implements IClozeFormatter {
}
}

export class QuestionTypeClozeInputFormatter implements IClozeFormatter {
asking(answer?: string, hint?: string): string {
return `<span style='color:#2196f3'><input class="cloze-input" type="text" size="${!answer ? 1 : answer.length}" />${!hint ? "" : `[${hint}]`}</span>`;
}

showingAnswer(answer: string, _hint?: string): string {
return `<span class="cloze-answer" style='color:#2196f3'>${answer}</span>`;
}

hiding(answer?: string, hint?: string): string {
return `<span style='color:var(--code-comment)'>${!hint ? "[...]" : `[${hint}]`}</span>`;
}
}

export class QuestionTypeFactory {
static create(questionType: CardType): IQuestionTypeHandler {
let handler: IQuestionTypeHandler;
Expand Down
2 changes: 2 additions & 0 deletions src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface SRSettings {
randomizeCardOrder: boolean;
flashcardCardOrder: string;
flashcardDeckOrder: string;
convertClozePatternsToInputs: boolean;
convertHighlightsToClozes: boolean;
convertBoldTextToClozes: boolean;
convertCurlyBracketsToClozes: boolean;
Expand Down Expand Up @@ -73,6 +74,7 @@ export const DEFAULT_SETTINGS: SRSettings = {
randomizeCardOrder: null,
flashcardCardOrder: "DueFirstRandom",
flashcardDeckOrder: "PrevDeckComplete_Sequential",
convertClozePatternsToInputs: false,
convertHighlightsToClozes: true,
convertBoldTextToClozes: false,
convertCurlyBracketsToClozes: false,
Expand Down