viernes, 19 de junio de 2015

Shortcode generator for WordPress

Shortcode generator for WordPress


Shortcodes

Everybody has seen them – those bracketed tags you put in your post or page that later transform in something fancy. They’ve been around since WordPress 2.5 and are pretty much everywhere these days. That’s why I’ll keep the introductory part short and instead I’ll concentrate on something interesting – a hard to produce by hand shortcode and a generator UI for easing the pain.

The Shortcode API

I’ll just summarize what you can find in the codex.

Three flavours of shortcodes

The most basic shortcode is this: [my_shortcode]. It has just a name and nothing else. But let’s change the name to something more meaningful. For example, [author]. We want the author shortcode to show some information about the current post’s author.
But what if the post was co-authored? We have to add some way of selecting which author we want to display. Not a problem, the shortcode API allow us to have parameters: [author id="4"].
Hmm… You don’t think this author’s description fits the current post’s theme? Well, we can add some description in the shorcode’s content: [author id="4"]This is the most freaking awesome author you can find in the universe. What an honour that he has written a post about peanuts[/author]
It look really similar to BBCode – you have a name of the shortcode, some attributes, and you may have a content which you’d need to enclose in the shortcode in a way similar to what HTML looks like.

Implementing the author shortcode

For this part I’ll, uhm… “borrow” Greg’s design for an author box  I won’t provide a detailed explanation of it, so please check his tutorial if you have any problems with the HTML/CSS part. I’ll just change the way it’s displayed in posts – instead of forcing it before the_content(), it will be accessed via a shortcode – the one I used to show you the shortcode syntax.
We’ll start with a basic function that is pretty much the most common way to start implementing a shortcode.
function sc_author($atts, $content = null, $code) {
        extract(shortcode_atts(array(
                'id' => get_the_author_meta('ID')
        ), $atts));

        $data = <<<HTML
HTML;

        return $data;
}
add_shortcode('author', 'sc_author');
Several things happen here. First, the add_shortcode call – it takes two parameters – the name of the shortcode and the function that does the job. Three arguments will be passed to this function:
1. the first is an associative array of the attributes, starting at $atts[1]. $atts[0] may hold the matched shortcode regex if it’s different from the callback name. You don’t have to worry about the $atts array actually, there’s a function that processes it, so it can become useful for you.
1. the second argument is the content of the shortcode. If the shortcode doesn’t have any content – this argument will be null.
1. the third argument is the shortcode’s name – in our example this will be “author”. We don’t need this, but I wanted to show you that it’s there.
Now, in the function you see a call to shortcode_atts(). This function takes two arguments. The first argument is an associative array of the default attributes for the shortcode. In our case, we want this to be set to the ID of the author of the current post by default. The second argument are the attributes that are actually passed to the shortcode. If an attribute is present in $atts, it’s default will be substituted. It is important to note that you must list all attributes in the defaults array, otherwise they will be ignored.
The last part of the function is a heredoc string for $data. We then return $data – the shortcode will be substituted with what sc_author() returns. Do not echo anything, because the shortcode processing takes place before the content is printed, so an echo inside sc_author() will mess things up.
Now that we have the ID of the desired author, we can add the HTML:
function sc_author($atts, $content = null, $code) {
        extract(shortcode_atts(array(
                'id' => get_the_author_meta('ID')
        ), $atts));

        $avatar = get_avatar( get_userdata($id)->user_email, '140', '' );
        $description = is_null($content) ? get_userdata($id)->user_description : $content;

        $data = <<<HTML
<div id=”author-box">
    <div id=”author-avatar”>{$avatar}</div>
    <div id=”author-content”>
        <h3>About me</h3>
        <p>{$description}</p>
    </div>
</div>
HTML;

        return $data;
}
add_shortcode('author', 'sc_author');
And that’s basically it. We get the avatar and the default description using the get_userdata() function. If our shortcode has any $content, we use this instead of the default.
Just add this css and you should have your author box working:
#author-box {
    background-color: #f9f9f9;
    -moz-box-shadow: 3px 3px 0 #cccccc;
    -webkit-box-shadow: 3px 3px 0 #cccccc;
    box-shadow: 3px 3px 0 #cccccc;
    border: 1px solid #e0e0e0;
    width: 600px;
    height: 160px;
}
#author-avatar {
    padding: 10px;
    float: left;
}
#author-content {
    padding: 10px 10px 10px 0;
}
#author-content h3 {
    font-size: 22px;
    color: #444444;
    padding-bottom: 10px;
}

