Using WordPress Pointers in Your Own Plugins

May I have your attention please?

WordPress pointers - Man by lake points to something specificDoes it annoy you when someone says that? Maybe so, but every now and then they actually have something worthwhile to say. That’s how a lot of people feel about WordPress Pointers that serve to draw attention to new features users may see in core releases.

Can I borrow your sign?

Perhaps the WordPress core crew knew how dangerous these pointers could be in the wrong hands, as they haven’t encouraged developers to use them. There isn’t even an entry in the WordPress Codex for how these things work. Indeed, overuse of pointers would rank right up there with ad pop-ups for me, so I understand the hesitation. However, there’s nothing stopping intrepid developers from using the same feature pointer methods in their own plugins and themes. It’s fairly easy! Let’s see how it’s done.

The Simplenote plugin

WordPress Pointers -- A pointer draws attention to a newly-activated plugin in the WordPress dashboard
Example WordPress Pointer

For this short tutorial, I made a small plugin to play around with. I call it “Simplenote,” and it:

  1. Provides a custom field on every posting page, where notes can be entered.
  2. Allows setting the notes to automatically be entered at the beginning or end of the post’s main content upon display.
  3. Allows this automatic display to be turned off.

While not particularly useful, we use the plugin as an example for WordPress Pointers, which we add to:

  1. The “Plugins” menu, warning the user of new plugin settings for “Simplenote.”
  2. The Simplenote custom field box on the edit screen, drawing attention to the new feature.

You can download the Simplenote plugin here to follow along and experiment.

Plan ahead before you start shouting

If you’re going to start popping up balloons to try grabbing users’ attention, you’d better plan things out first. Before you do anything else, decide what features–if any–are worth shouting about when a user activates your plugin or theme. Try to put yourself in their shoes, and ask “Is that really something I would have missed in the normal use of this product?”

Make a list, check it twice

For each pointer, you should decide and document the following:

  1. Where do you want this pointer to appear?
  2. What should the title of this pointer be?
  3. How should the text of this pointer read?
  4. What short ID tag will you use behind the scenes to keep track of this pointer?

Deciding where the pointer appears

First, decide what element on the dashboard side of things you want your pointer to draw attention to. Any HTML element with a unique ID can be targeted. For instance, a new plugin that needs settings checked before the user proceeds might display a pointer next to the “Plugins” menu item.

Then, look at the dashboard page’s HTML source with a handy tool like Firebug to determine what the unique ID is for that element. For instance, the “Plugins” menu item has a unique ID of “menu-plugins.”

Finally, think about where the pointer box should appear in relation to the target element. Should it be above the target, to the left, right, or where? For the “Plugins” menu item, it’s probably best to put the left edge of your pointer on the “Plugins” item and center the pointer vertically. You can easily edit this to your liking during development, so don’t fret too much.

Name your pointer well

Like any headline, the title of your pointer will probably be noticed before anything else. Make the title short and descriptive. I would argue that most of the time, the title should be a noun.

The shortest elevator ride in history

People talk about describing important ideas in 30 seconds or less–the proverbial “elevator pitch.” For your pointer’s descriptive text, imagine the elevator just got a major upgrade. You have time for about 5 seconds worth of text–2 short sentences. Anything longer doesn’t belong in these short attention-grabbers.

May I see some ID, please?

Each pointer needs its own short, unique ID so things don’t get out of hand. Among other things, this ID is used to remember which pointers have been dismissed by users so we can avoid showing the same pointers to a user over and over. While technically you can use a large string as ID, this string is stored along with every other pointer ID that has been dismissed in all of your site’s history–in one database field. (I’ll show that to you later.) I recommend you namespace your IDs and simply number them, keeping track of them in a more descriptive way in your documentation. Here’s an example ID we use in this tutorial:

pksn1

The goal is to make this id short, yet unique among all other pointer IDs on your site.

  1. “pk” are the developer’s initials. (Paul Kaiser.)
  2. “sn” are the product’s initials. (Simplenote.)
  3. “1” is simply a number, to be sequentially incremented for this product.

Of course, you could add a 3rd or 4th character to the namespacing, but try not to get carried away.

