We use an autocompleter.js library downloaded off the net and have a taf action that provides the search.

Here's the library:

________________________________________________________________________
TO UNSUBSCRIBE: Go to http://www.witango.com/developer/maillist.taf
//
// This is a port of the sciptaculous autocomplete which is distributed under the MIT license.
//
// Autocompleter.Base handles all the autocompletion functionality 
// Autocompleter.Base handles all the autocompletion functionality
// that's independent of the data source for autocompletion. This
// includes drawing the autocompletion menu, observing keyboard
// and mouse events, and similar.
//
// Specific autocompleters need to provide, at the very least, 
// a getUpdatedChoices function that will be invoked every time
// the text inside the monitored textbox changes. This method 
// should get the text for which to provide autocompletion by
// invoking this.getToken(), NOT by directly accessing
// this.element.value. This is to allow incremental tokenized
// autocompletion. Specific auto-completion logic (AJAX, etc)
// belongs in getUpdatedChoices.
//
// Tokenized incremental autocompletion is enabled automatically
// when an autocompleter is instantiated with the 'tokens' option
// in the options parameter, e.g.:
// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
// will incrementally autocomplete with a comma as the token.
// Additionally, ',' in the above example can be replaced with
// a token array, e.g. { tokens: [',', '\n'] } which
// enables autocompletion on multiple tokens. This is most 
// useful when one of the tokens is \n (a newline), as it 
// allows smart autocompletion after linebreaks.

var KEY_BACKSPACE = 8
var KEY_TAB =       9
var KEY_RETURN =   13
var KEY_ESC =      27
var KEY_LEFT =     37
var KEY_UP =       38
var KEY_RIGHT =    39
var KEY_DOWN =     40
var KEY_DELETE =   46

Autocompleter = function() {

}

Autocompleter.Base = function() {

}

