Unfold text before and after TextInput

PennController for IBEX Forums Support Unfold text before and after TextInput

Viewing 2 posts - 1 through 2 (of 2 total)
  • Author
    Posts
  • #9722
    stevenrfoley
    Participant

    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.

    #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

Viewing 2 posts - 1 through 2 (of 2 total)
  • You must be logged in to reply to this topic.