Codebank

Basic Supermodel pagination

This is a simple implementation of pagination, with previous/next links and numbered pages. The howMany value of the <sm:paginate /> tag determines how many items per page will be ouput, and the windowSize variable determines how many pages either side of the current one will be displayed in the page link list. Where there are more pages than those available, and ellipsis will be output.

The code below contains minimal CSS classes, so it will be up to you to style the output correctly.

<sm:paginate field="items" howMany="10" sort="date desc" var="_paginator" page="${empty param.page ? 0 : param.page - 1}" />
					
<sm:forEach paginator="_paginator" var="_item">
	[ output conten items here ]
</sm:forEach>						

<sm:ifNot name="windowSize"><sm:set var="windowSize" value="3"/></sm:ifNot>
<sm:choose>
	<sm:when test="${_paginator.pageCount < (1 + ( 2 * windowSize ))}">
		<sm:set var="_firstPage" value="0"/>
		<sm:set var="_lastPage" value="${_paginator.pageCount - 1}"/>
	</sm:when>
	<sm:otherwise>
		<sm:set var="_firstPage" value="${_paginator.pageIndex - windowSize  < 0 ? 0 : _paginator.pageIndex - windowSize}"/>
		<sm:set var="_lastPage" value="${_paginator.pageIndex + windowSize < _paginator.pageCount ? _paginator.pageIndex + windowSize : _paginator.pageCount - 1}"/>
	</sm:otherwise>
</sm:choose>
<sm:if test="${_paginator.pageCount > 1}">
	<nav class="pagination">
		<a href="?page=${_paginator.pageIndex + 0}"<sm:if test="${_paginator.pageIndex eq 0}"> disabled</sm:if>>
			Previous
		</a>
		
		<div class="pages">
			<sm:if test="${_firstPage > 0}">
				<a href="?page=1">1</a>
				<sm:if test="${_firstPage > 1}">...</sm:if>
			</sm:if>
			<sm:forEach start="${_firstPage}" end="${_lastPage}" varStatus="_status">
				<sm:choose>
					<sm:when test="${_status.index == _paginator.pageIndex}">
						<span>${_status.index + 1 }</span>
					</sm:when>
					<sm:otherwise>
						<a href="?page=${_status.index + 1}">${_status.index + 1 }</a>
					</sm:otherwise>
				</sm:choose>
			</sm:forEach>
			<sm:if test="${_lastPage < _paginator.pageCount - 2}">...</sm:if>
			<sm:if test="${_lastPage < _paginator.pageCount - 1}">
				<a href="?page=${_paginator.pageCount}">${_paginator.pageCount}</a>
			</sm:if>
		</div>

		<a href="?page=${_paginator.pageIndex + 2}"<sm:if test="${!_paginator.next}"> disabled</sm:if>>
			Next
		</a>
	</nav>
</sm:if>

Allow SVG as an image upload

By default Supermodel does not allow you to upload an SVG into an image field type, but you can allow this by adding configuration for that field to specify all the allowed types:

allowedTypes=image/gif
allowedTypes=image/jpeg
allowedTypes=image/png
allowedTypes=image/svg+xml
allowedTypes=image/webp

Note that you can't only add the missing SVG type, you must include all the types that you want available.

Pass attributes to a Supermodel include

When uaing <sm:include> to output a collection of data, it is possible to pass attributes that can be used by the included templates:

<sm:include field="blocks" page="/object/blocks/default">
  <sm:attribute name="theme" value="dark" />
  <sm:attribute name="parent" value="${object}" />
</sm:include>

Prevent faux bold or italics

The font-synthesis CSS property lets you specify whether or not the browser is allowed to synthesize the bold, italic, small-caps, and/or subscript and superscript typefaces when they are missing in the specified font-family. This is mostly important when fonts are included in a way where each weight is a separate file included using @font-face with the font-weight specified as normal, or if a font only has a single weight (i.e. a display font where bold and/or italics don’t exist).

Setting font-synthesis: none; tells the browser not to create fake versions of bold/italics if they don’t exist. A way to apply this across all your styles is to include this rule:

html {
	font-synthesis: none;	
}

Add custom attributes to Supermodel inputs

To add custom attributes to an <sm:input /> field, you can use the attributesText property:

<input:text className="field" id="fieldname" name="fieldname" attributesText="data-id='1234' data-another-value='blah'" />

Deploying an old Supermodel site from an M-series Mac

