PennController for IBEX › Forums › Support › Unfold text before and after TextInput › Reply To: Unfold text before and after TextInput
Hi,
There are multiple things at play here, but a crucial one is that print
and unfold
will add content to a new line each time, so you won’t be able to get everything on a single line using those commands
To 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 with textNode
elements
Because 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 a span
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