PennController for IBEX › Forums › Support › Unfold text before and after TextInput
- This topic has 1 reply, 2 voices, and was last updated 2 years, 11 months ago by
Jeremy.
-
AuthorPosts
-
November 19, 2022 at 12:12 pm #9722
stevenrfoleyParticipantHi there,
I’m building a fill-in-the-blank experiment (demo link), am trying to find a good way to force participants to read the entire sentence before entering in some text.
Stimuli look like this: “My client wasn’t in ____ right mind last night.” (Many sentences are longish, and will have linebreaks; also, the blank isn’t necessarily right in the middle of the sentence.)
I’d like the first half of the sentence to scroll, then for the text-input blank to appear, and then for the second half of the sentence to scroll. That I’ve managed to do with the following code, but it puts a linebreak before and after the blank, which I don’t want. I’ve tried the “before()” and “after()” functions, but those don’t seem to take “newText(…).scroll(delay)” arguments.
Template("FITB-master.csv", row => newTrial("fitb", newText("text_before", row.before_blank) .unfold(row.time_before) , newTimer("time_before", row.time_before) .start() .wait() , newTextInput("filledInBlank") .size("6em", "1.5em") .lines(0) .css({ "outline": "none", "resize": "none", "border": "0", "padding": "0", "margin": "0", "margin-left": "1ex", "margin-right": "1ex", "vertical-align": "-.33em", "background-color": "white", "border-bottom": "2px solid black", "display": "inline" }) .cssContainer({"display": "inline", "font-size": "16px"}) // This doesn't do what I want //.before(newText("before_blank", row.before_blank).unfold(row.time_before)) .print() , newText("text_after",row.after_blank) .unfold(row.time_after) , newTimer("time_after", row.time_after) .start() .wait() , newButton("moveOn", "Continue") .right() .print() .wait() ) );Is there an easy fix to this? I’m worried that the length of my stimuli makes the scroll function problematic, generally.
November 29, 2022 at 5:30 pm #9743
JeremyKeymasterHi,
There are multiple things at play here, but a crucial one is that
printandunfoldwill add content to a new line each time, so you won’t be able to get everything on a single line using those commandsTo really have the input box in the same stream as the surrounding text, ideally it needs to be contained in a simple
spanHTML element surrounded withtextNodeelementsBecause you won’t be able to use the PennController command
unfold, you will need to come up with a custom function that does the job of dynamically revealing the content of aspanelement. This is one such function, which you can place at the top of your script for example:function unfold(element,duration) { class TextNode { constructor(original){ this.original = original; this.replacer = document.createElement("SPAN"); this.replacer.style.visibility='hidden'; this.replacer.textContent = this.original.textContent; this.original.replaceWith(this.replacer); } unfold(n) { if (n>=this.original.length) return this.replacer.replaceWith(this.original); [...this.replacer.childNodes].forEach(n=>n.remove()); this.replacer.style.visibility = 'visible'; const vis = document.createTextNode(this.original.textContent.substring(0,n)); const hid = document.createElement("SPAN"); hid.style.visibility = 'hidden'; hid.innerText = this.original.textContent.substring(n); this.replacer.append(vis); this.replacer.append(hid); } } let toUnfold = (function buildArray(el){ const ar = []; if (el.childNodes.length>0) { for (let i=0; i<el.childNodes.length; i++) ar.push(...buildArray(el.childNodes[i],duration,'hold')); } else if (el.nodeName=="#text") ar.push(new TextNode(el)); else if (el.style.visibility!="hidden") { el.style.visibility='hidden'; ar.push(el); } return ar; })(element); let totalChar = 0; toUnfold = toUnfold.map(e=>{ const n = e instanceof TextNode ? e.original.length : 1; totalChar += n; return {element: e, len: n, total: totalChar}; }); const perChar = duration / totalChar; let startTime; return new Promise(r=> { const updateUnfold = timestamp => { if (startTime===undefined) startTime = timestamp; const elapsed = timestamp-startTime, progress = elapsed/perChar; if (elapsed >= duration) { toUnfold.forEach( e=>e.element instanceof TextNode ? e.element.unfold(e.len+1) : e.element.style.visibility='visible' ); r(); } else { let total = 0; toUnfold.find(e=>{ const lastToReveal = progress < e.total; if (e.element instanceof TextNode) e.element.unfold(lastToReveal ? progress-total : progress); else e.element.style.visibility = 'visible'; total += e.len; return lastToReveal; }); window.requestAnimationFrame(updateUnfold); } }; window.requestAnimationFrame(updateUnfold); }); }Then integrating it inside a trial is pretty easy:
newTextInput("filledInBlank") .size("6em", "1.5em") .lines(1) .css({ "outline": "none", "resize": "none", "border": "0", "padding": "0", "margin": "0", "margin-left": "1ex", "margin-right": "1ex", "vertical-align": "-.33em", "background-color": "white", "border-bottom": "2px solid black", "display": "inline" }) .cssContainer({"display": "inline", "font-size": "16px"}) .print() , newFunction( async ()=> { const answer = document.querySelector("textarea.PennController-filledInBlank"); const container = document.createElement("SPAN"); answer.replaceWith(container); const before = document.createTextNode( row.before_blank ); const after = document.createTextNode( row.after_blank ); container.append(before); container.append(answer); container.append(after); await unfold(container, parseInt(row.time_before)+parseInt(row.time_after) ); }).call() , newButton("moveOn", "Continue") .right() .print() .wait()Jeremy
-
AuthorPosts
- You must be logged in to reply to this topic.