For some of our older Supermodel sites you won't be able to run the deploy script successfully because the sites require and older version of Java. To fix this issue you need to tell the deploy to use Java 8 instead of whatever more current version you are running on your machine:

export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)

Fixing DNS for a Gandi subdomain when using Github Pages

When trying to point a subdomain to Github Pages from a Gandi DNS, I managed to get the domain working but Pages wouldn't allow me to enable the SSL option. When using the Github assistant to troubleshoot the issue it told me that an A record existed, when in fact I had only added a CNAME record.

After some searching I found this gist which proved to be the answer — the default Gandi live DNS includes some items that need to be deleted before Github Pages will be able to use SSL.

Create a row of equal height images that have different aspect ratios

Article here

Wrapping an unknown number of elements with CSS grid

A technique using grid layout to handle the layout of multiple blocks on a page that need to wrap at different points as the browser size changes, using only a couple of lines of code and no media queries (a technique called ’RAM’)

display: grid;
grid-template-columns: repeat(auto-fit, minmax(15em, 1fr));

Basic WYSIWYG field config

For adding basic configuration options so that only simple text is alowed in a field, add the following into the configuration for the field in Supermodel and adjust to suit.

tinyMCEConfig.height=150
tinyMCEConfig.toolbar=\"undo redo | bold italic | pastetext | link unlink | removeformat code\"
tinyMCEConfig.statusbar=false
tinyMCEConfig.valid_elements=\"p[class]\,em/i\,strong/b\,a[href|target|title]\,br\,span[class]\"

See the previous instructions if you need more detailed configuration.

Smooth scrolling to anchors

For a CSS-only methode to provide smooth scrolling action to anchors, you can add the following:

html {
	scroll-behavior: smooth;
}

It is good practice to also respect the users wishes if they want to avoid fast movement, so you can disable for those users by also adding:

@media screen and (prefers-reduced-motion: reduce) {
	html {
		scroll-behavior: auto;
	}
}

Create re-usable code blocks within a JSP template

Sometimes you may have a block of code that you would like to output in multiple places in the page, and for whatever reason don’t want to use includes or tags. To do this you can use the <sm:block /> feature. To start you set the block as a variable:

<sm:setBlock var="_blockname">
	...
</sm:setBlock>

Then later in the page you can output it:

<sm:block name="_blockname" />

Set a content type for admin previews

Often when editing content in Supermodel you will be editing an object that lives inside a larger page, and so doesn’t have its own page template for viewing. This means when you use the ‘Preview’ button in Supermodel there is nothing to show and you generally get an error page, which can be confusing for users. The solution is to explicitly set content types that can be previewed. When this configuration is added, any time you try to preview an object it will check to see if it is one of those content types, and if not then it will look up the chain at all parents until it finds the first one that does match, and use that to display as the preview template.

To set this configuration, add the following line to your supermodel.properties file:

supermodel.displayable.class=im:Displayable

Where im:Displayable here would match the model type for templates you want to match. This can be a comma-separated list.

forEach polyfill for IE11

if (window.NodeList && !NodeList.prototype.forEach) {
  NodeList.prototype.forEach = Array.prototype.forEach;
}

Generate sitemap.xml

To generate an automated sitemap file, uncomment the following code in the actions-config.xml file and edit to suit your field structure:

<action class="SitemapAction">
	<mapping url-pattern="/main/sitemap.xml"/>
	<mapping url-pattern="/sitemap.xml"/>
	<init-param>
		<param-name>root</param-name>
		<param-value>im_root</param-value>
	</init-param>
	<init-param>
		<param-name>fieldCodes</param-name>
		<param-value>navigation,footerNavigation,pages,products,customers,links,posts</param-value>
	</init-param>
	<init-param>
		<param-name>classCodes</param-name>
		<param-value>AbstractNavigable,FooterLinkGroup</param-value>
	</init-param>
	<init-param>
		<param-name>hiddenClassCodes</param-name>
		<param-value>Site,FooterLinkGroup</param-value>
	</init-param>
</action>

Convert images to an animated GIF

If you have ImageMagick installed you can use Terminal to convert a folder of images into an animated GIF. In Terminal, cd into the folder where your images are kept and then run the following script (modifying to suit):

convert -delay 10 -loop 0 *.png animated.gif

You can adjust the delay value to speed up or slow down the animation, and edit *.png to match your image naming/type (e.g it may be something like source-*.jpg). It can be a good idea to number your frame images to ensure proper ordering.

