Skip to main content

Essay Customization

On this page, we'll discuss how to extend Kookaburra's essay features. If you are unfamiliar with the essay features, please read Kookaburra Essay first.

Essay features can be extended via PHPlugins, using the essay_script hook. You might additionally use the head or scripts hooks, or Custom Stylesheets to supplament your essay extention.

That may be a lot to take in all at once, but all will hopefully become clear as we move through the examples.

Using our essay extension API, and other extensibility features, you can customize exising essay presentation types, or write entirely new ones. In fact, this is exactly how we write new essay presentations!

For reference, we keep an up-to-date PHPlugins template here:

backlight/modules/custom-resources/phplugins/phplugins-kookaburra-sample.php

I'm not going to reproduce that here, as this document would surely and quickly fall out-of-date. We will do a better job keeping that file up-to-date than we will this documentation. 😅

At time of writing, existing presentation types, by ID, are:

  • carousel
  • grid
  • img-comparison-slider
  • masonry
  • single
  • static

If you're ever unsure about the ID of a presentation, it's easy to find. Create an essay presentatino of the desired type. The code snippet will include a data-presentation attribute, the value of which is the ID. For example:

<div data-albums="519562" data-presentation="grid" data-columns="4"></div>

The ID for this presentation is "grid".

Extending an existing presentation type​

In this first example, we'll be extending the single presentation type, adding some custom options.

Presentation types are defined on the global renderAs object. By hooking into the page with the essay_script PHPlugins hook, you will have access to this object.

function essay_script() {
echo "
<script>

// examine the 'single' presentation definition by logging it to the console
console.log(renderAs['single']);

</script>
";
}

At time of writing, the single presentation is very simple, and has only template and title properties defined. By the time you read this, that might have changed! No matter.

We want to add fields. But in case you are extending a presentation that already has fields, we also want to protect it's original values. So we'll declare prevFields and gather any existing fields, or set it to an empty array, then we'll spread those fields into our updated fields property.

function essay_script() {
echo "
<script>
const prevFields = renderAs['single'].fields || [];
renderAs['single'].fields = [
...prevFields,
{
// we'll add new fields here
},
];
</script>
";
}

The fields we're going to add are align and width.

Field names should be one word, all lower case.

  • Bad names: image-alignment, imageAlignment
  • Good names: align, imagealignment

The align field will be a select field with three options, while width will be a text input. We might consider using a number input, but using text will allow us to specify width values in any CSS valid unit: em, px, rem, or %.

Here's the updated script, adding our two field definitions:

function essay_script() {
echo "
<script>
const prevFields = renderAs['single'].fields || [];
renderAs['single'].fields = [
...prevFields,
{
label: 'Alignment',
name: 'align',
options: [
{ label: 'Center', value: 'center' },
{ label: 'Left', value: 'left' },
{ label: 'Right', value: 'right' },
],
type: 'select',
value: 'center',
},
{
label: 'Width',
name: 'width',
type: 'text',
},
];
</script>
";
}

Now, when we open the essay presentation composer and select "Single" as our presentation type, we should see our new fields as options.

Using our options, we can see our choices reflected in the code snippet:

<div data-albums="519562" data-presentation="single" data-align="center" data-width="50%"></div>

Now, we need to use those values.

The width, we'll set on our presentation using JavaScript, and the onComplete property. Once again, we need to preserve any existing behavior, if it exists.

The onComplete property takes a function, the snippet element being passed in as the only argument.

This part of the script looks like this:

// store the pre-existing onComplete function
const prevOnComplete = renderAs['single'].onComplete;

renderAs['single'].onComplete = (el) => {
// if present, run the pre-existing onComplete function
if (prevOnComplete) {
prevOnComplete(el);
}

// extend onComplete with our new logic
const width = el.dataset.width;
el.style.width = width;
};

Putting it all together, our finished PHPlugins function is this:

function essay_script() {
echo "
<script>
const prevFields = renderAs['single'].fields || [];
renderAs['single'].fields = [
...prevFields,
{
label: 'Alignment',
name: 'align',
options: [
{ label: 'Center', value: 'center' },
{ label: 'Left', value: 'left' },
{ label: 'Right', value: 'right' },
],
type: 'select',
value: 'center',
},
{
label: 'Width',
name: 'width',
type: 'text',
},
];

const prevOnComplete = renderAs['single'].onComplete;
renderAs['single'].onComplete = (el) => {
if (prevOnComplete) {
prevOnComplete(el);
}

const width = el.dataset.width;
el.style.width = width;
};
</script>
";
}

Our width option is defined and applied -- done. Try using it, enter a width value of "50%", and you will see the size applied to your image on the page.

Next up, we'll write some CSS to make use of our alignment option, adding it to a Custom Stylesheet, and using [data-presentation="single"] to target our specific presentation. I'll include explanatory comments in the code:

/**
* For center-aligned images, we set the right and left margins to "auto"
**/
[data-presentation="single"][data-align="center"] {
margin-right: auto;
margin-left: auto;
}

/**
* For left-aligned images, we float left and apply some margin to the right side,
* allowing text to wrap around the image.
**/
[data-presentation="single"][data-align="left"] {
float: left;
margin-right: 18px;
}

/**
* For right-aligned images, we float right and apply some margin to the left side,
* allowing text to wrap around the image.
**/
[data-presentation="single"][data-align="right"] {
float: right;
margin-left: 18px;
}

/**
* Set a media query to address small displays, like phones
* - on small displays, there's not enough room to wrap text around images,
* so we remove our float and margins, centering the image
* - we are also overriding our defined width; 50% of a phone display is too small
**/
@media screen and (max-width: 800px) {
[data-presentation="single"][data-align="left"],
[data-presentation="single"][data-align="right"] {
float: none;
margin-right: auto;
margin-left: auto;
}

[data-presentation="single"] {
width: auto !important;
}
}

Of course, now that I've written all this code for the tutorial, it would be a shame for me not to use it. I'll add these features to the 6.3.0 update. But knowing how they came to be, you can hopefully use the knowledge to write your own customizations.

Creating a new presentation​

Scrounging around for something to use in this tutorial, I found Splide, a neat looking slideshow. Let's hook it up!

We'll use two PHPlugins hooks, head and essay_script. The CSS goes in head, and JavaScript assets into essay_script; essay_script is also where the magic happens.

You can download the Splide files and host them yourself, and I do recommend doing that if you plan to use it beyond following this tutorial. For now, we can use their CDN files to get rolling quickly.

There may be a newer version available, so check the "CDN" section of Splide's Getting Started page to get the latest assets.

Here's our setup:

    function head() {
echo <<<HTML

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@splidejs/splide@4.1.4/dist/css/splide.min.css">

HTML;
// do not indent line above
return false;
} // END /**/



function essay_script() {
echo <<<HTML

<script src="https://cdn.jsdelivr.net/npm/@splidejs/splide@4.1.4/dist/js/splide.min.js"></script>
<script>
renderAs['splide'] = {
title: 'Splide',
};
</script>

HTML;
// do not indent line above
return false;
} // END /**/

So, I've added the CSS and JS files, and I've declared the existence of our new Splide presentation by defining it on our renderAs object. It works like this:

renderAs['presentation-id']

Our ID is "splide", and this will later give us data-presentation="splide" in our code. Whatever you provide as title will be the name of the presentation when selecting it in the essay presentation composer.

If you open the essay presentation composer, you will now see Splide as a selectable option, and it will even generate a snippet for you:

<div data-albums="123456" data-presentation="splide"></div>

Good start! It just doesn't actually do anything yet.

Moving through the documentation, we find their basic template:

<section class="splide" aria-label="Splide Basic HTML Example">
<div class="splide__track">
<ul class="splide__list">
<li class="splide__slide">Slide 01</li>
<li class="splide__slide">Slide 02</li>
<li class="splide__slide">Slide 03</li>
</ul>
</div>
</section>

