Update autocomplete code.
authorMichael M Slusarz <slusarz@curecanti.org>
Sat, 17 Jan 2009 23:47:29 +0000 (16:47 -0700)
committerMichael M Slusarz <slusarz@curecanti.org>
Sat, 17 Jan 2009 23:49:17 +0000 (16:49 -0700)
kronolith/js/autocomplete.js [new file with mode: 0644]
kronolith/js/src/autocomplete.js [new file with mode: 0644]
kronolith/lib/Imple.php
kronolith/lib/Imple/ContactAutoCompleter.php

diff --git a/kronolith/js/autocomplete.js b/kronolith/js/autocomplete.js
new file mode 100644 (file)
index 0000000..9e3ea50
--- /dev/null
@@ -0,0 +1 @@
+var Autocompleter={};Autocompleter.Base=Class.create({baseInitialize:function(b,c,a){this.element=$(b);this.update=$(c).hide();this.active=this.changed=this.hasFocus=false;this.entryCount=this.index=0;this.observer=null;this.oldval=$F(this.element);this.options=Object.extend({paramName:this.element.name,tokens:[],frequency:0.4,minChars:1,onHide:this._onHide.bind(this),onShow:this._onShow.bind(this)},(this._setOptions?this._setOptions(a):(a||{})));if(!this.options.tokens.include("\n")){this.options.tokens.push("\n")}this.element.writeAttribute("autocomplete","off").observe("blur",this._onBlur.bindAsEventListener(this)).observe(Prototype.Browser.Gecko?"keypress":"keydown",this._onKeyPress.bindAsEventListener(this))},_onShow:function(a,e){var d,b=e.getStyle("position");if(!b||b=="absolute"){d=(Prototype.Browser.IE)?a.cumulativeScrollOffset():[0];e.setStyle({position:"absolute"}).clonePosition(a,{setHeight:false,offsetTop:a.offsetHeight,offsetLeft:d[0]})}new Effect.Appear(e,{duration:0.15})},_onHide:function(a,b){new Effect.Fade(b,{duration:0.15})},show:function(){if(!this.update.visible()){this.options.onShow(this.element,this.update)}if(Prototype.Browser.IE&&!this.iefix&&this.update.getStyle("position")=="absolute"){this.iefix=new Element("IFRAME",{src:"javascript:false;",frameborder:0,scrolling:"no"}).setStyle({position:"absolute",filter:"progid:DXImageTransform.Microsoft.Alpha(opactiy=0)",zIndex:1}).hide();this.update.setStyle({zIndex:2}).insert({after:this.iefix})}if(this.iefix){this._fixIEOverlapping.bind(this).delay(0.05)}},_fixIEOverlapping:function(){this.iefix.clonePosition(this.update).show()},hide:function(){this.stopIndicator();if(this.update.visible()){this.options.onHide(this.element,this.update);if(this.iefix){this.iefix.hide()}}},startIndicator:function(){if(this.options.indicator){$(this.options.indicator).show()}},stopIndicator:function(){if(this.options.indicator){$(this.options.indicator).hide()}},_onKeyPress:function(a){if(this.active){switch(a.keyCode){case Event.KEY_TAB:case Event.KEY_RETURN:this.selectEntry();a.stop();return;case Event.KEY_ESC:this.hide();this.active=false;a.stop();return;case Event.KEY_LEFT:case Event.KEY_RIGHT:return;case Event.KEY_UP:case Event.KEY_DOWN:if(a.keyCode==Event.KEY_UP){this.markPrevious()}else{this.markNext()}this.render();a.stop();return}}else{switch(a.keyCode){case 0:if(!Prototype.Browser.WebKit){break}case Event.KEY_TAB:case Event.KEY_RETURN:return}}this.changed=this.hasFocus=true;if(this.observer){clearTimeout(this.observer)}this.observer=this.onObserverEvent.bind(this).delay(this.options.frequency)},_onHover:function(c){var b=c.findElement("LI"),a=b.readAttribute("acIndex");if(this.index!=a){this.index=a;this.render()}c.stop()},_onClick:function(a){this.index=a.findElement("LI").readAttribute("acIndex");this.selectEntry()},_onBlur:function(a){this.hide.bind(this).delay(0.25);this.active=this.hasFocus=false},render:function(){var a=0;if(this.entryCount){this.update.down().childElements().each(function(b){[b].invoke(this.index==a++?"addClassName":"removeClassName","selected")},this);if(this.hasFocus){this.show();this.active=true}}else{this.active=false;this.hide()}},markPrevious:function(){if(this.index){--this.index}else{this.index=this.entryCount-1}this.getEntry(this.index).scrollIntoView(true)},markNext:function(){if(this.index<this.entryCount-1){++this.index}else{this.index=0}this.getEntry(this.index).scrollIntoView(false)},getEntry:function(a){return this.update.down().childElements()[a]},selectEntry:function(){this.active=false;this.updateElement(this.getEntry(this.index));this.hide()},updateElement:function(d){var e,g,b,c,a,h=this.options,f="";if(h.updateElement){h.updateElement(d);return}if(h.select){b=$(d).select("."+h.select)||[];if(b.size()){f=b[0].collectTextNodes(h.select)}}else{f=d.collectTextNodesIgnoreClass("informal")}e=this.getTokenBounds();if(e[0]!=-1){a=$F(this.element);g=a.substr(0,e[0]);c=a.substr(e[0]).match(/^\s+/);if(c){g+=c[0]}this.element.setValue(g+f+a.substr(e[1]))}else{this.element.setValue(f)}this.element.focus();if(h.afterUpdateElement){h.afterUpdateElement(this.element,d)}this.oldval=$F(this.element)},updateChoices:function(e){var a,d,c,b=0;if(!this.changed&&this.hasFocus){a=new Element("LI");c=new Element("UL");d=new RegExp("("+this.getToken()+")","i");e.each(function(f){c.insert(a.cloneNode(false).writeAttribute("acIndex",b++).update(f.gsub(d,"<strong>#{1}</strong>")))});this.update.update(c);this.entryCount=e.size();c.childElements().each(this.addObservers.bind(this));this.stopIndicator();this.index=0;if(this.entryCount==1&&this.options.autoSelect){this.selectEntry()}else{this.render()}}},addObservers:function(a){$(a).observe("mouseover",this._onHover.bindAsEventListener(this)).observe("click",this._onClick.bindAsEventListener(this))},onObserverEvent:function(){this.changed=false;if(this.getToken().length>=this.options.minChars){this.getUpdatedChoices()}else{this.active=false;this.hide()}this.oldval=$F(this.element)},getToken:function(){var a=this.getTokenBounds();return $F(this.element).substring(a[0],a[1]).strip()},getTokenBounds:function(){var j,e,f,c,d,g,m=this.options.tokens,k=$F(this.element),h=k.length,b=-1,a=Math.min(h,this.oldval.length);if(k.strip().empty()){return[-1,0]}j=a;for(e=0;e<a;++e){if(k[e]!=this.oldval[e]){j=e;break}}d=(j==this.oldval.length?1:0);for(f=0,c=m.length;f<c;++f){g=k.lastIndexOf(m[f],j+d-1);if(g>b){b=g}g=k.indexOf(m[f],j+d);if(g!=-1&&g<h){h=g}}return[b+1,h]}});Ajax.Autocompleter=Class.create(Autocompleter.Base,{initialize:function(c,d,b,a){this.baseInitialize(c,d,a);this.options=Object.extend(this.options,{asynchronous:true,onComplete:this._onComplete.bind(this),defaultParams:$H(this.options.parameters)});this.url=b;this.cache=$H()},getUpdatedChoices:function(){var b,d=this.options,a=this.getToken(),e=this.cache.get(a);if(e){this.updateChoices(e)}else{b=Object.clone(d.defaultParams);this.startIndicator();b.set(d.paramName,a);d.parameters=b.toQueryString();new Ajax.Request(this.url,d)}},_onComplete:function(a){this.updateChoices(this.cache.set(this.getToken(),a.responseJSON))}});Autocompleter.Local=Class.create(Autocompleter.Base,{initialize:function(c,d,a,b){this.baseInitialize(c,d,b);this.options.arr=a},getUpdatedChoices:function(){this.updateChoices(this._selector())},_setOptions:function(a){return Object.extend({choices:10,partialSearch:true,partialChars:2,ignoreCase:true,fullSearch:false},a||{})},_selector:function(){var b=this.getToken(),c=b.length,a=0,d=this.options;if(d.ignoreCase){b=b.toLowerCase()}return d.arr.findAll(function(e){if(a==d.choices){throw $break}if(d.ignoreCase){e=e.toLowerCase()}e=e.unescapeHTML();var f=e.indexOf(b);if(f!=-1&&((f==0&&e.length!=c)||(c>=d.partialChars&&d.partialSearch&&(d.fullSearch||/\s/.test(e.substr(f-1,1)))))){++a;return true}return false},this)}});
\ No newline at end of file
diff --git a/kronolith/js/src/autocomplete.js b/kronolith/js/src/autocomplete.js
new file mode 100644 (file)
index 0000000..dfc925c
--- /dev/null
@@ -0,0 +1,465 @@
+/**
+ * autocomplete.js - A javascript library which implements autocomplete.
+ * Requires prototype.js v1.6.0.2+ and scriptaculous v1.8.0+ (effects.js)
+ *
+ * Adapted from script.aculo.us controls.js v1.8.0
+ *   (c) 2005-2007 Thomas Fuchs, Ivan Krstic, and Jon Tirsen
+ *   Contributors: Richard Livsey, Rahul Bhargava, Rob Wills
+ *   http://script.aculo.us/
+ *
+ * The original script was freely distributable under the terms of an
+ * MIT-style license.
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ */
+
+var Autocompleter = {};
+Autocompleter.Base = Class.create({
+    baseInitialize: function(element, update, options)
+    {
+        this.element = $(element);
+        this.update = $(update).hide();
+        this.active = this.changed = this.hasFocus = false;
+        this.entryCount = this.index = 0;
+        this.observer = null;
+        this.oldval = $F(this.element);
+
+        this.options = Object.extend({
+            paramName: this.element.name,
+            tokens: [],
+            frequency: 0.4,
+            minChars: 1,
+            onHide: this._onHide.bind(this),
+            onShow: this._onShow.bind(this)
+        }, (this._setOptions ? this._setOptions(options) : (options || {})));
+
+        // Force carriage returns as token delimiters anyway
+        if (!this.options.tokens.include('\n')) {
+            this.options.tokens.push('\n');
+        }
+
+        this.element.writeAttribute('autocomplete', 'off').observe("blur", this._onBlur.bindAsEventListener(this)).observe(Prototype.Browser.Gecko ? "keypress" : "keydown", this._onKeyPress.bindAsEventListener(this));
+    },
+
+    _onShow: function(elt, update)
+    {
+        var c, p = update.getStyle('position');
+        if (!p || p == 'absolute') {
+            // Temporary fix for Bug #7074 - Fixed as of prototypejs 1.6.0.3
+            c = (Prototype.Browser.IE) ? elt.cumulativeScrollOffset() : [ 0 ];
+            update.setStyle({ position: 'absolute' }).clonePosition(elt, {
+                setHeight: false,
+                offsetTop: elt.offsetHeight,
+                offsetLeft: c[0]
+            });
+        }
+        new Effect.Appear(update, { duration: 0.15 });
+    },
+
+    _onHide: function(elt, update)
+    {
+        new Effect.Fade(update, { duration: 0.15 });
+    },
+
+    show: function()
+    {
+        if (!this.update.visible()) {
+            this.options.onShow(this.element, this.update);
+        }
+
+        if (Prototype.Browser.IE &&
+            !this.iefix &&
+            this.update.getStyle('position') == 'absolute') {
+            this.iefix = new Element('IFRAME', { src: 'javascript:false;', frameborder: 0, scrolling: 'no' }).setStyle({ position: 'absolute', filter: 'progid:DXImageTransform.Microsoft.Alpha(opactiy=0)', zIndex: 1 }).hide();
+            this.update.setStyle({ zIndex: 2 }).insert({ after: this.iefix });
+        }
+
+        if (this.iefix) {
+            this._fixIEOverlapping.bind(this).delay(0.05);
+        }
+    },
+
+    _fixIEOverlapping: function()
+    {
+        this.iefix.clonePosition(this.update).show();
+    },
+
+    hide: function()
+    {
+        this.stopIndicator();
+        if (this.update.visible()) {
+            this.options.onHide(this.element, this.update);
+            if (this.iefix) {
+                this.iefix.hide();
+            }
+        }
+    },
+
+    startIndicator: function()
+    {
+        if (this.options.indicator) {
+            $(this.options.indicator).show();
+        }
+    },
+
+    stopIndicator: function()
+    {
+        if (this.options.indicator) {
+            $(this.options.indicator).hide();
+        }
+    },
+
+    _onKeyPress: function(e)
+    {
+        if (this.active) {
+            switch (e.keyCode) {
+            case Event.KEY_TAB:
+            case Event.KEY_RETURN:
+                this.selectEntry();
+                e.stop();
+                return;
+
+            case Event.KEY_ESC:
+                this.hide();
+                this.active = false;
+                e.stop();
+                return;
+
+            case Event.KEY_LEFT:
+            case Event.KEY_RIGHT:
+                return;
+
+            case Event.KEY_UP:
+            case Event.KEY_DOWN:
+                if (e.keyCode == Event.KEY_UP) {
+                    this.markPrevious();
+                } else {
+                    this.markNext();
+                }
+                this.render();
+                e.stop();
+                return;
+            }
+        } else {
+            switch (e.keyCode) {
+            case 0:
+                if (!Prototype.Browser.WebKit) {
+                    break;
+                }
+                // Fall through to below case
+                //
+            case Event.KEY_TAB:
+            case Event.KEY_RETURN:
+                return;
+            }
+        }
+
+        this.changed = this.hasFocus = true;
+
+        if (this.observer) {
+            clearTimeout(this.observer);
+        }
+        this.observer = this.onObserverEvent.bind(this).delay(this.options.frequency);
+    },
+
+    _onHover: function(e)
+    {
+        var elt = e.findElement('LI'),
+            index = elt.readAttribute('acIndex');
+        if (this.index != index) {
+            this.index = index;
+            this.render();
+        }
+        e.stop();
+    },
+
+    _onClick: function(e)
+    {
+        this.index = e.findElement('LI').readAttribute('acIndex');
+        this.selectEntry();
+    },
+
+    _onBlur: function(e)
+    {
+        // Needed to make click events work
+        this.hide.bind(this).delay(0.25);
+        this.active = this.hasFocus = false;
+    },
+
+    render: function()
+    {
+        var i = 0;
+
+        if (this.entryCount) {
+            this.update.down().childElements().each(function(e) {
+                [ e ].invoke(this.index == i++ ? 'addClassName' : 'removeClassName', 'selected');
+            }, this);
+            if (this.hasFocus) {
+                this.show();
+                this.active = true;
+            }
+        } else {
+            this.active = false;
+            this.hide();
+        }
+    },
+
+    markPrevious: function()
+    {
+        if (this.index) {
+            --this.index;
+        } else {
+            this.index = this.entryCount - 1;
+        }
+        this.getEntry(this.index).scrollIntoView(true);
+    },
+
+    markNext: function()
+    {
+        if (this.index < this.entryCount - 1) {
+            ++this.index;
+        } else {
+            this.index = 0;
+        }
+        this.getEntry(this.index).scrollIntoView(false);
+    },
+
+    getEntry: function(index)
+    {
+        return this.update.down().childElements()[index];
+    },
+
+    selectEntry: function()
+    {
+        this.active = false;
+        this.updateElement(this.getEntry(this.index));
+        this.hide();
+    },
+
+    updateElement: function(elt)
+    {
+        var bounds, newValue, nodes, whitespace, v,
+            o = this.options,
+            value = '';
+
+        if (o.updateElement) {
+            o.updateElement(elt);
+            return;
+        }
+
+        if (o.select) {
+            nodes = $(elt).select('.' + o.select) || [];
+            if (nodes.size()) {
+                value = nodes[0].collectTextNodes(o.select);
+            }
+        } else {
+            value = elt.collectTextNodesIgnoreClass('informal');
+        }
+
+        bounds = this.getTokenBounds();
+        if (bounds[0] != -1) {
+            v = $F(this.element);
+            newValue = v.substr(0, bounds[0]);
+            whitespace = v.substr(bounds[0]).match(/^\s+/);
+            if (whitespace) {
+                newValue += whitespace[0];
+            }
+            this.element.setValue(newValue + value + v.substr(bounds[1]));
+        } else {
+            this.element.setValue(value);
+        }
+        this.element.focus();
+
+        if (o.afterUpdateElement) {
+            o.afterUpdateElement(this.element, elt);
+        }
+
+        this.oldval = $F(this.element);
+    },
+
+    updateChoices: function(choices)
+    {
+        var li, re, ul,
+            i = 0;
+
+        if (!this.changed && this.hasFocus) {
+            li = new Element('LI');
+            ul = new Element('UL');
+            re = new RegExp("(" + this.getToken() + ")", "i");
+
+            choices.each(function(n) {
+                ul.insert(li.cloneNode(false).writeAttribute('acIndex', i++).update(n.gsub(re, '<strong>#{1}</strong>')));
+            });
+
+            this.update.update(ul);
+            this.entryCount = choices.size();
+            ul.childElements().each(this.addObservers.bind(this));
+
+            this.stopIndicator();
+            this.index = 0;
+
+            if (this.entryCount == 1 && this.options.autoSelect) {
+                this.selectEntry();
+            } else {
+                this.render();
+            }
+        }
+    },
+
+    addObservers: function(elt)
+    {
+        $(elt).observe("mouseover", this._onHover.bindAsEventListener(this)).observe("click", this._onClick.bindAsEventListener(this));
+    },
+
+    onObserverEvent: function()
+    {
+        this.changed = false;
+        if (this.getToken().length >= this.options.minChars) {
+            this.getUpdatedChoices();
+        } else {
+            this.active = false;
+            this.hide();
+        }
+        this.oldval = $F(this.element);
+    },
+
+    getToken: function()
+    {
+        var bounds = this.getTokenBounds();
+        return $F(this.element).substring(bounds[0], bounds[1]).strip();
+    },
+
+    getTokenBounds: function()
+    {
+        var diff, i, index, l, offset, tp,
+            t = this.options.tokens,
+            value = $F(this.element),
+            nextTokenPos = value.length,
+            prevTokenPos = -1,
+            boundary = Math.min(nextTokenPos, this.oldval.length);
+
+        if (value.strip().empty()) {
+            return [ -1, 0 ];
+        }
+
+        diff = boundary;
+        for (i = 0; i < boundary; ++i) {
+            if (value[i] != this.oldval[i]) {
+                diff = i;
+                break;
+            }
+        }
+
+        offset = (diff == this.oldval.length ? 1 : 0);
+
+        for (index = 0, l = t.length; index < l; ++index) {
+            tp = value.lastIndexOf(t[index], diff + offset - 1);
+            if (tp > prevTokenPos) {
+                prevTokenPos = tp;
+            }
+            tp = value.indexOf(t[index], diff + offset);
+            if (tp != -1 && tp < nextTokenPos) {
+                nextTokenPos = tp;
+            }
+        }
+        return [ prevTokenPos + 1, nextTokenPos ];
+    }
+});
+
+Ajax.Autocompleter = Class.create(Autocompleter.Base, {
+    initialize: function(element, update, url, options)
+    {
+        this.baseInitialize(element, update, options);
+        this.options = Object.extend(this.options, {
+            asynchronous: true,
+            onComplete: this._onComplete.bind(this),
+            defaultParams: $H(this.options.parameters)
+        });
+        this.url = url;
+        this.cache = $H();
+    },
+
+    getUpdatedChoices: function()
+    {
+        var p,
+            o = this.options,
+            t = this.getToken(),
+            c = this.cache.get(t);
+
+        if (c) {
+            this.updateChoices(c);
+        } else {
+            p = Object.clone(o.defaultParams);
+            this.startIndicator();
+            p.set(o.paramName, t);
+            o.parameters = p.toQueryString();
+            new Ajax.Request(this.url, o);
+        }
+    },
+
+    _onComplete: function(request)
+    {
+        this.updateChoices(this.cache.set(this.getToken(), request.responseJSON));
+    }
+});
+
+Autocompleter.Local = Class.create(Autocompleter.Base, {
+    initialize: function(element, update, arr, options)
+    {
+        this.baseInitialize(element, update, options);
+        this.options.arr = arr;
+    },
+
+    getUpdatedChoices: function()
+    {
+        this.updateChoices(this._selector());
+    },
+
+    _setOptions: function(options)
+    {
+        return Object.extend({
+            choices: 10,
+            partialSearch: true,
+            partialChars: 2,
+            ignoreCase: true,
+            fullSearch: false
+        }, options || {});
+    },
+
+    _selector: function()
+    {
+        var entry = this.getToken(),
+            entry_len = entry.length,
+            i = 0,
+            o = this.options;
+
+        if (o.ignoreCase) {
+            entry = entry.toLowerCase();
+        }
+
+        return o.arr.findAll(function(t) {
+            if (i == o.choices) {
+                throw $break;
+            }
+
+            if (o.ignoreCase) {
+                t = t.toLowerCase();
+            }
+            t = t.unescapeHTML();
+
+            var pos = t.indexOf(entry);
+            if (pos != -1 &&
+                ((pos == 0 && t.length != entry_len) ||
+                 (entry_len >= o.partialChars &&
+                  o.partialSearch &&
+                  (o.fullSearch || /\s/.test(t.substr(pos - 1, 1)))))) {
+                ++i;
+                return true;
+            }
+            return false;
+        }, this);
+    }
+});
index bdcff39..87ee328 100644 (file)
@@ -65,9 +65,7 @@ class Imple {
     function attach()
     {
         Horde::addScriptFile('prototype.js', 'horde', true);
-        Horde::addScriptFile('builder.js', 'horde', true);
         Horde::addScriptFile('effects.js', 'horde', true);
-        Horde::addScriptFile('controls.js', 'horde', true);
     }
 
     /**
index 2e34a86..49967eb 100644 (file)
@@ -1,31 +1,15 @@
 <?php
 /**
- * $Horde: kronolith/lib/Imple/ContactAutoCompleter.php,v 1.5 2009/01/06 18:01:01 jan Exp $
- *
  * Copyright 2005-2009 The Horde Project (http://www.horde.org/)
  *
  * See the enclosed file COPYING for license information (GPL). If you
  * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
  *
- * @package Kronolith
- */
-
-/** Horde_MIME */
-require_once 'Horde/MIME.php';
-
-/**
  * @author  Michael Slusarz <slusarz@horde.org>
  * @package Kronolith
  */
-class Imple_ContactAutoCompleter extends Imple {
-
-    /**
-     * onShow javascript code.
-     *
-     * @var string
-     */
-    var $_onshow = ', onShow: function(e, u){ if(!u.style.position || u.style.position==\'absolute\') { u.style.position = \'absolute\'; Position.clone(e, u, { setHeight: false, offsetTop: e.offsetHeight }); } Effect.Appear(u,{duration:0.15, beforeSetup:function(effect) { effect.element.setOpacity(effect.options.from); effect.element.show(); u.style.height = Math.min(u.offsetHeight, ((window.innerHeight ? window.innerHeight : document.body.clientHeight) - Position.page(e)[1] - e.offsetHeight - 10)) + \'px\'; u.style.overflow = \'auto\'; } }); }';
-
+class Imple_ContactAutoCompleter extends Imple
+{
     /**
      * Constructor.
      *
@@ -35,7 +19,7 @@ class Imple_ContactAutoCompleter extends Imple {
      * 'resultsId' => TODO (optional)
      * </pre>
      */
-    function Imple_ContactAutoCompleter($params)
+    public function __construct($params)
     {
         if (empty($params['triggerId'])) {
             $params['triggerId'] = $this->_randomid();
@@ -44,17 +28,32 @@ class Imple_ContactAutoCompleter extends Imple {
             $params['resultsId'] = $params['triggerId'] . '_results';
         }
 
-        parent::Imple($params);
+        parent::__construct($params);
     }
 
     /**
      * Attach the Imple object to a javascript event.
      */
-    function attach()
+    public function attach()
     {
         parent::attach();
-        $url = Horde::url($GLOBALS['registry']->get('webroot', 'kronolith') . '/imple.php?imple=ContactAutoCompleter/input=' . rawurlencode($this->_params['triggerId']), true);
-        Kronolith::addInlineScript('Event.observe(window, "load", function() { new Ajax.Autocompleter("' . $this->_params['triggerId'] . '", "' . $this->_params['resultsId'] . '", "' . $url . '", { tokens: ",", indicator: "' . $this->_params['triggerId'] . '_loading_img"' . $this->_onshow . ', afterUpdateElement: function(f, t) { if (f.value.lastIndexOf(";") != (f.value.length - 1)) { f.value += ", "; } } }); });');
+        Horde::addScriptFile('autocomplete.js', 'kronolith', true);
+
+        $params = array(
+            '"' . $this->_params['triggerId'] . '"',
+            '"' . $this->_params['resultsId'] . '"',
+            '"' . Horde::url($GLOBALS['registry']->get('webroot', 'kronolith') . '/imple.php?imple=ContactAutoCompleter/input=' . rawurlencode($this->_params['triggerId']), true) . '"'
+        );
+
+        $js_params = array(
+            'tokens: [",", ";"]',
+            'indicator: "' . $this->_params['triggerId'] . '_loading_img"',
+            'afterUpdateElement: function(f, t) { if (!f.value.endsWith(";")) { f.value += ","; } f.value += " "; }'
+        );
+
+        $params[] = '{' . implode(',', $js_params) . '}';
+
+        Kronolith::addInlineScript('new Ajax.Autocompleter(' . implode(',', $params) . ')');
     }
 
     /**
@@ -64,63 +63,52 @@ class Imple_ContactAutoCompleter extends Imple {
      *
      * @return string  TODO
      */
-    function handle($args)
+    public function handle($args)
     {
         // Avoid errors if 'input' isn't set and short-circuit empty searches.
         if (empty($args['input']) ||
             !($input = Util::getFormData($args['input']))) {
-            return '<ul></ul>';
-        }
-
-        $results = $this->expandAddresses($input, true);
-        if (is_a($results, 'PEAR_Error')) {
-            // TODO: error handling
-            return '<ul></ul>';
+            return array();
         }
 
-        if (is_array($results)) {
-            $results = $results[0];
-            array_shift($results);
-        } else {
-            $results = array($results);
-        }
-
-        $html = '<ul>';
-        $input = htmlspecialchars($input);
-        $input_regex = '/(' . preg_quote($input, '/')  . ')/i';
-        foreach ($results as $result) {
-            $html .= '<li>' . str_replace(array('&lt;strong&gt;', '&lt;/strong&gt;'),
-                                          array('<strong>', '</strong>'),
-                                          htmlspecialchars(preg_replace($input_regex, '<strong>$1</strong>', $result))) . '</li>';
-        }
-        return $html . '</ul>';
+        return array_map('htmlspecialchars', $this->_expandAddresses($input));
     }
 
     /**
-     * Uses the Registry to expand names and returning error information for
-     * any address that is either not valid or fails to expand.
+     * Uses the Registry to expand names and return error information for
+     * any address that is either not valid or fails to expand. This function
+     * will not search if the address string is empty.
      *
      * @param string $addrString  The name(s) or address(es) to expand.
-     * @param boolean $full       If true generate a full, rfc822-valid address
-     *                            list.
      *
-     * @return mixed   Either a string containing all expanded addresses or an
-     *                 array containing all matching address or an error
-     *                 object.
+     * @return array  All matching addresses.
      */
-    function expandAddresses($addrString, $full = false)
+    protected function _expandAddresses($addrString)
     {
-        if (!preg_match('|[^\s]|', $addrString)) {
-            return '';
-        }
-
-        $search_fields = array();
+        return preg_match('|[^\s]|', $addrString)
+            ? $this->_getAddressList(reset(array_filter(array_map('trim', Horde_Mime_Address::explode($addrString, ',;')))))
+            : '';
+    }
 
+    /**
+     * Uses the Registry to expand names and return error information for
+     * any address that is either not valid or fails to expand.
+     *
+     * This method can be called statically, i.e.:
+     *   $ret = IMP_Compose::expandAddresses();
+     *
+     * @param string $search  The term to search by.
+     *
+     * @return array  All matching addresses.
+     */
+    static public function getAddressList($search = '')
+    {
         $src = explode("\t", $GLOBALS['prefs']->getValue('search_sources'));
         if ((count($src) == 1) && empty($src[0])) {
             $src = array();
         }
 
+        $fields = array();
         if (($val = $GLOBALS['prefs']->getValue('search_fields'))) {
             $field_arr = explode("\n", $val);
             foreach ($field_arr as $field) {
@@ -129,114 +117,35 @@ class Imple_ContactAutoCompleter extends Imple {
                     $tmp = explode("\t", $field);
                     if (count($tmp) > 1) {
                         $source = array_splice($tmp, 0, 1);
-                        $search_fields[$source[0]] = $tmp;
+                        $fields[$source[0]] = $tmp;
                     }
                 }
             }
         }
 
-        $arr = array_filter(array_map('trim', MIME::rfc822Explode($addrString, ',')));
-
-        $results = $GLOBALS['registry']->call('contacts/search', array($arr, $src, $search_fields, true));
-        if (is_a($results, 'PEAR_Error')) {
-            return $results;
-        }
-
-        /* Remove any results with empty email addresses. */
-        foreach (array_keys($results) as $key) {
-            for ($i = 0, $subTotal = count($results[$key]); $i < $subTotal; ++$i) {
-                if (empty($results[$key][$i]['email'])) {
-                    unset($results[$key][$i]);
-                }
-            }
+        $res = $GLOBALS['registry']->call('contacts/search', array($search, $src, $fields, true));
+        if (is_a($res, 'PEAR_Error') || !count($res)) {
+            Horde::logMessage($res, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return array();
         }
 
-        $ambiguous = $error = false;
-        $missing = array();
-        $vars = null;
-
-        require_once 'Mail/RFC822.php';
-        $parser = new Mail_RFC822(null, '@INVALID');
-
-        foreach ($arr as $i => $tmp) {
-            $address = MIME::encodeAddress($tmp, null, '');
-            if (!is_a($address, 'PEAR_Error') &&
-                ($parser->validateMailbox($address) ||
-                 $parser->_isGroup($address))) {
-                // noop
-            } elseif (!isset($results[$tmp]) || !count($results[$tmp])) {
-                /* Handle the missing/invalid case - we should return error
-                 * info on each address that couldn't be
-                 * expanded/validated. */
-                $error = true;
-                if (!$ambiguous) {
-                    $arr[$i] = PEAR::raiseError(null, null, null, null, $arr[$i]);
-                    $missing[$i] = $arr[$i];
-                }
-            } else {
-                $res = $results[$tmp];
-                if (count($res) == 1) {
-                    if ($full) {
-                        if (strpos($res[0]['email'], ',') !== false) {
-                            if ($vars === null) {
-                                $vars = get_class_vars('MIME');
-                            }
-                            $arr[$i] = MIME::_rfc822Encode($res[0]['name'], $vars['rfc822_filter'] . '.') . ': ' . $res[0]['email'] . ';';
-                        } else {
-                            list($mbox, $host) = explode('@', $res[0]['email']);
-                            $arr[$i] = MIME::rfc822WriteAddress($mbox, $host, $res[0]['name']);
-                        }
-                    } else {
-                        $arr[$i] = $res[0]['email'];
-                    }
+        /* The first key of the result will be the search term. The matching
+         * entries are stored underneath this key. */
+        $search = array();
+        foreach (reset($res) as $val) {
+            if (!empty($val['email'])) {
+                if (strpos($val['email'], ',') !== false) {
+                    $search[] = Horde_Mime_Address::encode($val['name'], 'personal') . ': ' . $val['email'] . ';';
                 } else {
-                    /* Handle the multiple case - we return an array
-                     * with all found addresses. */
-                    $arr[$i] = array($arr[$i]);
-                    foreach ($res as $one_res) {
-                        if (empty($one_res['email'])) {
-                            continue;
-                        }
-                        if ($full) {
-                            if (strpos($one_res['email'], ',') !== false) {
-                                if ($vars === null) {
-                                    $vars = get_class_vars('MIME');
-                                }
-                                $arr[$i][] = MIME::_rfc822Encode($one_res['name'], $vars['rfc822_filter'] . '.') . ': ' . $one_res['email'] . ';';
-                            } else {
-                                $mbox_host = explode('@', $one_res['email']);
-                                if (isset($mbox_host[1])) {
-                                    $arr[$i][] = MIME::rfc822WriteAddress($mbox_host[0], $mbox_host[1], $one_res['name']);
-                                }
-                            }
-                        } else {
-                            $arr[$i][] = $one_res['email'];
-                        }
+                    $mbox_host = explode('@', $val['email']);
+                    if (isset($mbox_host[1])) {
+                        $search[] = Horde_Mime_Address::writeAddress($mbox_host[0], $mbox_host[1], $val['name']);
                     }
-                    $ambiguous = true;
                 }
             }
         }
 
-        if ($ambiguous) {
-            foreach ($missing as $i => $addr) {
-                $arr[$i] = $addr->getUserInfo();
-            }
-            return $arr;
-        } elseif ($error) {
-            return PEAR::raiseError(_("Please resolve ambiguous or invalid addresses."), null, null, null, $arr);
-        } else {
-            $list = '';
-            foreach ($arr as $elm) {
-                if (substr($list, -1) == ';') {
-                    $list .= ' ';
-                } elseif (!empty($list)) {
-                    $list .= ', ';
-                }
-                $list .= $elm;
-            }
-            return $list;
-        }
+        return $search;
     }
 
 }