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); }); }