PennController for IBEX › Forums › Support › Unfold text before and after TextInput
- This topic has 1 reply, 2 voices, and was last updated 2 years ago by Jeremy.
-
AuthorPosts
-
November 19, 2022 at 12:12 pm #9722stevenrfoleyParticipant
Hi 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 #9743JeremyKeymasterHi,
There are multiple things at play here, but a crucial one is that
print
andunfold
will 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
span
HTML element surrounded withtextNode
elementsBecause 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 aspan
element. 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.