Note: the script above will layer each successive image on top of the previous, so if you are working with transparent images you may want to adjust it slightly so that the frame is wiped on each pass first:

convert -delay 10 -loop 0 *.png -set dispose background animated.gif

Output slug for an object

To output the Supermodel slug for any slugged object, you can use the following syntax:

${object:slug(object)}

Where the object in parenthesis is the object you wish to obtain the slug for.

If you find it’s not working, check that the following line exists in globals.jspf:

<%@ taglib prefix="object" uri="http://www.supermodel.co.nz/tld/object" %>

Combine images into an icon

Use Terminal to combine multiple PNG images into a single .ico icon file. Open Terminal, navigate into the folder containing the image files, then run this command:

convert 16.png 32.png 48.png 256.png favicon.ico

Note: since the most recent system update that uses ImageMagick 7 you now need to preface the command with 'magick' instead of 'convert':

magick 16.png 32.png 48.png 256.png favicon.ico

Depending on the colour profile of the PNG files you will sometimes get the error magick: Cannot write image with defined png:bit-depth or png:color-type. In this case you can add the -compress none option:

magick -compress none 16.png 32.png 48.png 256.png favicon.ico

Output srcset values from an image

To generate a comma-delimited list of images generated for the srcset of an image, loop through them with a forEach statement:

<sm:image field="image" 
  maxWidths="1980, 990, 500"
  var="_img"
/>

<sm:forEach name="_img" var="_i" varStatus="_s"><sm:url name="_i"/><sm:ifNot name="_s" field="last"/>,</sm:ifNot></sm:forEach>

This would generate a list of images in the format /img/size-1980.jpg, /img/size-990.jpg, /img/size-500jpg, which is useful if you need to add them as data attributes for reference in javscript.

Static images in the sm:image tag

This is how you can reference a static image asset inside the <sm:image /> tag, which can be useful when defining fallback images etc.

<sm:image value="/static/img/home-hero-placeholder.png" staticAsset="true" ... />

Fix overflow:hidden for video

This fixes issues where overflow hidden doesn’t work on elements in Webkit browsers (e.g. using border-radius to crop the edges of a video). Apply this mask to the containing overflow:hidden element.

-webkit-mask-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA5JREFUeNpiYGBgAAgwAAAEAAGbA+oJAAAAAElFTkSuQmCC);

Create a list object in Supermodel

Useful when you want to create a subset of values, for instance when needing to filter output and then do something with it.

<list:empty name="listName"/>
<sm:forEach field="items" var="_item">
	<list:add list="${listName}" value="${_item}"/>
</sm:forEach>

Hide empty elements with CSS

p:empty { display: none; }

Convert <br> elements to inline spaces

For when you want to remove a <br> so text runs inline. Using display: none; also removes the space, so if you are collapsing sentences this method is better (found in conversation here).

br {
	content: " ";
	
	&:after {
		content: " ";
	}
}

Force IE8/9’s best rendering mode

Add the following to the start of your page, after the usual global Supermodel includes.

<%response.setHeader("X-UA-Compatible", "IE=Edge"); %>

Test whether you are in a dev Supermodel environment

First add this import to the top of your page:

<%@page import="com.cactuslab.supermodel.Application"%>

Then you can test against the Application.isDevMode() value.

<% if (Application.isDevMode()) { ... } %>

Download a group of images

Use Terminal to download images from a group of URLs specified in a carriage-returned list within a text file. Open Terminal, navigate into the folder containing the text file, then run this command:

xargs -n1 curl -O textfilename

The above hasn’t always worked for me, but in those cases the following did the same trick:

cat textfilename | xargs -n 1 curl -O

Fix gap between borders of a rotated block element

When rotating a block element with borders on two edges by 45º (e.g. to create a chevron shape), sometimes where the borders meet there is a white gap. Adding a translate3D value (in particular having the Z value) seems to fix this.

transform: translate3D(0, 0, 0) rotate(45deg);

Redirect to first page in section

<%@include file="/WEB-INF/templates/frag/global.jspf" %>
<sm:url var="_url" name="object" field="pages" index="0" addContextPath="false"><sm:attribute name="section" value="${object}" /></sm:url>
<c:redirect url="${_url}" />

Where section in this instance is the variable set up by the routing rules.

Swap out 'no-js' class on HTML element