#author-content p {
    font-size: 13px;
    color: #606060;
    line-height: 18px;
}

Something more serious – a pricing list

Now that we have started “borrowing”  ideas, we’ll try to create something like this pricing list: http://basecamphq.com/signup
Here are our requirements:
  • three to five plans
  • each plan has these options:
    • title
    • price
    • description
    • button link
    • button text
    • featured (boolean, only one plan should be “featured”)

Design

Since this is a shortcodes tutorial, I’ll skip the design part and concentrate on the back end. However, we need some minimal design, so I’ve created something that should work in modern browsers:

HTML:

<div class="pricing col-3 clearfix">
    <div class="price">
        <h2>small</h2>
        <h3>2.99/month</h3>
        <div class="description">some text here</div>
        <a href="button link" title="button text" class="button">button text</a>
    </div>
    <div class="price featured">
        <h2>medium</h2>
        <h3>5.99/month</h3>
        <div class="description">some text here</div>
        <a href="button link" title="button text" class="button">button text</a>
    </div>
    <div class="price">
        <h2>large</h2>
        <h3>9.99/month</h3>
        <div class="description">some text here</div>
        <a href="button link" title="button text" class="button">button text</a>
    </div>
</div>

CSS:

.pricing { margin-top: 30px }

.pricing.col-3 .price { width: 32%; }
.pricing.col-3 .price.featured { width: 36%; }
.pricing.col-4 .price { width: 24%; }
.pricing.col-4 .price.featured { width: 28%; }
.pricing.col-5 .price { width: 19%; }
.pricing.col-5 .price.featured { width: 24%; }

.pricing .price {
    position: relative;
    float: left;
    text-align: center;
    color: #444;
    height: 250px;
    border: 1px solid #444;
    -moz-box-sizing: border-box;
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
    background: #f0f0f0;
    z-index: 5;
    border-right: 0;
    padding: 10px 0;
    font-family: sans-serif;
}

.pricing .price.featured {
    height: 300px;
    margin-top: -25px;
    background: #fff;
    z-index: 10;
    border-right: 1px solid #444;
    margin-right: -1px;
}

.pricing .price:last-child {
    border-right: 1px solid #444;
}

.pricing h2 {
    font-size: 34px;
    margin-bottom: 10px;
}

.pricing h3 {
    font-size: 26px;
    margin-bottom: 10px;
}

.pricing .description {
    border-top: 1px solid #ccc;
    padding: 10px;
}

.pricing .button {
    display: inline-block;
    position: absolute;
    bottom: 10px;
    left: 10%;
    right: 10%;
    text-decoration: none;
    color: #fff;
    background: #77b753;
    border-radius: 5px;
    padding: 5px;
    border: 1px solid #6ea94c;
    -moz-box-shadow: 1px 0 2px #6ea94c;
    -webkit-box-shadow: 1px 0 2px ##6ea94c;
    box-shadow: 1px 0 2px #6ea94c;
}

.pricing .featured .button {
    bottom: 25px;
    padding: 10px;
    font-weight: bold;
}

.pricing .button:hover {
    background: #83c95b;
}
Here’s what it looks like:


What should the shortcode look line the the editor?

Here’s what I will use.
[pricing]
    [price title="small" featured="false" amount="2.99/month" button_link="#" button_text="button text"]some text here[/price]
    [price title="medium" featured="true" amount="5.99/month" button_link="#" button_text="button text"]some text here[/price]
    [price title="large" featured="false" amount="9.99/month" button_link="#" button_text="button text"]some text here[/price]
[/pricing]
Nothe that I haven’t specified how many [price][/price] blocks are there. I’m going to parse them with a regex anyway, so I don’t need to expicitly state how many of them are in the shortcode. This way it will be easier to edit the shortcode.

The shortcode function

We start with something simple:
function sc_pricing($atts, $content = null, $code) {
    if(is_null($content)) return;

}
add_shortcode('pricing', 'sc_pricing');
Note that the actual shortcode is pricing, not the price blocks that are in [pricing]‘s content. We have to parse the price blocks manually. The following regex should match a price block:
preg_match_all('/\[price\b([^\]]*)\]((?:(?!\[\/price\]).)+)\[\/price\]/s', $content, $matches);

$sub_atts = $mathes[1];
$sub_contents = $matches[2];

