Reply To: Unfold text before and after TextInput

PennController for IBEX Forums Support Unfold text before and after TextInput Reply To: Unfold text before and after TextInput

#9743
Jeremy
Keymaster

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