+1 vote
by (1.2k points)

I've been bashing away at a game and one of the features I'm trying to implement involves running some JavaScript on the passage after it has rendered. I'm working with Sugarcube 2.20.0 and Twine 2.1.3.

The function I've got takes the contents of the passage class div, messes with the text strings (but preserves the HTML tags), and then outputs back into the same div. I've got it running outside Twine in a browser but there it's activated by pressing a button on a page (so the whole not running until the page is rendered thing isn't an issue there).

The general goal for this code is to have it activated for every passage (it'll then read the relevant State.variables variable and run based on that).

What I don't know is how best to accomplish that. I've tried to read the documentation but I'm unable to progress.
It's not clear whether the code should sit in one of the special passages, in a <<script>> block somewhere (a special passage?), the Story JavaScript area, or somewhere else. Or some combination of those things?

I suppose the other thing I need is to know whether this is how I should be reading/writing the passage contents?

document.getElementsByClassName("passage")[0].innerHTML;  

On a normal webpage that gives me the contents of a div but it seems that's not the way to do it in Sugarcube.

Hopefully that's enough to go on for someone to point me in the right direction. I'm learning JavaScript bit by bit and documenting like a lunatic to help myself understand but there's enormous gaps in my knowledge.

1 Answer

+1 vote
by (8.6k points)
edited by

The special passages are almost the right place, but the task objects might be more suitable here - specifically the postrender and postdisplay ones. In case of postrender, you can get the HTML element that will be displayed as the first argument of your function, so you don't even need to search for it in the page.

postrender["text transformation"] = function(content, taskname) {
    /*
     * Your text transformations on content go here;
     * taskname will be "text transformation" in case you ever want
     * to re-use one function in multiple places
     */
};

The alternative (in SugarCube 2.20.0 and above) is the ":passagerender" event, which functions about the same.

jQuery(document).on(':passagerender', function(e) {
    const content = e.content;
    /* Do with the "content" variable as you would in the example above */
});

 

by (1.2k points)

Thanks for the reply!

Are you saying that I should edit my Story's Javascript section (the same place one inserts custom macros, for example) and wrap my function like this:

postrender["text transformation"] = function(content, taskname) {
    function drunkify() {code}
};

?

With content being the passage text passed as a string variable?
If I write to var content will that then affect the passage?

Before posting this reply I gave that a shot and whilst it doesn't throw any errors console.log(content) doesn't provide any output. As far as I can tell the function doesn't run at all.

by (68.6k points)
edited by

It's not running because you're not invoking it.  Also, the content parameter is the HTML object which is the rendered form of the incoming passage.

 

Try something like the following:

function drunkify(content) {
	/* code to drunkify `content` here */
}

postrender["text transformation"] = function(content, taskname) {
	drunkify(content);
};

 

Alternatively, you could simply place the drunkify code in the task function.  Like so:

postrender["text transformation"] = function(content, taskname) {
	/* code to drunkify `content` here */
};

 

EDIT: And if your plan is to modify the .innerHTML property of the DOM object, know that doing so break virtually all interactive elements because you'll be destroying their event handlers.  Searching for and modifying its text nodes would work okay though.

by (1.2k points)

I don't know what a task function is but adding the first block of code you suggested worked perfectly. I figured it was something silly that I was overlooking or not understanding.

For anyone else that comes after this is the solution that worked:

function drunkify(content) {
	/* code to drunkify `content` here */
}

postrender["text transformation"] = function(content, taskname) {
	drunkify(content);
};

The code was placed in the Story Javascript area of Twine rather than a special passage.

I then used content.innerHTML to read and write to the passage.

Edit:

EDIT: And if your plan is to modify the .innerHTML property of the DOM object, know that doing so break virtually all interactive elements because you'll be destroying their event handlers.  Searching for and modifying its text nodes would work okay though.

My code takes the contents of the passage and breaks it apart into text strings and HTML tags. Tags are left alone so as to not break everything. I'm sure I'll find other mistakes in my code as I go but that should at least be possible now that I'm able to make it run yes

by (68.6k points)

I don't know what a task function is but adding the first block of code you suggested worked perfectly.

A task function is simply a function which has been assigned to a property on one of the task objects.  In your case, the function assigned to postrender["text transformation"].

by (8.6k points)

My code takes the contents of the passage and breaks it apart into text strings and HTML tags. Tags are left alone so as to not break everything. I'm sure I'll find other mistakes in my code as I go but that should at least be possible now that I'm able to make it run yes

Depending on how you do it, it might work fine - or it might break all the internal links.

If you're respecting the HTML objects and just rearranging some of them carefully, you should be fine.

if you're literally taking the rendered HTML, changing some of it, then replacing the content's HTML text with yours, your inner links will break. It's possible to fix them, but you should think carefully if you shouldn't be doing the former instead.

But that's for another question, I guess ...

by (1.2k points)

It does seem to have broken the links but I can't see why it has. Comparing the links with the script enabled and disabled shows them to be identical

<a data-passage="gameSetup" class="link-internal" tabindex="0">let's get started.</a>

becomes

<a data-passage="gameSetup" class="link-internal" tabindex="0">let'bs gezt starlted.</a>

What's being affected to break the links?

by (68.6k points)

I told you why in my in my fist comment:

EDIT: And if your plan is to modify the .innerHTML property of the DOM object, know that doing so break virtually all interactive elements because you'll be destroying their event handlers.  Searching for and modifying its text nodes would work okay though.

You cannot treat DOM objects as though they were strings.  Using any of the properties which return a string representation of the object (e.g. .innerHTML, .outerHTML, etc.) will not, cannot, preserve any event listeners which have been bound to the object.  You are, literally, throwing the interactivity away.

All of SugarCube's interactive elements are handled by binding event listeners to the associated DOM objects.

While it's possible to "fix" simple links (e.g. [[let's get started|gameSetup]]), as most of the necessary data is replicated within attributes, there is no way to fix most of the other types of interactive elements.