update(Autocompleter.Base.prototype, {

  baseInitialize: function(element, update, options) {
    this.element     = $(element);
    this.update      = $(update);
    this.hasFocus    = false; 
    this.changed     = false; 
    this.active      = false; 
    this.index       = 0;     
    this.entryCount  = 0;

    if (this.setOptions)
      this.setOptions(options);
    else
      this.options = options || {};

    this.options.paramName    = this.options.paramName || this.element.name;
    this.options.tokens       = this.options.tokens || [];
    this.options.frequency    = this.options.frequency || 0.4;
    this.options.minChars     = this.options.minChars || 1;
    this.options.onShow       = this.options.onShow || 
        bind(function(element, update){
          if(!update.style.position || update.style.position=='absolute') {
            update.style.position = 'absolute';
            this.clonePosition(element, update);
            //Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight});
          }
          // Todo: make it fade in ?
          showElement(update);
          //Effect.Appear(update,{duration:0.15});
        }, this);
    this.options.onHide = this.options.onHide || 
        function(element, update){
            // Todo: make if fade out like in the scriptaculous version?
            //new Effect.Fade(update,{duration:0.15})
            hideElement(update);
        };

    if (typeof(this.options.tokens) == 'string') 
      this.options.tokens = new Array(this.options.tokens);

    this.observer = null;
    
    this.element.setAttribute('autocomplete','off');

    hideElement(this.update);

    addToCallStack(this.element, "onblur", bind(this.onBlur, this));
    addToCallStack(this.element, "onkeypress", bind(this.onKeyPress, this));



    //Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
    //Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
  },

  /**
   * Copied from prototype.js version 1.4.0_rc3
   */
  cumulativeOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      element = element.offsetParent;
    } while (element);
    return [valueL, valueT];
  },

  /**
   * Copied from prototype.js version 1.4.0_rc3
   */
  clonePosition: function(source, target) {
    source = $(source);
    target = $(target);
    //target.style.position = 'absolute';
    var offsets = this.cumulativeOffset(source);
    //target.style.top    = offsets[1] + 'px';
    target.style.left   = offsets[0] + 'px';
    target.style.width  = source.offsetWidth + 'px';
    //target.style.height = source.offsetHeight + 'px';
  },


  show: function() {
    if(this.update.style.display == 'none') this.options.onShow(this.element, this.update);

    /*
    if(!this.iefix && 
      (navigator.appVersion.indexOf('MSIE')>0) &&
      (navigator.userAgent.indexOf('Opera')<0) &&
      (this.update.style.position=='absolute')) {
      appendChildNodes(this.update,
       '<iframe id="' + this.update.id + '_iefix" '+
       'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
       'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
      this.iefix = $(this.update.id+'_iefix');
    }
    if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
    */
  },
  
  fixIEOverlapping: function() {
    this.clonePosition(this.update, this.iefix);
    this.iefix.style.zIndex = 1;
    this.update.style.zIndex = 2;
    showElement(this.iefix);
  },

  hide: function() {
    this.stopIndicator();
    if(this.update.style.display !='none') this.options.onHide(this.element, this.update);
    if(this.iefix) hideElement(this.iefix);
  },

  startIndicator: function() {
    if(this.options.indicator) showElement(this.options.indicator);
  },

  stopIndicator: function() {
    if(this.options.indicator) hideElement(this.options.indicator);
  },

  onKeyPress: function(event) {
    if( event == null )
        event = window.event;

    if(this.active)
      switch(event.keyCode) {
       case KEY_TAB:
       case KEY_RETURN:
         this.selectEntry();
         this.stopEvent(event);
       case KEY_ESC:
         this.hide();
         this.active = false;
         this.stopEvent(event);
         return;
       case KEY_LEFT:
       case KEY_RIGHT:
         return;
       case KEY_UP:
         this.markPrevious();
         this.render();
         if(navigator.appVersion.indexOf('AppleWebKit')>0) this.stopEvent(event);
         return;
       case KEY_DOWN:
         this.markNext();
         this.render();
         if(navigator.appVersion.indexOf('AppleWebKit')>0) this.stopEvent(event);
         return;
      }
     else 
      if(event.keyCode==KEY_TAB || event.keyCode==KEY_RETURN)
        return;

    this.changed = true;
    this.hasFocus = true;

    if(this.observer) clearTimeout(this.observer);
      this.observer = 
        setTimeout(bind(this.onObserverEvent, this), this.options.frequency*1000);
  },

  onHover: function(event) {
    var element = this.findElement(event, 'LI');
    if(this.index != element.autocompleteIndex) 
    {
        this.index = element.autocompleteIndex;
        this.render();
    }
    this.stopEvent(event);
  },

  // find the first node with the given tagName, starting from the
  // node the event was triggered on; traverses the DOM upwards
  findElement: function(event, tagName) {
    if( event == null ) event = window.event;
    var element = event.target || event.srcElement;
    while (element.parentNode && (!element.tagName ||
        (element.tagName.toUpperCase() != tagName.toUpperCase())))
      element = element.parentNode;
    return element;
  },

  stopEvent: function(event) {
    if( event == null ) event = window.event;
    if (event.preventDefault) {
      event.preventDefault();
      event.stopPropagation();
    } else {
      event.returnValue = false;
      event.cancelBubble = true;
    }
  },
  
  onClick: function(event) {
    var element = this.findElement(event, 'LI');
    this.index = element.autocompleteIndex;
    this.selectEntry();
    this.hide();
  },
  
  onBlur: function(event) {
    // needed to make click events working
    setTimeout(bind(this.hide, this), 250);
    this.hasFocus = false;
    this.active = false;     
  }, 
  
  render: function() {
    if(this.entryCount > 0) {
      for (var i = 0; i < this.entryCount; i++)
        this.index==i ? 
          addElementClass(this.getEntry(i),"selected") :
          removeElementClass(this.getEntry(i),"selected");
        
      if(this.hasFocus) { 
        this.show();
        this.active = true;
      }
    } else {
      this.active = false;
      this.hide();
    }
  },
  
  markPrevious: function() {
    if(this.index > 0) this.index--
      else this.index = this.entryCount-1;
  },
  
  markNext: function() {
    if(this.index < this.entryCount-1) this.index++
      else this.index = 0;
  },
  
  getEntry: function(index) {
    return this.update.firstChild.childNodes[index];
  },
  
  getCurrentEntry: function() {
    return this.getEntry(this.index);
  },
  
  selectEntry: function() {
    this.active = false;
    this.updateElement(this.getCurrentEntry());
  },

  updateElement: function(selectedElement) {
    if (this.options.updateElement) {
      this.options.updateElement(selectedElement);
      return;
    }
    
    // the reverse, join is a workaround for a bug in the 1.1 version of MochiKit
    //
    var value = scrapeText(selectedElement, true).reverse().join("");

    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 {
      this.element.value = value;
    }
    this.element.focus();
    
    if (this.options.afterUpdateElement)
      this.options.afterUpdateElement(this.element, selectedElement);
  },

  updateChoices: function(choices) {

    if(!this.changed && this.hasFocus) {
      this.update.innerHTML = choices;

      // TODO: implement this via MochiKit
      //Element.cleanWhitespace(this.update);
      //Element.cleanWhitespace(this.update.firstChild);

      if(this.update.firstChild && this.update.firstChild.childNodes) {
        this.entryCount = 
          this.update.firstChild.childNodes.length;
        for (var i = 0; i < this.entryCount; i++) {
          var entry = this.getEntry(i);
          entry.autocompleteIndex = i;
          this.addObservers(entry);
        }
      } else { 
        this.entryCount = 0;
      }

      this.stopIndicator();

      this.index = 0;
      this.render();
    }
  },

  addObservers: function(element) {

    addToCallStack(element, "onmouseover", bind(this.onHover, this));
    addToCallStack(element, "onclick", bind(this.onClick, this));
  },

  onObserverEvent: function() {
    this.changed = false;   
    if(this.getToken().length>=this.options.minChars) {
      this.startIndicator();
      this.getUpdatedChoices();
    } else {
      this.active = false;
      this.hide();
    }
  },

  getToken: function() {
    var tokenPos = this.findLastToken();
    if (tokenPos != -1)
      var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
    else
      var ret = this.element.value;

    return /\n/.test(ret) ? '' : ret;
  },

  findLastToken: function() {
    var lastTokenPos = -1;

    for (var i=0; i<this.options.tokens.length; i++) {
      var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
      if (thisTokenPos > lastTokenPos)
        lastTokenPos = thisTokenPos;
    }
    return lastTokenPos;
  }
}
);



