PennController for IBEX › Forums › FAQ / Tips › DashedSentence in a PennController trial
Tagged: dashe display, DashedSentence, linebreaking, newController
- This topic has 44 replies, 10 voices, and was last updated 1 month, 1 week ago by
Jingqi.
-
AuthorPosts
-
June 22, 2020 at 1:18 pm #5696
CFWhitwell
ParticipantI 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 4 years, 2 months ago by
CFWhitwell.
June 22, 2020 at 2:18 pm #5700Jeremy
KeymasterHello,
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
October 16, 2020 at 5:31 am #6205daniela
ParticipantHi 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!
DanielaOctober 16, 2020 at 3:43 pm #6207Jeremy
KeymasterHi,
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
October 19, 2020 at 3:17 am #6216daniela
ParticipantPerfect, thank you!!
December 8, 2020 at 11:10 am #6435aliona
ParticipantHi 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!
AlionaHere’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; };
December 8, 2020 at 12:48 pm #6436Jeremy
KeymasterHi 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
December 8, 2020 at 1:01 pm #6437daniela
ParticipantHi 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,
DanielaJanuary 4, 2021 at 12:47 pm #6484Jeremy
KeymasterI 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/," ")).join(' ')) ]);
Jeremy
May 13, 2021 at 8:39 am #6956mschrumpf
ParticipantI 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).May 13, 2021 at 2:37 pm #6959Jeremy
KeymasterHi,
You can actually reuse DashedSentence’s CSS styling to rewrite
a simplified version ofdashed
:EDIT: I forgot about inserting manual linebreaks with
<br>
, so the code is slightly more complex nowdashed = (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 twospan
elements: the inner one is invisible and the outer one adds an underlineUse 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
May 17, 2021 at 6:42 am #6967mschrumpf
ParticipantHi 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 regardsMatthias
May 17, 2021 at 5:15 pm #6970Jeremy
KeymasterHi 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 ofdashed
):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
May 26, 2021 at 6:04 am #6993mschrumpf
ParticipantHello 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
July 25, 2024 at 3:41 am #11035Jingqi
ParticipantHello Jeremy,
I was trying to have a chunk-by-chunk SPR experiment in Chinese. I set manual line breaks <br> for each item and have dashes for replacement for each chunk. It works fine on most browsers but on some browsers I ran into the display issue. Since the Chinese character takes up two bytes so I used two underscores __ in my code for replacement. On my own browser, the two underscores __ will be displayed as one gray lines, which is good. Yet on some browsers the lines will be displayed as separate underscores. Can you possibly tell me what I could do to improve this?
I used the function as this:
// Dashed function to allow for manual line breaks
dashed = (name,sentence) => [
newText(name,””).css({display:’flex’,’flex-direction’:’row’,’flex-wrap’:’wrap’,’line-height’:’4em’,’max-width’:’70em’,’min-width’:’70em’}).print()
,
…sentence.split(/[*<>]+/).map( (w,i) => (w==”br”?
newText(“”).css(“width”,”100vw”).print(getText(name))
:
newText(name+’-‘+i, w.replace(/./g,’__’))
.css({margin:”0em 0.2em”,’font-family’:’monospace’,”font-size”:”20pt”,”color”:”gray”})
.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()
,
…sentence.split(/[*<>]+/).map((w,i)=>(w!=”br”?[
getText(name+’-‘+i).text(w).css({“color”:”black”})
,
newKey(i+”-“+w,” “).log().wait()
,
getText(name+’-‘+i).text(w.replace(/./g,’__’)).css({“color”:”gray”})
]:null))
]In my other experiment, I used : newController(“DashedSentence”, {s: row.Sentence, mode:”self-paced reading”, display: “dashed”, blankText: “#”,showAhead: true, showBehind: true}) . This function seems to align best with the lines and words. But that experiment doesn’t require line breaking and the item is not very long.
-
This reply was modified 4 years, 2 months ago by
-
AuthorPosts
- You must be logged in to reply to this topic.