WordPress Plugin Tutorial: Make Your Own Spoiler Shortcode

There are WordPress plugins for almost everything you could imagine. But basing crucial functionality on third parties’ work – especially if they give it away for free – does not give me peace of mind. If the developer abandons the plugin, you’re on your own. If you keep WordPress updated (as you should), plugins might break and you’re left with the choice of fixing it yourself or abandoning the plugin.

My requirements

  • The ability to hide spoilers, and make users show them if they want to.
  • shortcode so I can easily use this functionality in my content.

That’s the essence of it. Notice that I haven’t said anything about how I want the spoilers to work; that’s to be discovered during the process. I do have a fair idea of how it probably must be done, but I don’t feel this belongs in the specs.

The process

In this article I show you how I made this plugin myself, from scratch. I’m by no means a professional programmer, but hopefully this article will make you realize you don’t have to be to be able to do cool things with WordPress – professionally.

You’ll need a working WordPress installation, and it’s easiest if it’s installed locally on your computer. It’s outside the scope of this article to show you how, but my recommendation is that you install MAMP if you’re on a Mac, or WampServer if you’re on Windows.

While typing up this tutorial, I found a few things that could be improved, so what I present here is actually a slightly better version than my first draft – a win-win for both me and you!

Implementing a shortcode

Let’s start at the end of my requirements list, because the shortcode is how I’ll actually mark something up as a spoiler.

The WordPress Codex’ Shortcode API page gives a couple of cuick examples “for those in a hurry”, and we’ll grab the one with attributes and change it to make our first draft of our shortcode code:

<?php
/**
 * @package dag_spoiler
 * @version 0.1
 */
/*
Plugin Name: DaG Spoiler
Plugin URI: http://designerandgeek.com/dag-spoiler
Description: Spoilers!
Author: Designer and Geek
Version: 0.1
Author URI: http://designerandgeek.com/
*/


/**
* Adds a spoiler shortcode to WordPress.
* 
* @return	none	outputs HTML
*/

function dag_spoiler_func( $atts, $content = null ) {
	extract( shortcode_atts( array(
		'title' => 'Click to show spoiler',
	), $atts ) );
	$spoiler = '<div class="dag_spoiler">' .
		'<h2 class="dag_spoiler_header">' . $title . '</h2>' .
		'<div class="dag_spoiler_content">' .
		$content . 
		'</div>' . 
		'</div>';
	return $spoiler;
}
add_shortcode( 'dag_spoiler', 'dag_spoiler_func' );

/*EOF*/
?>

You’ll notice that I’ve prefixed functions, classes and IDs with dag_ (where “dag” is short for “Designer and Geek”). I do this to avoid any potential conflicts with other code, and you should too. In short: Prefix everything! (But you are of course free to substitute another prefix!)

The comments in the beginning of the code are PHPDoc tags, and the next section of comments are metadata for WordPress to pick up, so as long as you place this in your WordPress installation’s plugins folder (/wp-content/plugins/), WordPress will pick it up and display it in the plugins section of the admin interface.

Since this plugin is going to need more than just this one php file – it’ll be accompanied by css and javascript – it’s best to put it in a folder of its own. So make a new folder in the plugins folder with the name dag-spoiler, then save the code above as, for instance, dag-spoiler.php.

If you now check out your plugins section, it should show up there:

Click on Activate to activate it, then try using it by making a new post:

Check out how the post looks (I’m using a stock TwentyEleven theme in this example):

Of course, there’s still no actual hiding of spoilers going on, but we do have a working shortcode! The title attribute of the shortcode correctly becomes a h2 with class dag_spoiler_header, the content wrapped in our shortcode tags is placed inside a div with class dag_spoiler_content, and the whole thing is wrapped in a div with class dag_spoiler – as shown in Chrome’s element inspector:

Adding some style

We need to add some CSS to the HTML code our plugin produces. I’m going for super simple white-on-black headers and a 1px black border and 10px padding around the content. Also, the cursor for the header is set to “pointer”, so that it’s more obvious to users that it’s clickable when the mouse cursor is moved over it.

.dag_spoiler {
	margin: .5em 0 .5em 0;
}

