Thứ Tư, 11 tháng 6, 2014

Building a jQuery Voting Widget [Using jQuery]

Lets 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
tota vote score without reloading the entire page.  We’ll also add a role- based permission so only users with the “rate
content” permissioare allowed to vote. Because users can add only one point per vote, lets 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
*    simple  +1 voting widget.
*/
Next youll 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 aaccount or logging in.
/**
*   Implements hook_permission().
*/
function  plusone_permission() {
$perms = array('rate content' => array('title' => t('Rate content'),),);
return  $perms;}
Now youll 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 Drupawithout refreshing the entire page. 
jQuery  will intercept the clicking  of the Vote link an will send request to Drupal to save the vote and  return the
updated total. jQuery  will use the new value to updatthe 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 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 databasand  returns the new score  to jQuery  in JavaScript ObjecNotation (JSON) (OK, so we’re not using 
XMand  thus it’s not strictl 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 functioplusone_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 Drupals menu/
callbacsystem, 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 arrais 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 hoJSON 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 calle js and  will use the POST method. If the js parameter isn’tthere, we’ll know that the user  clicked  the Vote  link and  thbrowser itself is requesting the path—for example,
plusone/vote/3In that case, we don’t  return JSON, because the browser is expecting a regular HTML page. Instead, we update the vote tota 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 linkit fires
off an HTTP POST request to the URL it’s pointing to. The preceding code also demonstrates how jQuery capass
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  functionWe’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 mousclick) 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 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 plac 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 finthe 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