Add this into the <head> of your page to replace .no-js with .js when javascript is available (similar approach to Modernizr)

<script>
	document.documentElement.className=document.documentElement.className.replace(/\bno-js\b/,'') + ' js'; // Set approprate js class on html element
</script>

Check if a javascript variable is defined

if (typeof variable === 'undefined') {
	// variable is undefined
}

Tell git to ignore changes

To tell git to ignore any current changes for a particular file, you can type the following command in Terminal (replacing $FILE with the name of the file):

git update-index --assume-unchanged $FILE

To reverse the change, you can alter the line as follows:

git update-index --no-assume-unchanged $FILE

Use Terminal to base64 encode an image

Generate base64 version of an image:

openssl base64 < path/to/file

Do the same, but remove line breaks:

openssl base64 < path/to/file.png | tr -d '\n'

Do the above, but copy the result to the clipboard:

openssl base64 < path/to/file.png | tr -d '\n' | pbcopy

Using a test to restrict forEach results

<sm:forEach field="things" var="_thing" test="!empty _thing.values.fieldname">
    ...
</sm:forEach>

Note that the test does not require ${ } wrappers like you would usually use in test attributes.

Supermodel WYSIWYG config options

Configuration options for WYSIWYG fields in Supermodel. To add config for a specific field you will need to create a file (.jsp) inside /WEB-INF/supermodel-local/tinymce/config/. File names need to match the field name, but lowercased, and should live inside a subfolder with the same name as the parent object. e.g. if you want to add configuration for a 'content' field inside a block.text object, then the structure would look like:

config/
├─ block.text/
   ├─ content.jsp

Inside the file you can add your configuration:

<%@include file="/WEB-INF/templates/frag/global.jspf" %>
tinyMCEConfig.content_css = "<sm:static path="/static/css/admin/page-content.css" />";
tinyMCEConfig.height = "250";
tinyMCEConfig.theme_advanced_blockformats = "p,br";
tinyMCEConfig.theme_advanced_buttons1 = "styleselect,|,bold,italic,|,link,unlink";
tinyMCEConfig.valid_elements = "p[class],em/i,strong/b,a[href|target],br";
tinyMCEConfig.theme_advanced_statusbar_location = "none";

Ideally you will want to set the tinyMCEConfig.valid_elements to only allow elements that you have provided styling for. The theme_advanced_buttons1 values determind which buttons show up on the WYSIWYG toolbar, and the most common values are:

bold, italic, underline, strikethrough, justifyleft, justifycenter, justifyright, justifyfull, bullist, numlist, outdent, indent, cut, copy, paste, undo, redo, link, unlink, image, cleanup, code, hr, removeformat, formatselect, styleselect, sub, sup, blockquote,

You can also turn off all buttons by passing in an empty “” string. Note that this will hide the buttons but not the bar, so to remove that you’ll also need to add some javascript into the bottom of the config file:

if ( $('#supermodel-custom-admin-styles').length == 0 ) {
	var adminStyles = '';
	adminStyles += '#field_fieldCode_tbl tr.mceFirst { display:none; } #field_fieldCode_ifr, table#field_fieldCode_tbl.mceLayout { height: 54px !important; }';
	
	$('body').append('<style id="supermodel-custom-admin-styles">'+adminStyles+'</style>');
}

Where “fieldCode” needs to be replaced by the relevant ID in the markup for the WYSIWYG wrapper. Adjust the height values to suit based on the font size etc.

Check the TinyMCE docs for information about the valid_elements options.

Momentum scrolling on iOS

.scroll {
	overflow-y: scroll; /* has to be scroll */
	-webkit-overflow-scrolling: touch;
}

Create a tag file in Supermodel

Format the tag content like this, and include any relevant attributes:

<%@tag %>
<%@include file="/WEB-INF/templates/frag/global.jspf" %>
<%@attribute name="object" required="true" rtexprvalue="true" type="java.lang.Object" %>
<%@attribute name="something" required="false" rtexprvalue="true" type="java.lang.String" %>
<%@attribute name="test" required="false" rtexprvalue="true" type="java.lang.Boolean" %>

[ ... tag template code here, e.g. <sm:out name="something" /> ]

Format a 'double' field, show a single decimal if one exists

Useful for working with currency in Supermodel, or when specific decimal places are required.

<fmt:formatNumber value="${_object.values.fieldName}" pattern="#.#" />