.dag_spoiler h2.dag_spoiler_header {
	background: #000;
	color: #fff;
	cursor: pointer;
	font-size: 1.2em;
	line-height: 1.2em;
	margin: 0;
	padding: .33em;
}

.dag_spoiler div.dag_spoiler_content {
	border: 1px solid black;
	padding: 1em;
}

Again, note that everything is prefixed with dag_ so that the chance of this code clashing with anyone else’s is minimal.

Save this code as dag-spoiler.css inside your plugin folder.

How to include CSS in WordPress

There’s nothing stopping you from modifying the theme’s source code to include a link tag in the header to your plugin’s CSS file – apart from common sense. In general it’s good to live by this rule when it comes to WordPress: Do not modify code that’s not yours!

WordPress have ways of dealing with needs such as ours, namely including your code along with everyone else’s in an orderly fashion. For CSS and Javascript, there is an action hook called wp_enqueue_scripts, to which you supply the name of a function which will register and enqueue your styles and scripts.

For our CSS file, this can look something like this:

/**
 * Enqueue plugin css and javascript
 */
function dag_enqueue() {
    wp_register_style( 'dag-spoiler-style', plugins_url('/dag-spoiler.css', __FILE__) );
    wp_enqueue_style( 'dag-spoiler-style' );
}
add_action( 'wp_enqueue_scripts', 'dag_enqueue' );

wp_register_style lets you assign a handle (the first argument) to your stylesheet, so that you can easily reference it later. The function plugins_url helps find the correct url path to your CSS file by passing it the file name plus FILE, which is a server variable containing the URL to the current script file (i.e. your plugin).

Since you’ve registered a handle for your stylesheet, all you need to pass to wp_enqueue_style is that handle.

Add the above code to your dag_spoiler.php file, reload your test post and it should look something like this:

Sweet! Our stylesheet is properly included, and we haven’t touched so much as a byte of other people’s code.

Hiding spoilers

Since the point of making this spoiler plugin is to hide stuff, we should go ahead and do so. We add display: none; to the content div:

.dag_spoiler div.dag_spoiler_content {
	border: 1px solid black;
	display: none;
	padding: 1em;
}

Showing hidden spoilers with jQuery

To show the content we just hid, we’ll use jQuery, a Javascript library that comes included with WordPress.

It’s outside the scope of this article to explain thoroughly what jQuery is, so if you don’t have a clue, you should click the link and poke around on the jQuery website. But to quote that same website:

jQuery is a fast and concise JavaScript Library that simplifies HTML document traversing, event handling, animating, and Ajax interactions for rapid web development. jQuery is designed to change the way that you write JavaScript.

Not only makes jQuery much of the cool stuff you see on modern websites possible, it even makes it incredibly easy.

First, to make sure jQuery is included on any site your plugin is installed, add wp_enqueue_script("jquery"); to yourdag_enqueue function. If it isn’t already enqueued by some other part of your WordPress setup, now it will be by your plugin.

Then, we’ll go ahead and prematurely register and enqueue the Javascript file that we’re going to make in a minute. Our enqueue function should now look like this:

function dag_enqueue() {
	wp_register_style( 'dag-spoiler-style', plugins_url('/dag-spoiler.css', __FILE__) );
	wp_enqueue_style( 'dag-spoiler-style' );
	wp_enqueue_script( 'jquery' );
	wp_register_script( 'dag-spoiler-js', plugins_url('/dag-spoiler.js', __FILE__) );
	wp_enqueue_script( 'dag-spoiler-js' );
}

Now go ahead and make the Javascript file alongside the PHP and CSS files that are already in your plugin folder.

When ready …

We’ll start it off like this:

jQuery(document).ready( function( $ ) {
	// Spiffy code here!
});

If you’re not used to Javascript, anonymous functions and/or jQuery, all these parens, brackets and semicolons might seem a bit confusing. A little breakdown:

jQuery is, of course, the jQuery object, which contains a huge heap of wizardry in the form of functions. Normally this object is aliased to just $ for brevity. On the outside, we have to call it using its proper name, passing it the document as an argument, and calling its ready event:

jQuery(document).ready();

What do we pass as an argument to ready()? It should be a function that will be run when our document is ready for action. We could write a separate function and pass that:

