TextboxList meets Autocompletion
This post discusses a project which has its own page. Please refer to TextboxList for the most up-to-date information.

In my previous blogpost I explained how to extend TextboxList to add closing functionality via a link added to each box. But it was missing an important ingredient: autocompletion!
Again, all we have to do is extend the TextboxList class, override some methods, some events, and create some new ones (all prefixed by auto)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | var FacebookList = new Class({ Extends: TextboxList, data: [], options: { onInputFocus: function() { this.autoShow(); }, onInputBlur: function(el) { el.value = ''; this.autoHide(); }, onBoxDispose: function(item) { this.autoFeed(item.$attributes.$text); }, autocomplete: { 'opacity': 0.8, 'maxresults': 10, 'minchars': 1 } }, initialize: function(element, autoholder, options) { arguments.callee.parent(element, options); this.autoholder = $(autoholder).set('opacity', this.options.autocomplete.opacity); this.autoresults = this.autoholder.getElement('ul'); var children = this.autoresults.getElements('li'); children.each(function(el) { this.add(el.innerHTML); }, this); }, autoShow: function(search) { this.autoholder.setStyle('display', 'block'); this.autoholder.getElements('*').setStyle('display', 'none'); if(! search || ! search.trim() || (! search.length || search.length < this.options.autocomplete.minchars )) { this.autoholder.getElement('.default').setStyle('display', 'block'); this.resultsshown = false; } else { this.resultsshown = true; this.autoresults.setStyle('display', 'block').empty(); this.data.filter(function(str) { return str ? str.test(search, 'i') : false; }).each(function(result, ti) { if(ti >= this.options.autocomplete.maxresults) return; var el = new Element('li').set('html', this.autoHighlight(result, search)).inject(this.autoresults); el.$attributes.$result = result; if(ti == 0) this.autoFocus(el); }, this); } }, autoHighlight: function(html, highlight) { return html.replace(new RegExp(highlight, 'gi'), function(match) { return '<em>' + match + '</em>'; }); }, autoHide: function() { this.resultsshown = false; this.autoholder.setStyle('display', 'none'); }, autoFocus: function(el) { if(! el) return; if(this.autocurrent) this.autocurrent.removeClass('auto-focus'); this.autocurrent = el.addClass('auto-focus'); }, autoMove: function(direction) { if(!this.resultsshown) return; this.autoFocus(this.autocurrent['get' + (direction == 'up' ? 'Previous' : 'Next')]()); }, autoFeed: function(text) { if(this.data.indexOf(text) == -1) this.data.push(text); }, autoAdd: function(el) { if(!el || ! el.$attributes.$result) return; this.add(el.$attributes.$result); delete this.data[this.data.indexOf(el.$attributes.$result)]; this.autoHide(); this.current.$attributes.$input.value = ''; }, createInput: function(options) { var li = arguments.callee.parent(options); var input = li.$attributes.$input; input.addEvents({ 'keydown': function(e) { e = new Event(e); this.dosearch = false; switch(e.code) { case Event.Keys.up: return this.autoMove('up'); case Event.Keys.down: return this.autoMove('down'); case Event.Keys.enter: this.autoAdd(this.autocurrent); this.autocurrent = false; this.autoenter = true; break; default: this.dosearch = true; } }.bind(this), 'keyup': function() { if(this.dosearch) this.autoShow(input.value); }.bind(this) }); input.addEvent(Browser.Engine.trident ? 'keydown' : 'keypress', function(e) { if(this.autoenter) new Event(e).stop(); this.autoenter = false; }.bind(this)); return li; }, createBox: function(text, options) { var li = arguments.callee.parent(text, options); li.addEvents({ 'mouseenter': function() { this.addClass('bit-hover') }, 'mouseleave': function() { this.removeClass('bit-hover') } }); li.adopt(new Element('a', { 'href': '#', 'class': 'closebutton', 'events': { 'click': function(e) { new Event(e).stop(); if(! this.current) this.focus(this.maininput); this.dispose(li); }.bind(this) } })); li.$attributes.$text = text; return li; } }); window.addEvent('domready', function() { // init var tlist2 = new FacebookList('facebook-demo', 'facebook-auto'); // fetch and feed new Request.JSON({'url': 'json.html', 'onComplete': function(j) { j.each(tlist2.autoFeed, tlist2); }}).send(); }); |
It works by caching all the results from a JSON Request and feeding them to the autocompleter object. When a item is added as a box, it’ removed from the feed array, and when the box is disposed it’s added back, so that it becomes available in the list when the user types.
Another new feature is that you’ll be able to let it add boxes from the HTML directly:
1 2 3 4 5 6 7 8 9 | <label>FacebookList input</label> <input type="text" value="" id="facebook-demo" /> <div id="facebook-auto"> <div class="default">Type the name of an argentine writer you like</div> <ul class="feed"> <li>Jorge Luis Borges</li> <li>Julio Cortazar</li> </ul> </div> |
The constructor now takes new parameters to configure the autocompletion, like the minimum number of characters to trigger the dropdown, and more.
Changelog
- 0.1: initial release
- 0.2: added click support, removed $attributes use, code cleanup
Download
Click here to download the zip with code and examples
Displaying only a subset of the comments. Click here to display all comments.
-
-
one day ago I had not noticed the backspace bug too, because my page (send new message) is opened in a new tab and thus the browser has no way to back to the previous page.
after I sent a new message, the script back to the same page with a short sentence notifying the user that message has been sent. Here I re-use the page again by pressing enter in the address bar, now the page has chance to back to the previous page and the backspace bug can happen.
-
There are some serious problems with this component: first and foremost, hitting Backspace will first select and highlight the previous tag (this is good), but hitting backspace again will (instead of removing the selected tag as expected), perform a browser Back command, leaving the page and potentially losing all the information entered into the form!
Secondly, you should really accept auto-complete entries with the Tab key. This is my expected way auto-complete should be performed.