Autocompleter.Ajax = function() {
}

update(Autocompleter.Ajax.prototype, Autocompleter.Base.prototype, {
  init: function(element, update, url, options) {
        this.baseInitialize(element, update, options);
    this.options.asynchronous  = true;
    this.options.defaultParams = this.options.parameters || null;
    this.url                   = url;
  },

  getUpdatedChoices: function() {
    entry = encodeURIComponent(this.options.paramName) + '=' + 
      encodeURIComponent(this.getToken());

    this.options.parameters = this.options.callback ?
      this.options.callback(this.element, entry) : entry;

    if(this.options.defaultParams) 
      this.options.parameters += '&' + this.options.defaultParams;

    var url = this.url + '?' + this.options.parameters;

    var deferred = doSimpleXMLHttpRequest(url);
    deferred.addCallback( bind(this.onComplete, this) );
    deferred.addErrback( function(err) { alert("xmlhttp error:" + err); });

    //new Ajax.Request(this.url, this.options);
  },

  onComplete: function(request) {
    this.updateChoices(request.responseText);
  }

});

You need to point to a search function that returns the data as a <ul> list.

example

<ul class="autocompleter">
<@rows><li><@col 1><span class="informal"><@ifequal '<@col 2>' 'N'> [PubMed Journal]</@if></span></li></@rows>
</ul>