The final plan

Here’s the complete plan for the pointers we’ll add to Simplenotes.

First Pointer

  • Title: “New Simplenote Settings”
  • Unique internal ID: pksn1
  • Descriptive content: I hope you find Simplenote useful. You should probably check your settings before using it.
    Note: We’d like to link “check your settings” to the Simplenote settings page.
  • Page element to point at:Plugin menu item in dashboard–element id is #menu-plugins

 Second Pointer

  • Title: “Simplenote Entry Field”
  • Unique internal ID: pksn2
  • Descriptive content: Enter any text here to add a simple note for this post or page.
  • Page element to point at: Simplenote field on edit screen–element id is #pksn-box

Let’s make it happen

Now that you have a plan, set it in motion. This part is easy!

Enqueue special JavaScripts and CSS if needed

In Simplenote, we make a specific function responsible for deciding which pointers the current user has not yet dismissed, and then setting things in motion. In our plugin’s constructor, this function gets hooked to “admin_enqueue_scripts” so it can potentially add scripts and styles for any dashboard screens.

add_action( 'admin_enqueue_scripts', array( $this, 'pksimplenote_admin_scripts' ) );

In the function “pksimplenote_admin_scripts” we do a number of things. First, we check if the current user has already seen all of our pointers. If they have, we don’t need to do anything at all. The variable “$seen_it” will contain all the internal IDs of pointers the user has previously dismissed. We’ll use the variable “$do_add_script” as a flag to indicate whether or not any pointers need to be loaded.

function pksimplenote_admin_scripts() {
    // find out which pointer IDs this user has already seen
    $seen_it = explode( ',', (string) get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true ) );
    // at first assume we don't want to show pointers
    $do_add_script = false;

Now let’s see if the current user has already dismissed the first pointer on our list. If our unique internal id, “pksn1,” was not in the “$seen_it” array, we need to show this pointer. We can set our flag accordingly, and then hook in to a function responsible specifically for adding our first pointer. We hook this function to “admin_print_footer_scripts,” so its output goes along with other scripts in the footer.

// Handle our first pointer announcing the plugin's new settings screen.
// check for dismissal of pksimplenote settings menu pointer 'pksn1'
if ( ! in_array( 'pksn1', $seen_it ) ) {
   // flip the flag enabling pointer scripts and styles to be added later
   $do_add_script = true;
   // hook to function that will output pointer script just for pksn1
   add_action( 'admin_print_footer_scripts', array( $this, 'simplenote_pksn1_footer_script' ) );
} // end if

Now we move on to our other pointers. Simplenote has one more pointer to potentially show users. Our logic here is the same.

// Handle our second pointer highlighting the note entry field on edit screen.
// check for dismissal of pksimplenote note box pointer 'pksn2'
if ( ! in_array( 'pksn2', $seen_it ) ) {
   // flip the flag enabling pointer scripts and styles to be added later
   $do_add_script = true;
   // hook to function that will output pointer script just for pksn2
   add_action( 'admin_print_footer_scripts', array( $this, 'simplenote_pksn2_footer_script' ) );
}

Great–now for each pointer we want to display, we’ve hooked a function onto admin_print_footer_scripts. Before we move on to the guts of those two functions, we need to see about enqueueing the main script and css that allow our pointers to show up at all. These go in the page’s head area, so we use “wp_enqueue_script” and “wp_enqueue_style.” We’ll only do this if our flag “$do_add_script” is set to true.

// now finally enqueue scripts and styles if we ended up with do_add_script == TRUE
if ( $do_add_script ) {
   // add JavaScript for WP Pointers
   wp_enqueue_script( 'wp-pointer' );
   // add CSS for WP Pointers
   wp_enqueue_style( 'wp-pointer' );
   } // end if checking do_add_script
} // end pksimplenote_admin_scripts()

The footer scripts

For each pointer, we have a function responsible for building the pointer’s content and inserting the appropriate JavaScript where all footer scripts belong. First, we build the title and content for the pointer in a variable. Note the title needs to be an <h3> to display consistently.