$price_count = sizeof($sub_atts);
We then save the matched attributes in $sub_atts and the matched contents in $sub_contents. The length of the attributes array is the number of [price] blocks.
Here’s what we’ll start at:
function sc_pricing($atts, $content = null, $code) {
    if(is_null($content)) return;

    preg_match_all('/\[price\b([^\]]*)\]((?:(?!\[\/price\]).)+)\[\/price\]/s', $content, $matches);

    $sub_atts = $mathes[1];
    $sub_contents = $matches[2];

    $price_count = sizeof($sub_atts);

    ob_start();
?>

<?php
    return ob_get_clean();
}
add_shortcode('pricing', 'sc_pricing');
I use output buffering, because it looks ugly if I wrap everything in strings. Now we just have to put our content inside it.
Let’s begin by adding our static example, just to see if it works:
function sc_pricing($atts, $content = null, $code) {
    if(is_null($content)) return;

    preg_match_all('/\[price\b([^\]]*)\]((?:(?!\[\/price\]).)+)\[\/price\]/s', $content, $matches);

    $sub_atts = $matches[1];
    $sub_contents = $matches[2];

    $price_count = sizeof($sub_atts);

    ob_start();
?>
    <div class="pricing col-3 clearfix">
        <div class="price">
            <h2>small</h2>
            <h3>2.99/month</h3>
            <div class="description">some text here</div>
            <a href="button link" title="button text" class="button">button text</a>
        </div>
        <div class="price featured">
            <h2>medium</h2>
            <h3>5.99/month</h3>
            <div class="description">some text here</div>
            <a href="button link" title="button text" class="button">button text</a>
        </div>
        <div class="price">
            <h2>large</h2>
            <h3>9.99/month</h3>
            <div class="description">some text here</div>
            <a href="button link" title="button text" class="button">button text</a>
        </div>
    </div>
<?php
    return ob_get_clean();
}
add_shortcode('pricing', 'sc_pricing');
If you get something like the screenshot above – you’re good to go.
Now, we don’t need three .price divs, we need a loop:
<div class="pricing col-3 clearfix">
    <?php for($i=0; $i<$price_count; $i++): ?>
        <div class="price">
            <h2>small</h2>
            <h3>2.99/month</h3>
            <div class="description">some text here</div>
            <a href="button link" title="button text" class="button">button text</a>
        </div>
    <?php endfor ?>
</div>
That’s kind of a progress, but we show the same content three times now. We have to populate it with the right stuff:
<div class="pricing col-3 clearfix">
    <?php for($i=0; $i<$price_count; $i++): ?>
        <?php $sub_atts[$i] = shortcode_parse_atts($sub_atts[$i]); ?>
        <div class="price">
            <h2><?php echo $sub_atts[$i]['title'] ?></h2>
            <h3><?php echo $sub_atts[$i]['amount'] ?></h3>
            <div class="description"><?php echo $sub_contents[$i]?></div>
            <a href="<?php echo $sub_atts[$i]['button_link']?>" title="<?php echo $sub_atts[$i]['button_text']?>" class="button"><?php echo $sub_atts[$i]['button_text']?></a>
        </div>
    <?php endfor ?>
</div>
Notice the shortcode_parse_atts() function – you pass a string of shortcode-like attributes to it and it returns a nice associative array.
Just two more things remaining – adding the correct column count and adding a featured class to the “featured” price. Here’s the final version of our shortcode functions with these two implemented:
function sc_pricing($atts, $content = null, $code) {
    if(is_null($content)) return;

    preg_match_all('/\[price\b([^\]]*)\]((?:(?!\[\/price\]).)+)\[\/price\]/s', $content, $matches);

    $sub_atts = $matches[1];
    $sub_contents = $matches[2];

    $price_count = sizeof($sub_atts);

    ob_start();
?>
    <div class="pricing col-<?php echo $price_count?> clearfix">
        <?php for($i=0; $i<$price_count; $i++): ?>
            <?php $sub_atts[$i] = shortcode_parse_atts($sub_atts[$i]); ?>
            <div class="price <?php if($sub_atts[$i]['featured'] == 'true') echo 'featured'?>">
                <h2><?php echo $sub_atts[$i]['title'] ?></h2>
                <h3><?php echo $sub_atts[$i]['amount'] ?></h3>
                <div class="description"><?php echo $sub_contents[$i]?></div>
                <a href="<?php echo $sub_atts[$i]['button_link']?>" title="<?php echo $sub_atts[$i]['button_text']?>" class="button"><?php echo $sub_atts[$i]['button_text']?></a>
            </div>
        <?php endfor ?>
    </div>
<?php
    return ob_get_clean();
}
add_shortcode('pricing', 'sc_pricing');
# Shortcode generator
I have to admit that long shortcodes like the one I’ve shown you are difficult or at least slow to type by hand. And while I can’t read your mind and write it for you, we can ease the pain by implementing a generator. But because we’re all lazy, I’ll use the SmartMetaBox class that I’ve shown you a couple of weeks ago to generate the shortcode generator 
As a prerequisit, you have to get familiar with the SmartMetaBox tutorial link, because I won’t get into detail about the generator’s API in this tutorial.
We’ll start with a class ShortcodeGenerator which extends SmartMetaBox. We don’t need a save function, so we will omit it. Furthermore, we have to change the render function so that it prefixes the field’s ids and names with the id of the meta box – this way we will avoid collisions between different generator instances. We will also add a “Generate” button which will send the shortcode to the editor. And last, we will create a simple helper function “add_shortcode_generator” which will be similar to “add_smart_meta_box”, but it will work with ShortcodeGenerator.
Here’s what our new class looks like:
/**
 * A really basic shortcode generator which uses parts of SmartMetaBox
 *
 * @author: Nikolay Yordanov <me@nyordanov.com> http://nyordanov.com
 * @version: 1.0
 *
 */