(Note that in this case we get the argument, massage it a bit to clean it up, and then perform a case insensitive Oracle search. You can have some really complex stuff happen in this step behind the scenes.)

Anything you put between <span class="informal"> will show in the displayed list, but will NOT be passed as an argument. What I have not figured out how to do yet is to show one value and pass a different one, which I would really like to do in some cases.

And then put it into a tcf (entire code shown below between the <pre> tags so it won't show up as spam ), then made it into a custom tag we can call for any autocompletes...  Witango rocks when it comes to Ajax-ery. Note the obvious, that you have to call the library in the document's head section.

<pre>
<@ifempty <@var method$size>><@assign name="method$size" value="20"></@if>
<@ifempty <@var method$TAF>><@assign name="method$taf" value="<@cgi><@appfile>"></@if>

<input type="text" name="<@var method$fieldname>" id="<@var method$fieldname>" autocomplete="off" size="<@var method$size>">
<div id="<@var method$fieldname>hint" style="display: none;"></div>

<@if "('<@var method$function>' = '') and ('<@var method$args>' = '')">
<script
type="text/_javascript_">
new Ajax.Autocompleter("<@var method$fieldname>","<@var method$fieldname>hint","<@var method$TAF>?");
</script>
<@elseif "('<@var method$function>' != '') and ('<@var method$args>' = '')">
<script
type="text/_javascript_">
new Ajax.Autocompleter("<@var method$fieldname>","<@var method$fieldname>hint","<@var method$TAF>?_function=<@var method$function>");
</script>
<@elseif "('<@var method$function>' = '') and ('<@var method$args>' != '')">
<script
type="text/_javascript_">
new Ajax.Autocompleter("<@var method$fieldname>","<@var method$fieldname>hint","<@var method$TAF>?<@var method$args>");
</script>
<@else>
<script
type="text/_javascript_">
new Ajax.Autocompleter("<@var method$fieldname>","<@var method$fieldname>hint","<@var method$TAF>?_function=<@var method$function>&<@var method$args>");
</script>
</@if>

</pre>

Call it with this format, where the custom tag is <@ajax_completer

<@ajax_completer fieldname="" TAF="" function="" args="" size="">

where all except fieldname can be null - if this is a function in the same taf, taf should be null. If function is null, ditto. Size specifies the size of the input field, default is 20.

It helps to tweak styles for this, by the way. The data is output as a list, and is transparent without styles and really, really hard to read over other text. 

So if I set <@ajax_completer fieldname="journal" taf="journalfind.taf" size="50">, what would occur on the html page would be (less the <pre> tags!)

<pre>

<input type="text" name="journal" id="journal" autocomplete="off" size="50">
<div id="journalhint" style="display: none;"></div>

<script
type="text/_javascript_">
new Ajax.Autocompleter("journal","journalhint","journalfind.taf?");
</script>

</pre>


On Apr 11, 2007, at 2:15 PM, Stefan Gonick wrote:

Hi Everyone,

I want to do the simplest possible AJAX function for one of my clients.
I have never done any AJAX before, so I don't even know where to start
and am hesitant to plunge into the whole topic for this simple application.

What I am trying to do is as follows:

I have a table with 2500 entries that I would like to put in a pop-up list,
but it's too many items to be practical. Instead, I would like to use a small
text field where I would start typing in a few characters, which would
dynamically (AJAX-style) update a separate pop-up list (select field).
I probably wouldn't start populating the list until at least 2 characters
had been typed to keep the list size reasonable.

Has anyone created any simple AJAX already that could give me some
pointers for my application?

Thanks in advance,
Stefan

=====================================================
Database WebWorks: Dynamic web sites through database integration
http://www.DatabaseWebWorks.com

CoachVille: For coaches and people taking teleclasses
http :// www.cvcommunity.com?af=69474

________________________________________________________________________
TO UNSUBSCRIBE: Go to http://www.witango.com/developer/maillist.taf

Reply via email to