note: I strongly suggest reviewing the source code of SugarCube’s own <<link>>
macro as a guild to building your own ‘TOC’ macro, as it contains all the information & functionality for creating the ‘links’ you’ll need.
You didn’t supply an example of the Passage Names that will be included in the TOC, or how to distinguish those Passages from all the other Passages of the project.
A good place to start is defining the HTML element structure for your TOC, it will likely look something like the following TWEE Notation based example…
:: Start
<ul class="macro-toc">
<li><a data-passage="First" class="link-internal link-visited">First</a></li>
<li><a data-passage="Second" class="link-internal">Second</a></li>
<li><a data-passage="Third" class="link-internal">Third</a></li>
</ul>
:: A non TOC passage
A non TOC passage
:: First
The First passage
:: Second
The Second passage
:: Another non TOC passage
Another non TOC passage
:: Third
The Third passage
If you Play the project you should see a basic unordered list that contains links to the First, Second, and Third passages.
Next you would create a Macro definition that constructed the above HTML structure, it would look something like the following JavaScript, and it would be placed within the Story JavaScript area of your project…
Macro.add('toc', {
isAsync : true,
handler() {
/* Add argument checking if required. */
/* Obtain list of Passages to be shown in TOC. */
/* TODO: how to distingish them from other Passages? */
let passages = ['First', 'Second', 'Third'];
/* Construct the unordered list element */
const $ul = jQuery(document.createElement('ul'))
.addClass(`macro-${this.name}`);
/* For each Passage in list */
for (let passage of passages) {
/* Construct the list item element for this Passsage */
const $li = jQuery(document.createElement('li'))
.appendTo($ul);
/* Construct the anchor element for list item */
const $link = jQuery(document.createElement('a'))
.attr('data-passage', passage)
.addClass('link-internal')
.append(document.createTextNode(passage));
/* Has this Passage been visited? */
if (Config.addVisitedLinkClass && State.hasPlayed(passage)) {
$link.addClass('link-visited');
}
/* Add the onClick event handler */
$link.ariaClick({
namespace : '.macros',
role : 'link',
one : true
}, this.createShadowWrapper(
null,
() => Engine.play(passage)
));
$link.appendTo($li);
}
/* Add unordered list structure to Node buffer. */
$ul.appendTo(this.output);
}
});
note: You will likely notice that the Names of the Passages to show in the TOC have been hardwired, we will come back to the sourcing of them later.
To test the new <<toc>>
macro change the contents of the above Start Passage to the following, and Play the project again to see if the 2nd HTML structure looks & behaves like the 1st…
<ul class="macro-toc">
<li><a data-passage="First" class="link-internal link-visited">First</a></li>
<li><a data-passage="Second" class="link-internal">Second</a></li>
<li><a data-passage="Third" class="link-internal">Third</a></li>
</ul>
<<toc>>
Now for the part where we distinguish the TOC Passages from all the others, and the simplest why to do that is to add a known Passage Tag (like toc) to all of them. So if we do that then the above TWEE Notation example would look something like the following…
:: Start
<ul class="macro-toc">
<li><a data-passage="First" class="link-internal link-visited">First</a></li>
<li><a data-passage="Second" class="link-internal">Second</a></li>
<li><a data-passage="Third" class="link-internal">Third</a></li>
</ul>
<<toc>>
:: A non TOC passage
A non TOC passage
:: First [toc]
The First passage
:: Second [toc]
The Second passage
:: Another non TOC passage
Another non TOC passage
:: Third [toc]
The Third passage
So now that we can distinguish the TOC Passages we can use the Story.lookupWith()
method to find them programmatically, and the <Passage>.title
property of each found Passage to determine its Name.
The Macro definition would look something like the following, after the hard-wired list of Passage Names is replaced with the code to obtain them…
Macro.add('toc', {
isAsync : true,
handler() {
/* Add argument checking if required. */
/* Obtain list of Passages to be shown in TOC. */
let passages = Story.lookupWith(function (p) {
return p.tags.includes("toc");
})
.map(p => p.title);
/* Construct the unordered list element */
const $ul = jQuery(document.createElement('ul'))
.addClass(`macro-${this.name}`);
/* For each Passage in list */
for (let passage of passages) {
/* Construct the list item element for this Passsage */
const $li = jQuery(document.createElement('li'))
.appendTo($ul);
/* Construct the anchor element for list item */
const $link = jQuery(document.createElement('a'))
.attr('data-passage', passage)
.addClass('link-internal')
.append(document.createTextNode(passage));
/* Has this Passage been visited? */
if (Config.addVisitedLinkClass && State.hasPlayed(passage)) {
$link.addClass('link-visited');
}
/* Add the onClick event handler */
$link.ariaClick({
namespace : '.macros',
role : 'link',
one : true
}, this.createShadowWrapper(
null,
() => Engine.play(passage)
));
$link.appendTo($li);
}
/* Add unordered list structure to Node buffer. */
$ul.appendTo(this.output);
}
});
warning: The Story.lookupWith()
method returns the list of found Passages in alphabetical order, based on the Passage’s title, so if you want the TOC to be ordered otherwise (like alphabetically) then you will likely need to use the JavaScript <array>.sort()
method to do so…