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