mirror of
https://github.com/jspsych/jsPsych.git
synced 2025-05-10 11:10:54 +00:00
Compare commits
5 Commits
861cd1e646
...
2c103e5b44
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2c103e5b44 | ||
![]() |
1d6e191c3d | ||
![]() |
85ad7d40da | ||
![]() |
aff11c4291 | ||
![]() |
7e1b0f2df5 |
@ -5,107 +5,106 @@
|
|||||||
<script src="../packages/plugin-free-sort/dist/index.browser.js"></script>
|
<script src="../packages/plugin-free-sort/dist/index.browser.js"></script>
|
||||||
<script src="../packages/plugin-canvas-keyboard-response/dist/index.browser.js"></script>
|
<script src="../packages/plugin-canvas-keyboard-response/dist/index.browser.js"></script>
|
||||||
<script src="../packages/plugin-preload/dist/index.browser.js"></script>
|
<script src="../packages/plugin-preload/dist/index.browser.js"></script>
|
||||||
<link rel="stylesheet" href="../packages/jspsych/css/jspsych.css">
|
<link rel="stylesheet" href="../packages/jspsych/css/jspsych.css" />
|
||||||
</head>
|
</head>
|
||||||
<body></body>
|
<body></body>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
var jsPsych = initJsPsych({
|
var jsPsych = initJsPsych({
|
||||||
on_finish: function() {
|
on_finish: function() {
|
||||||
jsPsych.data.displayData();
|
jsPsych.data.displayData();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var preload = {
|
var preload = {
|
||||||
type: jsPsychPreload,
|
type: jsPsychPreload,
|
||||||
auto_preload: true
|
auto_preload: true
|
||||||
};
|
};
|
||||||
|
|
||||||
var trial_1 = {
|
var trial_1 = {
|
||||||
type: jsPsychFreeSort,
|
type: jsPsychFreeSort,
|
||||||
stimuli: ['img/happy_face_1.jpg','img/happy_face_3.jpg','img/sad_face_2.jpg','img/sad_face_4.jpg'],
|
stimuli: ['img/happy_face_1.jpg','img/happy_face_3.jpg','img/sad_face_2.jpg','img/sad_face_4.jpg'],
|
||||||
stim_height: 120,
|
stim_height: 120,
|
||||||
stim_width: 160,
|
stim_width: 160,
|
||||||
sort_area_height: 500,
|
sort_area_height: 500,
|
||||||
sort_area_width: 500,
|
sort_area_width: 500,
|
||||||
prompt: 'Please group similar expressions together. ',
|
prompt: 'Please group similar expressions together. ',
|
||||||
column_spread_factor: .5
|
column_spread_factor: .5
|
||||||
};
|
};
|
||||||
|
|
||||||
var trial_2 = {
|
var trial_2 = {
|
||||||
type: jsPsychFreeSort,
|
type: jsPsychFreeSort,
|
||||||
stimuli: ['img/happy_face_1.jpg','img/happy_face_3.jpg','img/sad_face_2.jpg','img/sad_face_4.jpg'],
|
stimuli: ['img/happy_face_1.jpg','img/happy_face_3.jpg','img/sad_face_2.jpg','img/sad_face_4.jpg'],
|
||||||
stim_height: 120,
|
stim_height: 120,
|
||||||
stim_width: 160,
|
stim_width: 160,
|
||||||
prompt: '<p>Arrange the faces.</p>',
|
prompt: '<p>Arrange the faces.</p>',
|
||||||
prompt_location: "below",
|
prompt_location: "below",
|
||||||
sort_area_height: 400,
|
sort_area_height: 400,
|
||||||
sort_area_width: 500,
|
sort_area_width: 500,
|
||||||
sort_area_shape: "square",
|
sort_area_shape: "square",
|
||||||
scale_factor: 1.2,
|
scale_factor: 1.2,
|
||||||
border_color_in: '#DCDCDC',
|
border_color_in: '#DCDCDC',
|
||||||
border_color_out: 'red'
|
border_color_out: 'red'
|
||||||
};
|
};
|
||||||
|
|
||||||
var trial_3 = {
|
var trial_3 = {
|
||||||
type: jsPsychCanvasKeyboardResponse,
|
type: jsPsychCanvasKeyboardResponse,
|
||||||
stimulus: function(c) {
|
stimulus: function(c) {
|
||||||
c.style.border = "5px solid #A9A9A9";
|
c.style.border = "5px solid #A9A9A9";
|
||||||
// present images on a canvas at specific locations
|
// present images on a canvas at specific locations
|
||||||
var ctx = c.getContext('2d');
|
var ctx = c.getContext('2d');
|
||||||
var img1 = new Image();
|
var img1 = new Image();
|
||||||
var img2 = new Image();
|
var img2 = new Image();
|
||||||
var img3 = new Image();
|
var img3 = new Image();
|
||||||
img1.src = 'img/1.gif';
|
img1.src = 'img/1.gif';
|
||||||
img2.src = 'img/2.gif';
|
img2.src = 'img/2.gif';
|
||||||
img3.src = 'img/3.gif';
|
img3.src = 'img/3.gif';
|
||||||
img1.addEventListener('load', function() {
|
img1.addEventListener('load', function() {
|
||||||
ctx.drawImage(img1, 0, 0, 90, 90);
|
ctx.drawImage(img1, 0, 0, 90, 90);
|
||||||
}, false);
|
}, false);
|
||||||
img2.addEventListener('load', function() {
|
img2.addEventListener('load', function() {
|
||||||
ctx.drawImage(img2, 400, 400, 90, 90);
|
ctx.drawImage(img2, 400, 400, 90, 90);
|
||||||
}, false);
|
}, false);
|
||||||
img3.addEventListener('load', function() {
|
img3.addEventListener('load', function() {
|
||||||
ctx.drawImage(img3, 300, 100, 90, 90);
|
ctx.drawImage(img3, 300, 100, 90, 90);
|
||||||
}, false);
|
}, false);
|
||||||
},
|
},
|
||||||
canvas_size: [500,500],
|
canvas_size: [500,500],
|
||||||
prompt: "Memorize the image locations (5s).",
|
prompt: "Memorize the image locations (5s).",
|
||||||
choices: "NO_KEYS",
|
choices: "NO_KEYS",
|
||||||
trial_duration: 5000
|
trial_duration: 5000
|
||||||
};
|
};
|
||||||
|
|
||||||
var trial_4 = {
|
var trial_4 = {
|
||||||
type: jsPsychFreeSort,
|
type: jsPsychFreeSort,
|
||||||
stimuli: ['img/1.gif','img/2.gif','img/3.gif'],
|
stimuli: ['img/1.gif','img/2.gif','img/3.gif'],
|
||||||
stim_height: 90,
|
stim_height: 90,
|
||||||
stim_width: 90,
|
stim_width: 90,
|
||||||
scale_factor: 1.0,
|
scale_factor: 1.0,
|
||||||
prompt: '<p>Where were the images?</p>',
|
prompt: '<p>Where were the images?</p>',
|
||||||
prompt_location: 'below',
|
prompt_location: 'below',
|
||||||
sort_area_shape: "square",
|
sort_area_shape: "square",
|
||||||
sort_area_height: 500,
|
sort_area_height: 500,
|
||||||
sort_area_width: 500,
|
sort_area_width: 500,
|
||||||
change_border_background_color: false,
|
change_border_background_color: false,
|
||||||
border_width: 5,
|
border_width: 5,
|
||||||
counter_text_unfinished: "Not done yet...",
|
counter_text_unfinished: "Not done yet...",
|
||||||
counter_text_finished: "Done!",
|
counter_text_finished: "Done!",
|
||||||
column_spread_factor: .5
|
column_spread_factor: .5
|
||||||
};
|
};
|
||||||
|
|
||||||
var trial_5 = {
|
var trial_5 = {
|
||||||
type: jsPsychFreeSort,
|
type: jsPsychFreeSort,
|
||||||
stimuli: ['img/4.gif','img/5.gif','img/6.gif'],
|
stimuli: ['img/4.gif','img/5.gif','img/6.gif'],
|
||||||
prompt: '<p>Arrange the images.</p><p>Images start at random locations inside the sort area.</p>',
|
prompt: '<p>Arrange the images.</p><p>Images start at random locations inside the sort area.</p>',
|
||||||
change_border_background_color: false,
|
change_border_background_color: false,
|
||||||
border_width: 3,
|
border_width: 3,
|
||||||
counter_text_unfinished: "",
|
counter_text_unfinished: "",
|
||||||
counter_text_finished: "",
|
counter_text_finished: "",
|
||||||
stim_starts_inside: true,
|
stim_starts_inside: true,
|
||||||
sort_area_shape: "square"
|
sort_area_shape: "square"
|
||||||
};
|
};
|
||||||
|
|
||||||
jsPsych.run([preload, trial_1, trial_2, trial_3, trial_4, trial_5]);
|
|
||||||
|
|
||||||
|
jsPsych.run([preload, trial_1, trial_2, trial_3, trial_4, trial_5]);
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
20
package-lock.json
generated
20
package-lock.json
generated
@ -10491,6 +10491,10 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/plugin-free-sort-ordered": {
|
||||||
|
"resolved": "packages/plugin-free-sort-ordered",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/possible-typed-array-names": {
|
"node_modules/possible-typed-array-names": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
|
||||||
@ -14026,6 +14030,22 @@
|
|||||||
"jspsych": ">=7.1.0"
|
"jspsych": ">=7.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/plugin-free-sort-ordered": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@citation-js/core": "^0.7.14",
|
||||||
|
"@citation-js/plugin-bibtex": "^0.7.14",
|
||||||
|
"@citation-js/plugin-cff": "^0.6.1",
|
||||||
|
"@citation-js/plugin-software-formats": "^0.6.1",
|
||||||
|
"@jspsych/plugin-free-sort": "^2.1.0",
|
||||||
|
"jspsych": "^8.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@jspsych/config": "^3.2.2",
|
||||||
|
"@jspsych/test-utils": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/plugin-fullscreen": {
|
"packages/plugin-fullscreen": {
|
||||||
"name": "@jspsych/plugin-fullscreen",
|
"name": "@jspsych/plugin-fullscreen",
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
|
41
packages/plugin-free-sort-ordered/CITATION.cff
Normal file
41
packages/plugin-free-sort-ordered/CITATION.cff
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
cff-version: 1.2.0
|
||||||
|
message: "If you use this software, please cite it as below."
|
||||||
|
authors:
|
||||||
|
- family-names: "Cherrie Chang" # Replace with last name
|
||||||
|
given-names: "Cherrie Chang" # Replace with first name
|
||||||
|
name-particle: "Cherrie Chang" # Replace with name particle(s)
|
||||||
|
orcid: "https://orcid.org/0000-0000-0000-0000" # Replace with ORCID
|
||||||
|
# More authors can be listed here in the same format as above
|
||||||
|
contact: # Contact person for this extension
|
||||||
|
- family-names: "Cherrie Chang"
|
||||||
|
given-names: "Cherrie Chang"
|
||||||
|
email: "{email}" # Replace with contact person's email
|
||||||
|
orcid: "https://orcid.org/0000-0000-0000-0000" # Replace with contact person's ORCID
|
||||||
|
title: "jsPsychPluginFreeSortOrdered"
|
||||||
|
version: 0.0.0
|
||||||
|
doi: 10.5281/zenodo.1234 # Replace with DOI
|
||||||
|
date-released: 2000-01-01
|
||||||
|
url: "{softwareUrl}" # Replace with URL to this extension
|
||||||
|
|
||||||
|
# If you wish to cite a paper on this extension instead, you can use the following template:
|
||||||
|
preferred-citation:
|
||||||
|
authors:
|
||||||
|
- family-names: "Cherrie Chang"
|
||||||
|
given-names: "Cherrie Chang"
|
||||||
|
name-particle: "Cherrie Chang"
|
||||||
|
orcid: "https://orcid.org/0000-0000-0000-0000"
|
||||||
|
# More authors can be listed here in the same format as above
|
||||||
|
date-published: 2023-05-11
|
||||||
|
doi: 10.21105/joss.12345
|
||||||
|
issn: 1234-5678
|
||||||
|
issue: 01
|
||||||
|
journal: Journal for Open Source Software
|
||||||
|
publisher:
|
||||||
|
name: Open Journals
|
||||||
|
start: 0001
|
||||||
|
title: "{title}"
|
||||||
|
type: article # Other options include: book, pamphlet, conference-paper...
|
||||||
|
url: "{linkToPublicationInJournal}"
|
||||||
|
volume: 1
|
||||||
|
|
||||||
|
# More information on the preffered-citation CFF format can be found at https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-citation-files#citing-something-other-than-software
|
21
packages/plugin-free-sort-ordered/README.md
Normal file
21
packages/plugin-free-sort-ordered/README.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# plugin-free-sort-ordered
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The free sort core plugin, but the images have to be sorted by placing into ordered boxes.
|
||||||
|
|
||||||
|
## Loading
|
||||||
|
|
||||||
|
*Enter instructions for loading the plugin package here.*
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
`plugin-free-sort-ordered` requires jsPsych v8.0.0 or later.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
See [documentation](/packages/plugin-free-sort-ordered/README.md)
|
||||||
|
|
||||||
|
## Author / Citation
|
||||||
|
|
||||||
|
[Cherrie Chang](https://github.com/cherriechang)
|
BIN
packages/plugin-free-sort-ordered/assets/blue-rec.png
Normal file
BIN
packages/plugin-free-sort-ordered/assets/blue-rec.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 419 B |
File diff suppressed because one or more lines are too long
BIN
packages/plugin-free-sort-ordered/assets/green-rec.png
Normal file
BIN
packages/plugin-free-sort-ordered/assets/green-rec.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 398 B |
BIN
packages/plugin-free-sort-ordered/assets/red-rec.png
Normal file
BIN
packages/plugin-free-sort-ordered/assets/red-rec.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 408 B |
@ -0,0 +1,33 @@
|
|||||||
|
# plugin-free-sort-ordered
|
||||||
|
|
||||||
|
The free sort core plugin, but the images have to be sorted by placing into ordered boxes.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
In addition to the [parameters available in all plugins](https://www.jspsych.org/latest/overview/plugins#parameters-available-in-all-plugins), this plugin accepts the following parameters. Parameters with a default value of undefined must be specified. Other parameters can be left unspecified if the default value is acceptable.
|
||||||
|
|
||||||
|
| Parameter | Type | Default Value | Description |
|
||||||
|
| ------------------- | ---------------- | ------------------ | ---------------------------------------- |
|
||||||
|
| | | | |
|
||||||
|
|
||||||
|
## Data Generated
|
||||||
|
|
||||||
|
In addition to the [default data collected by all plugins](https://www.jspsych.org/latest/overview/plugins#data-collected-by-all-plugins), this plugin collects the following data for each trial.
|
||||||
|
|
||||||
|
| Name | Type | Value |
|
||||||
|
| --------- | ------- | ---------------------------------------- |
|
||||||
|
| | | |
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
*Enter instructions for installing the plugin package here.*
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Title of Example
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var trial = {
|
||||||
|
type: jsPsychPluginFreeSortOrdered
|
||||||
|
}
|
||||||
|
```
|
39
packages/plugin-free-sort-ordered/examples/index.html
Normal file
39
packages/plugin-free-sort-ordered/examples/index.html
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>jsPsychPluginFreeSortOrdered Example</title>
|
||||||
|
<script src="https://unpkg.com/jspsych"></script>
|
||||||
|
<!-- Load the published plugin package here, e.g.
|
||||||
|
<script src="https://unpkg.com/plugin-free-sort-ordered"></script>
|
||||||
|
<script src="../dist/index.browser.js"></script> -->
|
||||||
|
<script src="../dist/index.browser.js"></script>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/jspsych/css/jspsych.css" />
|
||||||
|
<link rel="stylesheet" href="../src/styles.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body></body>
|
||||||
|
<script>
|
||||||
|
const jsPsych = initJsPsych();
|
||||||
|
|
||||||
|
const trial = {
|
||||||
|
type: jsPsychPluginFreeSortOrdered,
|
||||||
|
stimulus: [
|
||||||
|
'../assets/blue-rec.png',
|
||||||
|
'../assets/green-rec.png',
|
||||||
|
'../assets/red-rec.png',
|
||||||
|
],
|
||||||
|
stim_height: 100,
|
||||||
|
stim_width: 200,
|
||||||
|
stim_order: [2, 0, 1],
|
||||||
|
scale_factor: 1,
|
||||||
|
holding_area_height: 500,
|
||||||
|
holding_area_width: 1000,
|
||||||
|
prompt: 'Drag and drop the boxes to sort them in order.',
|
||||||
|
prompt_location: 'above',
|
||||||
|
button_label: 'Continue',
|
||||||
|
include_counter: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
jsPsych.run([trial])
|
||||||
|
</script>
|
||||||
|
</html>
|
1
packages/plugin-free-sort-ordered/jest.config.cjs
Normal file
1
packages/plugin-free-sort-ordered/jest.config.cjs
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require("@jspsych/config/jest").makePackageConfig(__dirname);
|
54
packages/plugin-free-sort-ordered/package.json
Normal file
54
packages/plugin-free-sort-ordered/package.json
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"name": "plugin-free-sort-ordered",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "The free sort core plugin, but the images have to be sorted by placing into ordered boxes.",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.cjs",
|
||||||
|
"exports": {
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.cjs"
|
||||||
|
},
|
||||||
|
"typings": "dist/index.d.ts",
|
||||||
|
"unpkg": "dist/index.browser.min.js",
|
||||||
|
"files": [
|
||||||
|
"src",
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"source": "src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "npm test -- --watch",
|
||||||
|
"tsc": "tsc",
|
||||||
|
"build": "rollup --config",
|
||||||
|
"build:watch": "npm run build -- --watch"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/jspsych/jsPsych.git",
|
||||||
|
"directory": ""
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"jsPsych"
|
||||||
|
],
|
||||||
|
"author": {
|
||||||
|
"name": "Cherrie Chang",
|
||||||
|
"url": "https://github.com/cherriechang"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/jspsych/jsPsych/issues"
|
||||||
|
},
|
||||||
|
"homepage": "/packages/plugin-free-sort-ordered/README.md",
|
||||||
|
"dependencies": {
|
||||||
|
"jspsych": "^8.0.0",
|
||||||
|
"@jspsych/plugin-free-sort": "^2.1.0",
|
||||||
|
"@citation-js/core": "^0.7.14",
|
||||||
|
"@citation-js/plugin-software-formats": "^0.6.1",
|
||||||
|
"@citation-js/plugin-bibtex": "^0.7.14",
|
||||||
|
"@citation-js/plugin-cff": "^0.6.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@jspsych/config": "^3.2.2",
|
||||||
|
"@jspsych/test-utils": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
3
packages/plugin-free-sort-ordered/rollup.config.mjs
Normal file
3
packages/plugin-free-sort-ordered/rollup.config.mjs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { makeRollupConfig } from "@jspsych/config/rollup";
|
||||||
|
|
||||||
|
export default makeRollupConfig("jsPsychPluginFreeSortOrdered");
|
19
packages/plugin-free-sort-ordered/src/index.spec.ts
Normal file
19
packages/plugin-free-sort-ordered/src/index.spec.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { startTimeline } from "@jspsych/test-utils";
|
||||||
|
|
||||||
|
import jsPsychPluginFreeSortOrdered from ".";
|
||||||
|
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
describe("my plugin", () => {
|
||||||
|
it("should load", async () => {
|
||||||
|
const { expectFinished, getHTML, getData, displayElement, jsPsych } = await startTimeline([
|
||||||
|
{
|
||||||
|
type: jsPsychPluginFreeSortOrdered,
|
||||||
|
parameter_name: 1,
|
||||||
|
parameter_name2: "img.png",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expectFinished();
|
||||||
|
});
|
||||||
|
});
|
386
packages/plugin-free-sort-ordered/src/index.ts
Normal file
386
packages/plugin-free-sort-ordered/src/index.ts
Normal file
@ -0,0 +1,386 @@
|
|||||||
|
import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych";
|
||||||
|
|
||||||
|
// import * as FreeSortPluginUtils from "@jspsych/plugin-free-sort/src/utils";
|
||||||
|
import * as FreeSortPluginUtils from "../../plugin-free-sort/src/utils";
|
||||||
|
import { version } from "../package.json";
|
||||||
|
import * as Utils from "./utils";
|
||||||
|
|
||||||
|
const info = <const>{
|
||||||
|
name: "plugin-free-sort-ordered",
|
||||||
|
version: version,
|
||||||
|
parameters: {
|
||||||
|
/** Each element of this array is an image path or SVG code. */
|
||||||
|
stimulus: {
|
||||||
|
type: ParameterType.INT | ParameterType.HTML_STRING,
|
||||||
|
default: undefined,
|
||||||
|
array: true,
|
||||||
|
},
|
||||||
|
/** The height of the images in pixels. */
|
||||||
|
stim_height: {
|
||||||
|
type: ParameterType.INT,
|
||||||
|
default: 100,
|
||||||
|
},
|
||||||
|
/** The width of the images in pixels. */
|
||||||
|
stim_width: {
|
||||||
|
type: ParameterType.INT,
|
||||||
|
default: 100,
|
||||||
|
},
|
||||||
|
/** The correct order of the stimuli. */
|
||||||
|
stim_order: {
|
||||||
|
type: ParameterType.INT,
|
||||||
|
default: undefined,
|
||||||
|
array: true,
|
||||||
|
},
|
||||||
|
/** How much larger to make the stimulus while moving (1 = no scaling). */
|
||||||
|
scale_factor: {
|
||||||
|
type: ParameterType.FLOAT,
|
||||||
|
default: 1.5,
|
||||||
|
},
|
||||||
|
/** The height of the container that the stimuli start in. */
|
||||||
|
holding_area_height: {
|
||||||
|
type: ParameterType.INT,
|
||||||
|
default: 700,
|
||||||
|
},
|
||||||
|
/** The width of the container that the stimuli start in. */
|
||||||
|
holding_area_width: {
|
||||||
|
type: ParameterType.INT,
|
||||||
|
default: 700,
|
||||||
|
},
|
||||||
|
/** This string can contain HTML markup. The intention is that it can be used to provide a reminder about the action the participant is supposed to take (e.g., which key to press). */
|
||||||
|
prompt: {
|
||||||
|
type: ParameterType.HTML_STRING,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
/** Indicates whether to show the prompt `"above"` or `"below"` the sorting area. */
|
||||||
|
prompt_location: {
|
||||||
|
type: ParameterType.SELECT,
|
||||||
|
options: ["above", "below"],
|
||||||
|
default: "above",
|
||||||
|
},
|
||||||
|
/** The text that appears on the button to continue to the next trial. */
|
||||||
|
button_label: {
|
||||||
|
type: ParameterType.STRING,
|
||||||
|
default: "Continue",
|
||||||
|
},
|
||||||
|
/** Whether to display counter indicating how many items are left to be sorted. */
|
||||||
|
include_counter: {
|
||||||
|
type: ParameterType.BOOL,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Text to display when there are one or more items that still need to be placed in a box.
|
||||||
|
* If "%n%" is included in the string, it will be replaced with the number of items that still need to be moved inside.
|
||||||
|
* If "%s%" is included in the string, a "s" will be included when the number of items remaining is greater than one.
|
||||||
|
*/
|
||||||
|
counter_text_unfinished: {
|
||||||
|
type: ParameterType.HTML_STRING,
|
||||||
|
default: "You still need to place %n% item%s% in a box.",
|
||||||
|
},
|
||||||
|
/** Text that will take the place of the counter_text_unfinished text when all items have been moved inside a box. */
|
||||||
|
counter_text_finished: {
|
||||||
|
type: ParameterType.HTML_STRING,
|
||||||
|
default: "All items placed. Feel free to reposition items if necessary.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
/** An array containing objects representing the initial locations of all the stimuli in the sorting area. Each element in the array represents a stimulus, and has a "src", "x", and "y" value. "src" is the image path, and "x" and "y" are the object location. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */
|
||||||
|
init_locations: {
|
||||||
|
type: ParameterType.STRING,
|
||||||
|
array: true,
|
||||||
|
},
|
||||||
|
/** An array containing objects representing all of the moves the participant made when sorting. Each object represents a move. Each element in the array has a "src", "x", and "y" value. "src" is the image path, and "x" and "y" are the object location after the move. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */
|
||||||
|
moves: {
|
||||||
|
type: ParameterType.COMPLEX,
|
||||||
|
array: true,
|
||||||
|
nested: {
|
||||||
|
src: {
|
||||||
|
type: ParameterType.STRING,
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
type: ParameterType.INT,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
type: ParameterType.INT,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
final_locations: {
|
||||||
|
type: ParameterType.COMPLEX,
|
||||||
|
array: true,
|
||||||
|
nested: {
|
||||||
|
src: {
|
||||||
|
type: ParameterType.STRING,
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
type: ParameterType.INT,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
type: ParameterType.INT,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
/** The response time in milliseconds for the participant to finish all sorting. */
|
||||||
|
rt: {
|
||||||
|
type: ParameterType.INT,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
citations: "__CITATIONS__",
|
||||||
|
};
|
||||||
|
|
||||||
|
type Info = typeof info;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* **plugin-free-sort-ordered**
|
||||||
|
*
|
||||||
|
* The free sort core plugin, but the images have to be sorted by placing into ordered boxes.
|
||||||
|
*
|
||||||
|
* @author Cherrie Chang
|
||||||
|
* @see {@link /packages/plugin-free-sort-ordered/README.md}}
|
||||||
|
*/
|
||||||
|
class FreeSortOrderedPlugin implements JsPsychPlugin<Info> {
|
||||||
|
static info = info;
|
||||||
|
|
||||||
|
constructor(private jsPsych: JsPsych) {}
|
||||||
|
|
||||||
|
trial(display_element: HTMLElement, trial: TrialType<Info>) {
|
||||||
|
var start_time = performance.now();
|
||||||
|
var stimulus = trial.stimulus;
|
||||||
|
|
||||||
|
// holding area
|
||||||
|
const holding_area_html = `
|
||||||
|
<div
|
||||||
|
id="jspsych-free-sort-ordered-holding-area"
|
||||||
|
class="jspsych-free-sort-ordered-holding-area"
|
||||||
|
style="position: relative; width: ${trial.holding_area_width}px; height: ${trial.holding_area_height}px; background-color: #CCCCCC; margin: auto;">
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// counter text if included
|
||||||
|
const counter_html = `
|
||||||
|
<p id="jspsych-free-sort-ordered-counter" style="display: inline-block;">
|
||||||
|
${trial.include_counter ? get_counter_text(stimulus.length) : ""}
|
||||||
|
</p>`;
|
||||||
|
|
||||||
|
// container for the target boxes
|
||||||
|
let box_container_html = `
|
||||||
|
<div
|
||||||
|
id="jspsych-free-sort-ordered-box-grid"
|
||||||
|
class="jspsych-free-sort-ordered-box-grid"
|
||||||
|
style="position: relative; max-width: 80vw; display: flex; flex-direction: row; flex-wrap: wrap; justify-content: center; align-items: center; margin: auto; padding: 20px;"
|
||||||
|
>`;
|
||||||
|
|
||||||
|
// create boxes for each stimulus
|
||||||
|
for (let i = 0; i < stimulus.length; i++) {
|
||||||
|
box_container_html += `
|
||||||
|
<div
|
||||||
|
id="jspsych-free-sort-ordered-box-${i}"
|
||||||
|
class="jspsych-free-sort-ordered-box"
|
||||||
|
style="width: ${trial.stim_width}px; height: ${trial.stim_height}px; background-color: #FFFFFF; border: 2px solid #000000; margin: 5px;"
|
||||||
|
></div>`;
|
||||||
|
}
|
||||||
|
box_container_html += "</div>";
|
||||||
|
|
||||||
|
// prompt text (and counter if included)
|
||||||
|
const prompt_counter_html = `
|
||||||
|
<div style="line-height: 1.0em;">
|
||||||
|
${trial.prompt + (trial.include_counter ? counter_html : "")}
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// button to continue
|
||||||
|
const button_html = `
|
||||||
|
<div><button id="jspsych-free-sort-ordered-done-btn" class="jspsych-btn" style="margin-top: 5px; margin-bottom: 15px; visibility: hidden;">
|
||||||
|
${trial.button_label}
|
||||||
|
</button></div>`;
|
||||||
|
|
||||||
|
// combine all HTML
|
||||||
|
let html =
|
||||||
|
trial.prompt_location === "above"
|
||||||
|
? prompt_counter_html + holding_area_html + box_container_html + button_html
|
||||||
|
: holding_area_html + box_container_html + prompt_counter_html + button_html;
|
||||||
|
|
||||||
|
display_element.innerHTML = html;
|
||||||
|
|
||||||
|
// store initial locations of stimuli
|
||||||
|
let init_locations = [];
|
||||||
|
|
||||||
|
// store locations of the boxes
|
||||||
|
let boxCoordinates = [];
|
||||||
|
for (let i = 0; i < stimulus.length; i++) {
|
||||||
|
const box = document.getElementById(`jspsych-free-sort-ordered-box-${i}`);
|
||||||
|
if (box) {
|
||||||
|
const rect = box.getBoundingClientRect();
|
||||||
|
boxCoordinates.push({
|
||||||
|
x: rect.left, // no need to adjust for scrolling because getBoundingClientRect() accounts for it
|
||||||
|
y: rect.top,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error(`Box element with id jspsych-free-sort-ordered-box-${i} not found.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// boxes as array of objects
|
||||||
|
const boxAreas = [];
|
||||||
|
boxCoordinates.forEach((boxCoord, i) => {
|
||||||
|
boxAreas.push({
|
||||||
|
id: i,
|
||||||
|
left: boxCoord.x,
|
||||||
|
top: boxCoord.y,
|
||||||
|
width: trial.stim_width,
|
||||||
|
height: trial.stim_height,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// place each stimulus in initial locations
|
||||||
|
for (let i = 0; i < stimulus.length; i++) {
|
||||||
|
var coords = FreeSortPluginUtils.random_coordinate(
|
||||||
|
trial.holding_area_width - trial.stim_width,
|
||||||
|
trial.holding_area_height - trial.stim_height
|
||||||
|
);
|
||||||
|
|
||||||
|
// add stimuli and their initial locations to the display
|
||||||
|
display_element.querySelector("#jspsych-free-sort-ordered-holding-area").innerHTML +=
|
||||||
|
"<img " +
|
||||||
|
'src="' +
|
||||||
|
stimulus[i] +
|
||||||
|
'" ' +
|
||||||
|
'data-src="' +
|
||||||
|
stimulus[i] +
|
||||||
|
'" ' +
|
||||||
|
'class="jspsych-free-sort-ordered-draggable" ' +
|
||||||
|
'draggable="false" ' +
|
||||||
|
'id="jspsych-free-sort-ordered-draggable-' +
|
||||||
|
i +
|
||||||
|
'" ' +
|
||||||
|
'style="position: absolute; cursor: move; width:' +
|
||||||
|
trial.stim_width +
|
||||||
|
"px; height:" +
|
||||||
|
trial.stim_height +
|
||||||
|
"px; top:" +
|
||||||
|
coords.y +
|
||||||
|
"px; left:" +
|
||||||
|
coords.x +
|
||||||
|
'px;">' +
|
||||||
|
"</img>";
|
||||||
|
|
||||||
|
// add initial locations to the init_locations array
|
||||||
|
init_locations.push({
|
||||||
|
src: stimulus[i],
|
||||||
|
x: coords.x,
|
||||||
|
y: coords.y,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// moves within a trial
|
||||||
|
let moves = [];
|
||||||
|
|
||||||
|
// are objects currently inside
|
||||||
|
let inside = new Array(stimulus.length).fill(false);
|
||||||
|
|
||||||
|
// button to finish sorting
|
||||||
|
const button: HTMLButtonElement = display_element.querySelector("#jspsych-free-sort-done-btn");
|
||||||
|
|
||||||
|
// save draggable items as array
|
||||||
|
const draggables = Array.prototype.slice.call(
|
||||||
|
display_element.querySelectorAll<HTMLImageElement>(".jspsych-free-sort-ordered-draggable")
|
||||||
|
);
|
||||||
|
|
||||||
|
// make each stimulus draggable by adding event listeners for when they are dragged and dropped
|
||||||
|
draggables.forEach((draggable, i) => {
|
||||||
|
draggable.addEventListener("pointerdown", function ({ clientX: pageX, clientY: pageY }) {
|
||||||
|
let x = pageX - this.offsetLeft;
|
||||||
|
let y = pageY - this.offsetTop - window.scrollY;
|
||||||
|
this.style.transform = "scale(" + trial.scale_factor + "," + trial.scale_factor + ")";
|
||||||
|
|
||||||
|
// on pointer move, check if the stimulus is inside a box and update its position
|
||||||
|
const on_pointer_move = ({ clientX, clientY }: PointerEvent) => {
|
||||||
|
inside[i] = Utils.inside_box(clientX - x, clientY - y, boxAreas);
|
||||||
|
this.style.top =
|
||||||
|
Math.min(
|
||||||
|
window.innerHeight - trial.stim_height, // Bottom boundary of the viewport
|
||||||
|
Math.max(0, clientY - y) // Top boundary of the viewport
|
||||||
|
) + "px";
|
||||||
|
|
||||||
|
this.style.left =
|
||||||
|
Math.min(
|
||||||
|
window.innerWidth - trial.stim_width, // Right boundary of the viewport
|
||||||
|
Math.max(0, clientX - x) // Left boundary of the viewport
|
||||||
|
) + "px";
|
||||||
|
|
||||||
|
// modify text and background if all items are inside
|
||||||
|
if (inside.every(Boolean)) {
|
||||||
|
button.style.visibility = "visible";
|
||||||
|
display_element.querySelector("#jspsych-free-sort-ordered-counter").innerHTML =
|
||||||
|
trial.counter_text_finished;
|
||||||
|
} else {
|
||||||
|
button.style.visibility = "hidden";
|
||||||
|
display_element.querySelector("#jspsych-free-sort-ordered-counter").innerHTML =
|
||||||
|
get_counter_text(inside.length - inside.filter(Boolean).length);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("pointermove", on_pointer_move);
|
||||||
|
|
||||||
|
// on pointer up, remove the event listeners and save the move
|
||||||
|
const on_pointer_up = (e) => {
|
||||||
|
document.removeEventListener("pointermove", on_pointer_move);
|
||||||
|
this.style.transform = "scale(1, 1)";
|
||||||
|
moves.push({
|
||||||
|
src: this.dataset.src,
|
||||||
|
x: this.offsetLeft,
|
||||||
|
y: this.offsetTop,
|
||||||
|
});
|
||||||
|
document.removeEventListener("pointerup", on_pointer_up);
|
||||||
|
};
|
||||||
|
document.addEventListener("pointerup", on_pointer_up);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
display_element
|
||||||
|
.querySelector("#jspsych-free-sort-ordered-done-btn")
|
||||||
|
.addEventListener("click", () => {
|
||||||
|
if (inside.every(Boolean)) {
|
||||||
|
const end_time = performance.now();
|
||||||
|
const rt = Math.round(end_time - start_time);
|
||||||
|
// gather data
|
||||||
|
const items = display_element.querySelectorAll<HTMLElement>(
|
||||||
|
".jspsych-free-sort-ordered-draggable"
|
||||||
|
);
|
||||||
|
// get final position of all items
|
||||||
|
let final_locations = [];
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
final_locations.push({
|
||||||
|
src: items[i].dataset.src,
|
||||||
|
x: parseInt(items[i].style.left),
|
||||||
|
y: parseInt(items[i].style.top),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const trial_data = {
|
||||||
|
init_locations: init_locations,
|
||||||
|
moves: moves,
|
||||||
|
final_locations: final_locations,
|
||||||
|
rt: rt,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.jsPsych.finishTrial(trial_data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function get_counter_text(n: number) {
|
||||||
|
var text_out = "";
|
||||||
|
var text_bits = trial.counter_text_unfinished.split("%");
|
||||||
|
for (var i = 0; i < text_bits.length; i++) {
|
||||||
|
if (i % 2 === 0) {
|
||||||
|
text_out += text_bits[i];
|
||||||
|
} else {
|
||||||
|
if (text_bits[i] == "n") {
|
||||||
|
text_out += n.toString();
|
||||||
|
} else if (text_bits[i] == "s" && n > 1) {
|
||||||
|
text_out += "s";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text_out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FreeSortOrderedPlugin;
|
8
packages/plugin-free-sort-ordered/src/styles.css
Normal file
8
packages/plugin-free-sort-ordered/src/styles.css
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.jspsych-free-sort-ordered-draggable {
|
||||||
|
touch-action: none; /* disable scrolling on touch-drag */
|
||||||
|
user-select: none; /* disable text selection */
|
||||||
|
-webkit-user-drag: none; /* disable Safari ghost */
|
||||||
|
-moz-user-select: none;
|
||||||
|
z-index: 100; /* make sure the dragged item is on top */
|
||||||
|
}
|
||||||
|
|
20
packages/plugin-free-sort-ordered/src/utils.ts
Normal file
20
packages/plugin-free-sort-ordered/src/utils.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export type boxArea = {
|
||||||
|
id: string;
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function inside_box(x: number, y: number, box_areas: boxArea[]): string | null {
|
||||||
|
for (const box_area of box_areas) {
|
||||||
|
const { id, left, top, width, height } = box_area;
|
||||||
|
const right = left + width;
|
||||||
|
const bottom = top + height;
|
||||||
|
|
||||||
|
if (x >= left && x <= right && y >= top && y <= bottom) {
|
||||||
|
return id; // point is inside this box
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null; // point not inside any box
|
||||||
|
}
|
9
packages/plugin-free-sort-ordered/tsconfig.json
Normal file
9
packages/plugin-free-sort-ordered/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "@jspsych/config/tsconfig.contrib.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user