Welcome back to A Sheet Author's Journey! This post got a little delayed by me working on some updates to the K-scaffold to fix a variety of bugs that had been reported, but it's finally week 5! And we're going to talk about adding flair and some actual navigation to our sheet. Last time, we had a pretty basic layout completed, but our sheet was still looking pretty rough. By the end of this blog post, our sheet will look like this though: Series Posts The Beginning Do the Pug Repeating Sections and sheetworkers Style and Layout Finding our Flair!
Roll on! Beautiful Translation How are we getting there though? The first part is that I finished the section specific layouts that we started looking at last week. The next part is getting rid of those red bits of bracketed text that we've had in our sheet since the beginning. Those red bits of text in our previous versions of the sheet meant that there wasn't a translation file available for the provided translation key. This is because we hadn't setup our translation file yet. Let's take a look at how the translation file works. The wiki has a pretty comprehensive description of everything that the translation system (known as i18n) can do on Roll20. First of all, the translation file for every sheet will be called translation.json. The translation data is stored in a JSON object with the keys being the lookup value and the values being the translated text. You can check out our freshly created translation.json file in the tutorial's repo . Here's a short snippet of what the file looks like: {
"character sheet button title":"Open the character sheet",
"character sheet":"character sheet",
"npc sheet button title":"Open the NPC sheet",
"npc sheet":"npc sheet",
"open the settings page":"Open the settings page"
} Keep in mind that JSON is a lot more restrictive than actual javascript objects when it comes to formatting. If you run into problems getting your translation file applied to your sheet, run it through a JSON linter like JSONlint.com . The rest of this transformation is thanks to our CSS. Beautiful Images The ripped paper effect that we're using here is just using the `border-image` series of CSS properties to nine-slice an image to create the background image and the border images. This SCSS code looks like: .paper-background{
border:var(--half-gap) solid transparent;
border-image:{
source:var(--paper-background);
slice:40 fill;
width:var(--grid-gap);
outset:var(--half-gap);
repeat:repeat;
};
filter:drop-shadow(var(--shadowColor1) 0px 3px 6px) drop-shadow(var(--shadowColor2) 0px 3px 6px);
}; If you want a deep dive into how border-image works, I recommend reading the articles at Mozilla Developer Network or w3schools . One piece of this that is hard to find in the documentation is that you have to set a border for the container that is going to use border-image, and it has to be defined before the border-image properties. So, with the use of a simple CSS property, the styling issue that I was worried about at the beginning becomes very easy to handle. And, with the increased style of our sheet, we can see it approaching an acutally usable state. Let's dive back into some functionality by setting up the navigation in our sheet. Getting Around Our sheet is pretty organized right now, but we'll eventually have a few tabs on this sheet; the character view that we have now, a settings page, and an NPC view to help out game masters. In order to make it easy to keep all of this straight for the user, we'll separate each of these views into a separate tab of the sheet that will be accessible from the top of the sheet window. We'll create the NPC sheet later on (probably a few weeks from now), but for now we'll make a simple settings sheet to allow players and GMs to set what type of sheet the character should use (character or NPC), and how rolls should be sent to chat. Here's what that settings page will look like: The creation of the settings page itself is the same process that we've done for the rest of the sheet, except that I've applied the .paper-background class to the settings article instead of a section within it. But, let's take a look at how our navigation is working. - varObjects.sheetTypes = ['character','npc'];
nav#main-nav.sheet-nav
each val in varObjects.sheetTypes
+navButton({name:val,class:`sheet-nav__tab ${val}`,'data-i18n':`${val} sheet`,'data-i18n-title':`${val} sheet button title`,role:'heading','aria-level':5,trigger:{triggeredFuncs:['navigateSheet']}})
+navButton({name:'settings',class:'pictos active sheet-nav__tab settings','data-i18n-title':'open the settings page',role:'heading','aria-level':5,trigger:{triggeredFuncs:['navigateSheet']}})
|y Our navigation functionality starts here, in our main nav element. We're creating three navigation buttons. We're using the navButton mixin because it will automatically export our navigation button names to our JS for use later on when we iterate through them. For a similar reason, we're storing the names of our two sheet types in the varObjects . Additionally, we've defined a triggerFunction for our navigation buttons that will handle hiding/revealing the appropriate sheet sections when the button is clicked. Sidebar - BEM Methodology You might be asking yourself what's with the crazy looking class names, like sheet-nav__tab . We're utilizing a slightly modified Block Element Modifier (aka BEM) methodology of class naming here. The short run down is that the sheet-nav__tab bit means that this is a tab within the sheet-nav element, and it is styled the way it is because it is. BEM may look a little strange, but it offers a way to semantically link your classes when you have styling that is occurring because of the relationship between two items. Now that our buttons are hooked up to our navigation function, let's take a look at what that function looks like. It needs to do two things; hide areas of the sheet that are not the currently selected tab, and show sections that are the currently selected tab. const navigateSheet = function({trigger,attributes}){
let name = trigger ?
trigger.name :
attributes.sheet_state;
let [,,page] = k.parseTriggerName(name);
page = page.replace(/^nav-|-action$/g,'');
navButtons.forEach((button)=>{
let element = button.replace(/-action/,'');
$20(`.${element}`).removeClass('active');
});
$20(`.${page}`).addClass('active');
attributes.sheet_state = page;
};
k.registerFuncs({navigateSheet},{type:['opener']}); This function is going to get called in two ways; when we click on a navigation button, and when the sheet is opened. This is because we're going to make use of Roll20's limited implementation of jQuery to show/hide the sections of our sheet. There's a few things to consider when using jQuery in a character sheets; 1) the changes made by jQuery are transient and will be wiped once the sheet is closed again, and 2) the changes made by jQuery are client-side only which means that they will not affect the view of anyone else viewing the sheet. Because this function is getting called in the K-scaffold's handling of the sheet opened event, it needs to be able to function with the trigger object being present, and without it. This is what the ternary in the first three lines of the function is doing. If a trigger is passed, which means we're calling it in response to a button click, use the trigger's name, but if there is no trigger the function is being called as part of the sheet opening and we just need to load the display from memory. The next few lines are simply sanitizing the name of the button to make it ready for use. The actual functionality of the function happens in the last few lines. navButtons.forEach((button)=>{
let element = button.replace(/-action/,'');
$20(`.${element}`).removeClass('active');
});
$20(`.${page}`).addClass('active');
attributes.sheet_state = page; Here, we're iterating through each of our navButton names and removing the active class from any element with a matching class. Then we add the active element back to elements that have the class that matches the current sheet state. And, finally, we store the new active page in the sheet_state attribute so that we open to the correct display the next time. Toggling this way allows us to properly handle elements that might have two or more of the navigation classes on them, although we don't have any sections like this in the sheet. Of course, all that the function does is add and remove a CSS class to elements. Let's take a look at the SCSS that responds to these changes. .nav-display{
&:not(.active){
display:none !important;
}
} This overrides the display attribute of our various sections when they are not active. The only thing that we had to add to our existing pug code was to tag our navigable sections with the .nav-display class, which is just our character and settings articles at this point. Usually, it's a good idea to stay away from the !important CSS flag , but we're using it here because if this is an element that should be hidden when not in use, we want it to always be hidden when not in use. How far have we come? It's been 5 posts (and 7 weeks) since we started on this sheet. We've now got a fully styled sheet that is navigable and calculates our attribute values for us, and we started with nothing but a sketch of what we wanted to do. That's some pretty good progress, but we aren't done yet! Next week, we'll look at designing a roll template and getting our roll buttons to actually function. The week after that, we'll look at what needs to change on the sheet to handle an NPC sheet. And finally we'll talk applying finishing touches and submitting the sheet to the roll20 repository on github so that everyone will be able to use it. Want input on what the next blog series will be about? Then d rop by the Kurohyou Studios Patreon ! See the previous post | Check out the Repository | See the Next Post