jQuery(document).ready( function( $ ) {
	// Spiffy code here!
});

In longer, more complicated code, this might even be a preferable way to do it, but since we’re going to write just one relatively short function in this Javascript file, we might as well just write the whole thing inside an anonymous function inside the ready() argument parens.

jQuery passes itself as an argument to handler functions like this, so we alias it to $ in the function argument parens, as is the custom, so that we can reference jQuery as $ inside the function.

Finding our spoilers

One thing jQuery does really jQuery does really well is finding DOM objects. DOM means “Document Object Model” and is the web browser’s internal model of all the elements that makes a web page. Through Javascript it’s possible to manipulate the DOM, and this is where jQuery shines.

Each spoiler on our web pages is wrapped in a div with the class dag_spoiler. If you call jQuery with a string containing a valid CSS selector, it will return every element matching that selector. So let’s see what happens if we search for “.dag_spoiler” elements:

jQuery(document).ready( function( $ ) {
	console.log($(".dag_spoiler"));
});

Save the file, and reload the post. Check out the console (in Chrome on Mac you can show it by selecting View → Developer → JavaScript Console):

Coolness! It finds three elements, which is exactly how many spoilers there are in our test post.

There’s a neat jQuery function (well, really, there are dozens upon dozens of neat jQuery functions!) called each()which you can use to iterate over an array of objects like this. Since $(".dag_spoiler") returns an array, we can just tack each() onto the end. The function expects a function as an argument that will perform some action with each element of the array.

Let’s test this, fetching the header of each spoiler:

jQuery(document).ready( function( $ ) {
	$(".dag_spoiler").each(function(){
		console.log( $(this).children("h2").html() );
	});
});

Save, reload, check out the console:

Inside functions you pass to jQuery to handle stuff, the keyword this refers to the DOM object found. In our case they are of type . But if you try doing jQuery wizardry with those objects, like this.children("h2"), it won’t work. To give them the jQuery magic, pass this to jQuery: $(this)Then you can call children("h2") to fetch all children of HTML element H2, and use html() to fetch the HTML code inside them.

Showing hidden stuff

jQuery has quite a few functions to hide and show stuff, with and without various kinds of animation. I won’t mention them all here; I’ll just pick the one I want to use: slideToggle(). It toggles the visibility of an object, using a sliding animation to show and hide it.

A natural way of showing and hiding spoilers would be by clicking on the title. So we want to hook onto the click event of our headers. That’s easy enough with jQuery’s click() function. As with so many other functions, it takes a function as an argument, and we choose to make it an anonymous one, like we did in the encapsulating ready() function.

jQuery(document).ready( function( $ ) {
	$(".dag_spoiler_header").each(function(){
		$(this).click( function() {
			$(this).siblings(".dag_spoiler_content").slideToggle();
		});
	});
});

Try clicking each header while checking out the console:

Note that Chrome’s console displays identical, consecutive log entries as one single entry with a count in the left margin instead of listing them one by one.

Inserting help texts

Currently there are no clues that the titles are clickable. How about inserting a helpful text after the title? Something like(Click to show). And it should change to (Click to hide) when the spoiler is visible.

It’s a good idea to use jQuery to insert this text, since otherwise it will be visible in the HTML source, in RSS feeds etc.

It’s also a good idea to make this text customisable – both in general (like through settings in the WordPress admin interface) and for each spoiler (through shortcode attributes).

Besides, one should pave the way for internationalization of the plugin. This means using the __() function for translatable strings, and _e() for outputting strings to the browser.

While we won’t be concerned with any of those right now, we’ll at least make sure our plugin is prepared for it.

So we’ll put the help texts in translatable variables in our PHP script, and then find a way to transfer them to Javascript.

Modify the start of dag_spoiler_func() like so (changes shown in bold):

