Auto-completion for tag lists

See the end of this post for an important update.

Standard Rails auto-completion is geared toward completing a single value entered in a text field. For most cases this is exactly what’s needed, but there is one prominent case that is different: auto-completion of tags. If something can have a tag at all, it’s part and parcel of the deal that it can have multiple tags.

As far as I can tell, some configuration tweak is not enough to make standard auto-completion do our bidding. But not all is lost. It’s just a small matter of programming to get what we want.

I’m assuming that you’re using Acts As Taggable or Acts As Taggable On Steroids for the model-level tagging, or otherwise that you know what you’re doing.

First, we need client-side support for picking out only the tag we’re currently editing.

Ajax.TagAutocompleter = Class.create();
Object.extend(Object.extend(Ajax.TagAutocompleter.prototype, Ajax.Autocompleter.prototype), {
  getToken: function() {
    var range = this.rangeForCaret();
    var value = this.element.value.substring(range.start, range.end);
    return value;
  },
  updateElement: function(selectedElement) {
    var oldValue = this.element.value;
    var range = this.rangeForCaret();
    var prefix = oldValue.substring(0, range.start);
    var suffix = oldValue.substring(range.end);
    var value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
    this.element.value = prefix + value + suffix;
    this.element.focus();
  },
  rangeForCaret: function() {
    var value = this.element.value;
    var start = 0, end = value.length;
    var pos;
    if (typeof this.element.selectionStart != 'undefined') {
      pos = this.element.selectionStart;
    } else if (document.selection) {
      var sel = document.selection.createRange();
      var selLength = sel.text.length;
      sel.moveStart('character', -(value.length));
      pos = sel.text.length - selLength;
    }
    if (pos != undefined) {
      end = pos;
      while (value.charAt(end) != ',' && end < value.length) {
        end++;
      }
      start = end - 1;
      while (value.charAt(start) != ',' && start > 0) {
        start--;
      }
      while (start < end && (value.charAt(start) == ' ' || value.charAt(start) == ',')) {
        start++;
      }
      while (start < end && value.charAt(end) == ' ') {
        end--;
      }
    }
    return {start: start, end: end};
  }
});

Yes, the code of rangeForCaret is a bit convoluted, let’s say traditional JavaScript-style. The general approach for getting the position of the text cursor (“caret”) is explained here. The purpose of all the stuff is to support auto-completion in the middle of the text field, not just at its end.

On the server-side, in the relevant controller, there’s still some work to do as the standard auto_complete_for can’t handle tags. Put the following method in your controller base class, app/controllers/application.rb.

def self.tag_auto_complete_for(object)
  define_method("auto_complete_for_#{object}_tag_list") do
    @items = Tag.find(:all,
      :conditions => [ 'LOWER(name) LIKE ?', '%' + params[object][:tag_list].downcase + '%' ], 
      :\order => 'name ASC',
      :limit => 10)
    render :inline => "<%= auto_complete_result @items, :name %>"
  end
end

Then, in an individual controller, create the tag auto-completion method like this

class PeopleController < ApplicationController
  tag_auto_complete_for :person
end

Almost done!

What’s still missing is a way to activate auto-completion for the tag input field. It would surely be possible to copy & change text_field_with_auto_complete or to monkey patch it appropriately. I prefer another way, in fact, I’m not using text_field_with_auto_complete at all, not even where it would work. Instead, I prefer more unobtrusive way.

The way starts with the class autocomplete to text fields that are amenable to auto-completion — including, of course, the tag list field.

  <%= f.text_field :tag_list, :class => 'autocomplete' %>

As this in itself is completely inert, we need to nudge it a bit in application.js. Here’s a function that looks for all text fields with class autocomplete and actually makes them do what they say.

function installAutocompletion() {
  $$('input.autocomplete[type=text]').each(function(element) {
    var fieldId = element.id;
    var completions = document.createElement('div');
    completions.id = fieldId + '_auto_complete';
    completions.className = 'auto_complete';
    completions.style.display = 'none';
    element.parentNode.insertBefore(completions, element.nextSibling);

    var url = 'auto_complete_for_' + fieldId;
    if (/_tag_list$/.test(fieldId)) {
      new Ajax.TagAutocompleter(element, completions, url);
    } else {
      new Ajax.Autocompleter(element, completions, url);
    }
  });
}

