mirror of
https://github.com/jspsych/jsPsych.git
synced 2025-05-10 11:10:54 +00:00
Merge pull request #1228 from kurokida/freesort
for using the free-sort plugin with a touch device.
This commit is contained in:
commit
bb7b3f0570
@ -9,6 +9,7 @@ The following people have contributed to the development of jsPsych by writing c
|
||||
* Gustavo Juantorena - https://github.com/GEJ1
|
||||
* Chris Jungerius - https://github.com/cjungerius
|
||||
* Jana Klaus - https://github.com/janakl4us
|
||||
* Peter Jes Kohler - https://github.com/pjkohler
|
||||
* Jonas Lambers
|
||||
* Shane Martin - https://github.com/shamrt
|
||||
* Adrian Oesch - https://github.com/adrianoesch
|
||||
|
@ -1,10 +1,6 @@
|
||||
# jspsych-free-sort plugin
|
||||
|
||||
The free-sort plugin displays a collection of images on the screen that the subject can interact with by clicking and dragging. All of the moves that the subject performs are recorded.
|
||||
|
||||
## Dependency
|
||||
|
||||
This plugin requires the jQuery UI library, available at [https://jqueryui.com/](https://jqueryui.com/). You must include the library in the `<head>` section of your experiment page. You can use the [Google-hosted version of the library](https://developers.google.com/speed/libraries/#jquery-ui).
|
||||
The free-sort plugin displays one or more images on the screen that the participant can interact with by clicking and dragging. When the trial starts, the images can be positioned outside or inside the sort area. All images must be moved into the sorting area before the participant can click a button to end the trial. All of the moves that the participant performs are recorded, as well as the final positions of all images. This plugin could be useful when asking participants to position images based on similarity to one another, or to recall image spatial locations.
|
||||
|
||||
## Parameters
|
||||
|
||||
@ -15,12 +11,20 @@ Parameter | Type | Default Value | Description
|
||||
stimuli | array | *undefined* | Each element of this array is an image path.
|
||||
stim_height | numeric | 100 | The height of the images in pixels.
|
||||
stim_width | numeric | 100 | The width of the images in pixels.
|
||||
sort_area_height | numeric | 800 | The height of the container that subjects can move the stimuli in. Stimuli will be constrained to this area.
|
||||
sort_area_width | numeric | 800 | The width of the container that subjects can move the stimuli in. Stimuli will be constrained to this area.
|
||||
prompt | string | null | This string can contain HTML markup. The intention is that it can be used to provide a reminder about the action the subject is supposed to take (e.g., which key to press).
|
||||
scale_factor | numeric | 1.5 | How much larger to make the stimulus while moving (1 = no scaling).
|
||||
sort_area_height | numeric | 800 | The height of the container that participants can move the stimuli in. Stimuli will be constrained to this area.
|
||||
sort_area_width | numeric | 800 | The width of the container that participants can move the stimuli in. Stimuli will be constrained to this area.
|
||||
sort_area_shape | string | "ellipse" | The shape of the sorting area, can be "ellipse" or "square".
|
||||
prompt | string | null | 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_location | string | "above" | Indicates whether to show the prompt `"above"` or `"below"` the sorting area.
|
||||
button_label | string | 'Continue' | The text that appears on the button to continue to the next trial.
|
||||
|
||||
button_label | string | 'Continue' | The text that appears on the button to continue to the next trial.
|
||||
change_border_background_color | boolean | true | If `true`, the sort area border color will change while items are being moved in and out of the sort area, and the background color will change once all items have been moved into the sort area. If `false`, the border will remain black and the background will remain white throughout the trial.
|
||||
border_color_in | string | '#a1d99b' | If `change_border_background_color` is `true`, the sort area border will change to this color when an item is being moved into the sort area, and the background will change to this color when all of the items have been moved into the sort area.
|
||||
border_color_out | string | '#fc9272' | If `change_border_background_color` is `true`, this will be the color of the sort area border when there are one or more items that still need to be moved into the sort area.
|
||||
border_width | numeric | null | The width in pixels of the border around the sort area. If `null`, the border width will be 3% of the `sort_area_height`.
|
||||
counter_text_unfinished | string | You still need to place %n% item%s% inside the sort area. | Text to display when there are one or more items that still need to be placed in the sort area. 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_finished | string | All items placed. Feel free to reposition items if necessary. | Text that will take the place of the counter_text_unfinished text when all items have been moved inside the sort area.
|
||||
stim_starts_inside | boolean | false | If `false`, the images will be positioned to the left and right of the sort area when the trial loads. If `true`, the images will be positioned at random locations inside the sort area when the trial loads.
|
||||
|
||||
## Data Generated
|
||||
|
||||
@ -31,7 +35,7 @@ Name | Type | Value
|
||||
init_locations | JSON string | A JSON-encoded object representing the initial locations of all the stimuli in the sorting area. The object is an array with one element per stimulus. 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.
|
||||
moves | JSON string | A JSON-encoded object representing all of the moves the participant made when sorting. The object is an array with each element representing 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.
|
||||
final_locations | JSON string | A JSON-encoded object representing the final locations of all the stimuli in the sorting area. The object is an array with one element per stimulus. 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.
|
||||
rt | numeric | The response time in milliseconds for the subject to finish all sorting.
|
||||
rt | numeric | The response time in milliseconds for the participant to finish all sorting.
|
||||
|
||||
## Examples
|
||||
|
||||
|
@ -3,19 +3,96 @@
|
||||
<head>
|
||||
<script src="../jspsych.js"></script>
|
||||
<script src="../plugins/jspsych-free-sort.js"></script>
|
||||
<script src="../plugins/jspsych-canvas-keyboard-response.js"></script>
|
||||
<link rel="stylesheet" href="../css/jspsych.css">
|
||||
</head>
|
||||
<body></body>
|
||||
<script>
|
||||
|
||||
var trials = {
|
||||
var trial_1 = {
|
||||
type: 'free-sort',
|
||||
stimuli: ['img/happy_face_1.jpg','img/happy_face_2.jpg','img/happy_face_3.jpg','img/happy_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_width: 160,
|
||||
sort_area_height: 500,
|
||||
sort_area_width: 500,
|
||||
prompt: 'Please group similar expressions together. '
|
||||
};
|
||||
|
||||
var trial_2 = {
|
||||
type: 'free-sort',
|
||||
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_width: 160,
|
||||
prompt: '<p>Arrange the faces.</p>',
|
||||
prompt_location: "below",
|
||||
sort_area_height: 400,
|
||||
sort_area_width: 500,
|
||||
sort_area_shape: "square",
|
||||
scale_factor: 1.2,
|
||||
border_color_in: '#DCDCDC',
|
||||
border_color_out: 'red'
|
||||
};
|
||||
|
||||
var trial_3 = {
|
||||
type: 'canvas-keyboard-response',
|
||||
stimulus: function(c) {
|
||||
c.style.border = "5px solid #A9A9A9";
|
||||
// present images on a canvas at specific locations
|
||||
var ctx = c.getContext('2d');
|
||||
var img1 = new Image();
|
||||
var img2 = new Image();
|
||||
var img3 = new Image();
|
||||
img1.src = 'img/1.gif';
|
||||
img2.src = 'img/2.gif';
|
||||
img3.src = 'img/3.gif';
|
||||
img1.addEventListener('load', function() {
|
||||
ctx.drawImage(img1, 0, 0, 90, 90);
|
||||
}, false);
|
||||
img2.addEventListener('load', function() {
|
||||
ctx.drawImage(img2, 500, 600, 90, 90);
|
||||
}, false);
|
||||
img3.addEventListener('load', function() {
|
||||
ctx.drawImage(img3, 300, 100, 90, 90);
|
||||
}, false);
|
||||
},
|
||||
canvas_size: [500,500],
|
||||
prompt: "Memorize the image locations (5s).",
|
||||
choices: jsPsych.NO_KEYS,
|
||||
trial_duration: 5000
|
||||
};
|
||||
|
||||
var trial_4 = {
|
||||
type: 'free-sort',
|
||||
stimuli: ['img/1.gif','img/2.gif','img/3.gif'],
|
||||
stim_height: 90,
|
||||
stim_width: 90,
|
||||
scale_factor: 1.0,
|
||||
prompt: '<p>Where were the images?</p>',
|
||||
prompt_location: 'below',
|
||||
sort_area_shape: "square",
|
||||
sort_area_height: 500,
|
||||
sort_area_width: 500,
|
||||
change_border_background_color: false,
|
||||
border_width: 5,
|
||||
counter_text_unfinished: "Not done yet...",
|
||||
counter_text_finished: "Done!"
|
||||
};
|
||||
|
||||
var trial_5 = {
|
||||
type: 'free-sort',
|
||||
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>',
|
||||
change_border_background_color: false,
|
||||
border_width: 3,
|
||||
counter_text_unfinished: "",
|
||||
counter_text_finished: "",
|
||||
stim_starts_inside: true,
|
||||
sort_area_shape: "square"
|
||||
};
|
||||
|
||||
jsPsych.init({
|
||||
timeline: [trials],
|
||||
timeline: [trial_1, trial_2, trial_3, trial_4, trial_5],
|
||||
on_finish: function(){jsPsych.data.displayData();}
|
||||
});
|
||||
|
||||
|
@ -22,36 +22,49 @@ jsPsych.plugins['free-sort'] = (function() {
|
||||
pretty_name: 'Stimuli',
|
||||
default: undefined,
|
||||
array: true,
|
||||
description: 'Images to be displayed.'
|
||||
description: 'items to be displayed.'
|
||||
},
|
||||
stim_height: {
|
||||
type: jsPsych.plugins.parameterType.INT,
|
||||
pretty_name: 'Stimulus height',
|
||||
default: 100,
|
||||
description: 'Height of images in pixels.'
|
||||
description: 'Height of items in pixels.'
|
||||
},
|
||||
stim_width: {
|
||||
type: jsPsych.plugins.parameterType.INT,
|
||||
pretty_name: 'Stimulus width',
|
||||
default: 100,
|
||||
description: 'Width of images in pixels'
|
||||
description: 'Width of items in pixels'
|
||||
},
|
||||
scale_factor: {
|
||||
type: jsPsych.plugins.parameterType.FLOAT,
|
||||
pretty_name: 'Stimulus scaling factor',
|
||||
default: 1.5,
|
||||
description: 'How much larger to make the stimulus while moving (1 = no scaling)'
|
||||
},
|
||||
sort_area_height: {
|
||||
type: jsPsych.plugins.parameterType.INT,
|
||||
pretty_name: 'Sort area height',
|
||||
default: 800,
|
||||
description: 'The height of the container that subjects can move the stimuli in.'
|
||||
default: 700,
|
||||
description: 'The height in pixels of the container that subjects can move the stimuli in.'
|
||||
},
|
||||
sort_area_width: {
|
||||
type: jsPsych.plugins.parameterType.INT,
|
||||
pretty_name: 'Sort area width',
|
||||
default: 800,
|
||||
description: 'The width of the container that subjects can move the stimuli in.'
|
||||
default: 700,
|
||||
description: 'The width in pixels of the container that subjects can move the stimuli in.'
|
||||
},
|
||||
sort_area_shape: {
|
||||
type: jsPsych.plugins.parameterType.STRING,
|
||||
pretty_name: 'Sort area shape',
|
||||
options: ['square','ellipse'],
|
||||
default: 'ellipse',
|
||||
description: 'The shape of the sorting area'
|
||||
},
|
||||
prompt: {
|
||||
type: jsPsych.plugins.parameterType.STRING,
|
||||
pretty_name: 'Prompt',
|
||||
default: null,
|
||||
default: '',
|
||||
description: 'It can be used to provide a reminder about the action the subject is supposed to take.'
|
||||
},
|
||||
prompt_location: {
|
||||
@ -66,6 +79,57 @@ jsPsych.plugins['free-sort'] = (function() {
|
||||
pretty_name: 'Button label',
|
||||
default: 'Continue',
|
||||
description: 'The text that appears on the button to continue to the next trial.'
|
||||
},
|
||||
change_border_background_color: {
|
||||
type: jsPsych.plugins.parameterType.BOOL,
|
||||
pretty_name: 'Change border background color',
|
||||
default: true,
|
||||
description: 'If true, the sort area border color will change while items are being moved in and out of '+
|
||||
'the sort area, and the background color will change once all items have been moved into the '+
|
||||
'sort area. If false, the border will remain black and the background will remain white throughout the trial.'
|
||||
},
|
||||
border_color_in: {
|
||||
type: jsPsych.plugins.parameterType.STRING,
|
||||
pretty_name: 'Border color - in',
|
||||
default: '#a1d99b',
|
||||
description: 'If change_border_background_color is true, the sort area border will change to this color '+
|
||||
'when an item is being moved into the sort area, and the background will change to this color '+
|
||||
'when all of the items have been moved into the sort area.'
|
||||
},
|
||||
border_color_out: {
|
||||
type: jsPsych.plugins.parameterType.STRING,
|
||||
pretty_name: 'Border color - out',
|
||||
default: '#fc9272',
|
||||
description: 'If change_border_background_color is true, this will be the color of the sort area border '+
|
||||
'when there are one or more items that still need to be moved into the sort area.'
|
||||
},
|
||||
border_width: {
|
||||
type: jsPsych.plugins.parameterType.INT,
|
||||
pretty_name: 'Border width',
|
||||
default: null,
|
||||
description: 'The width in pixels of the border around the sort area. If null, the border width '+
|
||||
'defaults to 3% of the sort area height.'
|
||||
},
|
||||
counter_text_unfinished: {
|
||||
type: jsPsych.plugins.parameterType.STRING,
|
||||
pretty_name: 'Counter text unfinished',
|
||||
default: 'You still need to place %n% item%s% inside the sort area.',
|
||||
description: 'Text to display when there are one or more items that still need to be placed in the sort area. '+
|
||||
'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_finished: {
|
||||
type: jsPsych.plugins.parameterType.STRING,
|
||||
pretty_name: 'Counter text finished',
|
||||
default: 'All items placed. Feel free to reposition items if necessary.',
|
||||
description: 'Text that will take the place of the counter_text_unfinished text when all items have been moved inside the sort area.'
|
||||
},
|
||||
stim_starts_inside: {
|
||||
type: jsPsych.plugins.parameterType.BOOL,
|
||||
pretty_name: 'Stim starts inside',
|
||||
default: false,
|
||||
description: 'If false, the images will be positioned to the left and right of the sort area when the trial loads. '+
|
||||
'If true, the images will be positioned at random locations inside the sort area when the trial loads.'
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -74,36 +138,106 @@ jsPsych.plugins['free-sort'] = (function() {
|
||||
|
||||
var start_time = performance.now();
|
||||
|
||||
var html = "";
|
||||
// check if there is a prompt and if it is shown above
|
||||
if (trial.prompt !== null && trial.prompt_location == "above") {
|
||||
html += trial.prompt;
|
||||
if (trial.change_border_background_color == false) {
|
||||
trial.border_color_out = "#000000";
|
||||
}
|
||||
|
||||
html += '<div '+
|
||||
if (trial.border_width == null) {
|
||||
trial.border_width = trial.sort_area_height*.03;
|
||||
}
|
||||
|
||||
let html =
|
||||
'<div '+
|
||||
'id="jspsych-free-sort-arena" '+
|
||||
'class="jspsych-free-sort-arena" '+
|
||||
'style="position: relative; width:'+trial.sort_area_width+'px; height:'+trial.sort_area_height+'px; border:2px solid #444;"'+
|
||||
'></div>';
|
||||
'style="position: relative; width:'+trial.sort_area_width+'px; height:'+trial.sort_area_height+'px; margin: auto;"</div>';
|
||||
|
||||
// check if prompt exists and if it is shown below
|
||||
if (trial.prompt !== null && trial.prompt_location == "below") {
|
||||
html += trial.prompt;
|
||||
// another div for border
|
||||
html += '<div '+
|
||||
'id="jspsych-free-sort-border" '+
|
||||
'class="jspsych-free-sort-border" '+
|
||||
'style="position: relative; width:'+trial.sort_area_width*.94+'px; height:'+trial.sort_area_height*.94+'px; '+
|
||||
'border:'+trial.border_width+'px solid '+trial.border_color_out+'; margin: auto; line-height: 0em; ';
|
||||
|
||||
if ( trial.sort_area_shape == "ellipse") {
|
||||
html += 'webkit-border-radius: 50%; moz-border-radius: 50%; border-radius: 50%"></div>'
|
||||
} else {
|
||||
html += 'webkit-border-radius: 0%; moz-border-radius: 0%; border-radius: 0%"></div>'
|
||||
}
|
||||
|
||||
// variable that has the prompt text and counter
|
||||
const html_text = '<div style="line-height: 1.0em;">' + trial.prompt +
|
||||
'<p id="jspsych-free-sort-counter" style="display: inline-block;">'+get_counter_text(trial.stimuli.length)+'</p></div>';
|
||||
|
||||
// position prompt above or below
|
||||
if (trial.prompt_location == "below") {
|
||||
html += html_text
|
||||
} else {
|
||||
html = html_text + html
|
||||
}
|
||||
// add button
|
||||
html += '<div><button id="jspsych-free-sort-done-btn" class="jspsych-btn" '+
|
||||
'style="visibility: hidden; margin: 5px; padding: 5px; text-align: center; font-weight: bold; font-size: 18px; border: 2px solid;">' +
|
||||
trial.button_label+'</button></div>';
|
||||
|
||||
display_element.innerHTML = html;
|
||||
|
||||
// store initial location data
|
||||
var init_locations = [];
|
||||
let init_locations = [];
|
||||
|
||||
for (var i = 0; i < trial.stimuli.length; i++) {
|
||||
var coords = random_coordinate(trial.sort_area_width - trial.stim_width, trial.sort_area_height - trial.stim_height);
|
||||
if (!trial.stim_starts_inside) {
|
||||
// determine number of rows and colums, must be a even number
|
||||
let num_rows = Math.ceil(Math.sqrt(trial.stimuli.length))
|
||||
if ( num_rows % 2 != 0) {
|
||||
num_rows = num_rows + 1
|
||||
}
|
||||
|
||||
// compute coords for left and right side of arena
|
||||
var r_coords = [];
|
||||
var l_coords = [];
|
||||
for (const x of make_arr(0, trial.sort_area_width - trial.stim_width, num_rows) ) {
|
||||
for (const y of make_arr(0, trial.sort_area_height - trial.stim_height, num_rows) ) {
|
||||
if ( x > ( (trial.sort_area_width - trial.stim_width) * .5 ) ) {
|
||||
//r_coords.push({ x:x, y:y } )
|
||||
r_coords.push({ x:x + (trial.sort_area_width) * .5 , y:y });
|
||||
} else {
|
||||
l_coords.push({ x:x - (trial.sort_area_width) * .5 , y:y });
|
||||
//l_coords.push({ x:x, y:y } )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// repeat coordinates until you have enough coords (may be obsolete)
|
||||
while ( ( r_coords.length + l_coords.length ) < trial.stimuli.length ) {
|
||||
r_coords = r_coords.concat(r_coords)
|
||||
l_coords = l_coords.concat(l_coords)
|
||||
}
|
||||
// reverse left coords, so that coords closest to arena is used first
|
||||
l_coords = l_coords.reverse()
|
||||
|
||||
// shuffle stimuli, so that starting positions are random
|
||||
trial.stimuli = shuffle(trial.stimuli);
|
||||
}
|
||||
|
||||
let inside = []
|
||||
for (let i = 0; i < trial.stimuli.length; i++) {
|
||||
var coords;
|
||||
if (trial.stim_starts_inside) {
|
||||
coords = random_coordinate(trial.sort_area_width - trial.stim_width, trial.sort_area_height - trial.stim_height);
|
||||
} else {
|
||||
if ( (i % 2) == 0 ) {
|
||||
coords = r_coords[Math.floor(i * .5)];
|
||||
} else {
|
||||
coords = l_coords[Math.floor(i * .5)];
|
||||
}
|
||||
}
|
||||
|
||||
display_element.querySelector("#jspsych-free-sort-arena").innerHTML += '<img '+
|
||||
'src="'+trial.stimuli[i]+'" '+
|
||||
'data-src="'+trial.stimuli[i]+'" '+
|
||||
'class="jspsych-free-sort-draggable" '+
|
||||
'draggable="false" '+
|
||||
'id="jspsych-free-sort-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>';
|
||||
|
||||
@ -112,78 +246,232 @@ jsPsych.plugins['free-sort'] = (function() {
|
||||
"x": coords.x,
|
||||
"y": coords.y
|
||||
});
|
||||
if (trial.stim_starts_inside) {
|
||||
inside.push(true);
|
||||
} else {
|
||||
inside.push(false);
|
||||
}
|
||||
}
|
||||
|
||||
display_element.innerHTML += '<button id="jspsych-free-sort-done-btn" class="jspsych-btn">'+trial.button_label+'</button>';
|
||||
// moves within a trial
|
||||
let moves = [];
|
||||
|
||||
var maxz = 1;
|
||||
// are objects currently inside
|
||||
let cur_in = false
|
||||
|
||||
var moves = [];
|
||||
// draggable items
|
||||
const draggables = display_element.querySelectorAll('.jspsych-free-sort-draggable');
|
||||
|
||||
var draggables = display_element.querySelectorAll('.jspsych-free-sort-draggable');
|
||||
// button (will show when all items are inside) and border (will change color)
|
||||
const border = display_element.querySelector("#jspsych-free-sort-border")
|
||||
const button = display_element.querySelector('#jspsych-free-sort-done-btn')
|
||||
|
||||
for(var i=0;i<draggables.length; i++){
|
||||
draggables[i].addEventListener('mousedown', function(event){
|
||||
var x = event.pageX - event.currentTarget.offsetLeft;
|
||||
var y = event.pageY - event.currentTarget.offsetTop - window.scrollY;
|
||||
var elem = event.currentTarget;
|
||||
elem.style.zIndex = ++maxz;
|
||||
// when trial starts, modify text and border/background if all items are inside (stim_starts_inside: true)
|
||||
if (inside.some(Boolean) && trial.change_border_background_color) {
|
||||
border.style.borderColor = trial.border_color_in;
|
||||
}
|
||||
if (inside.every(Boolean)) {
|
||||
if (trial.change_border_background_color) {
|
||||
border.style.background = trial.border_color_in;
|
||||
}
|
||||
button.style.visibility = "visible";
|
||||
display_element.querySelector("#jspsych-free-sort-counter").innerHTML = trial.counter_text_finished;
|
||||
}
|
||||
|
||||
var mousemoveevent = function(e){
|
||||
elem.style.top = Math.min(trial.sort_area_height - trial.stim_height, Math.max(0,(e.clientY - y))) + 'px';
|
||||
elem.style.left = Math.min(trial.sort_area_width - trial.stim_width, Math.max(0,(e.clientX - x))) + 'px';
|
||||
let start_event_name
|
||||
let move_event_name
|
||||
let end_event_name
|
||||
|
||||
if (typeof document.ontouchend === 'undefined'){ // for PC
|
||||
start_event_name = 'mousedown'
|
||||
move_event_name = 'mousemove'
|
||||
end_event_name = 'mouseup'
|
||||
} else { // for touch devices
|
||||
start_event_name = 'touchstart'
|
||||
move_event_name = 'touchmove'
|
||||
end_event_name = 'touchend'
|
||||
}
|
||||
|
||||
for(let i=0; i<draggables.length; i++){
|
||||
draggables[i].addEventListener(start_event_name, function(event){
|
||||
let pageX
|
||||
let pageY
|
||||
|
||||
if (typeof document.ontouchend === 'undefined'){ // for PC
|
||||
pageX = event.pageX
|
||||
pageY = event.pageY
|
||||
} else { // for touch devices
|
||||
event.preventDefault();
|
||||
const touchObject = event.changedTouches[0]
|
||||
pageX = touchObject.pageX
|
||||
pageY = touchObject.pageY
|
||||
}
|
||||
document.addEventListener('mousemove', mousemoveevent);
|
||||
|
||||
let x = pageX - event.currentTarget.offsetLeft;
|
||||
let y = pageY - event.currentTarget.offsetTop - window.scrollY;
|
||||
let elem = event.currentTarget;
|
||||
elem.style.transform = "scale(" + trial.scale_factor + "," + trial.scale_factor + ")";
|
||||
let mousemoveevent = function(e){
|
||||
let clientX
|
||||
let clientY
|
||||
if (typeof document.ontouchend === 'undefined'){ // for PC
|
||||
clientX = e.clientX
|
||||
clientY = e.clientY
|
||||
} else { // for touch devices
|
||||
const touchObject = e.changedTouches[0]
|
||||
clientX = touchObject.clientX
|
||||
clientY = touchObject.clientY
|
||||
}
|
||||
|
||||
cur_in = inside_ellipse(clientX - x, clientY - y,
|
||||
trial.sort_area_width*.5 - trial.stim_width*.5, trial.sort_area_height*.5 - trial.stim_height*.5,
|
||||
trial.sort_area_width*.5, trial.sort_area_height*.5,
|
||||
trial.sort_area_shape == "square");
|
||||
elem.style.top = Math.min(trial.sort_area_height - trial.stim_height*.5, Math.max(- trial.stim_height*.5, (clientY - y))) + 'px';
|
||||
elem.style.left = Math.min(trial.sort_area_width*1.5 - trial.stim_width, Math.max(-trial.sort_area_width*.5, (clientX - x)))+ 'px';
|
||||
|
||||
// modify border while items is being moved
|
||||
if (trial.change_border_background_color) {
|
||||
if (cur_in) {
|
||||
border.style.borderColor = trial.border_color_in;
|
||||
border.style.background = "None";
|
||||
} else {
|
||||
border.style.borderColor = trial.border_color_out;
|
||||
border.style.background = "None";
|
||||
}
|
||||
}
|
||||
|
||||
// replace in overall array, grab index from item id
|
||||
var elem_number = elem.id.split("jspsych-free-sort-draggable-")[1];
|
||||
inside.splice(elem_number, true, cur_in)
|
||||
|
||||
// modify text and background if all items are inside
|
||||
if (inside.every(Boolean)) {
|
||||
if (trial.change_border_background_color) {
|
||||
border.style.background = trial.border_color_in;
|
||||
}
|
||||
button.style.visibility = "visible";
|
||||
display_element.querySelector("#jspsych-free-sort-counter").innerHTML = trial.counter_text_finished;
|
||||
} else {
|
||||
border.style.background = "none";
|
||||
button.style.visibility = "hidden";
|
||||
display_element.querySelector("#jspsych-free-sort-counter").innerHTML = get_counter_text(inside.length - inside.filter(Boolean).length);
|
||||
}
|
||||
}
|
||||
document.addEventListener(move_event_name, mousemoveevent);
|
||||
|
||||
var mouseupevent = function(e){
|
||||
document.removeEventListener('mousemove', mousemoveevent);
|
||||
document.removeEventListener(move_event_name, mousemoveevent);
|
||||
elem.style.transform = "scale(1, 1)";
|
||||
if (trial.change_border_background_color) {
|
||||
if (inside.every(Boolean)) {
|
||||
border.style.background = trial.border_color_in;
|
||||
border.style.borderColor = trial.border_color_in;
|
||||
} else {
|
||||
border.style.background = "none";
|
||||
border.style.borderColor = trial.border_color_out;
|
||||
}
|
||||
}
|
||||
moves.push({
|
||||
"src": elem.dataset.src,
|
||||
"x": elem.offsetLeft,
|
||||
"y": elem.offsetTop
|
||||
});
|
||||
document.removeEventListener('mouseup', mouseupevent);
|
||||
document.removeEventListener(end_event_name, mouseupevent);
|
||||
}
|
||||
document.addEventListener('mouseup', mouseupevent);
|
||||
document.addEventListener(end_event_name, mouseupevent);
|
||||
});
|
||||
}
|
||||
|
||||
display_element.querySelector('#jspsych-free-sort-done-btn').addEventListener('click', function(){
|
||||
display_element.querySelector('#jspsych-free-sort-done-btn').addEventListener('click', function(){
|
||||
if (inside.every(Boolean)) {
|
||||
const end_time = performance.now();
|
||||
const rt = end_time - start_time;
|
||||
// gather data
|
||||
const items = display_element.querySelectorAll('.jspsych-free-sort-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)
|
||||
});
|
||||
}
|
||||
|
||||
var end_time = performance.now();
|
||||
var rt = end_time - start_time;
|
||||
// gather data
|
||||
// get final position of all objects
|
||||
var final_locations = [];
|
||||
var matches = display_element.querySelectorAll('.jspsych-free-sort-draggable');
|
||||
for(var i=0; i<matches.length; i++){
|
||||
final_locations.push({
|
||||
"src": matches[i].dataset.src,
|
||||
"x": parseInt(matches[i].style.left),
|
||||
"y": parseInt(matches[i].style.top)
|
||||
});
|
||||
const trial_data = {
|
||||
"init_locations": JSON.stringify(init_locations),
|
||||
"moves": JSON.stringify(moves),
|
||||
"final_locations": JSON.stringify(final_locations),
|
||||
"rt": rt
|
||||
};
|
||||
|
||||
// advance to next part
|
||||
display_element.innerHTML = '';
|
||||
jsPsych.finishTrial(trial_data);
|
||||
}
|
||||
|
||||
var trial_data = {
|
||||
"init_locations": JSON.stringify(init_locations),
|
||||
"moves": JSON.stringify(moves),
|
||||
"final_locations": JSON.stringify(final_locations),
|
||||
"rt": rt
|
||||
};
|
||||
|
||||
// advance to next part
|
||||
display_element.innerHTML = '';
|
||||
jsPsych.finishTrial(trial_data);
|
||||
});
|
||||
|
||||
function get_counter_text(n) {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
// helper functions
|
||||
|
||||
function random_coordinate(max_width, max_height) {
|
||||
var rnd_x = Math.floor(Math.random() * (max_width - 1));
|
||||
var rnd_y = Math.floor(Math.random() * (max_height - 1));
|
||||
function shuffle(array) {
|
||||
// define three variables
|
||||
let cur_idx = array.length, tmp_val, rand_idx;
|
||||
|
||||
// While there remain elements to shuffle...
|
||||
while (0 !== cur_idx) {
|
||||
// Pick a remaining element...
|
||||
rand_idx = Math.floor(Math.random() * cur_idx);
|
||||
cur_idx -= 1;
|
||||
|
||||
// And swap it with the current element.
|
||||
tmp_val = array[cur_idx];
|
||||
array[cur_idx] = array[rand_idx];
|
||||
array[rand_idx] = tmp_val;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
function make_arr(startValue, stopValue, cardinality) {
|
||||
const step = (stopValue - startValue) / (cardinality - 1);
|
||||
let arr = [];
|
||||
for (let i = 0; i < cardinality; i++) {
|
||||
arr.push(startValue + (step * i));
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
function inside_ellipse(x, y, x0, y0, rx, ry, square=false) {
|
||||
const results = [];
|
||||
if (square) {
|
||||
result = ( Math.abs(x - x0) <= rx ) && ( Math.abs(y - y0) <= ry )
|
||||
} else {
|
||||
result = (( x - x0 ) * ( x - x0 )) * (ry * ry) + ((y - y0) * ( y - y0 )) * ( rx * rx ) <= ( (rx * rx) * (ry * ry) )
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function random_coordinate(max_width, max_height) {
|
||||
const rnd_x = Math.floor(Math.random() * (max_width - 1));
|
||||
const rnd_y = Math.floor(Math.random() * (max_height - 1));
|
||||
return {
|
||||
x: rnd_x,
|
||||
y: rnd_y
|
||||
|
@ -59,7 +59,7 @@ describe('free-sort plugin', function(){
|
||||
auto_preload: false
|
||||
});
|
||||
|
||||
expect(jsPsych.getDisplayElement().innerHTML).toMatch(new RegExp('<img src=\"img/happy_face_1.jpg\" data-src=\"img/happy_face_1.jpg\" class=\"jspsych-free-sort-draggable\" draggable=\"false\" style=\"position: absolute; cursor: move; width:200px; height:200px'));
|
||||
expect(jsPsych.getDisplayElement().innerHTML).toMatch(new RegExp('<img src=\"img/happy_face_1.jpg\".+width:200px; height:200px'));
|
||||
});
|
||||
|
||||
test('should display prompt', function(){
|
||||
@ -90,7 +90,7 @@ describe('free-sort plugin', function(){
|
||||
auto_preload: false
|
||||
});
|
||||
|
||||
expect(jsPsych.getDisplayElement().innerHTML).toMatch(new RegExp('<p>This is a prompt</p><button id=\"jspsych-free-sort-done-btn\"'));
|
||||
expect(jsPsych.getDisplayElement().innerHTML).toMatch(new RegExp('<p>This is a prompt</p>.+<button id=\"jspsych-free-sort-done-btn\"'));
|
||||
});
|
||||
|
||||
test('should be able to change label of button', function(){
|
||||
@ -106,7 +106,7 @@ describe('free-sort plugin', function(){
|
||||
auto_preload: false
|
||||
});
|
||||
|
||||
expect(jsPsych.getDisplayElement().innerHTML).toMatch(new RegExp('<button id=\"jspsych-free-sort-done-btn\" class=\"jspsych-btn\">Finito</button>'));
|
||||
expect(jsPsych.getDisplayElement().innerHTML).toMatch(new RegExp('<button id=\"jspsych-free-sort-done-btn\" class=\"jspsych-btn\".+>Finito</button>'));
|
||||
});
|
||||
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user