Use sm:include for blocks of content

E.g. Store templates for each content type in /object/block/ and then add into main page like this:

<sm:include field="blocks" page="/object/block/default" />

This will iterate through all the objects in your collection and output the content using the matching template found in the folder specified by the page attribute.

You can optionally pass attributes which will be used by the includded file, which means includes may be used instead of tags in some cases:

<sm:include field="blocks" page="/object/block/default">
  <sm:attribute name="mycontent" value="..." />
</sm:include>

Get length of object in Supermodel

${fn:length(_object)}

Reverse output order of sm:forEach

<sm:forEach field="blah" sort="reverse">
    ...
</sm:forEach>

Increment index inside sm:forEach

<sm:forEach name="whatever" varStatus="_status">
  New index is: <sm:out value="${_status.index+1}" />
</sm:forEach>

Get parent object in Supermodel

<sm:parent var="_parent" />
<sm:out name="_parent" field="code" />

Start a simple server from Terminal

As of macOS Monterey 12.3 Python 2 no longer comes pre-installed with the OS, so you will need to install Python 3 (brew install python) if not already installed. You can then run a simple server by navigating to the folder in terminal and entering:

python3 -m http.server 8080

You can change the port number at the end to avoid clashes with any other services running. For machines running Python 2 you can use this command:

python -m SimpleHTTPServer 8080

Get file attributes in Supermodel

<sm:asset var="_file" name="download" field="file"/>
<sm:if name="_file">
	<sm:set var="_filename" name="download" field="name"/>
	<sm:ifNot name="download" field="name">
		<sm:set var="_filename" value="${_file.file.name}"/>
	</sm:ifNot>
	<a href="<sm:out name="_file" field="url"/>"><sm:out name="_filename" /> (${_file.fileSizeString} ${_file.suffix})</a> 
</sm:if>

Format colours from Supermodel

#${_colourField.hexString}
rgb(${_colourField.red}, ${_colourField.green}, ${_colourField.blue})

Strip http://www. from URLs

<sm:out field="url" format="unurl"><sm:transform pattern="www." replaceFirst="" /></sm:out>

Output list of pages within section

<ul>
    <sm:ancestor var="_section" classCodes="AbstractSection"/>
    <sm:forEach name="_section" field="pages" var="_page">
        <li<sm:if test="${object eq _page}"> class="selected"</sm:if>><a href="<sm:url name="_page"/>"><sm:out name="_page"/></a></li>
    </sm:forEach>
</ul>

Note that if you are using namespaced parent slugging then the <sm:ancestor /> part of this is likely unnecessary, as the routing will also set up a variable for the parent which you can reference (e.g. name="section").

Format a date in Supermodel

<fmt:formatDate value="${object.values.date}" pattern="d MMMM yyyy"/>

Here is a list of possible patterns that can be used.

Add namespaced routing for pages in sections

In routes.xml:

<route>
	<object name="object" class-code="AbstractPage" slug-parent="section" slug-space="pages"/>
	<object name="section" class-code="AbstractSection" child="object" />
	
	<route pattern="/{section}/{object}/" path="/object/default"/>
</route>

In configuration for page object in Supermodel model:

slug.space=pages
slug.parentClass=AbstractSection
slug.parentField=pages

Imagemagick command to assemble images into a sprite

Change the dimensions and tile layout to suit the size of images and number of rows/columns you desire.

montage -background transparent -geometry 1500x344+0+0 -tile 4x13 *.png sprite3.png

Add a ‘nowrap’ span around last word and following element

This is useful for times where there may be a count value at the end of a title, and you don’t ever want the count to be split from the last word (where it can wrap down to the next line all by itself).

<h2>
    <set var="_encoded"><out name="_item"></out></set><out name="_encoded" format="none"><sm:transform pattern="^(.*\s)?([^\s]+)$" replaceFirst="$1<span class='nowrap'>$2" /></out><small class="count">${fn:length(_item.value)}</small>
</h2>

The accompanying CSS style needs to be added for the .nowrap class.

Make block-styled links work in old IE

For when you have a link in your page styled as a block element, and the area of the link is not clickable in IE. Turns out adding a transparent background somehow makes it clickable.

.lte9 & {
	/* Hack because IE doesn't show link as clickable */
    background: url('data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==');
}

A sure telltale sign is if, when you have a border added, you can click the border but not the interior area. No idea why this happens, probably because IE...