If you really need to perform your modifications on-the-fly, then you need to traverse the DOM tree properly, modifying the text content only as necessary.

by (1.2k points)

It seems you didn't read my initial post properly either as I'm fairly sure I mentioned the "enormous gaps" in my knowledge wink

It looks like I either need to finally learn what this DOM malarky is all about (not to be confused with the chap from Easy Company that recently left us) or possibly just give the whole thing up as a bad job. It sounds like you think it's not feasible to do what I'd like to do and I trust your knowledge and judgement on the subject.

It was fun learning a bit of JavaScript though!

by (68.6k points)
edited by

I read your post.  I don't see how the situations are comparable.  Even if you didn't understand why I said, "you can't do that" (paraphrased), the understanding that you can't do that should have clear enough.  YMMV.  /shrug

Beyond that.  I never said what you want to do was infeasible, in general (though, it might be), simply that you can't do it in the manner you're attempting.  I haven't offered an example of what you could do because you haven't actually shown us what your drunkify() function looks like.  We can guess that it makes dialog drunken-y, but there's a long haul from that to knowing what it's actually doing.

by (1.2k points)

I read your post.  I don't see how the situations are comparable.  Even if you didn't understand why I said, "you can't do that" (paraphrased), the understanding that you can't do that should have clear enough.  YMMV.  /shrug

Just trying to make a joke. I suspect you'd hate the deliberately jovial way I comment my code!

 I never said what you want to do was infeasible, in general, simply that you can't do it in the manner you're attempting.

From what I can tell what you're saying is in essence that my knowledge of how this whole setup works is hilariously primitive and in order for my script to work I would need to do extensive reading to even begin to understand the underlying concepts, let alone mess with them. 

That's not a criticism - it's useful insight for me. I'm re-evaluating the cost/benefit of the task.

This script was going to be a fun feature for me to mess about with - it's not actually important to my game. I try not to add things to my game that I don't personally understand and in order to understand how to manipulate arbitrary text without breaking the invisible magic (as to me it might as well be!) of the web page the work just isn't worth it. That is to say your demonstration of knowledge shows me that what I want to do is unfeasible given the other elements in the equation.

I'm better off putting that time and energy towards the parts of my game's code that I already understand. Thank you for helping me see that.

by (68.6k points)
edited by

I think I may have snuck an edit by you as you were replying.


No worries, I don't have a problem with levity.  I'll simply note that it can be hard to determine when someone's joking and when they're being an arse when all you have to go by is text.  Smilies used to help, then jerks started using them to be cute, so their use is no longer a good gauge of intent.

Suffice it to say that your attempt at humor slipped right by me, which is probably my fault, as I tend to be fairly literal.  Mea culpa. blush


I won't lie, to correctly do what you want to do is not a beginner topic.  If you wanted to treat it as a learning experience, however, it is doable.

Regardless.  Good luck, and we'll be here if you need help.

by (1.2k points)

In my younger days I was quite the fighter online - these days I try to be a bit more mellow laugh I was only trying to call on you to have mercy because of my feeble understanding. There's no end to what I don't know!

Anyway I didn't bother posting the code because it basically does some letter addition and shuffling at the moment. Pretty simple stuff that might as well be "hello world" to a coder like yourself. If it worked I was going to then play with it and see about adding other fun things like doubling up on some words (so a sentence like "You know what the queue is usually like." might become "You-you know what the queue is usually like.").

It's interesting to learn how these things fit together and I'm having a lot of fun putting my first game together. I wanted to learn to program as a teenager and couldn't but over the last couple of years I've been enjoying it and understanding in a way I didn't back then.

I won't lie, to correctly do what you want to do would is not a beginner topic.  If you want to treat it as a learning experience, however, it is doable.

Good to know that the idea is doable. I might give it a go if I have an obsessive late night. Chances are I'll fail but perhaps with enough attempts I'll get it.

On the plus side the code works and the execution works. It's just the invisible magic that I hadn't accounted for that is the problem. When I started neither the code nor the execution worked!

Is there any kind of primer on what I need to know to get this to work? Something you could recommend as a spot of bedtime reading? smiley

by (8.6k points)

Here's a little trick which might get you going in the right direction. Using jQuery:

  • find all the non-text child nodes of the content
  • add the content one back to that list
  • get all the content (including the child nodes) into one big list
  • filter this list for just the text nodes (nodeType === Node.TEXT_NODE)
  • for each of the text node, run your text replacement on its nodeValue (the actual text)

In code:

postrender["text transformation"] = function(content, taskname) {
    jQuery(content)
        .find("*")
        .addBack()
        .contents()
        .filter(function() { return this.nodeType === Node.TEXT_NODE; })
        .each(function() { this.nodeValue = this.nodeValue.replace(/o/g, "รถ"); });
};

by (1.2k points)

I wrote something using JavaScript eventually. It grabs the nodelist for the main passage and then makes a list of index values of nodes fitting the relevant criteria. It then modifies only the .textContent properties of those whilst leaving the other bits of cleverness intact.

This not only preserves links but also means that the drunkify text transformation only applies to relevant text. Things with other tags (such as dialogue from other characters) remains unaffected. Brilliant!

The special PassageDone passage checks whether the player character's sobriety level is low enough and includes the drunkify script if appropriate. That script looks like this:

<<script>>

$(document).on(':passagedisplay', function (ev) {

-code-

}

<</script>>

 

...