Making the Flag Module's Confirmation form use AJAX
Chris Didcote
This article discusses how we can use a combination of techniques to take the standard 'Confirmation Form' provided by the Flag module and get it to load in a modal window rather than on its own page. We'll also extend this form slightly to allow the user to include some additional data before clicking 'Confirm'. As an example we'll use the Flag module to create an 'Abuse' flag that will apply to a comment entity - this is intended to allow a user to flag a comment as requiring moderations but before triggering the actions associated with the flag we need to display a modal popup to ask them why they're making the report. This seems to be reasonably standard functionality when it comes to reporting comments; however, the implementation of this within Drupal isn't as trivial as it first appears - as I found out!
Setting up the Basic Flagging Functionality
To add the ability to allow your users to flag a comment as requiring moderation is actually a very simple process - all the complexity surrounds adding in the modal form aspect. We begin by downloading and enabling the [Flag][flag] module. This module has everything we need to be able to add a link to comments to trigger the report, and if we didn't want to collect any additional information we could just enable the [Flag Actions][flag] sub-module to fire any number of triggers to do things like unpublish the comment or send an email. It's beyond the scope of this article to detail exactly how to configure the *Flag* module; however, detailed instructions are provided in its README.txt file.
One thing that we do need to explicitly mention regarding the configuration of your new flag is that you'll need to set its 'Display Options' to use a 'Confirmation Form' as this is what will give us a handle to allow the collection of additional information in the simplest way I could think of. We could have written a custom action but I thought as we've already got a form in place, our module would be better suited to just altering the way this is processed so if it's not enabled then the Flag module will just function as normal.
Extending the Standard Confirmation Form
Out of the box, the confirmation form provided by the *Flag* module does nothing more than display a form containing a submit button to confirm the action and a link to cancel it. We can just use
hook_form_alter()
to add a few of additional fields to this a basic implementation of which is shown below:
/**
* Implements hook_form_alter()
*/
function example_form_alter(&$form, &$form_state, $form_id){
if($form_id == 'flag_confirm'){
$form['reason'] = array(
'#type' => 'select',
'#title' => t('Reason for reporting this comment'),
'#options' => array(
'offensive' => t('Comment\'s offensive or unlawful'),
'spam' => t('Advertising / Spam'),
'other' => t('Another reason'),
),
);
$form['other'] = array(
'#type' => 'textfield',
'#title' => t('Reason'),
);
$form['comment_id'] = array(
'#type' => 'value',
'#value' => arg(4),
);
//Add our own submit handler to process this data.
$form['#submit'][] = 'example_confirm_form_submit';
}
}
There's nothing scary involved here. You'll notice we've added a hidden field to store the ID of the comment we are interested in - this just gives use easy access to it should we need to load the entity again in the submit handler. On this subject we've included our own submit handler to process this data. We need to do this as the Flag module will only care about the form elements that it has generated so in order to process these new fields we just add another function to the form's
$form['#submit']
array. The function this points to will just take the standard arguments we pass to a submit handler so as a basic example we need to include a function like:
/**
* Additional submit handler for the comment confirmation form
*/
function example_confirm_form_submit($form, &$form_state){
$params = $form_state['values'];
//Do something with these submitted values
//For example send them in an email to a moderator using php drupal_mail()
}
That's all we need to do with the confirmation form at this stage and if we didn't want to load it in a modal window then this would be enough to allow users to report comments and include additional information as part of that process. Working with modals makes things a bit more interesting.
AJAX and the Chaos Tool Suite
The reason why this is slightly more complicated than it first sounds is because we want the rendered form but we don't want to push it through the entire Drupal theme engine as this would mean we'd end up rendering another whole page within the popup when in fact all we want is the markup for the form. Loading forms in modal windows isn't actually that complicated in itself as the [Chaos Tool Suite][ctools] module provides some nice functionality to do the heavy lifting for us. What makes this particular example more complex is the fact that the form we want to render isn't being generated by our module and neither are any references to it so we need to extend the *Flag* module in a way that means our custom module can just slot in to add this modal functionality.
To get our module working with Chaos Tools we need to follow a similar technique to that described in our Make a link use ajax in Drupal 7 (it's easy) article. We begin by defining an implementation of
hook_menu()
as shown below:
/**
* Implements hook_menu()
*/
function example_menu(){
$items['comments/%ctools_js/confirm/%flag/%'] = array(
'title' => 'Contact',
'page callback' => 'example_test_modal',
'page arguments' => array(1, 3, 4),
'access arguments' => TRUE,
'type' => MENU_CALLBACK,
);
return $items;
}
You'll notice that there are a few placeholders in the path for our callback. We use these to tie into both the Chaos Tools and Flag modules and to pass through the ID of the comment being reported. The first wildcard is
$ctools_js
- this will trigger a function called
ctools_js_load()
to run within Chaos Tools to check whether or not the link is capable of running the JavaScript required to fire the AJAX request. If it is then this placeholder becomes
ajax
; if not it's set to
nojs
in exactly the same way as is shown in the Make a link use ajax in Drupal 7 (it's easy) article. However, in that example we explicitly define two callbacks whereas here the
%ctools_js
wildcard allows one callback to suffice as the Chaos Tools module will change the argument dynamically.
The next wildcard is
%flag
and this is what makes our menu callback work with the Flag module. When rendering the form the Flag module needs to reference an object that represents the flag that is being used as the trigger. This object is passed through to the form as an argument which means that it needs to be loaded as a variable before we can call the form. By including this placeholder we run the Flag module's implementation of
flag_load()
, which takes the string entered in the path and returns the relevant flag object.
The final wildcard is just a simple reference to the ID of the comment we're interacting with; again this will be needed to allow the Flag module to do its magic, as we'll see shortly.
Next we need to implement the page callback function we reference in
hook_menu()
as this will be what actually generates the response to any requests that hit a path matching our definition. We need to make sure it accepts three arguments corresponding to the wildcards discussed above; these parameters will have been set by each modules'
_load()
functions by the time we invoke the callback function.
function example_test_modal($js, $flag, $cid){
//If JavaScript isn't enabled the just go to the standard confirmation form
if (!$js) {
drupal_goto('flag/confirm/flag/abuse/' . $cid, array('query' => array('destination', $_GET['destination'])));
}
//Include the relevant code from CTools
ctools_include('modal');
ctools_include('ajax');
ctools_add_js('ajax-responder');
//Build up the $form_state array
//This is passed through to the form generated by the Flag module
$form_state = array(
'title' => t('Report Comment'),
'ajax' => TRUE,
'build_info' => array(
'args' => array(
0 => 'flag',
1 => $flag,
3 => $cid,
),
),
);
//Wrap the Flag module's form in a wrapper provided by CTools
$output = ctools_modal_form_wrapper('flag_confirm', $form_state);
if (!empty($form_state['executed'])) {
$output = array();
//This makes sure we go to the right place once we close the modal window
if (isset($_GET['destination'])) {
$output[] = ctools_ajax_command_redirect($_GET['destination']);
}
else {
$output[] = ctools_ajax_command_reload();
}
}
//Return the JSON string ready to be rendered back to the DOM
print ajax_render($output);
exit;
}
There's quite a lot going on in this function. First of all we check to see if we can use AJAX and render a modal version of the form. If not then we just redirect to the standard form which will be displayed on its own page. If JavaScript is enabled we then need to make sure we add all the code we need from Chaos Tools - this is just done by some simple helper functions provided by the module. The next thing we need to do is build up the
$form_state
array - this is an important stage as we also need to include the arguments in under a
build_info
key in order to get them over to the Flag module. This differs from how we'd usually do things if we weren't trying to render a modal form as it would be possible to just call
drupal_get_form()
and pass the arguments through as normal. Because we are using the Chaos Tools wrapper around the form we can't do this so we need to add them into the
$form_state
array. We also set the title of the modal window and the
ajax
key to
TRUE
.
We then use the Chaos Tools wrapper to add the form to the
$output
variable and apply a bit of logic to make sure we still honour the
destination
argument in the query string if it's present. If it's not then we just reload the current page when the modal window is closed. Finally we just print the JSON string and exit the function to stop it running through the theme engine and having markup added that will break the AJAX response.
Tying Everything Together
Now we have a function that will provide a valid AJAX response if requested, we need to start tying this into the links already being rendered by the *Flag* module. *Chaos Tools* is clever enough to realise that any link that has a class of
ctools-use-modal
needs to be loaded in a modal window if possible. So we need to add this class to the 'Flag' link on the comment to begin with. Next the link provided by the *Flag* module still points at the
MENU_CALLBACK
defined in that module so we need to rewrite this to point at our new page callback function defined in our implementation of
hook_menu()
. We could do all this using
hook_comment_view_alter()
; however, I opted to use jQuery to add the classes as this means that in real terms if JavaScript isn't enabled then the class won't be added and the link will never get pointed at our function so it will just work as normal.
(function ($) {
Drupal.behaviors.initModalFormsConfirm = {
attach: function (context, settings) {
$(".flag-link-confirm", context).once('init-modal-forms-contact', function () {
this.href = this.href.replace(/flag\/confirm\/flag\/abuse/,'comments/nojs/confirm/abuse');
}).addClass('ctools-use-modal ctools-modal-modal-popup-confirm');
}
};
})(jQuery);
The jQuery code above just looks for any link that has a class of
flag-link-confirm
and then rewrites its
href
attribute based on a regular expression matching the entire string up to the point where the flag placeholder and comment ID are appended. We then add the
ctools-use-modal
class; you'll notice we also add another class of
ctools-modal-modal-popup-confirm
- this is to allow us to control how the modal window is rendered and we'll look at this next. To add this code to the comment we just use
hook_comment_view()
to call
drupal_add_js()
. It is important to notice that we set the weight to
-20
- this ensures that this code runs before the Chaos Tools JavaScript. If you didn't do this then the
ctools-use-modal
class won't have been set in time for the Chaos Tools JavaScript to recognise it when it runs.
/**
* Implements hook_node_view_alter().
*/
function example_comment_view_alter($comment, $view_mode, $langcode) {
drupal_add_js(drupal_get_path('module', 'example') . '/js/example.js', array('weight' => -20));
}
At the moment we're not quite there - clicking the link still won't load the modal window even though the class has been added as required. This is because when we're viewing a comment no code has been added in from the Chaos Tools module so this class has no context. In our page callback function above we've had to add this code in but this only runs after the link has been clicked, so we need a way to ensure that the code is also included before. In order to get around this we use an implementation of
hook_init()
to invoke a function that will add all the required JavaScript to the current page. We also wrap it in some logic to stop the code from being added to any of the Drupal installation pages.
/**
* Implements hook_init().
*/
function example_init() {
if (!drupal_installation_attempted()) {
example_configure();
}
}
The code this calls will just add any JavaScript files that Chaos Tools needs in order to respond to the
ctools-use-modal
class and load the modal window. We also define some settings that will be added as JavaScript to help us theme the form; the code these reference is based on the Modal Forms module. This module provides some nice functionality to get some of the common core forms rendering in modal windows - for example using a 'login' link to load a modal version of the core user_login form. We're not actually using the module here but are borrowing the code it uses to render the modal window.
function example_configure(){
static $configured = FALSE;
if ($configured) {
return;
}
//Include the relevant CTools code
ctools_include('ajax');
ctools_include('modal');
ctools_modal_add_js();
$throbber = theme('image', array('path' => ctools_image_path('loading_animation.gif', 'modal_forms'), 'alt' => t('Loading...'), 'title' => t('Loading')));
$js_settings = array(
'modal-popup-confirm' => array(
'modalSize' => array(
'type' => 'fixed',
'width' => 500,
'height' => 200,
),
'modalOptions' => array(
'opacity' => 0.85,
'background' => '#000',
),
'animation' => 'fadeIn',
'modalTheme' => 'ModalFormsPopup',
'throbber' => $throbber,
'closeText' => t('Close'),
),
);
drupal_add_js($js_settings, 'setting');
//Add in some custom CSS and our jQuery template
ctools_add_css('example_popup', 'example');
ctools_add_js('example', 'example');
$configured = TRUE;
}
You can see that the
$js_settings
just take the form of an array with the key corresponding to the second class we added above. It also specifies that we should render the form using a 'theme' called
ModalFormsPopup
. This option just references a JavaScript file containing some code to override the standard Chaos Tools theming of the modal window - it's purely aesthetic. The
Drupal.theme.prototype
namespace was added in Drupal 6 to allow provide a method of cleanly overriding another module's JavaScript generated HTML code.
/**
* Provide the HTML to create the modal dialog.
*/
Drupal.theme.prototype.ModalFormsPopup = function () {
var html = ''
html += '';
html += ' ';
html += ' ';
html += ' ';
html += ' ';
html += ' ' + Drupal.CTools.Modal.currentSettings.closeText + '';
html += '
';
html += ' ';
html += ' ';
html += ' ';
html += '';
return html;
}
That's pretty much all there is to it. We've essentially just written a lot of glue to get the Flag module's confirmation form working with the Chaos Tools modal popup functionality. More importantly, we've not hacked either module and all the AJAX degrades gracefully. I've used the implementation of
example_configure()
to also add some CSS just to polish the final result a bit. I've also included a custom throbber that I generated using an online Tool just to make it tie nicely into my site's look and feel.