For a project that I am currently doing, they had some great ideas for great user experience throughout the site. One of them included a game, so I created a Tetris Shortcode (all future changes will only be maintained in the Github repository, not in the gist) where they could place the game anywhere they wanted. So, for your pleasure, enjoy!
How I Built It
I found a JavaScript library called blockrain.js that I refactored, documented, and extended. If you were to compare the original library and mine, there are some substantial differences. Essentially, I made these changes:
- Renamed blockrain.jquery.js to jquery.blockrain.js according to standard jQuery plugin naming convention and standards
- Attempted (i.e., not tested) to add touch support with jgestures.
- Added a preview of the next block and a onPreview API.
- Refactored the ShapeLibrary to be faster and not to reprocess for each next block.
- Extended keyPressHandler to execute onKeyPress for unsupported keys.
Below I will describe how I built the WordPress Shortcode plugin.
Plugin File
First, I started with a basic plugin header:
/** | |
* Plugin Name: Tetris | |
* Plugin URI: http://wpsmith.net/ | |
* Description: Adds tetris game shortcode. | |
* Version: 0.0.1 | |
* Author: Travis Smith, WP Smith | |
* Author URI: http://wpsmith.net | |
* Text Domain: tetris | |
* | |
* @copyright 2015 | |
* @author Travis Smith | |
* @link http://wpsmith.net/ | |
* @license http://www.gnu.org/licenses/old-licenses/gpl-2.0.html | |
* | |
* This program is distributed in the hope that it will be useful, | |
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | |
* | |
*/ |
Next, I added basic security that I add to every plugin base file (and probably should add on every file) to prevent direct access to the file.
// If this file is called directly, abort. | |
if ( ! defined( 'WPINC' ) ) { | |
die; | |
} |
Then, I add my three basic constants that I add to every plugin I build.
define( 'TETRIS_VERSION', '0.0.1' ); | |
define( 'TETRIS_SLUG', 'tetris' ); | |
define( 'TETRIS_FILE', __FILE__ ); |
I do not really use
TETRIS_VERSION
and probably could easily remove it, but I add it out of habit. TETRIS_SLUG
serves as both the plugin slug and my text domain. TETRIS_FILE
is a constant that I use to access __FILE__ of the root/main plugin file instead of having to worry about adding extra functions around __FILE__ on other files to get back to that root file reference.
Now, I add my auto-loader function. Essentially this function checks to see if the class does not exist and that the class name has a Tetris_
prefix. I name all my class files the same as my class contained within the file. If it meets those conditions, then I include the file.
spl_autoload_register( 'tetris_autoload' ); | |
/** | |
* SPL Class Autoloader for classes. | |
* | |
* @param string $class_name Class name being autoloaded. | |
* @link http://us1.php.net/spl_autoload_register | |
* @author Travis Smith | |
* @since 0.1.0 | |
*/ | |
function tetris_autoload( $class_name ) { | |
// Do nothing if class already exists, not prefixed WPS_ | |
if ( class_exists( $class_name, false ) || | |
( !class_exists( $class_name, false ) && false === strpos( $class_name, 'Tetris_' ) ) ) { | |
return; | |
} | |
// Set file | |
$file = plugin_dir_path( __FILE__ ) . "includes/classes/$class_name.php"; | |
// Load file | |
if ( file_exists( $file ) ) { | |
include_once( $file ); | |
} | |
} |
Normally, I add code for plugin activation and deactivation as well as for loading the text domain, but I am going to skip that for this post. For the purposes of the scope of the plugin, we are only instantiating the Shortcode class.
new Tetris_Shortcode(); |
Shortcode Class
Class Variables
The shortcode class has 3 variables: $debug
, $scripts_registered
, and $scripts_enqueued
. These three variables are for tracking whether the site is in debug mode, set by WP_DEBUG
and/or SCRIPT_DEBUG
. The other two variables track whether the scripts have already been registered and/or enqueued to ensure that the class does not add them again.
The Constructor
Once the plugin has been instantiated, the Shortcode class adds the shortcode, sets the $debug
variable, and registers the scripts on the init
hook.
/** | |
* Constructor | |
*/ | |
public function __construct() { | |
// Add the shortcode | |
add_shortcode( 'tetris', array( $this, 'shortcode' ) ); | |
// Set debug class var. | |
$this->debug = ( ( defined( 'WP_DEBUG' ) && WP_DEBUG ) || ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ); | |
// Register scripts!! | |
add_action( 'init', array( $this, 'register_scripts' ), 11 ); | |
} |
Registering the Scripts/Styles
This is really tricky with shortcodes primarily because the shortcode could or could not be used within the displayed page (content/post type agnostic). With shortcodes, I always register the scripts at the init
hook, just like core. The really cool part of how I register scripts is that I do it dynamically: if in debug mode, use the debug script (the non-minified file) otherwise, use the minified script (*.min.js
). What is more, I also do not use the TETRIS_VERSION
constant created earlier for my script versions any more. Call me lazy, but now I use filemtime()
. This bursts the cache every time I change the file, so I never have to worry about caching with my scripts/styles.
Registering Scripts/Styles Dynamically
The key to dynamically registering the script is my $debug
variable which uses the $debug
class variable.
$suffix = $this->debug ? '.js' : '.min.js'; |
Next is the portion that actually registers the jgestures
, blockrain
, and blockrain-init
scripts.
wp_register_script( | |
'jgestures', | |
plugins_url( "js/jgestures$suffix", TETRIS_FILE ), | |
array( 'jquery', ), | |
filemtime( plugin_dir_path( TETRIS_FILE ) . "js/jgestures$suffix" ), | |
false | |
); | |
wp_register_script( | |
'blockrain', | |
plugins_url( "js/jquery.blockrain$suffix", TETRIS_FILE ), | |
array( 'jquery', 'jquery-ui-widget', 'jgestures', ), | |
filemtime( plugin_dir_path( TETRIS_FILE ) . "js/jquery.blockrain$suffix" ), | |
false | |
); | |
wp_register_script( | |
'blockrain-init', | |
plugins_url( "js/blockrain.init$suffix", TETRIS_FILE ), | |
array( 'blockrain', ), | |
filemtime( plugin_dir_path( TETRIS_FILE ) . "js/blockrain.init$suffix" ), | |
false | |
); |
*Note the use of
filemtime()
.
Also, when I registered the blockrain
script, notice that I set jquery, jquery-ui-widget, and jgestures as dependent scripts, and when I registered the blockrain-init
script, notice that I set blockrain
as its dependent. This is extremely important later.
array( 'jquery', 'jquery-ui-widget', 'jgestures', ), |
array( 'blockrain', ), |
Next, I need to localize some scripts because blockrain
has a couple string variables.
$args = array( | |
'playText' => __( 'Let\'s play some Tetris', TETRIS_SLUG ), | |
'playButtonText' => __( 'Play', TETRIS_SLUG ), | |
'gameOverText' => __( 'Game Over', TETRIS_SLUG ), | |
'restartButtonText' => __( 'Play Again', TETRIS_SLUG ), | |
'scoreText' => __( 'Score', TETRIS_SLUG ), | |
); | |
wp_localize_script( 'blockrain-init', 'blockrainI10n', $args ); |
Finally, I register my style similarly to my scripts.
$suffix = $this->debug ? '.css' : '.min.css'; | |
wp_register_style( | |
'blockrain', | |
plugins_url( "css/blockrain$suffix", TETRIS_FILE ), | |
null, | |
filemtime( plugin_dir_path( TETRIS_FILE ) . "css/blockrain$suffix" ) | |
); |
Enqueuing Scripts
My next method enqueues the scripts with only two lines. These two lines output all the scripts because blockrain-init depends on blockrain which depends on jquery, jquery-ui-widget, and jgestures. So, WordPress's script manager class will output the scripts in the proper order:
- jquery
- jquery-ui-widget
- jgestures
- blockrain
- blockrain-init
/** | |
* Output the scripts for WordPress | |
* | |
* @uses wp_enqueue_script | |
*/ | |
public function enqueue_scripts() { | |
if ( $this->scripts_enqueued ) { | |
return; | |
} | |
// Ensure scripts are registered | |
$this->register_scripts(); | |
/* SCRIPTS */ | |
wp_enqueue_script( 'blockrain-init' ); | |
/* STYLES */ | |
wp_enqueue_style( 'blockrain' ); | |
// Prevent redundant calls | |
$this->scripts_enqueued = true; | |
} |
The Shortcode
Now, I have my scripts registered and I have my enqueue (script output) method. For my shortcode, I only want to accept two arguments: height and weight, which I will parse and merge using shortcode_atts()
, noting that someone can filter these options with shortcode_atts_tetris
filter.
$atts = shortcode_atts( array( | |
'height' => '100%', | |
'width' => '100%' | |
), $atts, 'tetris' ); |
Next, I want to output my scripts.
$this->enqueue_scripts(); |
Then I return my HTML markup.
return sprintf( '<div class="tetris-game" style="max-width:%s; max-height:%s;"></div>', $atts['width'], $atts['height'] ); |
For those who do not know what sprintf() does, let me explain it briefly. Simply, it takes the string and locates various sub-strings (e.g.,
%s
, %d
, %1$s
, %1$d
, etc.). The %s
replacement does a simple validation and replaces the %s
sub-string with only a string or an empty string. Likewise, %d
does numbers. If you see 1$
, 2$
, etc. between %s
or %d
(etc), that is identifying the index of the following parameters. For example, I could have written that line as:
return sprintf( '', $atts['width'], $atts['height'] );
So, 1$
refers to $atts['width']
and 2$
refers to $atts['width']
, both which must be strings.
That completes the shortcode. Again, all future changes will only be maintained in the Github repository, Tetris Shortcode, not in the gist found in this post.
Remaining Work
- Load the text domain
- Activation/Deactivation code
- Ensure that the Shortcode class can only be instantiated once as a true singleton.