{ Hi! I'm Mike }
I'm a core developer with The Horde Project and a founding parter of Horde LLC - the company behind the world's most flexible groupware platform. This is my personal blog full of random thoughts about development and life in general.
June 16, 2011

The Horde PrettyAutocompleter - Part One

Kronolith 3 includes new tagging features, including an autocomplete feature for adding new tags to events and calendars. Horde has had autocompletion code for ages - autocompletion of email addresses in IMP, for example. In Kronolith, we wanted a more dynamic and fresh interface for tags to go along with the brand new dynamic interface. The result was the PrettyAutocompleter widget. In this entry, I'll explain how it's implemented in Kronolith and how you can adapt it for use in other applications.

 

The PrettyAutocompleter is a stand alone javascript widget that is not limited to tags, in fact, it's also used for attendees in Kronolith's meeting scheduling interface. It's part of the Horde_Core package and lives in prettyautocomplete.js. It extends the Autocompleter object defined in autocomplete.js. You don't have to worry about including any of these files on your own though, the Imple object (see below) takes care of all of this for you.

There are two main components of the autocompleter: The UI and the supporting backend code. First, let's look at the HTML required for the autocompleter. The following is the minimum required to setup an autocompleter.

<input id="kronolithEventTags" name=tags" />
<span id="kronolithEventTags_loading_img" style="display:none;"><img src="spinner.gif" /></span>

Now that we have the HTML setup, let's hook it up to both the javascript that transforms it to the prettyautocompleter and to the backend so it can retrieve the autocomplete choices. For this we use a Horde_Core_Ajax_Imple object. These objects connect UI elements to Ajax actions. Each Horde application can define it's own Imples by adding the classes to application/lib/Ajax/Imple. In the case of Kronolith, we connect it with code similar to this:

$injector->getInstance('Horde_Core_Factory_Imple')->create(
    array('kronolith', 'TagAutoCompleter'),
    array(
        // The name to give the (auto-generated) element that acts as the
        // pseudo textarea.
        'box' => 'kronolithEventACBox',

        // Make it spiffy
        'pretty' => true,

        // The dom id of the existing element to turn into a tag autocompleter
        'triggerId' => 'kronolithEventTags',

        // A variable to assign the autocompleter object to
        'var' => 'eventTagAc'
    )
);

This code transforms the kronolithEventTags element in the above HTML into a PrettyAutocompleter. It also attaches the autocompleter to an Ajax action for retrieving the autocomplete choices. The Imple object that does this is kronolith/lib/Ajax/Imple/TagAutoCompleter.php and it looks like this:

class Kronolith_Ajax_Imple_TagAutoCompleter extends Horde_Core_Ajax_Imple_AutoCompleter
{
    /**
     * Attach the Imple object to a javascript event.
     * If the 'pretty' parameter is empty then we want a
     * traditional autocompleter, otherwise we get a spiffy pretty one.
     *
     * @param array $js_params  See
     *                          Horde_Core_Ajax_Imple_AutoCompleter::_attach().
     *
     * @return array  See Horde_Core_Ajax_Imple_AutoCompleter::_attach().
     */
    protected function _attach($js_params)
    {
        $js_params['indicator'] = $this->_params['triggerId'] . '_loading_img';

        $ret = array(
            'params' => $js_params
        );

        if (empty($this->_params['pretty'])) {
            $ret['ajax'] = 'TagAutoCompleter';
        } else {
            $ret['pretty'] = 'TagAutoCompleter';
        }

        if (!empty($this->_params['var'])) {
            $ret['var'] = $this->_params['var'];
        }

        return $ret;
    }

    /**
     * Method to obtain autocomplete choices. 
     *
     * @param array $args  Arguments passed from the Ajax action. The 'input'
     *                                    parameter contains the text fragment to autocomplete on.
     *
     * @return array  Returns an array of possible choices.
     */
    public function handle($args, $post)
    {
        // Avoid errors if 'input' isn't set and short-circuit empty searches.
        if (empty($args['input']) ||
            !($input = Horde_Util::getFormData($args['input']))) {
            return array();
        }

        $tagger = Kronolith::getTagger();
        return array_values($tagger->listTags($input));
    }

}

Note that there is very little functionality in this class. The bulk of the work is handled by the class it extends - The more general Horde_Core_Ajax_Imple_Autocompleter. The Kronolith_Ajax_Imple_Autocompleter#handle method is automatically called via Ajax by the autocompleter to fetch the choices and then display them in the UI. The $tagger variable is a Kronolith_Tagger object and contains Kronolith specific code for interacting with Horde's Content system. All you need to know right now is that the $tagger->listTags() call provides an array of tag names that start with the text in $input.

The only thing left to do is to initialize the autocompleter. This builds the DOM structure for all the required elements and applies all the styling needed. In Kronolith, this is done when the dynamic interface is loaded (the element remains hidden in Kronolith, however, until the event detail form is displayed).

eventTagAc.init();

The reset() method is used to clear the values from the autocompleter and optionally assign new values. For example, in Kronolith when an existing event is loaded for editing, we prepopulate the autocompleter with that event's existing tags:

// Reset the autocompleter and clear all tags
eventTagAc.reset();

// Reset autocompleter, and prepopulate with two tags.
eventTagAc.reset(['tagOne', 'tagTwo']);

Getting the tags out of the autocompleter to save them is also very easy. However, before actually getting the value out of the autocompleter, we need to make sure that all of it's input is processed. Since the autocompleter uses a comma to trigger adding a new tag to the list of displayed tags, it's possible for the user to type a new tag in the input area, then press save before a adding a comma. We must make sure this last tag is added. This is done by the shutdown() method. Once we are ready to get the value, we can simply access it via the id or name we gave to the initial input element. For example, we gave the input element a dom id of kronolithEventTags and a name of tags. So, to get the current tag value, we can just:

eventTagAc.shutdown();
var tags = $F('kronolithEventTags');

This will be the comma delimited list of tag names.

The following is a bare-bones example script pulling together everything.  Obviously the onload and onclick handlers would normally be written in a less obtrusive way, but for a quick and dirtly example, this is fine. I've written it as if it were part of the Kronolith application. So, if you want to actually test it out and play with the code, just drop it into the root Kronolith directory with a name like example.php. It will pull existing kronolith tag data for the autocompletion, but will obviously not write back out any data.

<?php
/**
 * Autocomplete example
 */

// Setup the application
require_once dirname(__FILE__) . '/lib/Application.php';
Horde_Registry::appInit('kronolith');

// Attach the autocompleter to the ajax action.
// @see Kronolith_Ajax_Imple_TagAutoCompleter
$injector->getInstance('Horde_Core_Factory_Imple')->create(
    array('kronolith', 'TagAutoCompleter'),
    array(
        // The name to give the (auto-generated) element that acts as the
        // pseudo textarea.
        'box' => 'kronolithEventACBox',
        // Make it spiffy
        'pretty' => true,
        // The dom id of the existing element to turn into a tag autocompleter
        'triggerId' => 'kronolithEventTags',

        // A variable to assign the autocompleter object to
        'var' => 'eventTagAc'
    )
);
?>
<head>
 <title>Autocomplete Example</title>
<?php
Horde::includeStylesheetFiles();
Horde::includeScriptFiles();
Horde::outputInlineScript();
?>
<body onload="eventTagAc.init()">
  <div class="kronolithDialogInfo"><?php echo _("To make it easier to find, you can enter comma separated tags related to the event subject.") ?></div>
  <input id="kronolithEventTags" name="tags" />
  <span id="kronolithEventTags_loading_img" style="display:none;"><?php echo Horde::img('loading.gif', _("Loading...")) ?></span>
  <br />
  <a href="#" onclick="eventTagAc.shutdown();alert($F('kronolithEventTags'));">Show me the tags</a>.<br />
  <a href="#" onclick="eventTagAc.reset();">Reset the autocompleter</a><br />
  <a href="#" onclick="eventTagAc.reset(['Personal', 'Fun']);">Reset, with prepopulated tags</a>.
</body>


For the next installment, we'll look at the Previously Used Tags functionality in Kronolith's interface. This is not part of the general PrettyAutocompleter code, but easy to implement.

September 27, 2009

Ansel, Kronolith, and more...

Wow, it's been since June 10th, almost 4 months since my last entry. Time flies...especially when you are busy. In the interest of keeping people informed, here are some of the new things I've been working on with regards to The Horde Project,  with an indication as to what version of Horde the work applies to:

Horde_Service_Twitter (H4 Only)

Since stating to use twitter, I figured it would be helpful to have my Twitter timeline appear in Horde, since that's what is usually loaded in my browser. Following my typical NIH rule when it comes to Horde, the result is the new Horde_Service_Twitter library and the twitter_timeline block for Horde's portal.  Horde_Service_Twitter supports authentication to Twitter via both the standard http authentication method as well as via OAuth. The latter making use of the Horde_Oauth library. The portal block allows you to publish a new tweet,  shows the most recent tweets by the people you are following and allows you to reply to a displayed tweet.

The addition of Horde_Service_Twitter, along with Horde_Service_Facebook, adds some exciting possibilities for integration points with other Horde applications. Horde already has some address book and calendar integration with Facebook, but other possibilities include things like automatically posting a notification to Twitter or Facebook when a set of new images are uploaded to Ansel, or maybe when a new blog post is published with Jonah.

 

Ansel (H3 and H4)

Ansel has gotten a fair amount of work recently and is ready for a 1.1 release. The most obvious change is full support of geo-tagging features.  Ansel has always been able to read,and display an image's meta data...but up until now you couldn't do much with any of the location data. Now, Ansel will recognize GPS coordinates in the meta data and display small thumbnails of those images in an embedded Google Map. There are various locations throughout Ansel where you can view these maps. You can also add location data to images that do not contain it as well as edit any existing location data. Full support for reverse geocoding means that you can (re)tag an image by either typing a textual name for the location (such as Philadelphia, PA) or by typing in actual GPS lat/lng coordinates. Of course, you can also (re)tag an image simply by browsing the Google Map and clicking where you want the image to be located.

Ansel's bleeding edge code has officially moved out of Horde's CVS repository and into the git repository, horde-hatchery. A fair amount of refactoring and internal improvements have already been done in getting Ansel and Horde_Image ready for Horde 4. Among these changes is better support for image meta data, with a new driver based on exif tool. This allows recognition of not only EXIF tags, but also IPTC and XMP data as well.

 

 

iPhoto/Aperture Export Plug-Ins (H3 and H4)

Related to the Ansel application, are new export plug-ins for both of Apple's image management applications, iPhoto and Aperture.  Currently available via Horde's horde-hatchery git repository, these plug-ins allow you to upload your images directly to an Ansel server from within iPhoto or Aperture. All meta data is retained when uploaded, including keywords that added using Aperture or iPhoto. You are able to create new galleries from the plug-in's interface, browse thumbnails of existing Ansel galleries (to see what images you have previously uploaded), and choose if the images should be resized (and to what size) before uploading.  Both plug-ins support configuring multiple Ansel servers if you happen to have access to different installations.

Even though these live in horde-hatchery, they will work with both Ansel 1.x as well as the bleeding edge Ansel code that lives in  the hatchery. The iPhoto exporter supports iPhoto '08 and later, and the Aperture exporter is written for Aperture 2.1 or later.  Both require OS X 10.5 or later. They should run on either PPC or Intel hardware, but have only been tested on Intel. Currently they are available only as source (which can easily be compiled using XCode) but a development build should be available shortly.

 

 

Kronolith (H4 only)

I've been tasked with adding support for resource scheduling to Kronolith, and the work is mostly complete. Resources may be invited to events by the event organizer using the existing attendees interface. Resources can be set up to automatically determine if they are available, and respond to the request automatically. There is also support for resource groups. Resource Groups are just a grouping of resources that are similar. When a group is invited to a meeting, the first available resource from that group will accept the invitation. For example, you have 10 projectors available and it doesn't really matter which projector is used for a meeting. Instead of going through all the projectors to see which one is available, you can just invite the projector group to the event. The first projector that is available during the meeting time will accept the invitation.