Stat Sheet Tutorial

From RPTools Wiki
Jump to navigation Jump to search

Languages:  English

INTERMEDIATE
THIS IS AN INTERMEDIATE ARTICLE

Tutorial for Building a Custom Stat Sheet

MapTool 1.14.3 has added the ability to define your own pop-up stat sheets using Handerbars, HTML, and CSS (and JavaScript!).

The new stat sheets are part of the add-on functionalty which is replacing lib: tokens. This is essentially my CliffNotes companion to the technical information at Stat Sheet.

Configuration

To be recognized, the Add-on needs to have the file stat_sheets.json in its root directory. This file is a JSON object; the key statSheets contains an array of objects defining each stat sheet. For example:

{
  "statSheets": [
    {
        "name": "Rev-Smoked-Glass",
        "description": "Glass",
        "propertyTypes": [“Basic”, “monster],
        "entry": "sheets/rev.glass.hbs"
    }
  ]
}

The above contents of stat_sheets.json only defines a single stat sheet (named Rev-Smoked-Glass) because there is only a single JSON object in the array.

  • description - appears in the dropdown list that is part of the MapTool user interface on the Config tab of the Token Editor dialog.
  • entry - the location of the Handlebars template file that defines this stat sheet. It is relative to the /library/public directory inside the Add-on.

Handlebars Templates

Handlebars is a templating language commonly used within HTML pages. Its goal is to allow repetitive content to be created dynamically and/or conditionally without the author having to repeat themselves within the document.

See the Handlebars web site for full details.

Within a MapTool Add-on, the Handlebars template has a filename extension of .hbs. This template is read by MapTool and the result of the template replacement process is the HTML5 that is displayed to the user.

Here's an example of such a stat sheet. Look for blocks of text that begin with a double open-brace and end with a double close-brace. Those are replaceable with the result of the embedded instructions.

In many cases, the embedded instructions are simply to replace the block with the value of a variable (such as {{portraitWidth}}).

