TextboxList meets Autocompletion

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

TextboxList Autocompletion

Demo here

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.

  1. @ninja
    Thanks for telling how to use update().

    I prefer to use return this.bits.getValues().join(this.options.separator). So the textboxlist.js not bounded to hidden element ($(’MESSAGEToNames’)) unless we put it as function parameter.

    Simply var data = vlist.update(); this will return text with separator defined.

    @itsik
    Thanks for the fix for Mootools 1.2. It works :D

    • gerrard8dg 6 months ago

      hey dude, could i please get the fix for mootools 1.2?
      i hope it addresses my problem.. cos i cant get the autocomplete to work with barackslides on the same index file. seems to be some conflict. thanks!

    • Guillermo Rauch 6 months ago

      Hey dude,

      There’s a big message at the top: http://cld.ly/6d1n1w

      This project has moved on beyond this post. Go to Projects > TextboxList.