class ShortcodeGenerator extends SmartMetaBox {
    // create meta box based on given data

    public function __construct($id, $opts) {
        if (!is_admin()) return;
        $this->meta_box = $opts;
        $this->id = $id;
        add_action('add_meta_boxes', array(&$this,
            'add'
        ));
    }

    // Callback function to show fields in meta box

    public function show($post) {
        echo '<table class="form-table">';
        foreach ($this->meta_box['fields'] as $field) {
            extract($field);
            $id = $this->id .'-'. $id;

            $value = isset($field['default']) ? $default : '';

            echo '<tr>', '<th style="width:20%"><label for="'.$id.'">'.$name.'</label></th>', '<td>';

            include dirname(__FILE__)."/../smart_meta_box/smart_meta_fields/$type.php"; // in my example this is where the field templates are

            if (isset($desc)) {
                echo '&nbsp;<span class="description">' . $desc . '</span>';
            }
            echo '</td></tr>';
        }
        echo '</table>';

        echo '<a href="#" class="generate-shortcode button">'.__('Generate').'</a>';
    }
};

function add_shortcode_generator($id, $opts) {
    new ShortcodeGenerator($id, $opts);
}
And here’s how to use it with the pricing shortcode:
include 'smart_meta_box/SmartMetaBox.php';
include 'shortcode_generator/ShortcodeGenerator.php';

function register_sh_gen() {

    $fields = array(
        array(
            'name' => 'Pricing options',
            'id' => 'pricing-options',
            'type' => 'select',
            'default' => 3,
            'options' => array(
                3=>3,4,5
            ),
        )
    );

    for($i=1; $i<=5; $i++) {
        $prefix = "Price $i - ";

        $fields[] = array(
            'name' => $prefix.'Title',
            'id' => 'title-'.$i,
            'type' => 'text',
        );

        $fields[] = array(
            'name' => $prefix.'Amount',
            'id' => 'amount-'.$i,
            'type' => 'text',
        );

        $fields[] = array(
            'name' => $prefix.'Featured?',
            'id' => 'featured-'.$i,
            'type' => 'checkbox',
            'default' => false
        );

        $fields[] = array(
            'name' => $prefix.'Button link',
            'id' => 'button_link-'.$i,
            'type' => 'text',
        );

        $fields[] = array(
            'name' => $prefix.'Button text',
            'id' => 'button_text-'.$i,
            'type' => 'text',
        );

        $fields[] = array(
            'name' => $prefix.'Description',
            'id' => 'decription-'.$i,
            'type' => 'textarea',
        );
    }

    add_shortcode_generator('pricing-shortcode', array(
        'title' => 'Pricing shortcode',
        'pages' => array('post', 'page'),
        'context' => 'normal',
        'priority' => 'high',
        'fields' => $fields
    ));

}
add_action('admin_init', 'register_sh_gen');
Now, all this does is render some not so pretty meta box:
meta
Now, we have several things to do here. First, the “pricing options” select should hide or show the correct number of options (notice how in the shortcode it says 3, but all five groups are shown – we have to change that). Second. I’d like to have some css for the textareas and maybe for the “Generate” button. This tutorial is more about the general idea and not about how thins look, but it about ten lines, so I don’t see a problem with adding it. And third, the “Generate” button should, well, generate the shortcode.
We’ll start by adding these two lines inside add_shortcode_generator(). This way we won’t load them if there’s no generator on the page. Note that add_smart_meta_box() and add_shortcode_generator() are not supposed to be called before the admin_init action has fired and with these additions, they should not be called after admin_print_styles, too.
wp_enqueue_style('shortcode-generator', get_bloginfo('template_directory').'/shortcode_generator/generator.css');
wp_enqueue_script('shortcode-generator', get_bloginfo('template_directory').'/shortcode_generator/generator.js', array('jquery'), false, true);
And here are the contents of generator.css:
.generate-shortcode {
    margin: 20px 0 10px 0;
    display: block;
    text-align: center;
}

