├── README.md ├── css └── jspsych.css ├── img ├── congruent_left.gif ├── congruent_right.gif ├── incongruent_left.gif └── incongruent_right.gif ├── index.html └── scripts ├── jspsych.js └── plugins ├── jspsych-animation.js ├── jspsych-call-function.js ├── jspsych-categorize-animation.js ├── jspsych-categorize.js ├── jspsych-free-sort.js ├── jspsych-html.js ├── jspsych-palmer.js ├── jspsych-same-different.js ├── jspsych-similarity.js ├── jspsych-single-stim.js ├── jspsych-survey-likert.js ├── jspsych-survey-text.js ├── jspsych-text.js ├── jspsych-visual-search-circle.js ├── jspsych-vsl-animate-occlusion.js ├── jspsych-vsl-grid-scene.js ├── jspsych-xab.js └── template └── jspsych-plugin-template.js /README.md: -------------------------------------------------------------------------------- 1 | This repository is intended to be cloned/forked in order to set up a working jsPsych experiment via GitHub pages. This makes it simple to start exploring jsPsych. 2 | 3 | **This project currently uses jsPsych v3.1** 4 | 5 | Instructions 6 | ------------ 7 | 8 | 1. You will need a GitHub account in order to use the GitHub hosting feature. [Register for free](http://www.github.com). 9 | 10 | 2. After you are logged into your account, click the "fork" button near the top right corner of the page. This will automaticaly copy the repository into your GitHub account. Once the copy is complete, you will be sent directly to your copy of this repository. 11 | 12 | 3. At this point, you must make a small change to the repository to trigger a fresh build of the website. Any change will work. For example, you could change the information in the `
', {
341 | html: data_string
342 | }));
343 | }
344 |
345 | // private function to save text file on local drive
346 | function saveTextToFile(textstr, filename) {
347 | var blobToSave = new Blob([textstr], {
348 | type: 'text/plain'
349 | });
350 | var blobURL = "";
351 | if (typeof window.webkitURL !== 'undefined') {
352 | blobURL = window.webkitURL.createObjectURL(blobToSave);
353 | }
354 | else {
355 | blobURL = window.URL.createObjectURL(blobToSave);
356 | }
357 |
358 | var display_element = jsPsych.getDisplayElement();
359 |
360 | display_element.append($('', {
361 | id: 'jspsych-download-as-text-link',
362 | href: blobURL,
363 | css: {
364 | display: 'none'
365 | },
366 | download: filename,
367 | html: 'download file'
368 | }));
369 | $('#jspsych-download-as-text-link')[0].click();
370 | }
371 |
372 | //
373 | // A few helper functions to handle data format conversion
374 | //
375 | function flattenData(data_object, append_data) {
376 |
377 | append_data = (typeof append_data === undefined) ? {} : append_data;
378 |
379 | var trials = [];
380 |
381 | // loop through data_object
382 | for (var i = 0; i < data_object.length; i++) {
383 | for (var j = 0; j < data_object[i].length; j++) {
384 | var data = $.extend({}, data_object[i][j], append_data);
385 | trials.push(data);
386 | }
387 | }
388 |
389 | return trials;
390 | }
391 |
392 | // this function based on code suggested by StackOverflow users:
393 | // http://stackoverflow.com/users/64741/zachary
394 | // http://stackoverflow.com/users/317/joseph-sturtevant
395 | function JSON2CSV(objArray) {
396 | var array = typeof objArray != 'object' ? JSON.parse(objArray) : objArray;
397 | var line = '';
398 | var result = '';
399 | var columns = [];
400 |
401 | var i = 0;
402 | for (var j = 0; j < array.length; j++) {
403 | for (var key in array[j]) {
404 | var keyString = key + "";
405 | keyString = '"' + keyString.replace(/"/g, '""') + '",';
406 | if ($.inArray(key, columns) == -1) {
407 | columns[i] = key;
408 | line += keyString;
409 | i++;
410 | }
411 | }
412 | }
413 |
414 | line = line.slice(0, - 1);
415 | result += line + '\r\n';
416 |
417 | for (var i = 0; i < array.length; i++) {
418 | var line = '';
419 | for (var j = 0; j < columns.length; j++) {
420 | var value = (typeof array[i][columns[j]] === 'undefined') ? '' : array[i][columns[j]];
421 | var valueString = value + "";
422 | line += '"' + valueString.replace(/"/g, '""') + '",';
423 | }
424 |
425 | line = line.slice(0, - 1);
426 | result += line + '\r\n';
427 | }
428 |
429 | return result;
430 | }
431 |
432 | return module;
433 |
434 | })();
435 |
436 | jsPsych.turk = (function() {
437 |
438 | // turk info
439 | var turk_info;
440 |
441 | var module = {};
442 |
443 | // core.turkInfo gets information relevant to mechanical turk experiments. returns an object
444 | // containing the workerID, assignmentID, and hitID, and whether or not the HIT is in
445 | // preview mode, meaning that they haven't accepted the HIT yet.
446 | module.turkInfo = function(force_refresh) {
447 | // default value is false
448 | force_refresh = (typeof force_refresh === 'undefined') ? false : force_refresh;
449 | // if we already have the turk_info and force_refresh is false
450 | // then just return the cached version.
451 | if (typeof turk_info !== 'undefined' && !force_refresh) {
452 | return turk_info;
453 | } else {
454 |
455 | var turk = {};
456 |
457 | var param = function(url, name) {
458 | name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
459 | var regexS = "[\\?&]" + name + "=([^]*)";
460 | var regex = new RegExp(regexS);
461 | var results = regex.exec(url);
462 | return (results == null) ? "" : results[1];
463 | };
464 |
465 | var src = param(window.location.href, "assignmentId") ? window.location.href : document.referrer;
466 |
467 | var keys = ["assignmentId", "hitId", "workerId", "turkSubmitTo"];
468 | keys.map(
469 |
470 | function(key) {
471 | turk[key] = unescape(param(src, key));
472 | });
473 |
474 | turk.previewMode = (turk.assignmentId == "ASSIGNMENT_ID_NOT_AVAILABLE");
475 |
476 | turk.outsideTurk = (!turk.previewMode && turk.hitId === "" && turk.assignmentId == "" && turk.workerId == "")
477 |
478 | turk_info = turk;
479 |
480 | return turk;
481 | }
482 |
483 | };
484 |
485 | // core.submitToTurk will submit a MechanicalTurk ExternalHIT type
486 |
487 | module.submitToTurk = function(data) {
488 |
489 | var turkInfo = core.turkInfo();
490 | var assignmentId = turkInfo.assignmentId;
491 | var turkSubmitTo = turkInfo.turkSubmitTo;
492 |
493 | if (!assignmentId || !turkSubmitTo) return;
494 |
495 | var dataString = [];
496 |
497 | for (var key in data) {
498 |
499 | if (data.hasOwnProperty(key)) {
500 | dataString.push(key + "=" + escape(data[key]));
501 | }
502 | }
503 |
504 | dataString.push("assignmentId=" + assignmentId);
505 |
506 | var url = turkSubmitTo + "/mturk/externalSubmit?" + dataString.join("&");
507 |
508 | window.location.href = url;
509 | }
510 |
511 | return module;
512 |
513 | })();
514 |
515 | jsPsych.randomization = (function() {
516 |
517 | var module = {};
518 |
519 | module.repeat = function(array, repetitions, unpack) {
520 |
521 | var arr_isArray = Array.isArray(array);
522 | var rep_isArray = Array.isArray(repetitions);
523 |
524 | // if array is not an array, then we just repeat the item
525 | if(!arr_isArray){
526 | if(!rep_isArray) {
527 | array = [array];
528 | repetitions = [repetitions];
529 | } else {
530 | repetitions = [repetitions[0]];
531 | console.log('Unclear parameters given to randomizeSimpleSample. Multiple set sizes specified, but only one item exists to sample. Proceeding using the first set size.');
532 | }
533 | } else {
534 | if(!rep_isArray) {
535 | var reps = [];
536 | for(var i = 0; i < array.length; i++){
537 | reps.push(repetitions);
538 | }
539 | repetitions = reps;
540 | } else {
541 | if(array.length != repetitions.length) {
542 | // throw warning if repetitions is too short,
543 | // throw warning if too long, and then use the first N
544 | }
545 | }
546 | }
547 |
548 | // should be clear at this point to assume that array and repetitions are arrays with == length
549 | var allsamples = [];
550 | for(var i = 0; i < array.length; i++){
551 | for(var j = 0; j < repetitions[i]; j++){
552 | allsamples.push(array[i]);
553 | }
554 | }
555 |
556 | var out = shuffle(allsamples);
557 |
558 | if(unpack) { out = unpackArray(out); }
559 |
560 | return shuffle(out);
561 | }
562 |
563 | module.factorial = function(factors, repetitions, unpack){
564 |
565 | var factorNames = Object.keys(factors);
566 |
567 | var factor_combinations = [];
568 |
569 | for(var i = 0; i < factors[factorNames[0]].length; i++){
570 | factor_combinations.push({});
571 | factor_combinations[i][factorNames[0]] = factors[factorNames[0]][i];
572 | }
573 |
574 | for(var i = 1; i< factorNames.length; i++){
575 | var toAdd = factors[factorNames[i]];
576 | var n = factor_combinations.length;
577 | for(var j = 0; j < n; j++){
578 | var base = factor_combinations[j];
579 | for(var k = 0; k < toAdd.length; k++){
580 | var newpiece = {};
581 | newpiece[factorNames[i]] = toAdd[k];
582 | factor_combinations.push($.extend({}, base, newpiece));
583 | }
584 | }
585 | factor_combinations.splice(0,n);
586 | }
587 |
588 | repetitions = (typeof repetitions === 'undefined') ? 1 : repetitions;
589 | var with_repetitions = module.repeat(factor_combinations, repetitions, unpack);
590 |
591 | return with_repetitions;
592 | }
593 |
594 | function unpackArray(array) {
595 |
596 | var out = {};
597 |
598 | for(var i = 0; i < array.length; i++){
599 | var keys = Object.keys(array[i]);
600 | for(var k = 0; k < keys.length; k++){
601 | if(typeof out[keys[k]] === 'undefined') {
602 | out[keys[k]] = [];
603 | }
604 | out[keys[k]].push(array[i][keys[k]]);
605 | }
606 | }
607 |
608 | return out;
609 | }
610 |
611 | function shuffle(array) {
612 | var m = array.length, t, i;
613 |
614 | // While there remain elements to shuffle…
615 | while (m) {
616 |
617 | // Pick a remaining element…
618 | i = Math.floor(Math.random() * m--);
619 |
620 | // And swap it with the current element.
621 | t = array[m];
622 | array[m] = array[i];
623 | array[i] = t;
624 | }
625 |
626 | return array;
627 | }
628 |
629 | return module;
630 |
631 | })();
632 |
633 | jsPsych.pluginAPI = (function() {
634 |
635 | // keyboard listeners
636 | var keyboard_listeners = [];
637 |
638 | var module = {};
639 |
640 | module.getKeyboardResponse = function(callback_function, valid_responses, rt_method, persist) {
641 |
642 | rt_method = (typeof rt_method === 'undefined') ? 'date' : rt_method;
643 | if (rt_method != 'date' && rt_method != 'performance') {
644 | console.log('Invalid RT method specified in getKeyboardResponse. Defaulting to "date" method.');
645 | rt_method = 'date';
646 | }
647 |
648 | var start_time;
649 | if (rt_method == 'date') {
650 | start_time = (new Date()).getTime();
651 | }
652 | if (rt_method == 'performance') {
653 | start_time = performance.now();
654 | }
655 |
656 | var listener_id;
657 |
658 | var listener_function = function(e) {
659 |
660 | var key_time;
661 | if (rt_method == 'date') {
662 | key_time = (new Date()).getTime();
663 | }
664 | if (rt_method == 'performance') {
665 | key_time = performance.now();
666 | }
667 |
668 | var valid_response = false;
669 | if (typeof valid_responses === 'undefined' || valid_responses.length === 0) {
670 | valid_response = true;
671 | }
672 | for (var i = 0; i < valid_responses.length; i++) {
673 | if (typeof valid_responses[i] == 'string') {
674 | if(typeof keylookup[valid_responses[i]] !== 'undefined'){
675 | if(e.which == keylookup[valid_responses[i]]) {
676 | valid_response = true;
677 | }
678 | } else {
679 | throw new Error('Invalid key string specified for getKeyboardResponse');
680 | }
681 | } else if (e.which == valid_responses[i]) {
682 | valid_response = true;
683 | }
684 | }
685 |
686 | if (valid_response) {
687 |
688 | var after_up = function(up) {
689 |
690 | if(up.which == e.which) {
691 | $(document).off('keyup', after_up);
692 |
693 | if($.inArray(listener_id, keyboard_listeners) > -1) {
694 |
695 | if(!persist){
696 | // remove keyboard listener
697 | module.cancelKeyboardResponse(listener_id);
698 | }
699 |
700 | callback_function({
701 | key: e.which,
702 | rt: key_time - start_time
703 | });
704 | }
705 | }
706 | };
707 |
708 | $(document).keyup(after_up);
709 | }
710 | };
711 |
712 | $(document).keydown(listener_function);
713 |
714 | // create listener id object
715 | listener_id = {type: 'keydown', fn: listener_function};
716 |
717 | // add this keyboard listener to the list of listeners
718 | keyboard_listeners.push(listener_id);
719 |
720 | return listener_id;
721 |
722 | };
723 |
724 | module.cancelKeyboardResponse = function(listener) {
725 | // remove the listener from the doc
726 | $(document).off(listener.type, listener.fn);
727 |
728 | // remove the listener from the list of listeners
729 | if($.inArray(listener, keyboard_listeners) > -1) {
730 | keyboard_listeners.splice($.inArray(listener, keyboard_listeners), 1);
731 | }
732 | };
733 |
734 | module.cancelAllKeyboardResponses = function() {
735 | for(var i = 0; i< keyboard_listeners.length; i++){
736 | $(document).off(keyboard_listeners[i].type, keyboard_listeners[i].fn);
737 | }
738 | keyboard_listeners = [];
739 | };
740 |
741 | // keycode lookup associative array
742 | var keylookup = {
743 | 'backspace': 8,
744 | 'tab': 9,
745 | 'enter': 13,
746 | 'shift': 16,
747 | 'ctrl': 17,
748 | 'alt': 18,
749 | 'pause': 19,
750 | 'capslock': 20,
751 | 'esc': 27,
752 | 'space':32,
753 | 'spacebar':32,
754 | ' ':32,
755 | 'pageup': 33,
756 | 'pagedown': 34,
757 | 'end': 35,
758 | 'home': 36,
759 | 'leftarrow': 37,
760 | 'uparrow': 38,
761 | 'rightarrow': 39,
762 | 'downarrow': 40,
763 | 'insert': 45,
764 | 'delete': 46,
765 | '0': 48,
766 | '1': 49,
767 | '2': 50,
768 | '3': 51,
769 | '4': 52,
770 | '5': 53,
771 | '6': 54,
772 | '7': 55,
773 | '8': 56,
774 | '9': 57,
775 | 'a': 65,
776 | 'b': 66,
777 | 'c': 67,
778 | 'd': 68,
779 | 'e': 69,
780 | 'f': 70,
781 | 'g': 71,
782 | 'h': 72,
783 | 'i': 73,
784 | 'j': 74,
785 | 'k': 75,
786 | 'l': 76,
787 | 'm': 77,
788 | 'n': 78,
789 | 'o': 79,
790 | 'p': 80,
791 | 'q': 81,
792 | 'r': 82,
793 | 's': 83,
794 | 't': 84,
795 | 'u': 85,
796 | 'v': 86,
797 | 'w': 87,
798 | 'x': 88,
799 | 'y': 89,
800 | 'z': 90,
801 | 'A': 65,
802 | 'B': 66,
803 | 'C': 67,
804 | 'D': 68,
805 | 'E': 69,
806 | 'F': 70,
807 | 'G': 71,
808 | 'H': 72,
809 | 'I': 73,
810 | 'J': 74,
811 | 'K': 75,
812 | 'L': 76,
813 | 'M': 77,
814 | 'N': 78,
815 | 'O': 79,
816 | 'P': 80,
817 | 'Q': 81,
818 | 'R': 82,
819 | 'S': 83,
820 | 'T': 84,
821 | 'U': 85,
822 | 'V': 86,
823 | 'W': 87,
824 | 'X': 88,
825 | 'Y': 89,
826 | 'Z': 90,
827 | '0numpad': 96,
828 | '1numpad': 97,
829 | '2numpad': 98,
830 | '3numpad': 99,
831 | '4numpad': 100,
832 | '5numpad': 101,
833 | '6numpad': 102,
834 | '7numpad': 103,
835 | '8numpad': 104,
836 | '9numpad': 105,
837 | 'multiply': 106,
838 | 'plus': 107,
839 | 'minus': 109,
840 | 'decimal': 110,
841 | 'divide': 111,
842 | 'F1': 112,
843 | 'F2': 113,
844 | 'F3': 114,
845 | 'F4': 115,
846 | 'F5': 116,
847 | 'F6': 117,
848 | 'F7': 118,
849 | 'F8': 119,
850 | 'F9': 120,
851 | 'F10': 121,
852 | 'F11': 122,
853 | 'F12': 123,
854 | '=': 187,
855 | ',': 188,
856 | '.': 190,
857 | '/': 191,
858 | '`': 192,
859 | '[': 219,
860 | '\\': 220,
861 | ']': 221
862 | };
863 |
864 | //
865 | // These are public functions, intended to be used for developing plugins.
866 | // They aren't considered part of the normal API for the core library.
867 | //
868 |
869 | module.normalizeTrialVariables = function(trial, protect) {
870 |
871 | protect = (typeof protect === 'undefined') ? [] : protect;
872 |
873 | var keys = getKeys(trial);
874 |
875 | var tmp = {};
876 | for (var i = 0; i < keys.length; i++) {
877 |
878 | var process = true;
879 | for (var j = 0; j < protect.length; j++) {
880 | if (protect[j] == keys[i]) {
881 | process = false;
882 | break;
883 | }
884 | }
885 |
886 | if (typeof trial[keys[i]] == "function" && process) {
887 | tmp[keys[i]] = trial[keys[i]].call();
888 | }
889 | else {
890 | tmp[keys[i]] = trial[keys[i]];
891 | }
892 |
893 | }
894 |
895 | return tmp;
896 |
897 | };
898 |
899 | // if possible_array is not an array, then return a one-element array
900 | // containing possible_array
901 | module.enforceArray = function(params, possible_arrays) {
902 |
903 | // function to check if something is an array, fallback
904 | // to string method if browser doesn't support Array.isArray
905 | var ckArray = Array.isArray || function(a) {
906 | return toString.call(a) == '[object Array]';
907 | };
908 |
909 | for (var i = 0; i < possible_arrays.length; i++) {
910 | if(typeof params[possible_arrays[i]] !== 'undefined'){
911 | params[possible_arrays[i]] = ckArray(params[possible_arrays[i]]) ? params[possible_arrays[i]] : [params[possible_arrays[i]]];
912 | }
913 | }
914 |
915 | return params;
916 | };
917 |
918 | function getKeys(obj) {
919 | var r = [];
920 | for (var k in obj) {
921 | if (!obj.hasOwnProperty(k)) continue;
922 | r.push(k);
923 | }
924 | return r;
925 | }
926 |
927 | return module;
928 | })();
929 |
930 | // methods used in multiple modules
931 |
932 | // private function to flatten nested arrays
933 | function flatten(arr, out) {
934 | out = (typeof out === 'undefined') ? [] : out;
935 | for (var i = 0; i < arr.length; i++) {
936 | if (Array.isArray(arr[i])) {
937 | flatten(arr[i], out);
938 | }
939 | else {
940 | out.push(arr[i]);
941 | }
942 | }
943 | return out;
944 | }
945 |
946 | })(jQuery);
--------------------------------------------------------------------------------
/scripts/plugins/jspsych-animation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * jsPsych plugin for showing animations and recording keyboard responses
3 | * Josh de Leeuw
4 | *
5 | * documentation: https://github.com/jodeleeuw/jsPsych/wiki/jspsych-animation
6 | */
7 |
8 | (function($) {
9 | jsPsych.animation = (function() {
10 |
11 | var plugin = {};
12 |
13 | plugin.create = function(params) {
14 |
15 | params = jsPsych.pluginAPI.enforceArray(params, ['choices', 'data']);
16 |
17 | var trials = new Array(params.stimuli.length);
18 | for (var i = 0; i < trials.length; i++) {
19 | trials[i] = {};
20 | trials[i].type = "animation";
21 | trials[i].stims = params.stimuli[i];
22 | trials[i].frame_time = params.frame_time || 250;
23 | trials[i].frame_isi = params.frame_isi || 0;
24 | trials[i].repetitions = params.repetitions || 1;
25 | trials[i].choices = params.choices || [];
26 | trials[i].timing_post_trial = (typeof params.timing_post_trial === 'undefined') ? 1000 : params.timing_post_trial;
27 | trials[i].prompt = (typeof params.prompt === 'undefined') ? "" : params.prompt;
28 | trials[i].data = (typeof params.data === 'undefined') ? {} : params.data[i];
29 | }
30 | return trials;
31 | };
32 |
33 | plugin.trial = function(display_element, block, trial, part) {
34 |
35 | // if any trial variables are functions
36 | // this evaluates the function and replaces
37 | // it with the output of the function
38 | trial = jsPsych.pluginAPI.normalizeTrialVariables(trial);
39 |
40 | var interval_time = trial.frame_time + trial.frame_isi;
41 | var animate_frame = -1;
42 | var reps = 0;
43 | var startTime = (new Date()).getTime();
44 | var animation_sequence = [];
45 | var responses = [];
46 | var current_stim = "";
47 |
48 | var animate_interval = setInterval(function() {
49 | var showImage = true;
50 | display_element.html(""); // clear everything
51 | animate_frame++;
52 | if (animate_frame == trial.stims.length) {
53 | animate_frame = 0;
54 | reps++;
55 | if (reps >= trial.repetitions) {
56 | endTrial();
57 | clearInterval(animate_interval);
58 | showImage = false;
59 | }
60 | }
61 | if (showImage) {
62 | show_next_frame();
63 | }
64 | }, interval_time);
65 |
66 | function show_next_frame() {
67 | // show image
68 | display_element.append($('
', {
69 | "src": trial.stims[animate_frame],
70 | "id": 'jspsych-animation-image'
71 | }));
72 |
73 | current_stim = trial.stims[animate_frame];
74 |
75 | // record when image was shown
76 | animation_sequence.push({
77 | "stimulus": current_stim,
78 | "time": (new Date()).getTime() - startTime
79 | });
80 |
81 | if (trial.prompt !== "") {
82 | display_element.append(trial.prompt);
83 | }
84 |
85 | if (trial.frame_isi > 0) {
86 | setTimeout(function() {
87 | $('#jspsych-animation-image').css('visibility', 'hidden');
88 | current_stim = 'blank';
89 | // record when blank image was shown
90 | animation_sequence.push({
91 | "stimulus": 'blank',
92 | "time": (new Date()).getTime() - startTime
93 | });
94 | }, trial.frame_time);
95 | }
96 | }
97 |
98 | var after_response = function(info) {
99 |
100 | responses.push({
101 | key_press: info.key,
102 | rt: info.rt,
103 | stimulus: current_stim
104 | });
105 |
106 | // after a valid response, the stimulus will have the CSS class 'responded'
107 | // which can be used to provide visual feedback that a response was recorded
108 | $("#jspsych-animation-image").addClass('responded');
109 | }
110 |
111 | // hold the jspsych response listener object in memory
112 | // so that we can turn off the response collection when
113 | // the trial ends
114 | var response_listener = jsPsych.pluginAPI.getKeyboardResponse(after_response, trial.choices, 'date', true);
115 |
116 | function endTrial() {
117 |
118 | jsPsych.pluginAPI.cancelKeyboardResponse(response_listener);
119 |
120 | block.writeData($.extend({}, {
121 | "trial_type": "animation",
122 | "trial_index": block.trial_idx,
123 | "animation_sequence": JSON.stringify(animation_sequence),
124 | "responses": JSON.stringify(responses)
125 | }, trial.data));
126 |
127 | if(trial.timing_post_trial > 0){
128 | setTimeout(function() {
129 | block.next();
130 | }, trial.timing_post_trial);
131 | } else {
132 | block.next();
133 | }
134 | }
135 | };
136 |
137 | return plugin;
138 | })();
139 | })(jQuery);
140 |
--------------------------------------------------------------------------------
/scripts/plugins/jspsych-call-function.js:
--------------------------------------------------------------------------------
1 | /**
2 | * jspsych-call-function
3 | * plugin for calling an arbitrary function during a jspsych experiment
4 | * Josh de Leeuw
5 | *
6 | * documentation: https://github.com/jodeleeuw/jsPsych/wiki/jspsych-call-function
7 | *
8 | **/
9 |
10 | (function($) {
11 | jsPsych['call-function'] = (function() {
12 |
13 | var plugin = {};
14 |
15 | plugin.create = function(params) {
16 | var trials = new Array(1);
17 | trials[0] = {
18 | "type": "call-function",
19 | "func": params.func,
20 | "args": params.args || [],
21 | "data": (typeof params.data === 'undefined') ? {} : params.data
22 | };
23 | return trials;
24 | };
25 |
26 | plugin.trial = function(display_element, block, trial, part) {
27 | var return_val = trial.func.apply({}, [trial.args]);
28 | if (typeof return_val !== 'undefined') {
29 | block.writeData($.extend({},{
30 | trial_type: "call-function",
31 | trial_index: block.trial_idx,
32 | value: return_val
33 | },trial.data));
34 | }
35 |
36 | block.next();
37 | };
38 |
39 | return plugin;
40 | })();
41 | })(jQuery);
42 |
--------------------------------------------------------------------------------
/scripts/plugins/jspsych-categorize-animation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * jspsych plugin for categorization trials with feedback and animated stimuli
3 | * Josh de Leeuw
4 | *
5 | * documentation: https://github.com/jodeleeuw/jsPsych/wiki/jspsych-categorize-animation
6 | **/
7 |
8 | (function($) {
9 | jsPsych["categorize-animation"] = (function() {
10 |
11 | var plugin = {};
12 |
13 | plugin.create = function(params) {
14 |
15 | params = jsPsych.pluginAPI.enforceArray(params, ['key_answer','text_answer','choices','data']);
16 |
17 | var trials = new Array(params.stimuli.length);
18 | for (var i = 0; i < trials.length; i++) {
19 | trials[i] = {};
20 | trials[i].type = "categorize-animation";
21 | trials[i].stims = params.stimuli[i];
22 | trials[i].reps = params.reps || 1;
23 | trials[i].key_answer = params.key_answer[i];
24 | trials[i].text_answer = (typeof params.text_answer === 'undefined') ? "" : params.text_answer[i];
25 | trials[i].choices = params.choices;
26 | trials[i].correct_text = params.correct_text || "Correct.";
27 | trials[i].incorrect_text = params.incorrect_text || "Wrong.";
28 | trials[i].allow_response_before_complete = params.allow_response_before_complete || false;
29 | trials[i].frame_time = params.frame_time || 500;
30 | trials[i].timing_feedback_duration = params.timing_feedback_duration || 2000;
31 | trials[i].timing_post_trial = (typeof params.timing_post_trial === 'undefined') ? 1000 : params.timing_post_trial;
32 | trials[i].prompt = (typeof params.prompt === 'undefined') ? '' : params.prompt;
33 | trials[i].data = (typeof params.data === 'undefined') ? {} : params.data[i];
34 | }
35 | return trials;
36 | };
37 |
38 | plugin.trial = function(display_element, block, trial, part) {
39 |
40 | // if any trial variables are functions
41 | // this evaluates the function and replaces
42 | // it with the output of the function
43 | trial = jsPsych.pluginAPI.normalizeTrialVariables(trial);
44 |
45 | var animate_frame = -1;
46 | var reps = 0;
47 |
48 | var showAnimation = true;
49 |
50 | var responded = false;
51 | var timeoutSet = false;
52 |
53 |
54 | var startTime = (new Date()).getTime();
55 |
56 | // show animation
57 | var animate_interval = setInterval(function() {
58 | display_element.html(""); // clear everything
59 | animate_frame++;
60 | if (animate_frame == trial.stims.length) {
61 | animate_frame = 0;
62 | reps++;
63 | // check if reps complete //
64 | if (trial.reps != -1 && reps >= trial.reps) {
65 | // done with animation
66 | showAnimation = false;
67 | }
68 | }
69 |
70 | if (showAnimation) {
71 | display_element.append($('
', {
72 | "src": trial.stims[animate_frame],
73 | "class": 'jspsych-categorize-animation-stimulus'
74 | }));
75 | }
76 |
77 | if (!responded && trial.allow_response_before_complete) {
78 | // in here if the user can respond before the animation is done
79 | if (trial.prompt !== "") {
80 | display_element.append(trial.prompt);
81 | }
82 | }
83 | else if (!responded) {
84 | // in here if the user has to wait to respond until animation is done.
85 | // if this is the case, don't show the prompt until the animation is over.
86 | if (!showAnimation) {
87 | if (trial.prompt !== "") {
88 | display_element.append(trial.prompt);
89 | }
90 | }
91 | }
92 | else {
93 | // user has responded if we get here.
94 |
95 | // show feedback
96 | var feedback_text = "";
97 | if (block.data[block.trial_idx].correct) {
98 | feedback_text = trial.correct_text.replace("%ANS%", trial.text_answer);
99 | }
100 | else {
101 | feedback_text = trial.incorrect_text.replace("%ANS%", trial.text_answer);
102 | }
103 | display_element.append(feedback_text);
104 |
105 | // set timeout to clear feedback
106 | if (!timeoutSet) {
107 | timeoutSet = true;
108 | setTimeout(function() {
109 | endTrial();
110 | }, trial.timing_feedback_duration);
111 | }
112 | }
113 |
114 |
115 | }, trial.frame_time);
116 |
117 |
118 | var keyboard_listener;
119 |
120 | var after_response = function(info){
121 | // ignore the response if animation is playing and subject
122 | // not allowed to respond before it is complete
123 | if (!trial.allow_response_before_complete && showAnimation) {
124 | return false;
125 | }
126 |
127 | var correct = false;
128 | if(trial.key_answer == info.key) {
129 | correct = true;
130 | }
131 |
132 | responded = true;
133 |
134 | var trial_data = {
135 | "trial_type": trial.type,
136 | "trial_index": block.trial_idx,
137 | "stimulus": trial.stims[0],
138 | "rt": info.rt,
139 | "correct": correct,
140 | "key_press": info.key
141 | };
142 |
143 | block.writeData($.extend({}, trial_data, trial.data));
144 |
145 | jsPsych.pluginAPI.cancelKeyboardResponse(keyboard_listener);
146 |
147 | }
148 |
149 | jsPsych.pluginAPI.getKeyboardResponse(after_response, trial.choices, 'date', true);
150 |
151 | function endTrial() {
152 | clearInterval(animate_interval); // stop animation!
153 | display_element.html(''); // clear everything
154 | if(trial.timing_post_trial > 0){
155 | setTimeout(function() {
156 | block.next();
157 | }, trial.timing_post_trial);
158 | } else {
159 | block.next();
160 | }
161 | }
162 | };
163 |
164 | return plugin;
165 | })();
166 | })(jQuery);
167 |
--------------------------------------------------------------------------------
/scripts/plugins/jspsych-categorize.js:
--------------------------------------------------------------------------------
1 | /**
2 | * jspsych plugin for categorization trials with feedback
3 | * Josh de Leeuw
4 | *
5 | * documentation: https://github.com/jodeleeuw/jsPsych/wiki/jspsych-categorize
6 | **/
7 |
8 | (function($) {
9 | jsPsych.categorize = (function() {
10 |
11 | var plugin = {};
12 |
13 | plugin.create = function(params) {
14 |
15 | params = jsPsych.pluginAPI.enforceArray(params, ['choices', 'stimuli', 'key_answer', 'text_answer', 'data']);
16 |
17 | var trials = [];
18 | for (var i = 0; i < params.stimuli.length; i++) {
19 | trials.push({});
20 | trials[i].type = "categorize";
21 | trials[i].a_path = params.stimuli[i];
22 | trials[i].key_answer = params.key_answer[i];
23 | trials[i].text_answer = (typeof params.text_answer === 'undefined') ? "" : params.text_answer[i];
24 | trials[i].choices = params.choices;
25 | trials[i].correct_text = (typeof params.correct_text === 'undefined') ? "Correct
" : params.correct_text;
26 | trials[i].incorrect_text = (typeof params.incorrect_text === 'undefined') ? "Incorrect
" : params.incorrect_text;
27 | // timing params
28 | trials[i].timing_stim = params.timing_stim || -1; // default is to show image until response
29 | trials[i].timing_feedback_duration = params.timing_feedback_duration || 2000;
30 | trials[i].timing_post_trial = (typeof params.timing_post_trial === 'undefined') ? 1000 : params.timing_post_trial;
31 | // optional params
32 | trials[i].show_stim_with_feedback = (typeof params.show_stim_with_feedback === 'undefined') ? true : params.show_stim_with_feedback;
33 | trials[i].is_html = (typeof params.is_html === 'undefined') ? false : params.is_html;
34 | trials[i].force_correct_button_press = (typeof params.force_correct_button_press === 'undefined') ? false : params.force_correct_button_press;
35 | trials[i].prompt = (typeof params.prompt === 'undefined') ? '' : params.prompt;
36 | trials[i].data = (typeof params.data === 'undefined') ? {} : params.data[i];
37 | }
38 | return trials;
39 | };
40 |
41 | var cat_trial_complete = false;
42 |
43 | plugin.trial = function(display_element, block, trial, part) {
44 |
45 | // if any trial variables are functions
46 | // this evaluates the function and replaces
47 | // it with the output of the function
48 | trial = jsPsych.pluginAPI.normalizeTrialVariables(trial);
49 |
50 | switch (part) {
51 | case 1:
52 | // set finish flag
53 | cat_trial_complete = false;
54 |
55 | if (!trial.is_html) {
56 | // add image to display
57 | display_element.append($('
', {
58 | "src": trial.a_path,
59 | "class": 'jspsych-categorize-stimulus',
60 | "id": 'jspsych-categorize-stimulus'
61 | }));
62 | }
63 | else {
64 | display_element.append($('', {
65 | "id": 'jspsych-categorize-stimulus',
66 | "class": 'jspsych-categorize-stimulus',
67 | "html": trial.a_path
68 | }));
69 | }
70 |
71 | // hide image after time if the timing parameter is set
72 | if (trial.timing_stim > 0) {
73 | setTimeout(function() {
74 | if (!cat_trial_complete) {
75 | $('#jspsych-categorize-stimulus').css('visibility', 'hidden');
76 | }
77 | }, trial.timing_stim);
78 | }
79 |
80 | // if prompt is set, show prompt
81 | if (trial.prompt !== "") {
82 | display_element.append(trial.prompt);
83 | }
84 |
85 | // start measuring RT
86 | var startTime = (new Date()).getTime();
87 |
88 | // create response function
89 | var after_response = function(info) {
90 |
91 | var correct = false;
92 | if(trial.key_answer == info.key) { correct = true; }
93 |
94 | cat_trial_complete = true;
95 |
96 | // save data
97 | var trial_data = {
98 | "trial_type": "categorize",
99 | "trial_index": block.trial_idx,
100 | "rt": info.rt,
101 | "correct": correct,
102 | "stimulus": trial.a_path,
103 | "key_press": info.key
104 | };
105 |
106 | block.writeData($.extend({}, trial_data, trial.data));
107 |
108 | display_element.html('');
109 |
110 | plugin.trial(display_element, block, trial, part + 1);
111 | }
112 |
113 | jsPsych.pluginAPI.getKeyboardResponse(after_response, trial.choices, 'date', false);
114 |
115 | break;
116 |
117 | case 2:
118 | // show image during feedback if flag is set
119 | if (trial.show_stim_with_feedback) {
120 | if (!trial.is_html) {
121 | // add image to display
122 | display_element.append($('
', {
123 | "src": trial.a_path,
124 | "class": 'jspsych-categorize-stimulus',
125 | "id": 'jspsych-categorize-stimulus'
126 | }));
127 | }
128 | else {
129 | display_element.append($('', {
130 | "id": 'jspsych-categorize-stimulus',
131 | "class": 'jspsych-categorize-stimulus',
132 | "html": trial.a_path
133 | }));
134 | }
135 | }
136 |
137 | // substitute answer in feedback string.
138 | var atext = "";
139 | if (block.data[block.trial_idx].correct) {
140 | atext = trial.correct_text.replace("%ANS%", trial.text_answer);
141 | }
142 | else {
143 | atext = trial.incorrect_text.replace("%ANS%", trial.text_answer);
144 | }
145 |
146 | // show the feedback
147 | display_element.append(atext);
148 |
149 | // check if force correct button press is set
150 | if (trial.force_correct_button_press && block.data[block.trial_idx].correct === false) {
151 |
152 | var after_forced_response = function(info) {
153 | plugin.trial(display_element, block, trial, part + 1);
154 | }
155 |
156 | jsPsych.pluginAPI.getKeyboardResponse(after_forced_response, trial.key_answer, 'date', false);
157 |
158 | }
159 | else {
160 | setTimeout(function() {
161 | plugin.trial(display_element, block, trial, part + 1);
162 | }, trial.timing_feedback_duration);
163 | }
164 | break;
165 | case 3:
166 | display_element.html("");
167 | if(trial.timing_post_trial > 0){
168 | setTimeout(function() {
169 | block.next();
170 | }, trial.timing_post_trial);
171 | } else {
172 | block.next();
173 | }
174 | break;
175 | }
176 | };
177 |
178 | return plugin;
179 | })();
180 | })(jQuery);
181 |
--------------------------------------------------------------------------------
/scripts/plugins/jspsych-free-sort.js:
--------------------------------------------------------------------------------
1 | /**
2 | * jspsych-free-sort
3 | * plugin for drag-and-drop sorting of a collection of images
4 | * Josh de Leeuw
5 | *
6 | * documentation: https://github.com/jodeleeuw/jsPsych/wiki/jspsych-free-sort
7 | */
8 |
9 | (function($) {
10 | jsPsych['free-sort'] = (function() {
11 |
12 | var plugin = {};
13 |
14 | plugin.create = function(params) {
15 |
16 | params = jsPsych.pluginAPI.enforceArray(params, ['data']);
17 |
18 | var trials = new Array(params.stimuli.length);
19 | for (var i = 0; i < trials.length; i++) {
20 | trials[i] = {
21 | "type": "free-sort",
22 | "images": params.stimuli[i], // array of images to display
23 | "stim_height": params.stim_height || 100,
24 | "stim_width": params.stim_width || 100,
25 | "timing_post_trial": (typeof params.timing_post_trial === 'undefined') ? 1000 : params.timing_post_trial,
26 | "prompt": (typeof params.prompt === 'undefined') ? '' : params.prompt,
27 | "prompt_location": params.prompt_location || "above",
28 | "sort_area_width": params.sort_area_width || 800,
29 | "sort_area_height": params.sort_area_height || 800,
30 | "data": (typeof params.data === 'undefined') ? {} : params.data[i]
31 | };
32 | }
33 | return trials;
34 | };
35 |
36 | plugin.trial = function(display_element, block, trial, part) {
37 |
38 | // if any trial variables are functions
39 | // this evaluates the function and replaces
40 | // it with the output of the function
41 | trial = jsPsych.pluginAPI.normalizeTrialVariables(trial);
42 |
43 | var start_time = (new Date()).getTime();
44 |
45 | // check if there is a prompt and if it is shown above
46 | if (trial.prompt && trial.prompt_location == "above") {
47 | display_element.append(trial.prompt);
48 | }
49 |
50 | display_element.append($('', {
51 | "id": "jspsych-free-sort-arena",
52 | "class": "jspsych-free-sort-arena",
53 | "css": {
54 | "position": "relative",
55 | "width": trial.sort_area_width,
56 | "height": trial.sort_area_height
57 | }
58 | }));
59 |
60 | // check if prompt exists and if it is shown below
61 | if (trial.prompt && trial.prompt_location == "below") {
62 | display_element.append(trial.prompt);
63 | }
64 |
65 | // store initial location data
66 | var init_locations = [];
67 |
68 | for (var i = 0; i < trial.images.length; i++) {
69 | var coords = random_coordinate(trial.sort_area_width - trial.stim_width, trial.sort_area_height - trial.stim_height);
70 |
71 | $("#jspsych-free-sort-arena").append($('
', {
72 | "src": trial.images[i],
73 | "class": "jspsych-free-sort-draggable",
74 | "css": {
75 | "position": "absolute",
76 | "top": coords.y,
77 | "left": coords.x
78 | }
79 | }));
80 |
81 | init_locations.push({
82 | "src": trial.images[i],
83 | "x": coords.x,
84 | "y": coords.y
85 | });
86 | }
87 |
88 | var moves = [];
89 |
90 | $('.jspsych-free-sort-draggable').draggable({
91 | containment: "#jspsych-free-sort-arena",
92 | scroll: false,
93 | stack: ".jspsych-free-sort-draggable",
94 | stop: function(event, ui) {
95 | moves.push({
96 | "src": event.target.src.split("/").slice(-1)[0],
97 | "x": ui.position.left,
98 | "y": ui.position.top
99 | });
100 | }
101 | });
102 |
103 | display_element.append($('