Your web browser is out of date. Update your browser for more security, speed and the best experience on this site.

Update your browser
• By Robin Mannering

A practical example of using the Donkeytail plugin with Tailwind and Alpine JS

Goodwork article image donkeytail 2022

Overview

In this article we’ll explore a practical example of using the Donkeytail plugin to display points of interest (pins) on an image of a lounge, highlighting the different pieces of furniture for sale and labelling the name of each item with a short description and price.

We’ll use Tailwind to style each Donkeytail pin, and Alpine JS to provide some interactivity that will open and close each pin to reveal the name, description and price.

For the sake of brevity we’ll assume you already have a preferred method of integrating both Tailwind and Alpine JS into your Craft project.

Here’s what we are creating…

Installation

Let’s begin by installing the Donkeytail plugin.

  • Open your terminal and go to your Craft project:
cd /path/to/project
  • Then we’ll use Composer and Craft to install the Donkeytail plugin:
composer require simplygoodwork/craft-donkeytail
php craft plugin/install craft-donkeytail

Create a new channel

Now that we’ve installed the Donkeytail plugin we can begin the initial Craft configuration.

As mentioned earlier, for each Donkeytail pin we’d like to display the following:

  • furniture name
  • short description
  • price

We’ll begin by setting up a new channel section, leaving both the Entry URI Format and Template empty.

For now we’ll assume that you’re using Donkeytail in a single site installation, so please ignore the French site.

Donkeytail Pins Channel Section

We’ll setup the Entry type as shown below: 

  • Short description is a plain text field
  • Price is a number field

Set these up as desired using new fields, or reusing existing fields you have already defined.

Entry Type

Create a new field

Let’s set up a new field that we’ll use for dropping Donkeytail into our Craft templates. Since a picture is worth a thousand words we’ll use the image below as a guide for setting up this field.

The Entry Sources field is of most interest, and we’d like to select from the channel we’ve just created which is named Donkey Tail Pins.

Donkeytail Field

Add the Donkeytail field into any existing page.

We’ll drop the newly created Donkeytail field into an existing page. In this example we’ll add it to the homepage (which is a standard single section).

Homepage Section

Add a canvas and your first pin.

Grab an image from Unsplash, or your preferred stock image library, and add it to the Donkeytail field in your homepage.

In Craft 3.7.x you can also add your first pin while editing the homepage, but in earlier versions you may need to add a Donkeytail pin entry first, and then add it to your homepage Donkeytail field.

As you add each pin you’ll see the pin placed directly in the center of the canvas. Use your mouse to drag and drop to the desired position. We positioned our first pin bottom left on the sofa as indicated below.

Lounge with first pin

And finally we can begin coding!

Now that we’ve wired everything up in Craft let’s begin by dropping in the following code into the relevant template.

{% extends '_layout' %}

{% set donkeytail = entry.donkeytail %}
{% set donkeyTailCanvas = donkeytail.canvas ?? false %}