#pricing-shortcode .form-table textarea {
    width: 100%;
}

#pricing-shortcode .form-table tr:nth-child(6n+1) {
    border-bottom: 1px solid #ddd;
}

#pricing-shortcode .form-table tr:nth-child(n+20) {
    display: none;
}
The most important part of this code are the last two selectors. The first one adds a border after each pricing group. The second hides the settings for the fourth and fifth pricing groups. We will show these with javascript.
Let’s start with making the “Pricing options” dropdown work as intended. The basic idea is really simple. On change of the select, we show every option and then we hide the redundant ones. Here’s how to do it:
(function($, undefined) {

$(function() {
    $('#pricing-shortcode-pricing-options').change(function() {
        var hide = $(this).val()*6+2;

        $('#pricing-shortcode .form-table tr').show();
        $('#pricing-shortcode .form-table tr:nth-child(n+'+hide+')').hide();
    });
});

})(jQuery);
And now the last part of the javascript that we need is to add the code which generated the shortcode. We start with a simple event handler:
$('#pricing-shortcode .generate-shortcode').click(function(e) {
    var count = $('#pricing-shortcode-pricing-options').val();
    var result = '[pricing] ';

    // we'll put a loop here iterating over the pricing options

    result += ' [/pricing]';
    send_to_editor("\n"+result+"\n");
    e.preventDefault();
});
We will also create a simple helper function so that we won’t need to write the full ids of the elements:
var get_price_option = function(name, num) {
    var el = $('#pricing-shortcode-'+name+'-'+num);

    return el.is(':checkbox')? el.is(':checked') : el.val();
}
And now, the loop. I have commented the important parts, but it’s pretty straightforward – get some values and stringify them 
var opts = ['title', 'amount', 'featured', 'button_link', 'button_text'];

for(i=1; i<=count; i++) {
    var sub_result = ' [price';

    for(j in opts) {
        sub_result += ' '+opts[j]+'="'+get_price_option(opts[j], i)+'"'; // we add a key="val" attribute for each setting
    }

    sub_result += '] ' + get_price_option('description', i) + ' [/price]'; // and as content we add the descriptions

    result += sub_result;
}
And here is the completed generator.js:
(function($, undefined) {

$(function() {
    $('#pricing-shortcode-pricing-options').change(function() {
        var hide = $(this).val()*6+2;

        $('#pricing-shortcode .form-table tr').show();
        $('#pricing-shortcode .form-table tr:nth-child(n+'+hide+')').hide();
    });

    var get_price_option = function(name, num) {
        var el = $('#pricing-shortcode-'+name+'-'+num);

        return el.is(':checkbox')? el.is(':checked') : el.val();
    }

    $('#pricing-shortcode .generate-shortcode').click(function(e) {
        var count = $('#pricing-shortcode-pricing-options').val();
        var result = '[pricing]';
        var opts = ['title', 'amount', 'featured', 'button_link', 'button_text'];

        for(i=1; i<=count; i++) {
            var sub_result = ' [price';

            for(j in opts) {
                sub_result += ' '+opts[j]+'="'+get_price_option(opts[j], i)+'"';
            }

            sub_result += '] ' + get_price_option('description', i) + ' [/price]';

            result += sub_result;
        }

        result += ' [/pricing]';
        send_to_editor("\n"+result+"\n");
        e.preventDefault();
    });
});

})(jQuery);
Well, we’re pretty much ready. This code is not so useful if you have multiple shortcodes that need to have a generator, but what I wanted to do is to explain some basic concepts. Feel free to modify the code for your needs and ask questions, if any. As with SmartMetaBox, I have uploaded the code to github.

No hay comentarios:

Publicar un comentario