We need to break this down into two parts, container and repeatable item. Pretty easy here; <li class="splide__slide"></li> is our repeatable item. The rest:

<section class="splide" aria-label="Splide Basic HTML Example">
<div class="splide__track">
<ul class="splide__list">

</ul>
</div>
</section>

... is our container.

We'll use two new renderAs properties, setup and template.

The setup property is optional, though we do need it here. The template property is required, unless you use a skipTemplate function instead, but that's a different tutorial.

The container markup goes into setup, while the repeatable markup goes into template.

    renderAs['splide'] = {
setup: (placeholder, group) => {
placeholder.innerHTML = (`
<section class="splide" aria-label="Splide Slideshow">
<div class="splide__track">
<ul class="splide__list"></ul>
</div>
</section>
`);
return placeholder.getElementsByTagName('ul')[0];
},
template: (photo, group, placeholder, i) => {
return (`
<li class="splide__slide"></li>
`);
},
title: 'Splide',
};

On setup, you have two arguments, placeholder and group.

Group is just a name you can optionally give to your presentation; we may or may not end up using it. The important thing is the placeholder, and that's your presentation element, this guy:

<div data-albums="519562" data-presentation="splide" data-group="tutorial"></div>

The setup function needs to return the placeholder, or another element inside of it. Whatever container we get back from setup will be used to wrap the output from template.

We want to put our repeatable items into the ul element, so that's what we're returning from setup.

At this point, if you've put your presentation snippet onto the page, you can inspect it to see that we're rendering the container markup, and as many pieces of template markup as we have images.

Let's render the images!

The template function receives photo and placeholder as arguments. The photo argument is all the data associated with each photo as we go through them. Use the placeholder argument to access configuration options from your presentation.

If you want to know what's available on your photos, just console.log(photo); in the template function, like this:

      template: (photo, group, placeholder, i) => {
console.log("photo", photo);
return (`
<li class="splide__slide"></li>
`);
},

... then view the data in your browser's JavaScript console.

To the business of images, we get what we need from the photo object, format it if we need to, and plug it into our template markup.

And because we're working with JS in PHP, we need to escape our $ characters, so \$. If you crash the page, probably you forgot the escape (I do it all the time).

      template: (photo, group, placeholder, i) => {
const {
photoSrc,
photoSrcset,
photoWidth,
photoHeight,
} = getImgSrcAttributes(photo);

return (`
<li class="splide__slide">
<img
alt="\${photo.filename}"
height="\${photoHeight}"
src="\${photoSrc}"
srcset="\${photoSrcset}"
width="\${photoWidth}"
/>
</li>
`);
},

I have provided a convenience function, getImgSrcAttributes, that you can see being used here. Pass your photo as argument, it will parse the data and return photoSrc, photoSrcset, thumbnailSrc, and thumbnailSrcset values that you can use in your IMG element. This is a global function that you can use in any presentation you might create.

Moving right along, we need to apply the Splide JS to our markup. For that, we add the onComplete property to our renderAs object.

According to Splide's docs, we need to instantiate the slideshow on the container element with the .splide class.

      onComplete: placeholder => {
const splideEl = placeholder.querySelector('.splide');
new Splide(splideEl).mount();
},

With that done, you now have a working slideshow. However, if your images are not all exactly the same size, you might find it quite ugly. Still, we're in a good place: the end of Splide's Getting Started page.

You can dive through Splide's many options on your own, but let's apply a few defaults, before our presentation declaration:

    Splide.defaults = {
heightRatio: 0.5625, // 16:9 aspect ratio
lazyload: true,
};

