Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cloze improvements #2846

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
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
5 changes: 5 additions & 0 deletions .changeset/spotty-mails-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@jspsych/plugin-cloze": minor
---

adds support for multiple correct answers and case sensitivity
3 changes: 2 additions & 1 deletion docs/plugins/cloze.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ In addition to the [parameters available in all plugins](../overview/plugins.md#

| Parameter | Type | Default Value | Description |
| ------------- | -------- | ------------------ | ---------------------------------------- |
| text | string | *undefined* | The cloze text to be displayed. Blanks are indicated by %% signs and automatically replaced by input fields. If there is a correct answer you want the system to check against, it must be typed between the two percentage signs (i.e. % correct solution %). |
| text | string | *undefined* | The cloze text to be displayed. Blanks are indicated by %% signs and automatically replaced by input fields. If there is a correct answer you want the system to check against, it must be typed between the two percentage signs (i.e. % correct solution %). To input multiple correct answers, add a / between each answer (i.e. %correct/alsocorrect%). |
| button_text | string | OK | Text of the button participants have to press for finishing the cloze test. |
| check_answers | boolean | false | Boolean value indicating if the answers given by participants should be compared against a correct solution given in the text (between % signs) after the button was clicked. If ```true```, answers are checked and in case of differences, the ```mistake_fn``` is called. In this case, the trial does not automatically finish. If ```false```, no checks are performed and the trial automatically ends when clicking the button. |
| allow_blanks | boolean | true | Boolean value indicating if the answers given by participants should be checked for completion after the button was clicked. If ```true```, answers are not checked for completion and blank answers are allowed. The trial will then automatically finish upon the clicking the button. If ```false```, answers are checked for completion, and in case there are some fields with missing answers, the ```mistake_fn``` is called. In this case, the trial does not automatically finish. |
| case_sensitivity | boolean | true | Boolean value indicating if the answers given by participants should also be checked to have the right case along with correctness. If set to ```false```, case is disregarded and participants may type in whatever case they please. |
| mistake_fn | function | ```function(){}``` | Function called if ```check_answers``` is set to ```true``` and there is a difference between the participant's answers and the correct solution provided in the text, or if ```allow_blanks``` is set to ```false``` and there is at least one field with a blank answer. |

## Data Generated
Expand Down
8 changes: 8 additions & 0 deletions examples/jspsych-cloze.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@
text: 'The %% is the largest terrestrial mammal. It lives in both %% and %%.'
});

// an example that allows the user to input a solution that doesn't require case sensitivity, and allows multiple responses
timeline.push({
type: jsPsychCloze,
text: 'The %CASE/door/EyE% is closed.',
check_answers: true,
case_sensitivity: false,
})