function dag_spoiler_func( $atts, $content = null ) {
	$default_title = __('Spoiler alert!');
	$helptext_show = __('(Click to show)');
	$helptext_hide = __('(Click to hide)');
	extract( shortcode_atts( array(
		'title' => $default_title,
	), $atts ) );

Then the question is how to transfer these variables to Javascript.

Custom data-* attributes in HTML 5

HTML 5 introduces data-* attributes, which makes it possible to add your own attributes to HTML tags while still writing valid HTML. This fits our needs perfectly! We can let the PHP code determine what the text should be (standard text or user submitted, standard or from shortcode attribute, in English or translated), and send it to Javascript on a per-spoiler basis.

Even though we’ll prefix our data attributes with data-, we’ll also add dag-spoiler- to that, since we’ve vowed to prefix everything.

Our modified code for generating spoiler HTML code looks like this:

$spoiler = '<div class="dag_spoiler">' .
	'<h2 class="dag_spoiler_header" data-dag-spoiler-show="' . $helptext_show .
		'" data-dag-spoiler-hide="' . $helptext_hide . '">' . $title . '</h2>' .
	'<div class="dag_spoiler_content">' .
	$content . 
	'</div>' . 
	'</div>';

Yes, the attribute names are long, but there’s also virtually no risk of them clashing with any other plugin, and they are easy to read.

We’ll also have to modify the Javascript to get at these new attributes. That’s as simple as using jQuery’s attr()function:

$(".dag_spoiler_header").each(function(){
	var show_text = $(this).attr('data-dag-spoiler-show');
	var hide_text = $(this).attr('data-dag-spoiler-hide');

Then, we append() a span to the end of the spoiler header:

$(this).append(' <span class="dag_spoiler_help_text">'+show_text+'</span>');

(Note the space before the span tag!)

And we add some style to our CSS file so the help text isn’t as prominent as the rest of the header:

.dag_spoiler_help_text {
	font-size: .67em;
	color: #999;
	white-space: nowrap;
}

white-space: nowrap; makes sure the help text won’t be split over two lines if the title is too long; instead, the whole help text will be put on the next line.

So now it should look something like this:

Next, we’ll tackle changing the help text when the spoiler visibility is toggled.

Using the callback function

The slideToggle() function may take three arguments. The first is the animation duration (either as milliseconds or as a string: “fast” (=200ms) or “slow” (=600ms)). We’re going to go with the “fast” setting.

The second (optional) argument is easing, which specifies the speed at which the animation progresses at different points within the animation (and which we’ll ignore since we’re happy with the default easing).

The third argument is a callback function, which is fired when the animation is complete. We’re going to use the callback function to change the help text.

Here’s the new click event handler:

$(this).click( function() {
	var help_text_span = $(this).children('span.dag_spoiler_help_text');
	$(this).siblings('.dag_spoiler_content').slideToggle( "fast", function() {
		help_text_span.html(
			help_text_span.html() == hide_text ? show_text : hide_text
		);
	});
});

First, we put a reference to the span element that we added (just before the click() function) in the variablehelp_text_span. Then, we set the animation speed to “fast” and provide an anonymous callback function.

In this function, we use the function html(). This function can either, with no arguments, return the HTML content inside the tags of the element (i.e. our help text inside the span tags), or set/replace the content of the element. So we the first form inside the second form in a conditional operator to determine which text we should switch to. Is it already the hide text, we switch to the show text, and vice versa.

Adding custom help texts

Since we’ve now thought ahead, adding support for custom help texts is easy-peasy. Just modify the start of our PHP function like this:

function dag_spoiler_func( $atts, $content = null ) {
	$default_title = __('Spoiler alert!');
	$default_helptext_show = __('(Click to show)');
	$default_helptext_hide = __('(Click to hide)');
	extract( shortcode_atts( array(
		'title' => $default_title,
		'helptext_show' => $default_helptext_show,
		'helptext_hide' => $default_helptext_hide,
	), $atts ) );

Done!

Congratulations! You’ve now made yourself a fully functional spoiler shortcode plugin for WordPress, and it’s even ready for internationalization.

And of course I’ve installed the plugin on this site, so here’s a live demo:

Secret stuff!

Booo!

Things to do next

Here are some ideas for further refinement of this plugin. I might follow up with articles along these lines at some point.

  • Comment and document the plugin properly.
  • Translate the plugin to another language.
  • Make an admin interface to customize the appearance (CSS) of spoilers.
  • Upload the plugin to a server so you can install it on several sites and keep them all updated.
  • http://www.nicoblog-games.com/ Nico

    Hmm…so much code for a simple feature, i guess i’ll go for a plugin, thanks though.