UNPKG

psytask

Version:

JavaScript Framework for Psychology task

9 lines (8 loc) 18.8 kB
/** * Psytask v1.0.0-rc1 * @author cubxx * @license MIT */ var p=Object.create;var{getPrototypeOf:l,defineProperty:R,getOwnPropertyNames:n}=Object;var o=Object.prototype.hasOwnProperty;var w=(q,z,Y)=>{Y=q!=null?p(l(q)):{};let Z=z||!q||!q.__esModule?R(Y,"default",{value:q,enumerable:!0}):Y;for(let $ of n(q))if(!o.call(Z,$))R(Z,$,{get:()=>q[$],enumerable:!0});return Z};var i=(q,z)=>()=>(z||q((z={exports:{}}).exports,z),z.exports);var N=i((Vq,b)=>{var s=(q)=>{let z=new Set;do for(let Y of Reflect.ownKeys(q))z.add([q,Y]);while((q=Reflect.getPrototypeOf(q))&&q!==Object.prototype);return z};b.exports=(q,{include:z,exclude:Y}={})=>{let Z=($)=>{let L=(G)=>typeof G==="string"?$===G:G.test($);if(z)return z.some(L);if(Y)return!Y.some(L);return!0};for(let[$,L]of s(q.constructor.prototype)){if(L==="constructor"||!Z(L))continue;let G=Reflect.getOwnPropertyDescriptor($,L);if(G&&typeof G.value==="function")q[L]=q[L].bind(q)}return q}});var y=w(N(),1);var _=w(N(),1);class j{getRootElement;areResponsesCaseSensitive;minimumValidRt;constructor(q,z=!1,Y=0){this.getRootElement=q;this.areResponsesCaseSensitive=z;this.minimumValidRt=Y;_.default(this),this.registerRootListeners()}listeners=new Set;heldKeys=new Set;areRootListenersRegistered=!1;registerRootListeners(){if(!this.areRootListenersRegistered){let q=this.getRootElement();if(q)q.addEventListener("keydown",this.rootKeydownListener),q.addEventListener("keyup",this.rootKeyupListener),this.areRootListenersRegistered=!0}}rootKeydownListener(q){for(let z of[...this.listeners])z(q);this.heldKeys.add(this.toLowerCaseIfInsensitive(q.key))}toLowerCaseIfInsensitive(q){return this.areResponsesCaseSensitive?q:q.toLowerCase()}rootKeyupListener(q){this.heldKeys.delete(this.toLowerCaseIfInsensitive(q.key))}isResponseValid(q,z,Y){if(!z&&this.heldKeys.has(Y))return!1;if(q==="ALL_KEYS")return!0;if(q==="NO_KEYS")return!1;return q.includes(Y)}getKeyboardResponse({callback_function:q,valid_responses:z="ALL_KEYS",rt_method:Y="performance",persist:Z,audio_context:$,audio_context_start_time:L,allow_held_key:G=!1,minimum_valid_rt:X=this.minimumValidRt}){if(Y!=="performance"&&Y!=="audio")console.log('Invalid RT method specified in getKeyboardResponse. Defaulting to "performance" method.'),Y="performance";let J=Y==="performance"?performance.now():L*1000;if(this.registerRootListeners(),!this.areResponsesCaseSensitive&&typeof z!=="string")z=z.map((W)=>W.toLowerCase());let K=(W)=>{let F=Math.round((Y=="performance"?performance.now():$.currentTime*1000)-J);if(F<X)return;let V=this.toLowerCaseIfInsensitive(W.key);if(this.isResponseValid(z,G,V)){if(W.preventDefault(),!Z)this.cancelKeyboardResponse(K);q({key:W.key,rt:F})}};return this.listeners.add(K),K}cancelKeyboardResponse(q){this.listeners.delete(q)}cancelAllKeyboardResponses(){this.listeners.clear()}compareKeys(q,z){if(typeof q!=="string"&&q!==null||typeof z!=="string"&&z!==null){console.error("Error in jsPsych.pluginAPI.compareKeys: arguments must be key strings or null.");return}if(typeof q==="string"&&typeof z==="string")return this.areResponsesCaseSensitive?q===z:q.toLowerCase()===z.toLowerCase();return q===null&&z===null}}class A{timeout_handlers=[];setTimeout(q,z){let Y=window.setTimeout(q,z);return this.timeout_handlers.push(Y),Y}clearAllTimeouts(){for(let q of this.timeout_handlers)clearTimeout(q);this.timeout_handlers=[]}}var C;((Q)=>{Q[Q.BOOL=0]="BOOL";Q[Q.STRING=1]="STRING";Q[Q.INT=2]="INT";Q[Q.FLOAT=3]="FLOAT";Q[Q.FUNCTION=4]="FUNCTION";Q[Q.KEY=5]="KEY";Q[Q.KEYS=6]="KEYS";Q[Q.SELECT=7]="SELECT";Q[Q.HTML_STRING=8]="HTML_STRING";Q[Q.IMAGE=9]="IMAGE";Q[Q.AUDIO=10]="AUDIO";Q[Q.VIDEO=11]="VIDEO";Q[Q.OBJECT=12]="OBJECT";Q[Q.COMPLEX=13]="COMPLEX";Q[Q.TIMELINE=14]="TIMELINE"})(C||={});var P=function(q,z,Y){if(Y||arguments.length===2){for(var Z=0,$=z.length,L;Z<$;Z++)if(L||!(Z in z)){if(!L)L=Array.prototype.slice.call(z,0,Z);L[Z]=z[Z]}}return q.concat(L||Array.prototype.slice.call(z))},r=function(){function q(z,Y,Z){this.name=z,this.version=Y,this.os=Z,this.type="browser"}return q}();var t=function(){function q(z){this.version=z,this.type="node",this.name="node",this.os=process.platform}return q}();var a=function(){function q(z,Y,Z,$){this.name=z,this.version=Y,this.os=Z,this.bot=$,this.type="bot-device"}return q}();var e=function(){function q(){this.type="bot",this.bot=!0,this.name="bot",this.version=null,this.os=null}return q}();var qq=function(){function q(){this.type="react-native",this.name="react-native",this.version=null,this.os=null}return q}();var zq=/alexa|bot|crawl(er|ing)|facebookexternalhit|feedburner|google web preview|nagios|postrank|pingdom|slurp|spider|yahoo!|yandex/,Yq=/(nuhk|curl|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask\ Jeeves\/Teoma|ia_archiver)/,T=3,Zq=[["aol",/AOLShield\/([0-9\._]+)/],["edge",/Edge\/([0-9\._]+)/],["edge-ios",/EdgiOS\/([0-9\._]+)/],["yandexbrowser",/YaBrowser\/([0-9\._]+)/],["kakaotalk",/KAKAOTALK\s([0-9\.]+)/],["samsung",/SamsungBrowser\/([0-9\.]+)/],["silk",/\bSilk\/([0-9._-]+)\b/],["miui",/MiuiBrowser\/([0-9\.]+)$/],["beaker",/BeakerBrowser\/([0-9\.]+)/],["edge-chromium",/EdgA?\/([0-9\.]+)/],["chromium-webview",/(?!Chrom.*OPR)wv\).*Chrom(?:e|ium)\/([0-9\.]+)(:?\s|$)/],["chrome",/(?!Chrom.*OPR)Chrom(?:e|ium)\/([0-9\.]+)(:?\s|$)/],["phantomjs",/PhantomJS\/([0-9\.]+)(:?\s|$)/],["crios",/CriOS\/([0-9\.]+)(:?\s|$)/],["firefox",/Firefox\/([0-9\.]+)(?:\s|$)/],["fxios",/FxiOS\/([0-9\.]+)/],["opera-mini",/Opera Mini.*Version\/([0-9\.]+)/],["opera",/Opera\/([0-9\.]+)(?:\s|$)/],["opera",/OPR\/([0-9\.]+)(:?\s|$)/],["pie",/^Microsoft Pocket Internet Explorer\/(\d+\.\d+)$/],["pie",/^Mozilla\/\d\.\d+\s\(compatible;\s(?:MSP?IE|MSInternet Explorer) (\d+\.\d+);.*Windows CE.*\)$/],["netfront",/^Mozilla\/\d\.\d+.*NetFront\/(\d.\d)/],["ie",/Trident\/7\.0.*rv\:([0-9\.]+).*\).*Gecko$/],["ie",/MSIE\s([0-9\.]+);.*Trident\/[4-7].0/],["ie",/MSIE\s(7\.0)/],["bb10",/BB10;\sTouch.*Version\/([0-9\.]+)/],["android",/Android\s([0-9\.]+)/],["ios",/Version\/([0-9\._]+).*Mobile.*Safari.*/],["safari",/Version\/([0-9\._]+).*Safari/],["facebook",/FB[AS]V\/([0-9\.]+)/],["instagram",/Instagram\s([0-9\.]+)/],["ios-webview",/AppleWebKit\/([0-9\.]+).*Mobile/],["ios-webview",/AppleWebKit\/([0-9\.]+).*Gecko\)$/],["curl",/^curl\/([0-9\.]+)$/],["searchbot",zq]],g=[["iOS",/iP(hone|od|ad)/],["Android OS",/Android/],["BlackBerry OS",/BlackBerry|BB10/],["Windows Mobile",/IEMobile/],["Amazon OS",/Kindle/],["Windows 3.11",/Win16/],["Windows 95",/(Windows 95)|(Win95)|(Windows_95)/],["Windows 98",/(Windows 98)|(Win98)/],["Windows 2000",/(Windows NT 5.0)|(Windows 2000)/],["Windows XP",/(Windows NT 5.1)|(Windows XP)/],["Windows Server 2003",/(Windows NT 5.2)/],["Windows Vista",/(Windows NT 6.0)/],["Windows 7",/(Windows NT 6.1)/],["Windows 8",/(Windows NT 6.2)/],["Windows 8.1",/(Windows NT 6.3)/],["Windows 10",/(Windows NT 10.0)/],["Windows ME",/Windows ME/],["Windows CE",/Windows CE|WinCE|Microsoft Pocket Internet Explorer/],["Open BSD",/OpenBSD/],["Sun OS",/SunOS/],["Chrome OS",/CrOS/],["Linux",/(Linux)|(X11)/],["Mac OS",/(Mac_PowerPC)|(Macintosh)/],["QNX",/QNX/],["BeOS",/BeOS/],["OS/2",/OS\/2/]];function k(q){if(q)return f(q);if(typeof document==="undefined"&&typeof navigator!=="undefined"&&navigator.product==="ReactNative")return new qq;if(typeof navigator!=="undefined")return f(navigator.userAgent);return Qq()}function $q(q){return q!==""&&Zq.reduce(function(z,Y){var Z=Y[0],$=Y[1];if(z)return z;var L=$.exec(q);return!!L&&[Z,L]},!1)}function f(q){var z=$q(q);if(!z)return null;var Y=z[0],Z=z[1];if(Y==="searchbot")return new e;var $=Z[1]&&Z[1].split(".").join("_").split("_").slice(0,3);if($){if($.length<T)$=P(P([],$,!0),Gq(T-$.length),!0)}else $=[];var L=$.join("."),G=Lq(q),X=Yq.exec(q);if(X&&X[1])return new a(Y,L,G,X[1]);return new r(Y,L,G)}function Lq(q){for(var z=0,Y=g.length;z<Y;z++){var Z=g[z],$=Z[0],L=Z[1],G=L.exec(q);if(G)return $}return null}function Qq(){var q=typeof process!=="undefined"&&process.version;return q?new t(process.version.slice(1)):null}function Gq(q){var z=[];for(var Y=0;Y<q;Y++)z.push("0");return z}function M(q,z,Y){let Z=document.createElement(q);if(typeof z!=="undefined"){for(let $ in z)if(O(z,$))if($==="style"){for(let L in z.style)if(O(z.style,L))Z.style[L]=z.style[L]}else Z[$]=z[$]}if(typeof Y!=="undefined")if(typeof Y==="string")Z.textContent=Y;else if(Array.isArray(Y))Z.append(...Y);else Z.appendChild(Y);return Z}function O(q,z){return Object.prototype.hasOwnProperty.call(q,z)}var v=O(Promise,"withResolvers")&&typeof Promise.withResolvers==="function"?Promise.withResolvers.bind(Promise):function(){let q,z;return{promise:new Promise((Z,$)=>(q=Z,z=$)),resolve:q,reject:z}};Symbol.dispose??=Symbol.for("Symbol.dispose");class U{#q=[];[Symbol.dispose](){for(let q of this.#q)q();this.#q.length=0}addCleanup(q){this.#q.push(q)}useEventListener(q,z,Y,Z){q.addEventListener(z,Y,Z),this.addCleanup(()=>q.removeEventListener(z,Y,Z))}}function Xq(q){let z=q.length,Y=q.reduce(($,L)=>$+L)/z,Z=Math.sqrt(q.reduce(($,L)=>$+Math.pow(L-Y,2),0)/(z-1));return{mean:Y,std:Z}}function Hq(q){function z(){if(document.visibilityState==="hidden")alert("Please keep the page visible on the screen during the FPS detection"),location.reload()}document.addEventListener("visibilitychange",z);let Y=0,Z=[],$=q.root.appendChild(M("p"));return new Promise((L)=>{window.requestAnimationFrame(function G(X){if(Y!==0)Z.push(X-Y);Y=X;let H=Z.length/q.framesCount;if($.textContent=`test fps ${Math.floor(H*100)}%`,H<1){window.requestAnimationFrame(G);return}document.removeEventListener("visibilitychange",z);let{mean:J,std:K}=Xq(Z),W=function F(V=1){let S=Z.filter((Q)=>J-K*V<=Q&&Q<=J+K*V);return S.length>0?S:F(V+0.1)}();console.log("detectFPS",{mean:J,std:K,valids:W}),L(W.reduce((F,V)=>F+V)/W.length)})})}async function x(q){let z={root:document.body,framesCount:60,...q},Y=z.root.appendChild(M("div",{style:{textAlign:"center",lineHeight:"100dvh"}})),Z=navigator.userAgent,$=k(Z);if(!$)throw new Error("Cannot detect browser environment");let L={ua:Z,os:$.os,browser:$.name+"/"+$.version,mobile:/Mobi/i.test(Z),"in-app":/wv|in-app/i.test(Z),screen_wh:[window.screen.width,window.screen.height],window_wh:function(){let G=[window.innerWidth,window.innerHeight];return window.addEventListener("resize",()=>{G[0]=window.innerWidth,G[1]=window.innerHeight}),G}(),frame_ms:await Hq({root:Y,framesCount:z.framesCount})};return z.root.removeChild(Y),console.log("env",L),L}window.jsPsychModule??={ParameterType:C};function h(q,z){let Y=z.type;if(typeof Y!=="function"||typeof Y.prototype==="undefined"||typeof Y.info==="undefined")return console.warn("jsPsych trial.type only supports jsPsych class plugins, but got",Y),q.text("jsPsych trial.type only supports jsPsych class plugins");for(let X in Y.info.parameters)if(!O(z,X))z[X]=Y.info.parameters[X].default;let Z=[new j(()=>document.body),new A].reduce((X,H)=>Object.assign(X,y.default(H)),{}),L=new Y({finishTrial(X){if(z.on_finish?.(Object.assign(G.data,z.data,X)),typeof z.post_trial_gap==="number")window.setTimeout(()=>G.close(),z.post_trial_gap);else G.close()},pluginAPI:Z}),G=q.scene(function(X){let H=M("div",{id:"jspsych-content",className:"jspsych-content"});X.root.appendChild(M("div",{className:"jspsych-display-element",style:{height:"100%",width:"100%"}},M("div",{className:"jspsych-content-wrapper"},H))),z.on_start?.(z);let J=z.css_classes;if(typeof J==="string")H.classList.add(J);else if(Array.isArray(J))H.classList.add(...J);return L.trial(H,z,()=>{z.on_load?.()}),()=>{}});return G}class D extends U{app;options;root=M("div");data={start_time:0};update;#q=!0;#z;constructor(q,z,Y={}){super();this.app=q;this.options=Y;this.close(),this.update=z(this),this.addCleanup(()=>this.app.root.removeChild(this.root));let Z=typeof Y.close_on==="undefined"?[]:typeof Y.close_on==="string"?[Y.close_on]:Y.close_on,$=this.close.bind(this);for(let L of Z)this.useEventListener(this.root,L,$)}config(q){return Object.assign(this.options,q),this}close(){if(!this.#q){console.warn("Scene is already closed");return}this.#q=!1,this.root.style.transform="scale(0)",this.#z?.resolve(this.data)}show(...q){if(this.#q)return console.warn("Scene is already shown"),this.data;if(this.#q=!0,this.root.style.transform="scale(1)",this.#z=v(),this.update(...q),typeof this.options.duration!=="undefined"&&this.options.duration<this.app.data.frame_ms)console.warn("Scene duration is shorter than frame_ms, it will show 1 frame");let z=(Y)=>{let Z=Y-this.data.start_time;if(typeof this.options.duration!=="undefined"&&Z>=this.options.duration-this.app.data.frame_ms*1.4){this.close();return}this.options.on_frame?.(Y),window.requestAnimationFrame(z)};return window.requestAnimationFrame((Y)=>{this.data.start_time=Y,z(Y)}),this.#z.promise}}class d extends U{root;data;constructor(q,z){super();this.root=q;this.data=z;if(window.getComputedStyle(document.documentElement).getPropertyValue("--psytask")==="")throw new Error("Please import psytask CSS file in your HTML file");this.useEventListener(window,"beforeunload",(Y)=>{return Y.preventDefault(),Y.returnValue="Leaving the page will discard progress. Are you sure?"}),this.useEventListener(document,"visibilitychange",()=>{if(document.visibilityState==="hidden")alert("Please keep the page visible on the screen during the task running")})}scene(...q){let z=new D(this,...q);return z.root.classList.add("psytask-scene"),this.root.appendChild(z.root),z}text(q,z){return this.scene(function(Y){let Z=M("p",{textContent:q});return Y.root.appendChild(M("div",{style:{textAlign:"center",lineHeight:"100dvh"}},Z)),($)=>{let L={...$};if(L.text)Z.textContent=L.text;if(L.size)Z.style.fontSize=L.size;if(L.color)Z.style.color=L.color}},z)}fixation(q){return this.text("+",q)}blank(q){return this.text("",q)}jsPsych(q){return h(this,q)}}async function Jq(q){let z={root:document.body,framesCount:60,...q};if(!z.root.isConnected)console.warn("Root element is not connected to the document, it will be mounted to document.body"),document.body.appendChild(z.root);return new d(z.root,await x(z))}class E{value=""}class u extends E{keys=[];transform(q){let z="";if(this.keys.length===0)this.keys=Object.keys(q),z=this.keys.reduce((Y,Z)=>Y+(Z.includes(",")?`"${Z}"`:Z)+",","");return z+=this.keys.reduce((Y,Z)=>{let $=q[Z];return Y+((""+$).includes(",")?`"${$}"`:$)+","},` `),this.value+=z,z}final(){return""}}class m extends E{transform(q){let z=(this.value===""?"[":",")+JSON.stringify(q);return this.value+=z,z}final(){return this.value+="]","]"}}class B extends U{filename;static stringifiers={csv:u,json:m};rows=[];#q=!1;stringifier;fileStream;constructor(q=`data-${Date.now()}.csv`,z){super();this.filename=q;let Y=q.match(/\.([^\.]+)$/),Z="csv",$=Y?Y[1]:(console.warn("Please specify the file extension in the filename"),Z);if(z instanceof E)this.stringifier=z;else{let L=Object.keys(B.stringifiers);if(L.includes($))this.stringifier=new B.stringifiers[$];else console.warn(`Please specify a valid file extension: ${L.join(", ")}, but got "${$}". Or, add your DataStringifier class to DataCollector.stringifiers.`),this.stringifier=new B.stringifiers[Z]}this.useEventListener(document,"visibilitychange",()=>{if(document.visibilityState==="hidden"&&!this.fileStream)this.backup()})}async withFileStream(q){if(!q)return this;let z=q.kind==="directory"?await q.getFileHandle(this.filename,{create:!0}):q;if(z.name!==this.filename)console.warn(`File handle name "${z.name}" does not match the collector filename "${this.filename}".`);if(await z.queryPermission({mode:"readwrite"})!=="granted"&&await z.requestPermission({mode:"readwrite"})!=="granted")return console.warn("File permission denied, no file stream will be created"),this;return this.fileStream=await z.createWritable(),this}async add(q){this.rows.push(q);let z=this.stringifier.transform(q);return await this.fileStream?.write(z),z}backup(q=".backup"){let z=URL.createObjectURL(new Blob([this.stringifier.value],{type:"text/plain"})),Y=M("a",{download:this.filename+q,href:z});document.body.appendChild(Y),Y.click(),document.body.removeChild(Y),URL.revokeObjectURL(z)}async save(){if(this.#q){console.warn("Repeated save is not allowed");return}let q=this.stringifier.final();if(this.fileStream)await this.fileStream.write(q),await this.fileStream.close();else this.backup("");this.#q=!0}}class I{options;#q=!1;#z=!1;constructor(q){this.options=q}[Symbol.iterator](){if(this.#z)throw new Error("Please create a new trial iterator, it can only be used once.");return this}next(){if(this.#q)throw new Error("Unexpected call to next() after the iterator is done");this.#z=!0;let q=this.nextValue();if(typeof q==="undefined")return this.#q=!0,{value:void 0,done:!0};return{value:q,done:!1}}}class c extends I{}class Mq extends I{#q=0;constructor(q){super({candidates:[...q.candidates],sampleSize:q.sampleSize??q.candidates.length,replace:q.replace??!0});if(this.options.candidates.length===0){console.warn("No candidates provided, iterator will not yield any values");return}if(!this.options.replace&&this.options.sampleSize>this.options.candidates.length)this.options.sampleSize=this.options.candidates.length,console.warn("Sample size should be <= the number of candidates when not replacing")}nextValue(){if(this.options.candidates.length===0)return;if(this.#q>=this.options.sampleSize)return;this.#q++;let q=Math.floor(Math.random()*this.options.candidates.length),z=this.options.candidates[q];if(!this.options.replace)this.options.candidates.splice(q,1);return z}}class Wq extends c{options;data=[];constructor(q){super(q);this.options=q}nextValue(){let q=this.data.length;if(q===0){let J=this.options.start;return this.data.push({value:J,response:!1,isReversal:!1}),J}let z=this.data.filter((J)=>J.isReversal).length;if(z>=this.options.reversal)return;let{step:Y,down:Z,up:$,max:L,min:G}=this.options,X=this.data.at(-1),H=X.value;if(z===0)H+=X.response?-Y:Y;else{if(q>=Z&&this.data.slice(-Z).every((J)=>J.value===X.value&&J.response===!0))H-=Y;if(q>=$&&this.data.slice(-$).every((J)=>J.value===X.value&&J.response===!1))H+=Y}if(typeof G==="number"&&H<G)H=G;if(typeof L==="number"&&H>L)H=L;return this.data.push({value:H,response:!1,isReversal:!1}),H}response(q){if(this.data.length===0){console.warn("Please iterate first to get a value");return}let z=this.data.at(-1);if(z.response=q,this.data.length>1){let Y=this.data.at(-2);if(q!==Y.response)z.isReversal=!0}}getThreshold(q=this.options.reversal){let z=this.data.filter((L)=>L.isReversal),Y=z.length;if(Y<q)console.warn(`Not enough reversals, only ${Y} found, but requested ${q}`);let Z=z.slice(-q);return Z.reduce((L,G)=>L+G.value,0)/Z.length}}export{M as h,x as detectEnvironment,Jq as createApp,I as TrialIterator,Wq as StairCase,c as ResponsiveTrialIterator,Mq as RandomSampling,m as JSONStringifier,E as DataStringifier,B as DataCollector,u as CSVStringifier};