347 lines
9.2 KiB
JavaScript
347 lines
9.2 KiB
JavaScript
/**
|
|
* An autocompleter implementation that provides a more advanced UI
|
|
* (completed elements are stored in separate DIV elements).
|
|
*
|
|
* Events handled by this class:
|
|
* - AutoComplete:focus
|
|
* - AutoComplete:handlers
|
|
* - AutoComplete:reset
|
|
* - AutoComplete:update
|
|
*
|
|
* Events triggered by this class:
|
|
* - AutoComplete:resize
|
|
*
|
|
* @author Michael Slusarz <slusarz@horde.org>
|
|
* @author Michael J Rubinsky <mrubinsk@horde.org>
|
|
* @copyright 2008-2015 Horde LLC
|
|
* @license GPL-2 (http://www.horde.org/licenses/gpl)
|
|
*/
|
|
var IMP_PrettyAutocompleter = Class.create({
|
|
|
|
// box,
|
|
// dimg,
|
|
// elt,
|
|
// input,
|
|
|
|
itemid: 0,
|
|
lastinput: '',
|
|
|
|
initialize: function(elt, params)
|
|
{
|
|
var ac, active, p_clone;
|
|
|
|
this.p = Object.extend({
|
|
// Outer div/fake input box and CSS class
|
|
// box (created below)
|
|
boxClass: 'hordeACBox',
|
|
boxClassFocus: '',
|
|
// <ul> CSS class
|
|
listClass: 'hordeACList',
|
|
listClassItem: 'hordeACListItem',
|
|
// input (created below)
|
|
// CSS class for real input field
|
|
growingInputClass: 'hordeACTrigger impACTrigger',
|
|
removeClass: 'hordeACItemRemove',
|
|
// Allow for a function that filters the display value
|
|
// This function should *always* return escaped HTML
|
|
displayFilter: function(t) { return t.escapeHTML(); },
|
|
filterCallback: this.filterChoices.bind(this),
|
|
maxItemSize: 50,
|
|
onAdd: Prototype.K,
|
|
onRemove: Prototype.K,
|
|
processValueCallback: this.processValueCallback.bind(this),
|
|
requireSelection: false
|
|
}, params || {});
|
|
|
|
// The original input element is transformed into the hidden input
|
|
// field that hold the text values.
|
|
this.elt = $(elt);
|
|
|
|
this.box = new Element('DIV', { className: this.p.boxClass });
|
|
|
|
// The input element and the <li> wrapper
|
|
this.input = new Element('INPUT', {
|
|
autocomplete: 'off',
|
|
className: this.p.growingInputClass
|
|
});
|
|
|
|
// Build the outer box
|
|
this.box.insert(
|
|
// The list - where the chosen items are placed as <li> nodes
|
|
new Element('UL', { className: this.p.listClass }).insert(
|
|
new Element('LI').insert(this.input)
|
|
)
|
|
);
|
|
|
|
// Replace the single input element with the new structure and
|
|
// move the old element into the structure while making sure it's
|
|
// hidden.
|
|
active = (document.activeElement && (document.activeElement == this.elt));
|
|
this.box.insert(this.elt.replace(this.box).hide());
|
|
if (active) {
|
|
this.focus();
|
|
}
|
|
|
|
// Look for clicks on the box to simulate clicking in an input box
|
|
this.box.observe('click', this.clickHandler.bindAsEventListener(this));
|
|
|
|
// Double-clicks cause an edit on existing entries.
|
|
this.box.observe('dblclick', this.dblclickHandler.bindAsEventListener(this));
|
|
|
|
this.input.observe('blur', this.blur.bind(this));
|
|
this.input.observe('keydown', this.keydownHandler.bindAsEventListener(this));
|
|
|
|
new PeriodicalExecuter(this.inputWatcher.bind(this), 0.25);
|
|
|
|
p_clone = $H(this.p).toObject();
|
|
p_clone.onSelect = this.updateElement.bind(this);
|
|
p_clone.paramName = this.elt.readAttribute('name');
|
|
p_clone.tokens = [];
|
|
|
|
ac = new Ajax.Autocompleter(this.input, this.p.uri, p_clone);
|
|
ac.getToken = function() {
|
|
return $F(this.input);
|
|
}.bind(this);
|
|
|
|
this.reset();
|
|
|
|
document.observe('AutoComplete:focus', function(e) {
|
|
if (e.memo == this.elt) {
|
|
this.focus();
|
|
e.stop();
|
|
}
|
|
}.bindAsEventListener(this));
|
|
document.observe('AutoComplete:handlers', function(e) {
|
|
e.memo[this.elt.identify()] = this;
|
|
}.bind(this));
|
|
document.observe('AutoComplete:reset', this.reset.bind(this));
|
|
document.observe('AutoComplete:update', this.processInput.bind(this));
|
|
},
|
|
|
|
focus: function()
|
|
{
|
|
this.input.focus();
|
|
this.box.addClassName(this.p.boxClassFocus);
|
|
},
|
|
|
|
blur: function()
|
|
{
|
|
this.box.removeClassName(this.p.boxClassFocus);
|
|
},
|
|
|
|
reset: function()
|
|
{
|
|
this.currentEntries().invoke('remove');
|
|
this.updateInput('');
|
|
this.addNewItem(this.processValue($F(this.elt)));
|
|
},
|
|
|
|
processInput: function()
|
|
{
|
|
this.addNewItem($F(this.input));
|
|
this.updateInput('');
|
|
},
|
|
|
|
processValue: function(val)
|
|
{
|
|
if (this.p.requireSelection) {
|
|
return val;
|
|
}
|
|
|
|
return this.p.processValueCallback(this, val.replace(/^\s+/, ''));
|
|
},
|
|
|
|
processValueCallback: function(ob, val)
|
|
{
|
|
var chr, pos = 0;
|
|
|
|
chr = val.charAt(pos);
|
|
while (chr !== "") {
|
|
if (ob.p.tokens.indexOf(chr) === -1) {
|
|
++pos;
|
|
} else {
|
|
if (!pos) {
|
|
val = val.substr(1);
|
|
} else {
|
|
ob.addNewItem(val.substr(0, pos));
|
|
val = val.substr(pos + 2);
|
|
pos = 0;
|
|
}
|
|
}
|
|
|
|
chr = val.charAt(pos);
|
|
}
|
|
|
|
return val.replace(/^\s+/, '');
|
|
},
|
|
|
|
// Used as the updateElement callback.
|
|
updateElement: function(item)
|
|
{
|
|
if (this.addNewItem(item)) {
|
|
this.updateInput('');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Adds a new element to the UI, ignoring duplicates.
|
|
*
|
|
* @return boolean True on success, false on failure/duplicate.
|
|
*/
|
|
addNewItem: function(value)
|
|
{
|
|
var displayValue;
|
|
|
|
// Don't add if it's already present.
|
|
if (value.empty() || !this.filterChoices([ value ]).size()) {
|
|
return false;
|
|
}
|
|
|
|
displayValue = this.p.displayFilter(value.truncate(this.p.maxItemSize));
|
|
|
|
this.input.up('LI').insert({
|
|
before: new Element('LI', {
|
|
className: this.p.listClassItem,
|
|
title: value
|
|
})
|
|
.insert(displayValue)
|
|
.insert(this.deleteImg().clone(true).show())
|
|
.store('raw', value)
|
|
.store('itemid', ++this.itemid)
|
|
});
|
|
|
|
// Add to hidden input field.
|
|
this.updateHiddenInput();
|
|
|
|
this.p.onAdd(value);
|
|
|
|
return true;
|
|
},
|
|
|
|
filterChoices: function(c)
|
|
{
|
|
var cv = this.currentValues();
|
|
|
|
return c.select(function(v) {
|
|
return !cv.include(v);
|
|
});
|
|
},
|
|
|
|
currentEntries: function()
|
|
{
|
|
return this.input.up('UL').select('LI.' + this.p.listClassItem);
|
|
},
|
|
|
|
currentValues: function()
|
|
{
|
|
return this.currentEntries().invoke('retrieve', 'raw');
|
|
},
|
|
|
|
updateInput: function(input)
|
|
{
|
|
if (Object.isElement(input)) {
|
|
input = input.remove().retrieve('raw');
|
|
this.updateHiddenInput();
|
|
}
|
|
|
|
this.input.setValue(input);
|
|
this.resize();
|
|
},
|
|
|
|
updateHiddenInput: function()
|
|
{
|
|
this.elt.setValue(this.currentValues().join(', '));
|
|
},
|
|
|
|
resize: function()
|
|
{
|
|
this.input.setStyle({
|
|
width: Math.max(80, $F(this.input).length * 9) + 'px'
|
|
});
|
|
this.input.fire('AutoComplete:resize');
|
|
},
|
|
|
|
toObject: function(elt)
|
|
{
|
|
var ob = {};
|
|
|
|
this.currentEntries().each(function(c) {
|
|
ob[c.retrieve('itemid')] = elt ? c : c.retrieve('raw');
|
|
});
|
|
|
|
return ob;
|
|
},
|
|
|
|
deleteImg: function()
|
|
{
|
|
if (!this.dimg) {
|
|
this.dimg = new Element('IMG', {
|
|
className: this.p.removeClass,
|
|
src: this.p.deleteIcon
|
|
}).hide();
|
|
this.box.insert(this.dimg);
|
|
}
|
|
|
|
return this.dimg;
|
|
},
|
|
|
|
/* Event handlers. */
|
|
|
|
clickHandler: function(e)
|
|
{
|
|
var elt = e.element();
|
|
|
|
if (elt.hasClassName(this.p.removeClass)) {
|
|
elt.up('LI').remove();
|
|
this.updateHiddenInput();
|
|
}
|
|
|
|
this.focus();
|
|
},
|
|
|
|
dblclickHandler: function(e)
|
|
{
|
|
var elt = e.findElement('LI');
|
|
|
|
if (elt && elt.hasClassName(this.p.listClassItem)) {
|
|
this.addNewItem($F(this.input));
|
|
this.updateInput(elt);
|
|
} else {
|
|
this.focus();
|
|
}
|
|
},
|
|
|
|
keydownHandler: function(e)
|
|
{
|
|
var tmp;
|
|
|
|
switch (e.which || e.keyCode || e.charCode) {
|
|
case Event.KEY_DELETE:
|
|
case Event.KEY_BACKSPACE:
|
|
if (!$F(this.input).length &&
|
|
(tmp = this.currentEntries().last())) {
|
|
this.updateInput(tmp);
|
|
e.stop();
|
|
}
|
|
break;
|
|
}
|
|
},
|
|
|
|
inputWatcher: function()
|
|
{
|
|
var input = $F(this.input), processed;
|
|
|
|
if (input != this.lastinput) {
|
|
processed = this.processValue(input);
|
|
if (processed != input) {
|
|
this.input.setValue(processed);
|
|
}
|
|
this.lastinput = $F(this.input);
|
|
this.resize();
|
|
// Pre-load the delete image now.
|
|
this.deleteImg();
|
|
}
|
|
}
|
|
|
|
});
|