// Each pointer has its own function responsible for putting appropriate JavaScript into footer
function simplenote_pksn1_footer_script() {
   // Build the main content of your pointer balloon in a variable
   $pointer_content = '<h3>New Simplenote Settings</h3>'; // Title should be <h3> for proper formatting.
   $pointer_content .= '<p>I hope you find Simplenote useful. You should probably <a href="';
   $pointer_content .= bloginfo( 'wpurl' );
   $pointer_content .= '/wp-admin/options-general.php?page=PKSimplenote_Options">check your settings</a> before using it.</p>';
// this is not a typo -- we are dropping out of PHP but still in the function
?>

Next, we spit out some JavaScript that is largely the same for every pointer, but with a few changes.

  1. “#menu-plugins” needs to be the unique id of whatever DOM element in your HTML you want to attach your pointer balloon to.
  2. “pksn1” needs to be the unique id, for internal use, of this pointer
  3. “position” — edge indicates which horizontal spot to hang on to; align indicates how to align with element vertically
<script type="text/javascript">// <![CDATA[
jQuery(document).ready(function($) {
    /* make sure pointers will actually work and have content */
    if(typeof(jQuery().pointer) != 'undefined') {
        $('#menu-plugins').pointer({
            content: '<?php echo $pointer_content; ?>',
            position: {
                edge: 'left',
                align: 'center'
            },
            close: function() {
                $.post( ajaxurl, {
                    pointer: 'pksn1',
                    action: 'dismiss-wp-pointer'
                });
            }
        }).pointer('open');
    }
});
// ]]></script>
    <?php
} // end simplenote_pksn1_footer_script()

The second pointer gets a similar function. This one changes the way the pointer is positioned (just to show you how.) For “position” — we use a different method, indicating:

  1. What spot on the targeted element do we hang on to?
  2. What spot on our pointer do we want hanging on to the targeted element?

For this pointer, we take the top left corner of our pointer and hang it by the bottom left corner of the entry field box.

function simplenote_pksn2_footer_script() {
    // Build the main content of your pointer balloon in a variable
    $pointer_content = '<h3>Simplenote Entry field</h3>'; // Title should be <h3> for proper formatting.
    $pointer_content .= '<p>Enter any text here to add a simple note for this post or page.</p>';
    // In JavaScript below:
    // * "position" -- we use a different method, indicating:
    // ** What spot on the targeted element do we hang on to?
    // ** What spot on our pointer do we want hanging on to the targeted element?
    // Here we take the top left corner of our pointer and hang it by the bottom left corner of the entry field box.
    ?>
    <script type="text/javascript">// <![CDATA[
    jQuery(document).ready(function($) {
        /* make sure pointers will actually work and have content */
        if(typeof(jQuery().pointer) != 'undefined') {
            $('#pksn-box').pointer({
                content: '<?php echo $pointer_content; ?>',
                position: {
                    at: 'left bottom',
                    my: 'left top'
                },
                close: function() {
                    $.post( ajaxurl, {
                        pointer: 'pksn2',
                        action: 'dismiss-wp-pointer'
                    });
                }
            }).pointer('open');
        }
    });
    // ]]></script>
    <?php
} // end simplenote_pksn2_footer_script())

And that–as they say–is that! The Simplenote plugin now makes use of 2 properly-functioning WordPress pointers for drawing attention to new features.

One final warning

Remember how I stressed that your internal IDs for pointers should be short? Remember how I talked about only using pointers that really will provide a benefit? Let me show you one more reason why these points are important.

In your WordPress database, each pointer that a user clicks “Dismiss” on gets recorded so the user doesn’t have to see it again. WordPress keeps track of this by storing your internal pointer in the “usermeta” table, in an entry named “dismissed_wp_pointers.” My development WordPress installation is pretty lean, and yet this entry already has numerous entries.

Value of usermeta “dismissed_wp_pointers”

wp330_toolbar, wp330_media_uploader, wp330_saving_widgets, wp340_choose_image_from_library, wp340_customize_current_theme_link, pksn1, pksn2

If every plugin and theme author used only 2 or 3 pointers, this field would still get fairly full in a short time. If you really like pointers that much, you might consider building your own similar functions that store dismissed pointers in the options table, specifically for your plugin or theme.

Credits