Also note that the MapTool Preferences allow the user to set their preferred size for the portrait image. Be polite when you create your own stat sheet and respect the user's preferences! (Note the use of {{portraitWidth}} in the img element in the example, below, circa line 21.)

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, height=device-height, user-scalable=0, initial-scale=1" />
<link rel="stylesheet" href="lib://net.rptools.maptool/css/mt-stat-sheet.css" />
<link rel="stylesheet" href="./css/rev.statsheet.css" />
<link rel="stylesheet" href="./css/rev.glass.css" />
<style>
:root {
  --portrait-width:{{portraitWidth}}px;
  --portrait-height:{{portraitHeight}}px;
}
</style>
</head>
<body>
<div id="statSheet" class="{{statSheetLocation}} {{#if gm}}gm{{else}}player{{/if}} {{tokenType}}">

  <div id="portrait-container-container" class="box portrait-container-container">
    <div id="portrait-container" class="portrait-container">
      <img id="portrait" class="portrait"
           src="{{~#if portrait}}{{~portrait~}}
                {{~else if handout}}{{~handout~}}
                {{~else if image}}{{~image~}}{{~/if}}"
           width="{{portraitWidth}}"
           height="{{portraitHeight}}"
      />
    </div>
  </div> 
  <div id="token-type-container" class="box token-type-container">
    <div id="token-type" class="token-type">{{tokenType}}</div>
  </div> 
  {{#if properties~}}
  <div id="properties-container-container" class="box properties-container-container">
    <div id="properties-container" class="properties-container">
      <div id="properties" class="properties">
      {{#each properties~}}
        <div class="property-container property-{{~slugify this.displayName}} {{#if this.gmOnly}}gm-only{{/if}}">
          <div class="property-label">
            {{~#if this.shortName}}
              {{~this.shortName~}}
            {{~else}}
              {{~this.displayName~}}
            {{~/if}}
          </div>
          <div class="property-value-container">
          <div class="property-value">
            {{~this.value~}}
          </div>
          </div>
        </div>
      {{~/each}}   
      </div>
    </div>
  </div>
  {{~/if}}
 
</div>
</body>
</html>
Other examples are linked from the Stat Sheet page.

If you look through the example, above, you'll find handlebars blocks that contain a single string -- a variable name. MapTool provides multiple predefined variable names when the template is invoked.

Comments

Comments can be added in two ways. First, you can use normal HTML comments of the form . They will be passed through handlebars and embedded inside the resulting HTML. There's not much need for these kinds of comments since you won't be able to see them within MapTool, but if you like the idea of using your desktop web browser to develop the stat sheet and move it into an Add-on only when you're done, you might like the HTML comments as your browser's debug panel should show them to you when you view the HTML.

Second, you can use handlebars comments. They start with {{! and end with }}. These are interpreted as comments by handlebars and, as comments, they are stripped from the resulting HTML.

Helpers

Handlebars provides "helpers" as well. Conditional logic for if statements, loops, and much more. For a full list of helpers built into the MapTool implementation, see this GitHub issue.

For example, this block from the example shows how an if statement can be embedded inside the template. If the expression is true, the HTML div element will be present in the output; if the expression is false, the div will be missing from the element.

{{~#if gmName}}
		<div id="name-gm" class="name-gm gm-only">{{gmName}}</div>
{{~/if}}

In the example, the {{gmName}} is a reference to a handlebars variable, one that is predefined by MapTool.

Conditionals

Here's an example of creating a loop. The variable properties is predefined by MapTool and represents all properties that are visible to the current MapTool user (meaning that players will not be able to see properties that are GM only). (More info at Stat Sheet.)

Inside the loop, one property at a time will be available. The variable this refers to that single property.

As you may guess, the {{#if}} is the beginning of an if block and it ends at the corresponding {{/if}}. Else blocks can also be used, as shown. The expression inside the {{#if}} block is considered true when a single variable is given and that variable is a non-empty value -- in other words, anything that JavaScript would not consider null, undefined, or an empty string ("").

{{#each properties}}
<div class="property-container {{#if this.gmOnly}}gm-only{{/if}}”>
		<div class="property-label">
				{{#if this.shortName}}
						{{this.shortName}}
				{{else}}
						{{this.displayName}}
				{{/if}}
		</div>
		<div class="property-value">{{this.value}}</div>
</div>
{{/each}}

The example above will render into HTML as shown here for GM only properties:

<div class="property-container gm-only”>
		<div class="property-label">
				<!-- this.shortName or this.displayName as a fallback -->
		</div>
		<div class="property-value"> <!-- this.value --> </div>
</div>

<div class="property-container gm-only”>
		<!-- and so on... -->
</div>

The only difference when the property is NOT GM only is that the class won't appear on the leading div (likely causing a slightly different visual look for this row of property information).

In general, extra whitespace in HTML is ignored. However, such whitespace within the href attribute of an anchor element effectively changes the URL! So there must be a way to trim such whitespace from the output, and... there is. Note the use of the ~ character in the following snippet. When you see a tilde at the beginning of a handlebars helper, it means whitespace in front of the helper's output should be removed. Similarly, a tilde at the tail of a helper means to trim whitespace from the end of the helper's output.

{{#each properties~}}
<div class="property-container {{#if this.gmOnly}}gm-only{{/if}}”>
		<div class="property-label">
				{{~#if this.shortName~}}
						{{~this.shortName~}}
				{{~else~}}
						{{~this.displayName~}}
				{{~/if}}
		</div>
		<div class="property-value">{{~this.value~}}</div>
</div>
{{~/each}}

The above would generate slightly different HTML. In the output, below, note how the first nested div has its closing tag on a separate line? That's because the preceding {{/if}} block didn't have a trailing tilde.

<div class="property-container gm-only”>
		<div class="property-label"><!-- this.shortName or this.displayName as a fallback -->
		</div>
		<div class="property-value"><!-- this.value --></div>
</div>

Modifications

Handlebars provides many helpers that can modify the content of dsata before passing it through as HTML. For example, suppose you have a variable msg that contains the string my answer is no. There might be a situation where you want that string converted to a format that would work as a valid CSS class name. To do that, the spaces would need to be eliminated. Deleting them entirely can make the HTML difficult to read later, though, so handlebars provides the slugify helper that replaces the spaces with hyphens.

<div class="{{~slugify this.displayName~}}">

would produce the following as output:

<div class="my-answer-is-no">

Note that the tildes on either end of the slugify helper aren't really needed in this case -- extra spaces in the class attribute will be ignored in the HTML.

Other Requirements

The handlebars template will need to include the following line in the <head> block:

<link rel="stylesheet" href="lib://net.rptools.maptool/css/mt-stat-sheet.css" />

(There's no need for the usual ?cachelib=false nonsense here, since the text being included will not change (except perhaps between versions of MapTool). This means there's no need to workaround the built in caching that the HTML engine employs.)

The container of your stat sheet, ie. the top-level HTML element that will enclose all of your visible content (likely a div), must have id="statSheet" so that MapTool can locate said element. It must also include statSheetLocation amongst the list of elements for the class attribute. (See Stat Sheet for more information on how one can use statSheetLocation.)

Gotchas

  • HTML ids are case-sensitive, as is pretty much anything inside quotes in HTML (such as class names or other attribute values). Make sure you use "statSheet" and not "statsheet"!
  • CSS has specificity rules. You may find that your rules are not overriding the built in rules. (This is one reason why I like building my HTML output in a regular file and viewing it with a web browser -- browsers have developer tools specifically designed to help with this kind of thing.) You can add !important to your rules to cause the rendering engine to always use them, but once you start down that path, it can be a slippery slope as you start to need it everywhere! Generally, using an id to narrow down the scope of your CSS initially, then using class names for additional categories is sufficient.
  • The & CSS selector didn't work when I created the examples, but may be available by the time you read this. It doesn't hurt to try it and see.
  • Keep accessibility and theming in mind. What looks good on one platform may not look good on others. This is often a function of font selection and the typeface support within the rendering engine.
  • Similar to the above, MapTool provides a set of predefined CSS variables to represent colors from the different themes. Check out Stat Sheet for a complete list.
  • Be careful with sizing when using images. An image can look great at a particular size and/or scale, but the user gets to specify how big they want the portrait rendered in their Preferences so keep that in mind. (A user on a smaller resolution display may choose a very tiny portrait to save screen space for other things, for example.)
  • Relative URLs start at the location of the handlebars template. Keep that mind when using ../ in a URL. Remember that absolute URLs, those start with /, always begin their search starting at /library/public.
  • Output from handlebars is normally HTML-escaped. So a variable containing <b>Bold</b> will literally display those characters! The word Bold will not be output! To prevent that mechanism, use a "triple-stash", ie. {{{this.displayName}}}.
  • Animated images aren't on macOS (at the time of this writing). This is likely a limitation of the JavaFX rendering engine and should be expected to change in the future, as new releases of JavaFX come out.
  • In fact, that's a good warning in general -- expect things to change! JavaFX is still a moving target and bugs are being found and squashed all the time. Doing something in as simple a way as possible is likely more future proof than taking advantage of the latest browser feature.
  • When developing, use a lib: token to help fine tune things. Linking to the token lib: macro allows you to tweak CSS and JS on-the-fly. For my Add-on developing, I used a token lib:aodev containing macros statsheet.css and statsheet.js, then included these two lines in the <head> section of the handlebars template:
<link rel="stylesheet" href="lib://aodev/macro/statsheet.css?cachelib=false" />
<script delay src="lib://aodev/macro/statsheet.js?cachelib=false"></script>
  • Test your stat sheet in each of the eight possible locations on a token. The goal is to use strange and "will never happen" values for everything, including really long and really short values that will screw up your layout. It's okay to use HTML tables when they are appropriate -- things that should be displayed in a table (like token properties, for example) should use tables. Tables can be helpful for folks with visual handicaps who are using screen readers. They can also be harmful when used to position elements on the screen when the data itself isn't tabular (such as when columns in a row are not related to each other and are only there for the visual layout). Use of CSS Grid and Flex can typically obviate the need for tables for layout purposes.