Let’s write a slightly more complicated jQuery-enabled Drupal module. We’ll build an AJAX voting widget as shown in
Figure18-7, which lets users add a single point to a post they like. We’ll use jQuery to cast the vote and change the
total vote score without reloading the entire page. We’ll also add a role- based permission so only users with the “rate
content” permission are allowed to vote. Because users can add only one point per vote, let’s name the module
plus one.
Figure 18-7. The voting widget.
We’ll have to get some basic module building out of the way before we can get to the actual jQuery part of plusone. Please see Chapter 2 if you’ve never built a module before. Otherwise, let’s get to it!
Create a directory in sites/all/modules/custom, and name it plusone (you might need to create the sites/all/modules/custom directory). Inside the plusone directory, create the file plusone.info, which contains the following lines:
name = Plus One
description = "A +1 voting widget for nodes. " package = Pro Drupal Development
core = 7.x files[]=plusone.module
This file registers the module with Drupal so it can be enabled or disabled within the administrative interface.
Next, you’ll create the plusone.install file. The functions within this PHP file are invoked when the module is enabled,
disabled, installed, or uninstalled, usually to create or delete tables from the database. In this case, we’ll want to keep
track of who votedon which node:
<?php
/**
* Implements hook_install().
*/
function plusone_install() {
// Create tables.
drupal_install_schema('plusone');
}
/**
* Implements hook_schema().
*/
function plusone_schema() {
$schema['plusone_votes'] = array('description' => t('Stores votes from the plusone module.'), 'fields' => array(
'uid' => array( 'type' => 'int', 'not null' => TRUE,'default' => 0,'description' => t('The {user}.uid of the user casting the vote.'),),'nid' => array( 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0,
'description' => t('The {node}.nid of the node being voted on.'),),'vote_count' => array( 'type' => 'int','not null' => TRUE,'default' => 0,'description' => t('The number of votes cast.'),),),'primary key' => array('uid', 'nid'), 'indexes' => array('nid' => array('nid'), 'uid' => array('uid'),),);return $schema;}
Also, add the file sites/all/modules/custom/plusone/plusone.css. This file isn’t strictly needed, but it makes the votingwidget a little prettier for viewing, as shown in Figure 18-8.
Figure 18-8. Comparison of voting widget with and without CSS
Add the following content to plusone.css: div.plusone-widget {
width: 100px;
margin-bottom: 5px; text-align: center;
}
div.plusone-widget .score { padding: 10px;
border: 1px solid #999; background-color: #eee; font-size: 175%;}div.plusone-widget .vote { padding: 1px 5px;
margin-top: 2px;
border: 1px solid #666; background-color: #ddd;
}
Now that you have the supporting files created, let’s focus on the module file and the jQuery JavaScript file. Create twoempty files: sites/all/modules/custom/plusone/plusone.js and sites/all/modules/custom/plusone/plusone.module.
You’ll begradually adding code to these files in the next few steps. To summarize, you should have the following files:
sites/ all/
modules/ custom/
plusone/ plusone.js plusone.css plusone.info plusone.install plusone.module
plusone-widget.tpl.php
Building the Module
Open up the empty plusone.module in a text editor and add the standard Drupal header documentation:
<?php
/**
* @file
* A simple +1 voting widget.
*/
Next you’ll start knocking off the Drupal hooks you’re going to use. An easy one is hook_ permissions(), which lets
you add the “rate content” permission to Drupal’s role-based access control page. You’ll use this permission to prevent
anonymous users from voting without first creating an account or logging in.
/**
* Implements hook_permission().
*/
function plusone_permission() {
$perms = array('rate content' => array('title' => t('Rate content'),),);
return $perms;}
Now you’ll begin to implement some AJAX functionality. One of the great features of jQuery is its ability to submit its
own HTTP GET or POST requests, which is how you’ll submit the vote to Drupal without refreshing the entire page.
jQuery will intercept the clicking of the Vote link and will send a request to Drupal to save the vote and return the
updated total. jQuery will use the new value to update the score on the page. Figure 18-9 shows a “big picture”
overview of where we’re going.
Once jQuery intercepts the clicking of the Vote link, it needs to be able to call a Drupal function via a URL. We’ll use hook_menu() to map the vote URL submitted by jQuery to a Drupal PHP function. The PHP function saves the vote
to the database and returns the new score to jQuery in JavaScript Object Notation (JSON) (OK, so we’re not using
XML and thus it’s not strictly AJAX).
/**
* Implements hook_menu().
*/
function plusone_menu() {
$items['plusone/vote'] = array( 'title' => 'Vote','page callback' => 'plusone_vote',
'access arguments' => array('rate content'), 'type' => MENU_SUGGESTED_ITEM,);
return $items;}
In the preceding function, whenever a request for the path plusone/vote comes in, the function plusone_vote() handles itwhen the user requesting the path has the “rate content” permission.
Figure 18-9. Overview of the vote updating process
The path plusone/vote/3 translates into the PHP function call plusone_vote(3) (see Chapter 4, about Drupal’s menu/
callback system, for more details).
/**
* Called by jQuery, or by browser if JavaScript is disabled.
* Submits the vote request. If called by jQuery, returns JSON.
* If called by the browser, returns page with updated vote total.
*/
function plusone_vote($nid) {
global $user;
$nid = (int)$nid;
// Authors may not vote on their own posts. We check the node table
// to see if this user is the author of the post.
$is_author = db_query('SELECT uid from {node} where nid = :nid AND uid = :uid', array(":nid" => (int)$nid, ":uid" => (int)$user->uid))->fetchField();
if ($nid > 0 && !$is_author) {
// get current vote count for this user;
$vote_count = plusone_get_vote($nid, $user->uid); echo "Vote count is: $vote_count<br/>";
if (!$vote_count) {
echo "Yep was existing votes<br/>";
// Delete existing vote count for this user.
db_delete('plusone_votes')->condition('uid', $user->uid)->condition('nid', $nid)
->execute(); db_insert('plusone_votes')->fields(array('uid' => $user->uid, 'nid' => $nid,'vote_count' => $vote_count + 1,))
->execute();}}
$total_votes = plusone_get_total($nid);
// Check to see if jQuery made the call. The AJAX call used
// the POST method and passed in the key/value pair js = 1.
if (!empty($_POST['js'])) {
// jQuery made the call
// This will return results to jQuery's request drupal_json(array(
'total_votes' => $total_votes,
'voted' => t('You Voted'))); exit();}
// It was a non-JavaScript call. Redisplay the entire page
// with the updated vote total by redirecting to node/$nid
// (or any URL alias that has been set for node/$nid).
$path = drupal_get_path_alias('node/'. $nid); drupal_goto($path);}
The preceding plusone_vote() function saves the current vote and returns information to jQuery in the form of an
associative array containing the new total and the string You voted, which replaces the Vote text underneath the voting widget. This array is passed into drupal_json(), which converts PHP variables into their JavaScript equivalents, in this
case converting a PHP associative array to a JavaScript object, and sets the HTTP header to Content-type: text/javascript. For more on how JSON works, see http://en.wikipedia.org/wiki/JSON.
Notice that we’ve written the preceding function to degrade gracefully. When we write the jQuery code, we’ll make sure
that the AJAX call from jQuery will pass along a parameter called js and will use the POST method. If the js parameter isn’tthere, we’ll know that the user clicked the Vote link and the browser itself is requesting the path—for example,
plusone/vote/3. In that case, we don’t return JSON, because the browser is expecting a regular HTML page. Instead, we update the vote total to reflect the fact that the user voted, and then we redirect the browser back to the original page, which will be rebuilt by Drupal and will show the new vote total. We called plusone_get_vote() and plusone_get_
total() in the preceding code, so let’s create those:
/**
* Return the number of votes for a given node ID/user ID pair
*/
function plusone_get_vote($nid, $uid) {
$vote_count = db_query('SELECT vote_count FROM {plusone_votes} WHERE
nid = :nid AND uid = :uid', array(':nid' => $nid, ':uid' => $uid))->fetchField(); return $vote_count;}
/**
* Return the total vote count for a node.
*/
function plusone_get_total($nid) {
$total_count = db_query('SELECT SUM(vote_count) from {plusone_votes} where nid = :nid', array(':nid' => $nid));
return ($total_count);}
Now, let’s focus on getting the voting widget to display alongside the posts. There are two parts to this. First, I’ll gather the information required to display the widget on hook_node_load().
/**
* Load the values required to make the widget work
* And output the widget on hook_node_load
*/
function plusone_node_view($node, $view_mode) { global $user;
$total = plusone_get_total($node->nid);
$is_author = db_query('SELECT uid from {node} where nid = :nid AND uid = :uid', array(":nid" =>
$node->nid, ":uid" => $user->uid))->fetchField();
if ($is_author) {
$is_author = TRUE;} else {$is_author = FALSE;}
$voted = plusone_get_vote($node->nid, $user->uid); if ($view_mode == 'full') {
$node->content['plusone_vote'] = array('#markup' => theme('plusone_widget', array('nid' =>(int)$node->nid, 'total'
=>(int)$total, 'is_author' => $is_author, 'voted' => $voted)), '#weight' => 100,);
return $node;}}
We’ll need to create a JavaScript/jQuery script that will handle users clicking the vote button and calling the appropriate
function in the plusone module to record the user’s vote. This JavaScript adds an event listener to a.plusone-link (remember we defined plusone-link as a CSS class selector?), so that when users click the link, it fires
off an HTTP POST request to the URL it’s pointing to. The preceding code also demonstrates how jQuery can pass
data back into Drupal. After the AJAX request is completed, the return value (sent over from Drupal) is passed as
the data parameter into the anonymous function that’s assigned to the variable voteSaved. The array is referenced by theassociative array keys that were initially built in the plusone_vote() functioninside Drupal. Finally, the JavaScript updatesthe score and changes the Vote text to You voted. To preventthe entire page from reloading (because the JavaScript
handled the click), use a return value of false from the JavaScript jQuery function. We’ll create a plusone.js file in the plusone module directory with the following content:
// Run the following code when the DOM has been fully loaded.
jQuery(document).ready(function () {
// Attach some code to the click event for the
// link with class "plusone-link".
jQuery('a.plusone-link').click(function () {
// When clicked, first define an anonymous function
// to the variable voteSaved.
var voteSaved = function (data) {
// Update the number of votes.
jQuery('div.score').html(data.total_votes);
// Update the "Vote" string to "You voted".
jQuery('div.vote').html(data.voted);}
// Make the AJAX call; if successful the
// anonymous function in voteSaved is run.
jQuery.ajax({type: 'POST', // Use the POST method.
url: this.href, dataType: 'json', success: voteSaved,
data: 'js=1' // Pass a key/value pair.
});
// Prevent the browser from handling the click.
return false;
});});
Finally I’ll create the pluseone-widget.tpl.php file in the plusone module directory. The content of the tpl file is as
follows:
<?php
/**
* @file
* Template for displaying the voting widget
*/
// Add the javascipt and CSS files drupal_add_js(drupal_get_path('module', 'plusone') .'/plusone.js'); drupal_add_css
(drupal_get_path('module', 'plusone') .'/plusone.css');
// build the output structure
$output = '<div class="plusone-widget">';
$output .= '<div class="score">'. $total .'</div>';
$output .= '<div class="vote">';
// Based on the attributes – display the appropriate label
// below the vote count.
if ($is_author || !user_access('rate content')) {
// User is author; not allowed to vote.
$output .= t('Votes');
}elseif ($voted > 0) {
// User already voted; not allowed to vote again.
$output .= t('You voted');}
else {
// User is eligible to vote.
$output .= l(t('Vote'), "plusone/vote/$nid", array( 'attributes' => array('class' => 'plusone-link')));}
$output .= '</div>'; // Close div with class "vote".
$output .= '</div>'; // Close div with class "plusone-widget".
print $output;
In the preceding code, we used the variables set in the hook_node_load in the plusone- widget.tpl.php—enabling us to display the widget. Creating a separate theme template rather than building the HTML inside the module
itself allows designers tooverride this function if they want to change the markup.
The HTML of the widget that would appear on the page http://example.com/?q=node/4 would look like this
<div class="plusone-widget">
<div class="score">0</div>
<div class="vote">
<a class="plusone-link" href="/plusone/vote/4">Vote</a>
</div>
</div>
Using Drupal.behaviors
JavaScript interaction works by attaching behaviors (i.e., actions triggered by events such as a mouse click) to elements in
the DOM. A change in the DOM can result in this binding being lost. So while the plusone.js file we used previously
willwork fine for a basic Drupal site, it might have trouble if other JavaScript files manipulate the DOM. Drupal provides a central
object called Drupal.behaviors with which JavaScript functions may register to ensure that rebinding of behaviors takes place when necessary. The following version of plusone.js allows voting via AJAX just like the previous version
but safeguards our bindings by registering withDrupal.behaviors:
Drupal.behaviors.plusone = function (context) { jQuery('a.plusone-link:not(.plusone-processed)', context)
.click(function () {
var voteSaved = function (data) { jQuery('div.score').html(data.total_votes); jQuery('div.vote').html(data.voted);
} jQuery.ajax({type: 'POST',url: this.href, dataType: 'json', success: voteSaved, data: 'js=1'});
return false;
}).addClass('plusone-processed');}
For more details on Drupal.behaviors, see misc/drupal.js.
Ways to Extend This Module
A nice extension to this module would be to allow the site administrator to enable the voting widget for only certain
node types. You could do that the same way we did for the node annotation module we built in Chapter 2. Then you would need to check whether voting was enabled for a given node type inside hook_node_view() before adding the widget.
There are plenty of other possible enhancements, like weighting votes based on roles or limiting a user to a certain
number of votes per 24-hour period. Our purpose here was to keep the module simple to emphasize the interactions
between Drupal and jQuery.
Compatibility
jQuery compatibility, as well as a wealth of information about jQuery, can be found at http://docs.jquery.com/. In short,
jQuery supports the following browsers:
• Internet Explorer 6.0 and greater
• Mozilla Firefox 1.5 and greater
• Apple Safari 2.0.2 and greater
• Opera 9.0 and greater
More detailed information on browser compatibility can be found at http://docs.jquery.com/ Browser_Compatibility.
Next Steps
To learn more about how Drupal leverages jQuery, take a look at the misc directory of your Drupal installation. There,
you’ll find the JavaScript files responsible for form field automatic completion, batch processing, fieldset collapsibility,
progress bar creation, draggable table rows, and more. See also the Drupal JavaScript Group at http://groups.drupal.org/
Summary
In this chapter, you learned
• What jQuery is.
• The general concepts of how jQuery works.
• How to include JavaScript files with your module.
• How jQuery and Drupal interact to pass requests and data back and forth.
• How to build a simple voting widget.
Không có nhận xét nào:
Đăng nhận xét