psytask
Version:
JavaScript Framework for Psychology tasks
4 lines (3 loc) • 5.32 kB
JavaScript
import{createTimer as z,EventEmitter as O,Scene as L}from"@psytask/core";export*from"@psytask/core";const p=n=>{throw Error(n)},y=Object,k=(n,e)=>y.assign(n,e),g=document,w=(n,e=g.body)=>e.appendChild(n),R=new Proxy({},{get:(n,e)=>t=>k(g.createElement(e),t)}),T=(n,e,t)=>n<e?e:n>t?t:n,C=n=>Array.isArray(n)?n:[n],{div:b,a:I,style:M}=R,N=n=>y.entries(n).reduce((e,[t,s])=>e+`${t}:${s};`,""),_=(n,e,t,s)=>(n.addEventListener(e,t,s),()=>n.removeEventListener(e,t,s)),x=n=>_(g,"visibilitychange",()=>g.hidden&&n()),A=(n,e)=>new Proxy(n,{get:(t,s)=>t[s]??e[s]}),D=async n=>{const e=w(b({className:"psytask-scene psytask-center"}),n.root),t=x(()=>(alert(n.leave_alert),history.go())),s=await z(o=>{const r=o.length/(n.frames_count+1);return e.innerText=`Detecting FPS... ${(r*100).toFixed(0)}%`,r>=1}).start();return e.remove(),t(),s.map((o,r,c)=>r>0?o-c[r-1]:0).slice(1)},P=n=>{if(n==null)return"";const e=typeof n=="object"?JSON.stringify(n):n+"";return/[,"\n\r]/.test(e)?`"${e.replace(/"/g,'""')}"`:e},$={csv:{header:n=>y.keys(n).reduce((e,t,s)=>e+(s?",":"")+P(t),""),body:n=>y.values(n).reduce((e,t,s)=>e+(s?",":`
`)+P(t),""),footer:()=>""},json:{header:()=>"[",body:(n,e)=>(e.length?",":"")+JSON.stringify(n),footer:()=>"]"}};class j extends O{constructor(e=`data-${Date.now()}.csv`,t){super(),this.filename=e;const s=e.match(/\.([^.]+)$/),o=s?s[1]:p(`Can't detect extension from "${e}".`);if(t?.serializer)this.#e=t.serializer;else{const r=y.keys($);this.#e=r.includes(o)?$[o]:p(`Unsupported file extension: "${o}", please use one of: ${r.join(", ")}.
Or add custom Serializer to Collector.serializers.`)}(t?.backup_on_leave??!0)&&this.on("dispose",x(()=>this.download(`.${Date.now()}.bak`)))}static serializers=$;rows=[];#e;#t="";add(e){this.emit("add",e);const{rows:t}=this,s=(this.#t?"":this.#e.header(e,t))+this.#e.body(e,t);return t.push(e),this.emit("chunk",s).#t+=s}final(){const e=this.#t?this.#e.footer(this.rows):"";return this.emit("chunk",e).#t+e}download(e=""){const t=this.final();if(!t)return;const s=URL.createObjectURL(new Blob([t],{type:"text/plain"})),o=w(I({download:this.filename+e,href:s}));o.click(),URL.revokeObjectURL(s),o.remove()}}w(M(),g.head).innerText=".psytask-scene{all:unset;position:fixed;inset:0;overflow:hidden;}.psytask-center{display:flex;flex-direction:column;align-items:center;justify-content:center;white-space:pre-wrap;height:100%;}";const U=["left","middle","right"],F={key:"keydown",mouse:"mousedown"};class E extends O{constructor(e,t){super(),this.root=e,this.data=t,this.on("dispose",()=>e.remove())}collector(...e){return new j(...e).on("add",t=>k(t,this.data))}scene(...[e,t]){const s=a=>t.duration!=null&&a[a.length-1]-a[0]>t.duration-this.data.frame_ms*1.5,o={root:w(b({className:"psytask-scene",oncontextmenu:a=>a.preventDefault()}),this.root),timer:()=>z(s),...t},r=new L(e,o).on("close",()=>y.keys(t).map(a=>t[a]=o[a])),c=()=>r.close();return k(r,{config(a){return k(t,a),r}}).on("show",()=>{if(t.close_on==null)return;const a=C(t.close_on),u=new Set(a);let h=0,f=0;const d=a.map(i=>{const l=F[i.split(":")[0]]??i;return l==="keydown"?!h++&&_(o.root,l,m=>{(u.has(l)||u.has(`key:${m.key}`))&&c()}):l==="mousedown"?!f++&&_(o.root,l,m=>{(u.has(l)||u.has(`mouse:${U[m.button]??"unknown"}`))&&c()}):_(o.root,l,c)});r.once("close",()=>d.map(i=>i&&i()))})}}const Q=async({root:n=w(b()),alert_on_leave:e=!0,i18n:t={leave_alert_on_fps:"Please DON'T leave the page during the FPS detection!",leave_alert_on_task:"Please DON'T leave the page during the task!",beforeunload_alert:"Your progress will be lost. Are you sure?"},frames_count:s=10,frame_calcer:o=r=>{const c=[...r].sort((i,l)=>i-l),a=c.length/4,u=c[Math.floor(a)],h=c[Math.floor(a*3)],f=r.filter(i=>u<=i&&i<=h),d=f.reduce((i,l)=>i+l)/f.length;return console.info("Detect fps",d,{durations:r,Q1:u,Q3:h,valid_durations:f}),d}}={})=>{const r={frame_ms:o(await D({root:n,leave_alert:t.leave_alert_on_fps,frames_count:s})),leave_count:0},c=[x(()=>(++r.leave_count,e&&alert(t.leave_alert_on_task))),_(window,"beforeunload",a=>e&&(a.preventDefault(),a.returnValue=t.beforeunload_alert))];return new E(n,r).on("dispose",()=>c.map(a=>a()))},S=n=>(...e)=>{let t,s,o=0;const r=n(...e);return{[Symbol.iterator]:()=>({next(){o&&p("Iterator already done");const c=r.next(t);return c.done&&(s=c.value,o=1),c}}),response(c){t=c},get data(){return o||p("Iterator not done yet"),s}}},B=S(function*({candidates:n,sample:e=n.length,replace:t=!0}){const s=[...n],o=s.length;for(!t&&e>o&&p(`Sample size should be <= ${o} without replacement`);s.length&&e--;){const r=Math.floor(Math.random()*s.length);yield s[r],t||s.splice(r,1)}}),J=S(function*({start:n,step:e,down:t,up:s,reversals:o,trials:r=1/0,max:c=1/0,min:a=-1/0}){const u=[];for(;;){const h=u.length,f=u.filter(m=>m.reversal).length;if(f>=o||h>=r)break;let d;const i=u[h-1];if(!i)d=n;else{const m=d=i.value;f?(h>=t&&u.slice(-t).every(v=>v.value===m&&v.response)&&(d-=e),h>=s&&u.slice(-s).every(v=>v.value===m&&!v.response)&&(d+=e)):d+=i.response?-e:e}d=T(d,a,c);const l=yield d;typeof l!="boolean"&&p("StairCase iterator requires boolean response"),u.push({value:d,response:l,reversal:(i?.response??l)!==l})}return u});export{E as App,j as Collector,B as RandomSampling,J as StairCase,Q as createApp,S as createIterableBuilder,N as css,A as defaultProps,D as detectFPS,_ as on};