Christopher Williams  
             
            
             
            Posts: 88
             
            Nickname: sgtcoolguy
             
            Registered: Apr, 2006
             
Christopher Williams is a Ruby, Rails and Java programmer
 
 
 
  
             
             
         
        
         
        
        
        Prototype based Tag Autocompletion 
         
		 
        
        
        Posted: Sep 19, 2006 9:55 AM
         
         
        
            
         
     
    
        
        
 
 
Advertisement
 
 
  
 
    I've been working on a website that uses tags internally. The acts_as_taggable  plugin has been great, but it only takes care of the ActiveRecord portion of tagging. One of the pieces of functionality I needed was to create an autocompleter for users to enter a list of tags on a record. Below is a pure copy-paste dump of my implementation. I'm not claiming it's good code or the right way top do things, but for those looking to use an autocompleter for a text field holding tags might find it useful. The implementation is close to that used by delicious  in the tags field on posting new bookmarks.
Here's an example usage:
<label for="tags">Tags</label>
<%= text_field_tag 'tags', @tags.join(' '), :size => 80, :autocomplete => "off" %>
<div class="auto_complete" id="tags_auto_complete"></div> 
And the prototype-based class in javascript. You'll probably want to stick this in your application.js file if you're using Rails .
String.prototype.trim = function(){ return this.replace(/^\s+|\s+$/g,'') }
Autocompleter.Tag = Class.create();
Autocompleter.Tag.prototype = Object.extend(new Autocompleter.Base(), {
  lastEdit: '',
  currentTag: {},
  initialize: function(element, update, array, options) {
    this.baseInitialize(element, update, options);
    this.options.array = array;
  },
  
  getCurrentTag: function() {
	if(this.element.value == this.lastEdit) return true // no edit
	if(this.element == '') return false
	this.currentTag = {}
	var tagArray=this.element.value.toLowerCase().split(' '), oldArray=this.lastEdit.toLowerCase().split(' '), currentTags = [], matched=false, t,o
	for (t in tagArray) {
		for (o in oldArray) {
			if(typeof oldArray[o] == 'undefined') { oldArray.splice(o,1); break }
			if(tagArray[t] == oldArray[o]) { matched = true; oldArray.splice(o,1); break; }
		}
		if(!matched) currentTags[currentTags.length] = t
		matched=false
	}
	// more than one word changed... abort
	if(currentTags.length > 1) {
	  // hideSuggestions(); TODO Fix this!
	  return false;
	}
	this.currentTag = { text:tagArray[currentTags[0]], index:currentTags[0] }
	return true
  },
  getUpdatedChoices: function() {
    this.updateChoices(this.options.selector(this));
  },
  
  onKeyPress: function(event) {
    if(this.active)
      switch(event.keyCode) {
       case Event.KEY_TAB:
       case Event.KEY_RETURN:
         this.selectEntry();
         Event.stop(event);
       case Event.KEY_ESC:
         this.hide();
         this.active = false;
         Event.stop(event);
         return;
       case Event.KEY_LEFT:
       case Event.KEY_RIGHT:
         return;
       case Event.KEY_UP:
         this.markPrevious();
         this.render();
         if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
         return;
       case Event.KEY_DOWN:
         this.markNext();
         this.render();
         if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
         return;
      }
     else 
       if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || 
         (navigator.appVersion.indexOf('AppleWebKit') > 0 && event.keyCode == 0)) return;
    this.lastEdit = this.element.value;
    this.changed = true;
    this.hasFocus = true;
    if(this.observer) clearTimeout(this.observer);
      this.observer = 
        setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
  },
  
  updateElement: function(selectedElement) {
    if (this.options.updateElement) {
      this.options.updateElement(selectedElement);
      return;
    }
    var value = '';
    if (this.options.select) {
      var nodes = document.getElementsByClassName(this.options.select, selectedElement) || [];
      if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
    } else
      value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
    
    var lastTokenPos = this.findLastToken();
    if (lastTokenPos != -1) {
      var newValue = this.element.value.substr(0, lastTokenPos + 1);
      var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
      if (whitespace)
        newValue += whitespace[0];
      this.element.value = newValue + value;
    } else {
      tagArray = this.element.value.trim().split(' ')
      if (!this.getCurrentTag()) return;  // We had a problem getting the current tag, just return?
      tagArray[this.currentTag.index] = value;
      this.element.value = tagArray.join(' ');
    }
    this.element.focus();
    
    if (this.options.afterUpdateElement)
      this.options.afterUpdateElement(this.element, selectedElement);
  },
  setOptions: function(options) {
    this.options = Object.extend({
      choices: 10,
      partialSearch: true,
      partialChars: 2,
      ignoreCase: true,
      fullSearch: false,
      selector: function(instance) {
          var ret       = []; // Beginning matches
        var partial   = []; // Inside matches
        var entry     = instance.getToken();
        
        entry = entry.trim().split(' ').pop()
        
        var count     = 0;
        for (var i = 0; i < instance.options.array.length &&  
          ret.length < instance.options.choices ; i++) { 
          var elem = instance.options.array[i];
          var foundPos = instance.options.ignoreCase ? 
            elem.toLowerCase().indexOf(entry.toLowerCase()) : 
            elem.indexOf(entry);
          while (foundPos != -1) {
            if (foundPos == 0 && elem.length != entry.length) { 
              ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" + 
                elem.substr(entry.length) + "</li>");
              break;
            } else if (entry.length >= instance.options.partialChars && 
              instance.options.partialSearch && foundPos != -1) {
              if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
                partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
                  elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
                  foundPos + entry.length) + "</li>");
                break;
              }
            }
            foundPos = instance.options.ignoreCase ? 
              elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : 
              elem.indexOf(entry, foundPos + 1);
          }
        }
        if (partial.length)
          ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
        return "<ul>" + ret.join('') + "</ul>";
      }    
    }, options || {});
  }
});
       
Read: Prototype based Tag Autocompletion