{% block content %}

  <div class="container relative">
    {% if donkeyTailCanvas %}

      {% do donkeyTailCanvas.setTransform({ width: 1024 }) %}
      {{ tag('img', {
        src: donkeyTailCanvas.url,
        width: donkeyTailCanvas.width,
        height: donkeyTailCanvas.height,
        srcset: donkeyTailCanvas.getSrcset(['1.5x', '2x', '3x']),
        alt: 'Lounge',
        class: 'w-full'
      }) }}

      {% for pin in donkeytail.pins %}

        {% set pinTitle = pin.element.title %}
        {% set pinShortDescription = pin.element.shortDescription %}
        {% set pinPrice = pin.element.price %}

        <div class="absolute"
            style="transform:translate(-50%, -50%);
                   top:{{ pin.y }}%;
                   left:{{ pin.x }}%;">

          {# open svg #}
          <svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
            <circle cx="20" cy="20" r="18.75" stroke="#ffffff" stroke-width="2.5"/>
            <path d="M20 10V30" stroke="#ffffff" stroke-width="2.5"/>
            <path d="M10 20H30" stroke="#ffffff" stroke-width="2.5"/>
          </svg>
        </div>
      {% endfor %}

    {% endif %}
  </div>

{% endblock %}

The above code will take care of the basics:

  • placing the Donkeytail canvas on the page
  • iterating each Donkeytail pin and placing an SVG in the appropriate position

The relative class on the container div, and the nested absolute class, are of particular importance here in the successful positioning of the pins.

The style attribute for each pin contains a transform to re-center the SVG so that its center sits on the pin position defined by top and left

Screenshot 01


Directly after the absolutely positioned pin/​svg, let’s add what will become our popover containing the name, description and price of the item.

<div class="absolute z-20 bg-white p-6 w-72 shadow"
      style="top:{{ pin.y }}%; left:{{ pin.x }}%;
            transform:translate(-50%, 60px);">

  {# rotate white square to add a triangle #}
  <div class="absolute bg-white z-10
              -top-5 h-10 w-10 transform rotate-45"
      style="left: 7.75rem;">
  </div>

  <div class="font-bold">{{ pinTitle }}, {{ pinPrice}}USD</div>
  <div class="mt-1">{{ pinShortDescription }}</div>

</div>
Screenshot 02

Alpine JS to the rescue

We don’t want the popover displaying for all our pins, which could lead to a very cluttered image with a lot of the image hidden behind information.

Let’s begin (amending the existing code) to add a little interactivity using a sprinkling of Alpine JS adding:

  • x-data
  • x-show
  • x-on:click


Step 1

Everything in Alpine starts with the x-data directive.

<div x-data="{ selected: '' }"
     class="container relative">
    {% if donkeyTailCanvas %}

Step 2

Set selected to the clicked pinId using x-on:click

<div x-on:click="selected === '{{ pinId }}'
                 ? selected = ''
                 : selected = '{{ pinId }}'"
      class="absolute"
      style="transform:translate(-50%, -50%);
            top:{{ pin.y }}%;
            left:{{ pin.x }}%;">

Step 3

Inside the for loop, if the current pin has been selected show the close svg icon else show the open svg icon.

{# open svg #}
<svg x-show="selected != '{{ pinId }}'" 
      width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
  <circle cx="20" cy="20" r="18.75" stroke="#ffffff" stroke-width="2.5"/>
  <path d="M20 10V30" stroke="#ffffff" stroke-width="2.5"/>
  <path d="M10 20H30" stroke="#ffffff" stroke-width="2.5"/>
</svg>
{# close svg #}
<svg x-show="selected === '{{ pinId }}'"
      width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
  <circle cx="20" cy="20" r="20" stroke="#ffffff" stroke-width="2.5" />
  <path d="M10 20H30" stroke="white" stroke-width="2.5"/>
</svg>

Finally

Only show the popover for the currently selected pin.

{# popover #}
<div x-show="selected === '{{ pinId }}'"
     class="absolute z-20 bg-white p-6 w-72 shadow"
     style="top:{{ pin.y }}%; left:{{ pin.x }}%;
            transform:translate(-50%, 60px);">

That’s it, we’re done folks.

Below is a copy of the full source code created in this post.

{% extends '_layout' %}

{% set donkeytail = entry.donkeytail %}
{% set donkeyTailCanvas = donkeytail.canvas ?? false %}

{% block content %}

  <div class="container relative"
       x-data="{ selected: '' }">
    {% if donkeyTailCanvas %}

      {% do donkeyTailCanvas.setTransform({ width: 1024 }) %}
      {{ tag('img', {
        src: donkeyTailCanvas.url,
        width: donkeyTailCanvas.width,
        height: donkeyTailCanvas.height,
        srcset: donkeyTailCanvas.getSrcset(['1.5x', '2x', '3x']),
        alt: 'Lounge',
        class: 'w-full'
      }) }}

      {% for pin in donkeytail.pins %}

        {% set pinId = pin.element.id %}
        {% set pinTitle = pin.element.title %}
        {% set pinShortDescription = pin.element.shortDescription %}
        {% set pinPrice = pin.element.price %}

      <div x-on:click="selected === '{{ pinId }}'
                        ? selected = ''
                        : selected = '{{ pinId }}'"
            class="absolute"
            style="transform:translate(-50%, -50%);
                  top:{{ pin.y }}%;
                  left:{{ pin.x }}%;">

        {# open svg #}
        <svg x-show="selected != '{{ pinId }}'" 
              width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
          <circle cx="20" cy="20" r="18.75" stroke="#ffffff" stroke-width="2.5"/>
          <path d="M20 10V30" stroke="#ffffff" stroke-width="2.5"/>
          <path d="M10 20H30" stroke="#ffffff" stroke-width="2.5"/>
        </svg>
        {# close svg #}
        <svg x-show="selected === '{{ pinId }}'"
              width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
          <circle cx="20" cy="20" r="20" stroke="#ffffff" stroke-width="2.5" />
          <path d="M10 20H30" stroke="white" stroke-width="2.5"/>
        </svg>

        {# popover #}
        <div x-show="selected === '{{ pinId }}'"
              class="absolute z-20 bg-white p-6 w-72 shadow"
              style="top:{{ pin.y }}%; left:{{ pin.x }}%;
                    transform:translate(-50%, 60px);">

          {# rotate white square to add a triangle #}
          <div class="absolute bg-white z-10
                      -top-5 h-10 w-10 transform rotate-45"
              style="left: 7.75rem;">
          </div>

          <div class="font-bold">{{ pinId }}, {{ pinTitle }}, {{ pinPrice}}</div>
          <div class="mt-1">{{ pinShortDescription }}</div>

        </div>
      </div>
      {% endfor %}

    {% endif %}
  </div>

{% endblock %}