210 lines
5.7 KiB
JavaScript
210 lines
5.7 KiB
JavaScript
import { ParameterType } from 'jspsych';
|
|
|
|
const info = {
|
|
name: 'die-roll',
|
|
parameters: {
|
|
prompt: {
|
|
type: ParameterType.HTML_STRING,
|
|
default: '',
|
|
},
|
|
rigged_value: {
|
|
type: ParameterType.INT,
|
|
default: null,
|
|
},
|
|
roll_duration: {
|
|
type: ParameterType.INT,
|
|
default: 2000,
|
|
},
|
|
animation_interval: {
|
|
type: ParameterType.INT,
|
|
default: 120,
|
|
},
|
|
button_label: {
|
|
type: ParameterType.STRING,
|
|
default: 'Continue',
|
|
},
|
|
result_template: {
|
|
type: ParameterType.STRING,
|
|
default: 'You rolled a {{value}}.',
|
|
},
|
|
},
|
|
};
|
|
|
|
class DieRollPlugin {
|
|
constructor(jsPsych) {
|
|
this.jsPsych = jsPsych;
|
|
}
|
|
|
|
trial(display_element, trial) {
|
|
const start_time = performance.now();
|
|
const sanitized_rigged_value = this.get_rigged_value(trial.rigged_value);
|
|
const final_value =
|
|
sanitized_rigged_value ??
|
|
this.jsPsych.randomization.sampleWithoutReplacement(
|
|
[1, 2, 3, 4, 5, 6],
|
|
1
|
|
)[0];
|
|
|
|
display_element.innerHTML = this.build_markup(
|
|
trial.prompt,
|
|
trial.button_label
|
|
);
|
|
|
|
const face_element = display_element.querySelector(
|
|
'.jspsych_die_roll_face'
|
|
);
|
|
const result_element = display_element.querySelector(
|
|
'.jspsych_die_roll_result'
|
|
);
|
|
const button_element = display_element.querySelector(
|
|
'.jspsych_die_roll_button'
|
|
);
|
|
|
|
let is_rolling = false;
|
|
let has_rolled = false;
|
|
let roll_started_at = null;
|
|
let roll_stopped_at = null;
|
|
let die_click_count = 0;
|
|
|
|
const clear_timeouts = () => {
|
|
this.jsPsych.pluginAPI.clearAllTimeouts();
|
|
};
|
|
|
|
const cycle_face = () => {
|
|
if (!is_rolling) {
|
|
return;
|
|
}
|
|
const value = this.random_face(final_value);
|
|
face_element.textContent = value;
|
|
this.jsPsych.pluginAPI.setTimeout(cycle_face, trial.animation_interval);
|
|
};
|
|
|
|
const finalize_roll = () => {
|
|
if (!is_rolling || has_rolled) {
|
|
return;
|
|
}
|
|
is_rolling = false;
|
|
has_rolled = true;
|
|
die_click_count += 1;
|
|
roll_stopped_at = performance.now();
|
|
clear_timeouts();
|
|
face_element.textContent = final_value;
|
|
face_element.classList.remove('jspsych_die_roll_face--active');
|
|
face_element.classList.remove('jspsych_die_roll_face--interactive');
|
|
face_element.classList.add('jspsych_die_roll_face--locked');
|
|
face_element.setAttribute('aria-disabled', 'true');
|
|
face_element.removeAttribute('tabindex');
|
|
face_element.style.pointerEvents = 'none';
|
|
result_element.textContent = trial.result_template.replace(
|
|
/\{\{value\}\}/gi,
|
|
final_value
|
|
);
|
|
button_element.disabled = false;
|
|
button_element.focus();
|
|
};
|
|
|
|
const begin_roll = () => {
|
|
if (is_rolling || has_rolled) {
|
|
return;
|
|
}
|
|
is_rolling = true;
|
|
die_click_count += 1;
|
|
roll_started_at = performance.now();
|
|
result_element.textContent = 'Rolling... click again to stop the die.';
|
|
face_element.classList.add('jspsych_die_roll_face--active');
|
|
button_element.disabled = true;
|
|
clear_timeouts();
|
|
cycle_face();
|
|
};
|
|
|
|
const handle_face_interaction = () => {
|
|
if (!is_rolling && !has_rolled) {
|
|
begin_roll();
|
|
} else if (is_rolling && !has_rolled) {
|
|
finalize_roll();
|
|
}
|
|
};
|
|
|
|
button_element.disabled = true;
|
|
face_element.textContent = '';
|
|
result_element.textContent =
|
|
'Click the die to start, then click it again to lock in your number.';
|
|
face_element.classList.add('jspsych_die_roll_face--interactive');
|
|
face_element.setAttribute('tabindex', '0');
|
|
face_element.setAttribute('role', 'button');
|
|
face_element.setAttribute(
|
|
'aria-label',
|
|
'Virtual die. Click to start and stop.'
|
|
);
|
|
|
|
face_element.addEventListener('click', handle_face_interaction);
|
|
face_element.addEventListener('keydown', event => {
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
event.preventDefault();
|
|
handle_face_interaction();
|
|
}
|
|
});
|
|
|
|
const end_trial = () => {
|
|
is_rolling = false;
|
|
clear_timeouts();
|
|
const trial_data = {
|
|
roll: final_value,
|
|
rigged_value: sanitized_rigged_value,
|
|
rigged: sanitized_rigged_value !== null,
|
|
die_click_count: die_click_count,
|
|
roll_started_after_ms: roll_started_at
|
|
? roll_started_at - start_time
|
|
: null,
|
|
roll_duration_ms:
|
|
roll_started_at && roll_stopped_at
|
|
? roll_stopped_at - roll_started_at
|
|
: null,
|
|
rt: performance.now() - start_time,
|
|
};
|
|
display_element.innerHTML = '';
|
|
this.jsPsych.finishTrial(trial_data);
|
|
};
|
|
|
|
button_element.addEventListener('click', () => {
|
|
if (!button_element.disabled) {
|
|
end_trial();
|
|
}
|
|
});
|
|
}
|
|
|
|
build_markup(prompt, button_label) {
|
|
const prompt_html = prompt
|
|
? `<div class="jspsych_die_roll_prompt">${prompt}</div>`
|
|
: '';
|
|
return `
|
|
<div class="jspsych_die_roll">
|
|
${prompt_html}
|
|
<div class="jspsych_die_roll_face">?</div>
|
|
<div class="jspsych_die_roll_result" aria-live="polite"></div>
|
|
<button class="jspsych-btn jspsych_die_roll_button" type="button">${button_label}</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
random_face(exclude_value) {
|
|
let value = Math.floor(Math.random() * 6) + 1;
|
|
if (value === exclude_value) {
|
|
value = (value % 6) + 1;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
get_rigged_value(value) {
|
|
const numeric = Number(value);
|
|
if (Number.isFinite(numeric) && numeric >= 1 && numeric <= 6) {
|
|
return Math.round(numeric);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
DieRollPlugin.info = info;
|
|
|
|
export default DieRollPlugin;
|