Be sure to call this method some time after the page is loaded. If you don’t have a function from where to call it, add this to application.js

  Event.observe(window, 'load', installAutocompletion);

Update, 2007-02-05

I may not have seen the wood for the trees when looking at the original code of the Rails/Script.aculo.us autocompleter. Of course it is fully capable of auto-completing more than one token in the same text field — with a very little configuration tweak, no less.

What it can’t do is auto-completion in the middle of the line; it only works at the end. That’s an acceptable restriction, to my mind. So, forget about Ajax.TagAutocompleter from above and instead install your autocompleters like this

function installAutocompletion() {
  $$('input.autocomplete[type=text]').each(function(element) {
    var fieldId = element.id;
    var completions = document.createElement('div');
    completions.id = fieldId + '_auto_complete';
    completions.className = 'auto_complete';
    completions.style.display = 'none';
    element.parentNode.insertBefore(completions, element.nextSibling);

    var url = 'auto_complete_for_' + fieldId;
    var options = {};
    if (/_tag_list$/.test(fieldId)) {
      options.tokens = ',';
    }
    new Ajax.Autocompleter(element, completions, url, options);
  });
}

Eclipse: Printing Editor Templates using XSLT, CSS, and Firefox

I’m using Eclipse with RadRails to edit my Ruby and Rails code (and, yes, I’ve used it for Java, way back when).

Inside Eclipse the various text editors offer so-called templates. These are snippets of common code with placeholders for variable bits. Type in the abbreviation for a template, then type Ctrl-Space and the abbreviation is expanded to the template code. For editing Ruby/Rails templates are available from the RadRails Templates site.

I’ve only recently started to use templates and I’m still early on the learning curve, that is, I haven’t memorized many of the abbreviations. Unfortunately, Eclipse itself only has a list of templates with their expansions in its modal Preferences window. That’s not very helpful when coding. What Eclipse does have is a function to export templates to XML and that’s what I did.

The exported file looks like this

<?xml version="1.0" encoding="UTF-8"?>
<templates>
  <template autoinsert="true" context="ruby" deleted="false" description="tm - all? { |e| .. }" enabled="true" name="all">all? { |${e}| ${cursor} }
  </template>
  <template autoinsert="true" context="ruby" deleted="false" description="tm - alias_method .." enabled="true" name="am">alias_method :${new_name}, :${old_name}
  </template>
  ...
</templates>

The easiest approach I could think of to make this into something printable is this. First, transform it into HTML using an XSL stylesheet, then make the HTML pretty by styling it with an CSS stylesheet. Luckily, most of this can be done inside a web browser such as Firefox that understands XSLT.

Here’s the XSLT

<?xml version="1.0" encoding="iso-8859-1"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:template match="/">
  <html>
    <head>
      <xsl:apply-templates mode="head" />
      <link rel="stylesheet" type="text/css" href="templates.css" />
    </head>
    <body>
      <xsl:apply-templates mode="body" />
    </body>
  </html>
</xsl:template>

<xsl:template match="title" mode="head">
  <title>
    <xsl:value-of select="." />
  </title>
</xsl:template>

<xsl:template match="title" mode="body">
  <h1>
    <xsl:value-of select="." />
  </h1>
</xsl:template>

<xsl:template match="templates" mode="body">
  <dl>
    <xsl:apply-templates mode="body" />
  </dl>
</xsl:template>


<xsl:template match="template" mode="body">
  <dt>
    <xsl:value-of select="@name" />
  </dt>
  <dd>
    <xsl:value-of select="." />
  </dd>
</xsl:template>

</xsl:stylesheet>

And the CSS stylesheet

html {
  font-family: sans-serif;
}

dt {
  border-top: 1px solid #aaa;
  padding-top: 0.2em;
  font-weight: bold;
}

dd {
  margin-top: 0.3em;
  margin-bottom: 1em;
  white-space: pre;
  font-family: monospace;
}

Now, these parts need to be connected. For this, it is necessary to slightly edit the original XML template file. For good measure, I throw in a title.

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="templates.xsl" ?>
<templates>
  <title>RadRails Ruby Templates</title>
  <template ...>
    ...
  </template>

You may find that the indentation for the template code looks wrong. The reason is that the code contains tabs for indentation. Ruby convention is to indent by 2 spaces for each level, by contrast, Firefox apparently expands tabs to 8 spaces. A small glitch that can be rectified easily

$ sed -i 's/\t/  /g' template.xml