DashedSentence in a PennController trial

PennController for IBEX Forums FAQ / Tips DashedSentence in a PennController trial

Viewing 14 posts - 31 through 44 (of 44 total)
  • Author
    Posts
  • #5696
    CFWhitwell
    Participant

    I am looking to present chunks of a sentence in place in a self-paced reading task. The code runs now, but not in the format I want (it is now displaying full sentence with empty blanks; I want in place words/chunks)

    I believe the changes need to be made in the line starting with “let words =” in order to remove the dashes and make the words appear in place, but I’m at a loss for what I need to use.

    This is the code:

    dashed = (sentence, remove) => {
        let words = sentence.split('*'),  blanks = words.map(w=>w.split('').map(c=>'_').join('') );
        let textName = 'dashed'+words.join('');
        // We'll return cmds: the first command consists in creating (and printing) a Text element with dashes
        let cmds = [ newText(textName, blanks.join(' ')).print() .settings.css("font-family","times new roman")]; // times new roman as font
        // We'll go through each word, and add two command blocks per word
        for (let i = 0; i <= words.length; i++)
        cmds = cmds.concat([ newKey('dashed'+i+words[i], " ").log().wait() , // Wait for (and log) a press on Space
        getText(textName).text(blanks.map((w,n)=>(n==i?words[n]:w)).join(' ')) ]); // Show word
        if (remove)  // Remove the text after the last key.wait() is parameter specified
        cmds.push(getText(textName).remove());
        return cmds;
    }
    //Introduction screen with button
    newTrial(
        newText("intro", "These are the instructions. Click to start.")
            .print()
            ,
        newButton("Start")
            .print()
            .wait()
            .remove()
    );
    
    //Experimental task with in place self-paced reading followed by a comprehension question. Sentence and question are pulled from the table. 
    Template(variable =>     
        newTrial(
            newText("test", "Press space bar to read the sentence.")
                .css("color", "red")
                .css("font-syle", "italic")
                .css("line-height", "1.5")
                .print()
                ,
            newText("test", "Press <b>A</b> for true. Press <b>L</b> for false.")
                .css("font-syle", "italic")
                .css("line-height", "1.5")
                .print()
                ,
            ...dashed(variable.Sentence, 
                    'remove')
            ,
            newText("cq", variable.Question)
                .css("line-height", "2.5")
                .print()
            ,
            newKey("AL")
                .log()
                .wait()
        )
    );
    
    • This reply was modified 3 years, 10 months ago by CFWhitwell.
    #5700
    Jeremy
    Keymaster

    Hello,

    You should probably write your sentences in your table using underscores (_) instead of spaces (or instead of wildcard * if this is what you’ve been using) inside chunks, and reserve space characters to delimit the chunks. For example: This_is_the_first_chunk and_this_is_the_second_chunk. Then just inject the native-Ibex DashedSentence controller, like this:

    //Introduction screen with button
    newTrial(
        newText("intro", "These are the instructions. Click to start.")
            .print()
            ,
        newButton("Start")
            .print()
            .wait()
            .remove()
    );
    
    //Experimental task with in place self-paced reading followed by a comprehension question. Sentence and question are pulled from the table. 
    Template(variable =>     
        newTrial(
            newText("test", "Press space bar to read the sentence.")
                .css("color", "red")
                .css("font-syle", "italic")
                .css("line-height", "1.5")
                .print()
            ,
            newText("test", "Press <b>A</b> for true. Press <b>L</b> for false.")
                .css("font-syle", "italic")
                .css("line-height", "1.5")
                .print()
            ,
            newController("DashedSentence", {s: variable.Sentence, display: "in place", hideUnderscores: true})
                .print()
                .log()
                .wait()
                .remove()
            ,
            newText("cq", variable.Question)
                .css("line-height", "2.5")
                .print()
            ,
            newKey("AL")
                .log()
                .wait()
        )
    );

    Jeremy

    #6205
    daniela
    Participant

    Hi Jeremy,

    I’ve had some successful Ibex experiments using the dashed function you wrote for me some months ago, and am now trying to adapt it for cumulative presentation. I’ve been playing with the getText() line in the function but haven’t been able to get it to work. Do you have a fix for this?

    // create dashed function
    dashed = (sentence, remove) => {
        let words = sentence.split('*'),  blanks = words.map(w=>w.split('').map(c=>'_').join('') ); // 'sentence.spilot('*')' = '*' defines the chunk boundaries (in the .csv)
        let textName = 'dashed'+words.join('');
        // We'll return cmds: the first command consists in creating (and printing) a Text element with dashes
        let cmds = [ newText(textName, blanks.join(' ')).print() .settings.css("font-family","courier") .settings.css("font-size", "20px") .settings.center()]; // COURIER as font
        // We'll go through each word, and add two command blocks per word
        for (let i = 0; i <= words.length; i++)
        cmds = cmds.concat([ newKey('dashed'+i+words[i], " ").log().wait() , // Wait for (and log) a press on Space
        getText(textName).text(blanks.map((w,n)=>(n==i?words[n]:w)).join(' ')) ]); // Show word
        if (remove)  // Remove the text after the last key.wait() is parameter specified
        cmds.push(getText(textName).remove());
        return cmds;
    };

    Many thanks!
    Daniela

    #6207
    Jeremy
    Keymaster

    Hi,

    If by “cumulative” you mean that the words that have already been read should not be replaced by underscores when you’re reading the next words, replacing n==i with n<=i should be all you need to do

    Jeremy

    #6216
    daniela
    Participant

    Perfect, thank you!!

    #6435
    aliona
    Participant

    Hi Jeremy,

    I was wondering if there is a way to prevent the dashed function from deviding the chunks and presenting them on different lines? So if there’s not enough space for the whole sentence region on the first line, the presentation starts from the second line. Also right now it happens sometimes that you move your gaze to the next line thinking the next word will apear there, but then another word is added to the previous line:

    Jeremy surely know how to solve
    ——-
    Jeremy surely knows how to solve this
    problem.

    Is there an easy fix for it?

    Thanks a lot!
    Aliona

    Here’s my function:

    // create cumulative function
    cumulative = (sentence, remove) => {
        let words = sentence.split('*'),  blanks = words.map(w=>w.split('').map(c=>'_').join('') ); // 'sentence.split('*')' = '*' defines the chunk boundaries (in the .csv)
        let textName = 'cumulative'+words.join('');
        // We'll return cmds: the first command consists in creating (and printing) a Text element with dashes
        let cmds = [ newText(textName, blanks.join(' '))
        //.print()
        .settings.css("font-family","courier")
        .settings.css("font-size", "20px")
        .cssContainer({"width": "80vw"})
        .print("10vw","50vh")
        //.settings.css("font-size", "0.5em")  
        //.cssContainer({"width": "10vw"})
        ];
    // COURIER as font
    // We'll go through each word, and add two command blocks per word
    for (let i = 0; i <= words.length; i++)
        cmds = cmds.concat([ newKey('cumulative'+i+'_'+words[i], " ").log().wait() , // Wait for (and log) a press on Space; will log "cumulative"+number-of-region_sentence-chunk
                             getText(textName).text(blanks.map((w,n)=>(n<=i?words[n]:w)).join(' ')) ]); // Show word; to make cumulative changed n==i?words to n<=i?words (print words less than or equal to i-region)
    if (remove)  // Remove the text after the last key.wait() is parameter specified
        cmds.push(getText(textName).remove());
    return cmds;
    };
    #6436
    Jeremy
    Keymaster

    Hi Aliona,

    I can’t seem to reproduce the floating-word problem as long as I use the Courier font: because Courier is a monospaced font, the underscores take as much space as the characters they replace, so if there’s room to fit one variant there’s room to fit the other.

    In any case, you can add white-space: nowrap to your Text element’s CSS to prevent it from inserting linebreaks. So you can replace your two .settings.css commands (the .settings prefix is deprecated) with this:

    .css({"font-family":"courier","font-size":"20px","white-space":"nowrap"})

    Note that if your page/screen is not wide enough, the text will overflow to the right, forcing your participant to scroll in order to see the end of the text

    Let me know if you have questions

    Jeremy

    #6437
    daniela
    Participant

    Hi Jeremy,

    I’m jumping in for Aliona (collaborator), as she’s done for the day. Our problem is that we do want to have a line break, but our ‘cumulative’ function seems to not respect the sentence chunk boundaries once the sentence chunks are revealed. In other words, when ‘dashed’, a sentence chunk will not be split across two lines. However, once the chunk is revealed, one word that might be able to fit on the first line jumps up there, and so the sentence chunk is split (and the boundaries are not consistent between ‘dashed’ and revealed).

    So basically, we want to tell ‘cumulative’ to not allow a revealed chunk to be spilt across two lines (I hope that’s clear…?). An alternative could be to force a line break after x number of sentence chunks, although this wouldn’t be ideal, as there’s likely lots of variability between participants’ screen dimensions.

    If it helps, I can e-mail you a link to our current set-up.

    Best,
    Daniela

    #6484
    Jeremy
    Keymaster

    I never followed up on this issue here. The problem was as follows:

    • The cumulative function accepts a string whose chunks are separated by the wildcard (*) character
    • Each of those wildcard-separated chunks can itself contain space characters, which are not chunk separators
    • Before a chunk is revealed, any inner space character (ie. non-separator) is masked as an underscore (_) character, like any other character from the chunk
    • Pre-reveal, if the full sentence is too long, it will be split across multiple lines where the wildcards (displayed as space characters) appear in the string passed to cumulative
    • Once a chunk is revealed, any _ corresponding to an inner space character is now replaced with a space character. As a result, inner and outer space characters are no longer distinguishable and the browser can decide to revise where it inserts line breaks and split the sentence at inner space characters instead of outer space characters, effectively “breaking chunks” visually

    The solution was to replace this line from the cumulative function:

    getText(textName).text(blanks.map((w,n)=>(n<=i?words[n]:w)).join(' ')) ]);

    with this:

    getText(textName).text(blanks.map((w,n)=>(n<=i?words[n]:w).replace(/\s/,"&nbsp;")).join(' ')) ]);

    Jeremy

    #6956
    mschrumpf
    Participant

    I have to reiterate my question from earlier on in this thread for the current version of PennController. How do I edit the DashedSentence controller so that it keeps punctuation and line breaks intact? We want the overall structure of the sentence to remain visible for the participants. So if there is a comma or a stop somewhere in the item, we want them to be able to see it.
    I tried the solution suggested by Jeremy here with the current version of PennController, but I keep getting errors. Scanning through the DashedSentence controller, it also didn’t become clear to me where it is specified which characters are hidden and transformed into dashes and which ones remain visible (blank spaces or asterisks, depending on the settings).

    #6959
    Jeremy
    Keymaster

    Hi,

    You can actually reuse DashedSentence’s CSS styling to rewrite a simplified version of dashed:

    EDIT: I forgot about inserting manual linebreaks with <br>, so the code is slightly more complex now

    dashed = (name,sentence) => [
        newText(name,"").css({display:'flex','flex-direction':'row','flex-wrap':'wrap','line-height':'2em','max-width':'100%'}).print()
        ,
        ...sentence.split(/[\s\t<>]+/).map( (w,i) => (w=="br"?
                newText("").css("width","100vw").print(getText(name))
                :
                newText(name+'-'+i, w.replace(/([^.,?:;!]+)/g,"<span class='DashedSentence-ospan'><span class='DashedSentence-ispan'>$1</span></span>"))
                    .css("margin","0em 0.2em")
                    .print(getText(name))
        ))
        ,
        ...sentence.split(/[\s\t<>]+/).map((w,i)=>(w!="br"?[newKey(i+"-"+w," ").log().wait(),getText(name+'-'+i).text(w)]:null))
    ]

    The important part for you in the code above is [^.,?:;!]: it’s a regular expression that will match any character that is not in the list .,?:;!. All those non-punctuation characters will be wrapped in two span elements: the inner one is invisible and the outer one adds an underline

    Use it like this:

    newTrial(
        dashed("myDashed",  "This is a test. This is the second, longer part of the test!<br>\
                             And now this is a third part, just to test whether it will automatically insert a linebreak")
        ,
        newKey(" ").wait()
        ,
        getText("myDashed").remove()
        ,
        newButton("Finish").print().wait()
    )

    Jeremy

    #6967
    mschrumpf
    Participant

    Hi Jeremy,
    thanks a lot for your help.
    Is it possible to integrate the function into the DashedSentence.js file? As is, it works, but previous words in the sentence remain visible until the end. Plus, the elements in our material consist of two or three words each. I previously used the “hideUnderscores: true” option for this, but I would have to find another way to work around that now. So I would really appreciate it if you could point me in the right direction as to how to include a portion of code that hides everything except punctuation in the DashedSentence controller.
    Best regards

    Matthias

    #6970
    Jeremy
    Keymaster

    Hi Matthias,

    Since what you need is not natively supported by Ibex’s DashedSentence controller, and is not immediately implementable in PennController from a simple string, you must resort to at least some javascript code. The snippet from my previous message defines a function that takes a string and automatically generates a series of PennController commands that reproduce certain behaviors of the native-Ibex DashedSentence controller

    PennController lets you inject some native-Ibex controller inside your trial’s script, but you cannot use (bits of) PennController code to edit a native-Ibex controller. This means that you cannot simply integrate the function above (which, again, simply outputs a simple series of PennController commands) into the DashedSentence controller

    I decided to exclusively rely on PennController for several reasons. First, this is a PennController support space 😉 This means that people here know at least some PennController code, but they do not necessarily know the quite advanced javascript making up the DashedSentence controller’s code. Second, PennController gives you more control over the elements in your trial (technically, you could access each chunk separately, since each is a Text element). Third, I think it’s much easier to understand. This is the output of dashed("myDashed", "Hello world") (for the above definition of dashed):

    newText("myDashed","").css({display:'flex','flex-direction':'row','flex-wrap':'wrap','line-height':'2em','max-width':'100%'}).print()
    ,
    newText("myDashed-0", "<span class='DashedSentence-ospan'><span class='DashedSentence-ispan'>Hello</span></span>"))
        .css("margin","0em 0.2em")
        .print(getText("myDashed"))
    ,
    newText("myDashed-1", "<span class='DashedSentence-ospan'><span class='DashedSentence-ispan'>world</span></span>"))
        .css("margin","0em 0.2em")
        .print(getText("myDashed"))
    ,
    newKey("0-Hello"," ").log().wait(),
    getText("myDashed-0").text("Hello")
    ,
    newKey("0-Hello"," ").log().wait(),
    getText("myDashed-1").text("world")

    This output is reasonably simple PennController code. The tricky part is seeing how exactly the dashed function maps the string onto that series of commands. This part does most of the job ...sentence.split(/[\s\t<>]+/): it splits the string at every space/tab/</> character, and generates commands for each chunk. The code is further obscured by the need to handle <br>s, which include linebreaks (rendered as content-less Text elements which occupy 100% of the available page’s width, visually resulting in linebreaks)

    Replacing the space/tab separator character with * so as to split chunks of words rather than individual words is pretty straightforward, all that’s needed is to replace the regular expression /[\s\t<>]+/ with /[*<>]+/. Masking the previous word again when revealing the next word requires some slight rearrangement, so that the function outputs “reveal word; wait for keypress; hide word” for each chunk instead of just “wait for keypress; reveal word.” as it does now.

    So here’s what you get (I also made it use hyphens instead of underscores after I went back to your previous messages):

    dashed = (name,sentence) => [
        newText(name,"").css({display:'flex','flex-direction':'row','flex-wrap':'wrap','line-height':'2em','max-width':'100%'}).print()
        ,
        ...sentence.split(/[*<>]+/).map( (w,i) => (w=="br"?
                newText("").css("width","100vw").print(getText(name))
                :
                newText(name+'-'+i, w.replace(/([^.,?:;!\s])/g,'-'))
                    .css({margin:"0em 0.2em",'font-family':'monospace',"font-size":"large"})
                    .print(getText(name))
        ))
        ,
        newKey(name+'-start', " ").log().wait() // first keypress, to reveal first chunk
        ,
        ...sentence.split(/[*<>]+/).map((w,i)=>(w!="br"?[
            getText(name+'-'+i).text(w) // reveal chunk
            ,
            newKey(i+"-"+w," ").log().wait() // wait for keypress
            ,
            getText(name+'-'+i).text(w.replace(/([^.,?:;!\s])/g,'-')) // hide chunk
        ]:null))
    ]
    
    newTrial(
        dashed("myDashed",  "This is a test.*This is the second,*longer part of the test!*"+
                            "<br>And now*this is a third part,*just to test whether*it will automatically*insert a linebreak")
        ,
        getText("myDashed").remove()
        ,
        newButton("Finish").print().wait()
    )

    Here’s a live example: https://farm.pcibex.net/r/frrjaU/

    Let me know if you have any questions

    Jeremy

    #6993
    mschrumpf
    Participant

    Hello Jeremy,
    thank you very much for your elaborate answer.
    I implemented the functions into my experiment and it works like a charm.
    The only change I made was changing the hidden text into gray underlines, just like they would in the native controller. My bosses are going back and forth on that one 😉

    Matthias

Viewing 14 posts - 31 through 44 (of 44 total)
  • You must be logged in to reply to this topic.