Tutorial 4: Drupalizing external libraries
Why reinvent, especially when there's so much great open source software available out there?
Probably the quickest and easiest way to get rich functionality is to "Drupalize" external libraries. This tutorial gives some pointers on how to make existing Javascript libraries work seamlessly with Drupal.
Likely candidates
The first step is to identify a likely candidate for Drupalization.
Of course, the best candidates are going to be those that most closely match the Drupal approach. Keep in mind the Drupal criteria of (a) "graceful degradation" or "progressive enhancement"--that is, Javascript should contribute additional functionality to pages that are fully usable without the Javascript, and (b) avoiding mixing of code and content--attaching behaviours to page elements on the basis of CSS classes.
If you're planning to share your work with the community, it will also help to work with code that's GPL or LGPL licensed, so that you can include it in Drupal's CVS repository.
Sample Drupalization: Jscalendar
As an example, we're going to take the popular and richly featured Jscalendar library. Jscalendar is a popup (or static) interactive calendar often used as a date picker.
Step 1: analyze the library
To start off, we need to look at what the library does out of the box. What page content does it use or produce? How does it attach behaviours to page elements?
After loading its needed files, Jscalendar typically includes code like the following (taken from the Jscalendar documentation):
<form ...>
<input type="text" id="data" name="data" />
<button id="trigger">...</button>
</form>A textfield and accompanying button are output. Then an inline script adds calendar behaviours to the elements:
<script type="text/javascript">
Calendar.setup(
{
inputField : "data", // ID of the input field
ifFormat : "%m %d, %Y", // the date format
button : "trigger" // ID of the button
}
);
</script>Straightforward, but some red flags in terms of the Drupal approach. We don't want to be outputting page elements (e.g., the button) that will have no use if Javascript is disabled. And we want to avoid inline scripts. So our challenge starts to clarify: we're going to need to dynamically append a button (or similar selector) as needed, and then attach behaviours to it. We'll take our usual approach: use class selectors to designate content that behaviours should apply to. In place of the two code blocks given in the Jscalendar documentation, we'll look for a simple text field element with a class assigned to it:
<form ...>
<input type="text" id="data" name="data" class="jscalendar" />
</form>Step 2: PHP work
Before we get to whatever Javascript we need to write, we first need a module to handle the server-side tasks. At the least, we need to:
- load files as needed--the .js and .css files of the Jscalendar distribution
- output content--the textfield with a class
We may come up with some additional tasks as we dig into things.
The actual adding of class names to textfields we can leave to users/developers using our module. They'll use the regular Forms API approach:
<?php
$form['date'] = array(
'#type' => 'textfield',
'#attributes' => array('class' => 'jscalendar')
);
?>We just need to determine if the library files need to be loaded. We can do so in a _form_alter hook, by testing if any form element has the 'jscalendar' class set:
<?php
function jscalendar_form_alter($form_id, &$form) {
foreach (element_children($form) as $key) {
if (isset($form[$key]) && isset($form[$key]['#attributes']) && isset($form[$key]['#attributes']['class']) && !(strpos($form[$key]['#attributes']['class'], 'jscalendar') === FALSE)) {
jscalendar_load();
}
}
}
?>In jscalendar_load() we'll use drupal_add_js() and theme_add_style to add the appropriate .js and .css files.
Step 3: Javascript
With the needed library loaded and appropriately classed textfields to work with, we just need to insert the needed additional content - a button - and attach behaviours.
We can do so in a very few lines.
if (isJsEnabled()) {
addLoadEvent(function() {
// Select all input elements
inputs = document.getElementsByTagName('input');
for (var i = 0; input = inputs[i]; ++i) {
if (input && (input.getAttribute('type') == 'text') && hasClass(input, 'jscalendar')) {This first part of the script will look familar if you've read the previous tutorials. As usual, we're adding a load event if appropriate Javascript support is present. In the load function, we grab all the inputs (this is the node type of the text fields we're interested in) and iterate through them, seeing if they have the 'jscalendar' class.
var button = document.createElement('button');
button.appendChild(document.createTextNode(' ... '));
button.setAttribute('id', input.getAttribute('id') + '-button');
input.parentNode.insertBefore(button, input.nextSibling);In this second part of the script, we're creating the button we need and then inserting it next to the textfield in question. Before inserting the button, we give it an id. This is in preparation for the next step.
Calendar.setup(
{
inputField : input.id, // ID of the input field
ifFormat : "%Y-%m-%d %H:%M:%S", // the date format
button : input.getAttribute('id') + '-button', // ID of the button
showsTime : true,
timeFormat : 12
}
);
}
}
});
}The last part of the script is taken pretty much straight out of that Jscalendar documentation. We're attaching the calendar behaviour, passing the ids of the textfield and it's newly minted button.
And that's it.
Step 4: Bugfixes and refinements
But not so fast. It turns out that there's an ugly bug showing up: the calendar displays, yes, but it's stretched the full width of the page. What's this about?
Finding the problem might take some digging--or, in this case, a friendly hint from another developer, yched, who points to conflicting CSS in drupal.css:
.calendar table {
border-collapse: collapse;
width: 100%;
border: 1px solid #000;
}Jscalendar, it turns out, also uses the 'calendar' class for a div, and also has a table within that div. That width: 100%; is our culprit. And the fix, thankfully, is easy. A tiny .css file:
.calendar table {
width: auto;
}Then, of course, we'll inevitably want to do a but of tweaking. Jscalendar comes with localization language files--we'll want to load the appropriate one. We can do this by looking at the global $locale variable. The above PHP code tested only the first-level $form elements, but would miss elements nested in trees. Rather than hard-coding everything in the Javascript file, it would be handy to allow form designers to designate what calendar behaviours they want--we can do this by setting $form attributes and then reading them into the Javascript.
And so on. For the current code, see the Jscalendar module in the Javascript Tools package.
All in all, though, we've been able to leverage quite a bit from a small amount of work and a few lines of code. We get the functionality, without the heavy lifting. But, by taking a bit of time to do it "right", we get code that works easily and seamlessly in Drupal. Hence the attraction of integrating external libraries.
