+1 vote
by (1.3k points)
edited by

So, I think I'll be having quite a long passage, the more I add to it. And it would be helpful if I could direct the user to a specific point in that passage when something new happens there.

I had thought to try something like:

[[link|AboutOthers#sistersFriends]]

But that's not right. Maybe it's not even possible to do this. What think? At the destination passage, I do have:

<a id="sistersFriends">@@.castTitle;~ your sister's friends ~@@</a>

So, the part I want to jump to is uniquely identified. Anyone know how I could do this kind of thing?

===

Actually, you know what? I don't like the fact that the titles are now clickable links. As demonstrated here:

https://ibb.co/d9m8um

So, how do I do this without it looking retarded?

===

So, I went ahead and changed it to this:

<span id="sistersFriends">@@.castTitle;~ your sister's friends ~@@</span>

So, now I just need to find out how to jump to it. :P Any ideas?

2 Answers

+1 vote
by (154k points)
edited by
 
Best answer

You could try using the Javascript Element.scrollIntoView() function (*) within a SugarCube postdisplay event handler.

Place the following Javascript example within the Story Javascript area of your story, it checks for the existence of a story variable named anchor, and if found it tries to locate an element with and ID of the variable value, and if found tries to scroll to that element. The anchor variable is deleted if it existed.

postdisplay["Scroll to anchor"] = function (taskName) {
	if (State.variables.hasOwnProperty('anchor')) {
		var el = document.getElementById(State.variables['anchor']);
		if (el !== null) {
			el.scrollIntoView();
		}
		delete State.variables['anchor'];
	}	
};

... you could then use a Setter Link (Link with Setter) like the following to activate the above handler.

[[Link Text|Target Passage Name][$anchor to "abc"]]

(*) web-browser compatibility table states function is not supported by Internet Explorer Phone.

by (1.3k points)
Nice, this works exactly how I pictured it. I did have to enclose it within <<script>>...<</script>>. But, after that it worked great.

I don't see it scrolling down. I just appears to display it at the point of the element. But, that's neither here nor there.

I think I'll just go with this solution, until I transition over to that other method I had in mind.

Oh, I did have a question. So... I put that postdisplay script into each and every passage that has that kind of link? Could I instead give passages a special tag that then called this script when transitioning?

If I did have that, where would this postdisplay script live? In the javascript place?
by (154k points)

I was not clear enough about where the Javascript based postdisplay event handler code should be placed within your story, I have changed the original answer so that it now explains that it needs to be placed within your story's Story Javascript area.

In my defence I did include a link to the relevant SugarCube documentation.

by (1.3k points)
Oh right. That does seem pretty efficient. Does it run after every page render? If I did have it embedded within individual pages, would it then only run at the end of that page? Or do postdisplay functions run regardless?
by (154k points)

Does it run after every page render? ... Or do postdisplay functions run regardless?

Did you read the Overview section of the SugarCube 2 Passage Events & Task Objects documentation I linked to in the original answer?

Links in answers are generally there for a reason and that reason is generally so that the person asking a question can learn more about the components that makeup a supplied answer, especially if the further information is too verbose or generic to cut-and-paste into the contents of the answer.

If I did have it embedded within individual pages.

In this use-case I would use whats generally known as a single use task handler, which is one that only activates a single time. The structure of one of these handlers generally looks something like the following.

<<script>>
postdisplay["Some Task Name"] = function (taskName) {

	/* Delete this handler so that it is never triggered again. */
	delete postdisplay[taskName];

	/* The JavaScript code you want this handler to run. */
};
<</script>>

... this type of handler can also be used for task other than postdisplay.

by (1.3k points)
edited by

OK, fair enough. I don't exactly like reading technical docs.

But, I had a look at them, and have decided to move to passage events, since that's the new hotness.

So, I have this in the passage:

<<script>>
	/* Execute the handler function exactly once. */
	$(document).one(':passagedisplay', function (ev) {
		if (State.variables.hasOwnProperty('anchor')) {
			var el = document.getElementById(State.variables['anchor']);
			if (el !== null) {
				el.scrollIntoView();
			}
			delete State.variables['anchor'];
		}
	});
<</script>>

But, it doesn't work. This /doesn't work/.

But, if I use the non-single use version, it works. This one:

/* Execute the handler function each time the event triggers. */
$(document).on(':passagedisplay', function (ev) {
    /* JavaScript code */
});

So, it's looking like I can't use it as I need it. I just have to set it off and let it run in the background.

It's the same with your single use task handler suggestion, greyelf. I tried that one, before moving across, and it doesn't work. I'm not sure why it doesn't work. But it doesn't.

So, anyone work out why I can't have a single-use solution to this? Is there an obvious reason, perhaps?

by (1.3k points)

Oh, but I have to mention that the other passage event I converted does work. This one:

<<script>>
	/* Execute the handler function exactly once. */
	$(document).one(':passagerender', function (ev) {
		$(ev.content).find("#set-button button").prop("disabled", true);
	});
<</script>>

So, something in the way code we're using is maybe screwing things up? But what?

by (1.3k points)
edited by

By the way. I tried to change:

document.getElementById(State.variables['anchor']);

to

$(ev.content).getElementById(State.variables['anchor']);

So that I'd be using the passed in data. But, no joy. I don't really understand what ev.content is. But most of the examples use it, so I figured... But, no. No luck at all.

===

Oh, and only now do I see that I'm attempting to use a combo of javascript and jquery. I guess... I need to convert it fully into jquery.

===

Nope, I can't figure it out. So, Imma just go back to the original.

by (154k points)
edited by

I don't exactly like reading technical docs

If you want to develop a game with non-trivial functionality then you better get used to reading documentation.

A couple of things:

1. As explained in the documentation you need to be using at least v2.20.0 of SugarCube 2 for the new events to work, so you will either need to be using the latest version of Twine 2 or have manually updated the version of SugarCube 2 yourself.

2. Putting the $(document).on(....) versions of the new event handlers within your Story Javascript area does work, I just tested it on a new project.

$(document).on(':passagedisplay', function (ev) {
	if (State.variables.hasOwnProperty('anchor')) {
		var el = document.getElementById(State.variables['anchor']);
		if (el !== null) {
			el.scrollIntoView();
		}
		delete State.variables['anchor'];
	}	
});

3. Putting the $(document).one(....) versions of the new event handlers within a <<script>> macro within the Passage(s) containing the ID'ed anchor elements does work, I tested this as well.

Some long block of text.

<a id="abc" />

Some other block of text.

<<script>>
$(document).one(':passagedisplay', function (ev) {
	if (State.variables.hasOwnProperty('anchor')) {
		var el = document.getElementById(State.variables['anchor']);
		if (el !== null) {
			el.scrollIntoView();
		}
		delete State.variables['anchor'];
	}	
});
<</script>>

4. The jQuery one() function documentation explains about it's parameters, and the Event object that is passed to the supplied callback function.

5. An element generally needs to exist within the current page's Document Object Model structure before you can scroll to it, this is why I suggested using a "post display" related task (or event)

edit:
6. The getElementById() function is a DOM function, not a jQuery one and as such should be generally replace with the jQuery find() function when trying to locate an element relative to an existing context. In the use-case you listed you could do something like the following (untested) if you wanted to use jQuery functionality to locale the ID'ed anchor element.

val el = $("#" + State.variables['anchor']);

 

by (1.3k points)

Well, thanks for your help. But, sometimes it makes sense to test things a little more carefully before dismissing them outright.

Steps for the experiment:

1. click link

(you'll see the result)

2. click back

3. click start2

4. click link

(you'll see the result)

What think? Did you notice anything? So, code:

:: start
[[link1|dest][$anchor = "abc"]]

[[start2]]

<<script>>
$(document).one(':passagedisplay', function (ev) {
	if (State.variables.hasOwnProperty('anchor')) {
		var el = document.getElementById(State.variables['anchor']);
		if (el !== null) {
			el.scrollIntoView();
		}
		delete State.variables['anchor'];
	}	
});
<</script>>\

:: start2
[[link1|dest][$anchor = "abc"]]

<<script>>
$(document).on(':passagedisplay', function (ev) {
	if (State.variables.hasOwnProperty('anchor')) {
		var el = document.getElementById(State.variables['anchor']);
		if (el !== null) {
			el.scrollIntoView();
		}
		delete State.variables['anchor'];
	}	
});
<</script>>\

:: dest
Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.

<a id="abc" />

Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.
Some long block of text.

I'm not sure you can actually copy that code from there. It seems to be an image or something. So, I might upload the html again. To make it easier to confirm.

Anyway, I'm not sure why it doesn't work. But it doesn't.

testtesttest.html

As for all the other info... thanks. Every bit helps. I don't intend to sit down and become a specialist in twine/sugarcube. I just want to write. A game. I just come here when something stops me from doing that. I like writing. That's what I do.

by (154k points)

There are a number of issues with your last example.

1. You are using both the singe-use version of the Event handler $(document).one( ... ) in your start Passage and the multiple-use version of the Event handler $(document).on( ... ) in your start2 Passage, and you should only be using one of them.
The multiple-use version would normally be setup within your story's Story Javascript area, and the single-use version would be setup each time you need it.

I strongly suggest you use the multiple-use version that is setup within your Story Javascript area..

2. You are incorrectly setting up your singe-use Event handler within the Passage containing the markup based link (your start passage), and I stated in point 3 of my previous comment that you need to setup that type of Event handler within the Passage containing the ID'ed HTML anchor element (your dest passage)

The need to setup single-use Event handlers within each & every possible Target Passage you want the scroll-to behaviour in is one of the reasons why I originally suggested using the Task version that is setup with your Story Javascript area, and why I then suggested using the Event version that is setup in the same area.

by (1.3k points)
I see! I didn't have a firm grasp of the overall layout. The mechanism. But, now that it's explained, I understand.

As for what version of it I use... I tend to favor the one that doesn't get automatically called on each and every passage transition. But, since I really have no idea what the overall efficiency of the sugarcube header is, I can't say that it's even a worthwhile pursuit. It probably isn't.

But, right now, the only destination is one passage. The idea being that it'll exponentially expand as the story progresses. Hence the idea for a direct link to the sections as they update.

I'm likely going to be moving away from that super massive passage, as I go on though, so it's maybe a moot point.

I think this answers everything. So, I'm happy to just leave it here. It works. Works great. Thanks.
by (1.2k points)
Thank you for putting me onto scrollIntoView()! It allowed me to solve an issue I'd been putting off fixing with just a few lines of code! Cheers!

(The issue in question being how to best append text messages to a virtual phone interface without the player needing to scroll down to see the latest message as it was appended.)
0 votes
by (61.6k points)
edited by
<a id="sistersFriends">@@.castTitle;~ your sister's friends ~@@</a>

 You kind of did this backwards.  I think you wanted something like:

<a href="#sistersFriends">Link</a>

@@.castTitle;#sistersFriends;~ your sister's friends ~@@

The above creates a link that will bring the #sistersFriends element into view.

So, I went ahead and changed it to this:

<span id="sistersFriends">@@.castTitle;~ your sister's friends ~@@</span>

So, now I just need to find out how to jump to it. :P Any ideas?

Using the span will also work, but this is completely redundant:

THIS CODE:
<span id="sistersFriends">@@.castTitle;~ your sister's friends ~@@</span>

IS PARSED INTO:
<span id="sistersFriends"><span class='castTitle'>~ your sister's friends ~</span></span>

THIS CODE:
@@.castTitle;#sistersFriends;~ your sister's friends ~@@

IS PARSED INTO:
<span id="sistersFriends" class='castTitle'>~ your sister's friends ~</span>

You can use either the custom style markup (i.e. @@ ... @@) or the equivalent HTML, but generally you'll want stick to one or the other. And you will want to avoid making more elements than you need.  Your code is generating two elements instead of one, and while it may not seem like a huge deal, it is wasteful.

Anyway, this solution is a bit rough; it'll show up as an external link and it will snap the window to the correct place instantly.  Here's a slightly more professional-looking approach:

In story JavaScript:

setup.scrollToID = function (id, time) {
    if (!id || typeof id !== 'string' || id[0] !== '#') {
        console.error('scrollToID() -> ' + id + ' is not a valid selector.');
        return;
    }
    
    time = Number(time);
    if (Number.isNaN(time) || time < 0) {
        time = 500;
    }
    
    $('html, body').animate({
        scrollTop: $(id).offset().top
    }, time);
    
};

Then, in a widget-tagged passage:

<<widget 'linkToSel'>>\
    <<link $args[0]>>
        <<run setup.scrollToID($args[1], $args[2])>>
    <</link>>\
<</widget>>

And finally, to use it:

<<linkToSel 'link text' '#selector' time>>

    * 'link text' - the text of the link
    * '#selector' - a selector, must be an ID
    * time - optional, must be in milliseconds, determines length of animation, default 500ms

/% using your example %/

<<linkToSel 'Click here.' '#sistersFriends'>>

[...]

@@.castTitle;#sistersFriends;~ your sister's friends ~@@

Clicking a link generated by this widget should scroll the part of the page you want into view smoothly.

Edit. 

See below. 

by (64.2k points)

I think you wanted something like:

<a href="#sistersFriends">Link</a>

@@.castTitle#sistersFriends;~ your sister's friends ~@@

The above creates a link that will bring the #sistersFriends element into view.

That use of the custom styles markup is invalid.  You cannot, currently, conjoin IDs and classes, so it needs to be something like the following (with a semi-colon in between):

@@.castTitle;#sistersFriends;~ your sister's friends ~@@

/* OR */

@@#sistersFriends;.castTitle;~ your sister's friends ~@@

 

by (1.3k points)

Hmm, thanks for all the help, Chapel.

But, I'm encountering some problems. I started off putting it all in my on-going project. But, encountered the error.

So, I made a very simple project with only this code in it. And I encounter the same problem.

If fact, neither of these methods seems to work. 

https://ibb.co/fbCUH6

If I publish it, and use an external browser, the first method does seem to want to go to: TestingTesting.html#sistersFriends

But, it doesn't seem to move to the passage. It just stays on the starting page.

Oh, and the picture that shows the error message is from using the second method.

If you wanna look at the code, here's a compiled version: testtesting.html

I could supply the project code too, but I'm not sure how you'd import that or stuff. But, really it is only the code that you already shared here.

So... any good at debugging? :P

by (61.6k points)

But, it doesn't seem to move to the passage. It just stays on the starting page.

Currently, both methods I provided require the links and the content being linked to to be on the same passage, which seemed to be what you were asking for. While a link that navigates passages and then adjusts the scroll is possible, it's a bit strange. 

At any rate, if it's for some reason not possible to have the links in the same passage as the content, I can try to work something out for you, but not until I get back to my computer later on. Someone else might be able to help in the mean time. 

by (1.3k points)
Yeah, thanks for the offer.

I did think of how I could alternatively structure the content. I'm only really starting out writing my game, so a lot of what I'm doing now could change in the future. And things that I haven't even thought about might be required down the track.

So, right now, I'm thinking to have a link (from the sidebar) that takes the user to a list of cast. The cast page will be updated when characters are introduced. Filled that way. From there, I'll have links to the sections I'd thought to use the current question stuff with. It probably won't be needed though, since each write-up will be on its own passage.

In the long term, I think this would be a better approach for me to take in any case. Because the sections themselves will work a bit like a diary. Something in the story happens, and thoughts about it are later revealed  in this "diary section". I had thought to copy over the entries, to keep things from blowing out, but now I see that I could keep everything, so the user can flip back and read the entries in sequence.

I haven't thought of how to do that, but I've seen webpages that do similar. A navigation bar up top that shows links to pages. And then the page with the content underneath that. So... I think I could probably just go with that.

Then, when I have a link in the story that takes them to that updated entry, it'll just be a simple passage transition, like [(link)|sistersFriends03]. I'm not sure how my proposed navigation bar works with that. But, I'll have a play and maybe ask another question if I can't work it out.

But, thanks a lot Chapel. I really appreciate your solution to this. I'll refer back to it if I should have a situation with link and content on the same page. You obviously are pretty talented with this stuff. :P
by (100 points)
I have been looking for a solution to this as well, specifically to create a glossary for the player. Ideally, the player runs into a word he doesn't recognize and clicks the glossary, and it links directly to the relevant entry in the Glossary passage.
by (61.6k points)
The code @greyelf posted should work for that.
Welcome to Twine Q&A, where you can ask questions and receive answers from other members of the community.

You can also find hints and information on Twine on the official wiki and the old forums archive.

See a spam question? Flag it instead of downvoting. A question flagged enough times will automatically be hidden while moderators review it.
...