367 lines
9.4 KiB
JavaScript
367 lines
9.4 KiB
JavaScript
/**
|
|
* autocomplete.js - A javascript library which implements autocomplete.
|
|
* Requires prototype.js v1.6.0.2+, scriptaculous v1.8.0+ (effects.js),
|
|
* and keynavlist.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.
|
|
*
|
|
* Usage:
|
|
* ------
|
|
* TODO: options = autoSelect, frequency, minChars, onSelect, onShow, onType,
|
|
* paramName, tokens
|
|
*
|
|
* @copyright 2007-2015 Horde LLC
|
|
* @license LGPL-2.1 (http://www.horde.org/licenses/lgpl21)
|
|
*/
|
|
|
|
var Autocompleter = {};
|
|
|
|
Autocompleter.Base = Class.create({
|
|
baseInitialize: function(elt, opts)
|
|
{
|
|
this.elt = elt = $(elt);
|
|
this.changed = false;
|
|
this.observer = null;
|
|
this.oldval = $F(elt);
|
|
this.opts = Object.extend({
|
|
frequency: 0.4,
|
|
indicator: null,
|
|
minChars: 1,
|
|
onSelect: Prototype.K,
|
|
onShow: Prototype.K,
|
|
onHide: Prototype.K,
|
|
onType: Prototype.K,
|
|
filterCallback: Prototype.K,
|
|
paramName: elt.readAttribute('name'),
|
|
tokens: [],
|
|
keydownObserver: this.elt
|
|
}, (this._setOptions ? this._setOptions(opts) : (opts || {})));
|
|
|
|
// Force carriage returns as token delimiters anyway
|
|
if (!this.opts.tokens.include('\n')) {
|
|
this.opts.tokens.push('\n');
|
|
}
|
|
|
|
elt.writeAttribute('autocomplete', 'off');
|
|
elt.observe("keydown", this._onKeyDown.bindAsEventListener(this));
|
|
},
|
|
|
|
_onKeyDown: function(e)
|
|
{
|
|
if (!this._checkActiveElt()) {
|
|
return;
|
|
}
|
|
|
|
switch (e.keyCode) {
|
|
case 0:
|
|
if (!Prototype.Browser.WebKit) {
|
|
break;
|
|
}
|
|
// Fall-through
|
|
|
|
// Ignore events caught by KevNavList
|
|
case Event.KEY_DOWN:
|
|
case Event.KEY_ESC:
|
|
case Event.KEY_RETURN:
|
|
case Event.KEY_TAB:
|
|
case Event.KEY_UP:
|
|
return;
|
|
}
|
|
|
|
if (!this.changed) {
|
|
this.oldval = $F(this.elt);
|
|
this.changed = true;
|
|
}
|
|
|
|
if (this.observer) {
|
|
clearTimeout(this.observer);
|
|
}
|
|
|
|
this.observer = this.onObserverEvent.bind(this).delay(this.opts.frequency);
|
|
},
|
|
|
|
updateChoices: function(choices)
|
|
{
|
|
if (this.changed || !this._checkActiveElt()) {
|
|
return;
|
|
}
|
|
|
|
var c = [], re;
|
|
|
|
if (this.opts.indicator) {
|
|
$(this.opts.indicator).hide();
|
|
}
|
|
|
|
choices = this.opts.filterCallback(choices);
|
|
if (!choices.size()) {
|
|
if (this.knl) {
|
|
this.knl.hide();
|
|
}
|
|
this.getNewVal(this.lastentry);
|
|
} else if (choices.size() == 1 && this.opts.autoSelect) {
|
|
this.onSelect(choices.first());
|
|
if (this.knl) {
|
|
this.knl.hide();
|
|
}
|
|
} else {
|
|
re = new RegExp(this.getToken(), "i");
|
|
|
|
choices.each(function(n) {
|
|
var out = { l: '', v: n },
|
|
m = n.match(re);
|
|
|
|
if (m) {
|
|
m.each(function(m) {
|
|
var idx = n.indexOf(m);
|
|
out.l += n.substr(0, idx).escapeHTML() + '<strong>' + m.escapeHTML() + '</strong>';
|
|
n = n.substr(idx + m.length);
|
|
});
|
|
}
|
|
|
|
if (n.length) {
|
|
out.l += n.escapeHTML();
|
|
}
|
|
|
|
c.push(out);
|
|
});
|
|
|
|
if (!this.knl) {
|
|
this.knl = new KeyNavList(this.elt, {
|
|
onChoose: this.onSelect.bind(this),
|
|
onShow: this.opts.onShow.bind(this),
|
|
onHide: this.opts.onHide.bind(this),
|
|
domParent: this.opts.domParent,
|
|
keydownObserver: this.opts.keydownObserver
|
|
});
|
|
}
|
|
|
|
this.knl.show(c);
|
|
}
|
|
},
|
|
|
|
onObserverEvent: function()
|
|
{
|
|
this.changed = false;
|
|
|
|
if (!this._checkActiveElt()) {
|
|
return;
|
|
}
|
|
|
|
var entry = this.getToken();
|
|
|
|
entry = (entry.length >= this.opts.minChars)
|
|
? this.opts.onType(entry)
|
|
: '';
|
|
|
|
if (entry.length) {
|
|
if (this.opts.indicator) {
|
|
$(this.opts.indicator).show();
|
|
}
|
|
this.lastentry = entry;
|
|
this.getUpdatedChoices(entry);
|
|
} else if (this.knl) {
|
|
this.knl.hide();
|
|
}
|
|
},
|
|
|
|
_checkActiveElt: function()
|
|
{
|
|
if (document.activeElement == this.elt) {
|
|
return true;
|
|
}
|
|
|
|
if (this.opts.indicator) {
|
|
$(this.opts.indicator).hide();
|
|
}
|
|
|
|
if (this.knl) {
|
|
this.knl.hide();
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
getToken: function()
|
|
{
|
|
var bounds = this.getTokenBounds();
|
|
return $F(this.elt).substring(bounds[0], bounds[1]).strip();
|
|
},
|
|
|
|
getTokenBounds: function()
|
|
{
|
|
var diff, i, index, l, offset, tp,
|
|
t = this.opts.tokens,
|
|
value = $F(this.elt),
|
|
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 ];
|
|
},
|
|
|
|
onSelect: function(entry)
|
|
{
|
|
if (entry) {
|
|
this.elt.focus();
|
|
this.elt.setValue(this.opts.onSelect(this.getNewVal(entry)));
|
|
if (this.knl) {
|
|
this.knl.markSelected();
|
|
}
|
|
}
|
|
},
|
|
|
|
getNewVal: function(entry)
|
|
{
|
|
var bounds = this.getTokenBounds(), newval, v, ws;
|
|
|
|
if (bounds[0] == -1) {
|
|
newval = entry;
|
|
} else {
|
|
v = $F(this.elt);
|
|
newval = v.substr(0, bounds[0]);
|
|
ws = v.substr(bounds[0]).match(/^\s+/);
|
|
if (ws) {
|
|
newval += ws[0];
|
|
}
|
|
newval += entry + v.substr(bounds[1]);
|
|
}
|
|
|
|
return newval;
|
|
}
|
|
|
|
});
|
|
|
|
Ajax.Autocompleter = Class.create(Autocompleter.Base, {
|
|
|
|
initialize: function(element, url, opts)
|
|
{
|
|
this.baseInitialize(element, opts);
|
|
this.opts = Object.extend(this.opts, {
|
|
asynchronous: true,
|
|
onComplete: this._onComplete.bind(this),
|
|
defaultParams: $H(this.opts.parameters)
|
|
});
|
|
this.url = url;
|
|
this.cache = $H();
|
|
},
|
|
|
|
getUpdatedChoices: function(t)
|
|
{
|
|
var p,
|
|
o = this.opts,
|
|
c = this.cache.get(t);
|
|
|
|
if (c) {
|
|
this.updateChoices(c);
|
|
} else {
|
|
p = Object.clone(o.defaultParams);
|
|
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, arr, opts)
|
|
{
|
|
this.baseInitialize(element, opts);
|
|
this.opts.arr = arr;
|
|
},
|
|
|
|
getUpdatedChoices: function(entry)
|
|
{
|
|
var choices,
|
|
csort = [],
|
|
entry_len = entry.length,
|
|
i = 0,
|
|
o = this.opts;
|
|
|
|
if (o.ignoreCase) {
|
|
entry = entry.toLowerCase();
|
|
}
|
|
|
|
choices = o.arr.findAll(function(t) {
|
|
if (i == o.choices) {
|
|
return false;
|
|
}
|
|
|
|
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);
|
|
|
|
if (o.score) {
|
|
choices.each(function(term) {
|
|
csort.push({ s: LiquidMetal.score(term, entry), t: term });
|
|
}.bind(this));
|
|
// Sort the terms
|
|
csort.sort(function(a, b) { return b.s - a.s; });
|
|
choices = csort.pluck('t');
|
|
}
|
|
|
|
this.updateChoices(choices);
|
|
},
|
|
|
|
_setOptions: function(opts)
|
|
{
|
|
return Object.extend({
|
|
choices: 10,
|
|
fullSearch: false,
|
|
ignoreCase: true,
|
|
partialChars: 2,
|
|
partialSearch: true,
|
|
score: false
|
|
}, opts || {});
|
|
}
|
|
|
|
});
|