renderAs['splide'] = {
// ...

And we should, at this point, introduce a Custom Stylesheet. This will help our images to fit better within the slideshow:

.splide__slide img {
object-fit: contain;
width: 100%; height: 100%;
}

With these simple changes, our slideshow is looking much better.

Assuming we might not want every slideshow to have the same aspect ratio, we should make it a presentation option. Add a field, then access the data in our onComplete to apply it to our slideshow.

    renderAs['splide'] = {
fields: [
{
label: 'Height ratio',
name: 'ratio',
type: 'text',
value: '0.5625',
},
],
onComplete: placeholder => {
const heightRatio = placeholder.dataset.ratio || '0.5625';

const splideEl = placeholder.querySelector('.splide');
new Splide(splideEl, { heightRatio }).mount();
},

And that's about it for this tutorial.

By now, you know how to:

  • create a new essay presentation type
  • add options for presentation customization
  • style your presentation using custom stylesheets

If you want to use this Splide presentation type, you might want to download and host your own copy of the assets; just replace the CDN copies here. And dive into the available options to decide how you want to configure your Splide slideshows. Some options you will probably want as defaults, while others you might like to add as fields so that you can customize them individually.

Here's our final code:

PHPlugin

    function head() {
echo <<<HTML

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@splidejs/splide@4.1.4/dist/css/splide.min.css">

HTML;
// do not indent line above
return false;
} // END /**/



function essay_script() {
echo <<<HTML

<script src="https://cdn.jsdelivr.net/npm/@splidejs/splide@4.1.4/dist/js/splide.min.js"></script>
<script>
Splide.defaults = {
heightRatio: 0.5625,
lazyload: true,
};

renderAs['splide'] = {
fields: [
{
label: 'Height ratio',
name: 'ratio',
type: 'text',
value: '0.5625',
},
],
onComplete: placeholder => {
const heightRatio = placeholder.dataset.ratio || '0.5625';

const splideEl = placeholder.querySelector('.splide');
new Splide(splideEl, { heightRatio }).mount();
},
setup: (placeholder, group) => {
placeholder.innerHTML = (`
<section class="splide" aria-label="Splide Slideshow">
<div class="splide__track">
<ul class="splide__list"></ul>
</div>
</section>
`);
return placeholder.querySelector('.splide__list');
},
template: (photo, group, placeholder, i) => {
const {
photoSrc,
photoSrcset,
photoWidth,
photoHeight,
} = getImgSrcAttributes(photo);

return (`
<li class="splide__slide">
<img
alt="\${photo.filename}"
height="\${photoHeight}"
src="\${photoSrc}"
srcset="\${photoSrcset}"
width="\${photoWidth}"
/>
</li>
`);
},
title: 'Splide',
};
</script>

HTML;
// do not indent line above
return false;
} // END /**/

Apply whatever addition styling you want.

Custom Stylesheet

.splide {
background-color: #212121;
border: 1px solid #191919;
margin: 0 auto 1.5rem;
}

.splide__slide img {
object-fit: contain;
width: 100%; height: 100%;
}

Useful things​

Kookaburra has some things built in that can be useful when writing your own presentations.

getImgSrcAttributes​

In the template function, you can use this to parse IMG data from the photo object, to render either photo or thumbnail image renditions.

template: (photo, group, placeholder, i) => {
const {
photoSrc,
photoSrcset,
photoWidth,
photoHeight,
} = getImgSrcAttributes(photo);

return (`
<img
alt="\${photo.filename}"
height="\${photoHeight}"
src="\${photoSrc}"
srcset="\${photoSrcset}"
width="\${photoWidth}"
/>
`);
},

useColumns + column styles​

If you'd like to use columns in your presentation, the styling already exists.

  • Set useColumns: true on your presentation;
  • wrap your images in the FIGURE element.
template: (photo, group, placeholder, i) => {
const {
thumbnailSrc,
thumbnailSrcset,
thumbnailWidth,
thumbnailHeight,
} = getImgSrcAttributes(photo);

return (`
<figure>
<img
alt="\${photo.filename}"
height="\${thumbnailHeight}"
src="\${thumbnailSrc}"
srcset="\${thumbnailSrcset}"
width="\${thumbnailWidth}"
/>
</figure>
`);
},
useColumns: true,