// another example with checking if all the blanks are filled in
timeline.push({
type: jsPsychCloze,
Expand Down
39 changes: 39 additions & 0 deletions packages/plugin-cloze/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ const getInputElementById = (id: string) => document.getElementById(id) as HTMLI

const clickFinishButton = () => clickTarget(document.querySelector("#finish_cloze_button"));

// reset DOM
beforeEach(() => {
document.body.innerHTML = "";
});

describe("cloze", () => {
test("displays cloze", async () => {
const { getHTML, expectFinished } = await startTimeline([
Expand Down Expand Up @@ -84,6 +89,21 @@ describe("cloze", () => {
await expectFinished();
});

test("ends trial on button click when answers are checked and correct without case sensitivity", async () => {
const { expectFinished } = await startTimeline([
{
type: cloze,
text: "This is a %cloze% text.",
check_answers: true,
case_sensitivity: false,
},
]);

getInputElementById("input0").value = "CLOZE";
clickTarget(document.querySelector("#finish_cloze_button"));
await expectFinished();
});

test("ends trial on button click when all answers are checked for completion and are complete", async () => {
const { expectFinished } = await startTimeline([
{
Expand Down Expand Up @@ -185,6 +205,25 @@ describe("cloze", () => {
await expectFinished();
});

test.skip("calls mistake function on button click when answers are checked and do not belong to a multiple answer blank", async () => {
const mistakeFn = jest.fn();

const { expectFinished } = await startTimeline([
{
type: cloze,
text: "This is a %cloze/jspsych% text.",
check_answers: true,
mistake_fn: mistakeFn,
},
]);

getInputElementById("input0").value = "not fitting in answer";
await clickFinishButton();
expect(mistakeFn).toHaveBeenCalled();

await expectFinished();
});

test("response data is stored as an array", async () => {
const { getData, expectFinished } = await startTimeline([
{
Expand Down
76 changes: 55 additions & 21 deletions packages/plugin-cloze/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ const info = <const>{
name: "cloze",
version: version,
parameters: {
/** The cloze text to be displayed. Blanks are indicated by %% signs and automatically replaced by input fields. If there is a correct answer you want the system to check against, it must be typed between the two percentage signs (i.e. % correct solution %). */
/**
* The cloze text to be displayed. Blanks are indicated by %% signs and automatically replaced by
* input fields. If there is a correct answer you want the system to check against, it must be typed
* between the two percentage signs (i.e. % correct solution %). If you would like to input multiple
* solutions, type a slash between each responses (i.e. %1/2/3%).
*/
text: {
type: ParameterType.HTML_STRING,
default: undefined,
Expand All @@ -16,24 +21,47 @@ const info = <const>{
type: ParameterType.STRING,
default: "OK",
},
/** Boolean value indicating if the answers given by participants should be compared against a correct solution given in the text (between % signs) after the button was clicked. If ```true```, answers are checked and in case of differences, the ```mistake_fn``` is called. In this case, the trial does not automatically finish. If ```false```, no checks are performed and the trial automatically ends when clicking the button. */
/**
* Boolean value indicating if the answers given by participants should be compared
* against a correct solution given in `text` after the submit button was clicked.
* If ```true```, answers are checked and in case of differences, the ```mistake_fn```
* is called. In this case, the trial does not automatically finish. If ```false```,
* no checks are performed and the trial ends when clicking the submit button.
*/
check_answers: {
type: ParameterType.BOOL,
default: false,
},
/** Boolean value indicating if the answers given by participants should be checked for completion after the button was clicked. If ```true```, answers are not checked for completion and blank answers are allowed. The trial will then automatically finish upon the clicking the button. If ```false```, answers are checked for completion, and in case there are some fields with missing answers, the ```mistake_fn``` is called. In this case, the trial does not automatically finish. */
/**
* Boolean value indicating if the answers given by participants should be checked for
* completion after the button was clicked. If ```true```, answers are not checked for
* completion and blank answers are allowed. The trial will then automatically finish
* upon the clicking the button. If ```false```, answers are checked for completion,
* and in case there are some fields with missing answers, the ```mistake_fn``` is called.
* In this case, the trial does not automatically finish.
*/
allow_blanks: {
type: ParameterType.BOOL,
default: true,
},
/** Function called if ```check_answers``` is set to ```true``` and there is a difference between the participant's answers and the correct solution provided in the text, or if ```allow_blanks``` is set to ```false``` and there is at least one field with a blank answer. */
/** Boolean value indicating if the solutions checker must be case sensitive. */
case_sensitivity: {
type: ParameterType.BOOL,
pretty_name: "Case sensitivity",
default: true,
},
/**
* Function called if either `check_answers` is `true` or `allow_blanks` is `false`
* and there is a discrepancy between the set answers and the answers provided, or
* if all input fields aren't filled out, respectively.
*/
mistake_fn: {
type: ParameterType.FUNCTION,
default: () => {},
},
},
data: {
/** Answers the partcipant gave. */
/** Answers the participant gave. */
response: {
type: ParameterType.STRING,
array: true,
Expand All @@ -58,7 +86,7 @@ class ClozePlugin implements JsPsychPlugin<Info> {
var html = '<div class="cloze">';
// odd elements are text, even elements are the blanks
var elements = trial.text.split("%");
const solutions = this.getSolutions(trial.text);
const solutions = this.getSolutions(trial.text, trial.case_sensitivity);

let solution_counter = 0;
for (var i = 0; i < elements.length; i++) {
Expand All @@ -75,16 +103,18 @@ class ClozePlugin implements JsPsychPlugin<Info> {
display_element.innerHTML = html;

const check = () => {
var answers: String[] = [];
var answers: string[] = [];
var answers_correct = true;
var answers_filled = true;

for (var i = 0; i < solutions.length; i++) {
var field = document.getElementById("input" + i) as HTMLInputElement;
answers.push(field.value.trim());
answers.push(
trial.case_sensitivity ? field.value.trim() : field.value.toLowerCase().trim()
);

if (trial.check_answers) {
if (answers[i] !== solutions[i]) {
if (!solutions[i].includes(answers[i])) {
field.style.color = "red";
answers_correct = false;
} else {
Expand Down Expand Up @@ -114,15 +144,18 @@ class ClozePlugin implements JsPsychPlugin<Info> {
trial.button_text +
"</button>";
display_element.querySelector("#finish_cloze_button").addEventListener("click", check);

(display_element.querySelector("#input0") as HTMLElement).focus();
}

private getSolutions(text: string) {
const solutions = [];
private getSolutions(text: string, case_sensitive: boolean): string[][] {
const solutions: string[][] = [];
const elements = text.split("%");
for (let i = 0; i < elements.length; i++) {
if (i % 2 == 1) {
solutions.push(elements[i].trim());
}

for (let i = 1; i < elements.length; i += 2) {
solutions.push(
case_sensitive ? elements[i].trim().split("/") : elements[i].toLowerCase().trim().split("/")
);
}

return solutions;
Expand All @@ -144,13 +177,14 @@ class ClozePlugin implements JsPsychPlugin<Info> {
}

private create_simulation_data(trial: TrialType<Info>, simulation_options) {
const solutions = this.getSolutions(trial.text);
const responses = [];
for (const word of solutions) {
if (word == "") {
responses.push(this.jsPsych.randomization.randomWords({ exactly: 1 }));
const solutions = this.getSolutions(trial.text, trial.case_sensitivity);
const responses: string[] = [];
for (const wordList of solutions) {
if (wordList.includes("")) {
var word = this.jsPsych.randomization.randomWords({ exactly: 1 });
responses.push(word[0]);
} else {
responses.push(word);
responses.push(wordList[Math.floor(Math.random() * wordList.length)]);
}
}

Expand Down
Loading