365 lines
10 KiB
JavaScript
365 lines
10 KiB
JavaScript
/* Simple autocomplete for text inputs.
|
|
Usage:
|
|
1) include hinter.css, hinter.js, offsetRect.js
|
|
2) var hint = new SimpleAutocomplete(input, dataLoader, onChangeListener, maxHeight, emptyText);
|
|
Parameters:
|
|
input - the input, either id or DOM element
|
|
dataLoader(hint, value) - callback which should load autocomplete options
|
|
and call hint.replaceItems([ [ name, value ], [ name, value ], ... ])
|
|
Optional parameters:
|
|
onChangeListener - callback which is called when an item is selected through the drop-down list
|
|
maxHeight - maximum hint dropdown height in pixels
|
|
emptyText - text to show when dataLoader returns no options
|
|
if emptyText === false, the hint will be hidden
|
|
Homepage: http://yourcmc.ru/wiki/SHint_JS
|
|
(c) Vitaliy Filippov 2011
|
|
*/
|
|
|
|
var SimpleAutocomplete = function(input, dataLoader, onChangeListener, maxHeight, emptyText)
|
|
{
|
|
if (typeof(input) == 'string')
|
|
input = document.getElementById(input);
|
|
if (emptyText === undefined)
|
|
emptyText = 'No items found';
|
|
|
|
// Parameters
|
|
var self = this;
|
|
self.input = input;
|
|
self.dataLoader = dataLoader;
|
|
self.onChangeListener = onChangeListener;
|
|
self.maxHeight = maxHeight;
|
|
self.emptyText = emptyText;
|
|
|
|
// Variables
|
|
self.items = [];
|
|
self.skipHideCounter = 0;
|
|
self.selectedIndex = -1;
|
|
self.id = input.id;
|
|
self.disabled = false;
|
|
|
|
// Initialise hinter
|
|
self.init = function()
|
|
{
|
|
var e = self.input;
|
|
var p = getOffset(e);
|
|
|
|
// Create hint layer
|
|
var t = self.hintLayer = document.createElement('div');
|
|
t.className = 'hintLayer';
|
|
t.style.display = 'none';
|
|
t.style.position = 'absolute';
|
|
t.style.top = (p.top+e.offsetHeight) + 'px';
|
|
t.style.zIndex = 1000;
|
|
t.style.left = p.left + 'px';
|
|
if (self.maxHeight)
|
|
{
|
|
t.style.overflowY = 'scroll';
|
|
try { t.style.overflow = '-moz-scrollbars-vertical'; } catch(exc) {}
|
|
t.style.maxHeight = self.maxHeight+'px';
|
|
if (!t.style.maxHeight)
|
|
self.scriptMaxHeight = true;
|
|
}
|
|
document.body.appendChild(t);
|
|
|
|
// Remember instance
|
|
e.SimpleAutocomplete_input = self;
|
|
t.SimpleAutocomplete_layer = self;
|
|
SimpleAutocomplete.SimpleAutocompletes.push(self);
|
|
|
|
// Set event listeners
|
|
var msie = navigator.userAgent.match('MSIE') && !navigator.userAgent.match('Opera');
|
|
if (msie)
|
|
addListener(e, 'keydown', self.onKeyPress);
|
|
else
|
|
{
|
|
addListener(e, 'keydown', self.onKeyDown);
|
|
addListener(e, 'keypress', self.onKeyPress);
|
|
}
|
|
addListener(e, 'keyup', self.onKeyUp);
|
|
addListener(e, 'change', self.onChange);
|
|
addListener(e, 'focus', self.onInputFocus);
|
|
addListener(e, 'blur', self.onInputBlur);
|
|
self.onChange();
|
|
};
|
|
|
|
// obj = [ [ name, value, disabled ], [ name, value ], ... ]
|
|
self.replaceItems = function(items)
|
|
{
|
|
self.hintLayer.innerHTML = '';
|
|
self.hintLayer.scrollTop = 0;
|
|
self.items = [];
|
|
if (!items || items.length == 0)
|
|
{
|
|
if (self.emptyText)
|
|
{
|
|
var d = document.createElement('div');
|
|
d.className = 'hintEmptyText';
|
|
d.innerHTML = self.emptyText;
|
|
self.hintLayer.appendChild(d);
|
|
}
|
|
else
|
|
self.disable();
|
|
return;
|
|
}
|
|
self.enable();
|
|
for (var i in items)
|
|
self.hintLayer.appendChild(self.makeItem(items[i][0], items[i][1]));
|
|
if (self.maxHeight)
|
|
{
|
|
self.hintLayer.style.height =
|
|
(self.hintLayer.scrollHeight > self.maxHeight
|
|
? self.maxHeight : self.hintLayer.scrollHeight) + 'px';
|
|
}
|
|
};
|
|
|
|
// Create a drop-down list item
|
|
self.makeItem = function(name, value)
|
|
{
|
|
var d = document.createElement('div');
|
|
d.id = self.id+'_item_'+self.items.length;
|
|
d.className = 'hintItem';
|
|
d.title = value;
|
|
d.appendChild(document.createTextNode(name));
|
|
addListener(d, 'mouseover', self.onItemMouseOver);
|
|
addListener(d, 'mousedown', self.onItemClick);
|
|
self.items.push([name, value]);
|
|
return d;
|
|
};
|
|
|
|
// Handle item mouse over
|
|
self.onItemMouseOver = function()
|
|
{
|
|
return self.highlightItem(this);
|
|
};
|
|
|
|
// Handle item clicks
|
|
self.onItemClick = function()
|
|
{
|
|
self.selectItem(parseInt(this.id.substr(self.id.length+6)));
|
|
return true;
|
|
};
|
|
|
|
// Move highlight forward or back by 'by' items (integer)
|
|
self.moveHighlight = function(by)
|
|
{
|
|
var n = self.selectedIndex+by;
|
|
if (n < 0)
|
|
n = 0;
|
|
var elem = document.getElementById(self.id+'_item_'+n);
|
|
if (!elem)
|
|
return true;
|
|
return self.highlightItem(elem);
|
|
};
|
|
|
|
// Make item 'elem' active (highlighted)
|
|
self.highlightItem = function(elem)
|
|
{
|
|
if (self.selectedIndex >= 0)
|
|
{
|
|
var c = self.getItem();
|
|
if (c)
|
|
c.className = 'hintItem';
|
|
}
|
|
self.selectedIndex = parseInt(elem.id.substr(self.id.length+6));
|
|
elem.className = 'hintActiveItem';
|
|
return false;
|
|
};
|
|
|
|
// Get index'th item, or current when index is null
|
|
self.getItem = function(index)
|
|
{
|
|
if (index == null)
|
|
index = self.selectedIndex;
|
|
if (index < 0)
|
|
return null;
|
|
return document.getElementById(self.id+'_item_'+self.selectedIndex);
|
|
};
|
|
|
|
// Select index'th item - change the input value and hide the hint
|
|
self.selectItem = function(index)
|
|
{
|
|
self.input.value = self.items[index][1];
|
|
self.hide();
|
|
if (self.onChangeListener)
|
|
self.onChangeListener(self, index);
|
|
};
|
|
|
|
// Handle user input, load new items
|
|
self.onChange = function()
|
|
{
|
|
var v = self.input.value.trim();
|
|
if (v != self.curValue)
|
|
{
|
|
self.curValue = v;
|
|
self.dataLoader(self, v);
|
|
}
|
|
return true;
|
|
};
|
|
|
|
// Handle Enter key presses, cancel handling of arrow keys
|
|
self.onKeyUp = function(ev)
|
|
{
|
|
ev = ev||window.event;
|
|
if (ev.keyCode != 10 && ev.keyCode != 13)
|
|
self.show();
|
|
if (ev.keyCode == 38 || ev.keyCode == 40 || ev.keyCode == 10 || ev.keyCode == 13)
|
|
return stopEvent(ev, true, true);
|
|
self.onChange();
|
|
return true;
|
|
};
|
|
|
|
// Cancel handling of Enter key
|
|
self.onKeyDown = function(ev)
|
|
{
|
|
ev = ev||window.event;
|
|
if (ev.keyCode == 10 || ev.keyCode == 13)
|
|
return stopEvent(ev, true, true);
|
|
return true;
|
|
};
|
|
|
|
// Handle arrow keys and Enter
|
|
self.onKeyPress = function(ev)
|
|
{
|
|
ev = ev||window.event;
|
|
if (ev.keyCode == 38) // up
|
|
self.moveHighlight(-1);
|
|
else if (ev.keyCode == 40) // down
|
|
self.moveHighlight(1);
|
|
else if (ev.keyCode == 10 || ev.keyCode == 13) // enter
|
|
{
|
|
if (self.selectedIndex >= 0)
|
|
self.selectItem(self.selectedIndex);
|
|
return stopEvent(ev, true, true);
|
|
}
|
|
else
|
|
return true;
|
|
// scrolling
|
|
if (self.selectedIndex >= 0)
|
|
{
|
|
var c = self.getItem();
|
|
var t = self.hintLayer;
|
|
var ct = getOffset(c).top + t.scrollTop - t.style.top.substr(0, t.style.top.length-2);
|
|
var ch = c.scrollHeight;
|
|
if (ct+ch-t.offsetHeight > t.scrollTop)
|
|
t.scrollTop = ct+ch-t.offsetHeight;
|
|
else if (ct < t.scrollTop)
|
|
t.scrollTop = ct;
|
|
}
|
|
return stopEvent(ev, true, true);
|
|
};
|
|
|
|
// Called when input receives focus
|
|
self.onInputFocus = function()
|
|
{
|
|
self.show();
|
|
return true;
|
|
};
|
|
|
|
// Called when input loses focus
|
|
self.onInputBlur = function()
|
|
{
|
|
self.hide();
|
|
return true;
|
|
};
|
|
|
|
// Hide hinter
|
|
self.hide = function()
|
|
{
|
|
if (!self.skipHideCounter)
|
|
self.hintLayer.style.display = 'none';
|
|
else
|
|
self.skipHideCounter = 0;
|
|
};
|
|
|
|
// Show hinter
|
|
self.show = function()
|
|
{
|
|
if (!self.disabled)
|
|
self.hintLayer.style.display = '';
|
|
};
|
|
|
|
// Disable hinter, for the case when there is no items and no empty text
|
|
self.disable = function()
|
|
{
|
|
self.disabled = true;
|
|
self.hide();
|
|
};
|
|
|
|
// Enable hinter
|
|
self.enable = function()
|
|
{
|
|
var show = self.disabled;
|
|
self.disabled = false;
|
|
if (show)
|
|
self.show();
|
|
}
|
|
|
|
// *** Call initialise ***
|
|
self.init();
|
|
};
|
|
|
|
// Global variable
|
|
SimpleAutocomplete.SimpleAutocompletes = [];
|
|
|
|
// Global mousedown handler, hides dropdowns when clicked outside
|
|
SimpleAutocomplete.GlobalMouseDown = function(ev)
|
|
{
|
|
var target = ev.target || ev.srcElement;
|
|
var esh;
|
|
while (target)
|
|
{
|
|
esh = target.SimpleAutocomplete_input;
|
|
if (esh)
|
|
break;
|
|
else if (target.SimpleAutocomplete_layer)
|
|
{
|
|
target.SimpleAutocomplete_layer.skipHideCounter++;
|
|
return true;
|
|
}
|
|
target = target.parentNode;
|
|
}
|
|
for (var i in SimpleAutocomplete.SimpleAutocompletes)
|
|
if (SimpleAutocomplete.SimpleAutocompletes[i] != esh)
|
|
SimpleAutocomplete.SimpleAutocompletes[i].hide();
|
|
return true;
|
|
};
|
|
|
|
// Cross-browser adding of event listeners (remove if you already have it)
|
|
var addListener = function()
|
|
{
|
|
if (window.addEventListener)
|
|
{
|
|
return function(el, type, fn) { el.addEventListener(type, fn, false); };
|
|
}
|
|
else if (window.attachEvent)
|
|
{
|
|
return function(el, type, fn) {
|
|
var f = function() { return fn.call(el, window.event); };
|
|
el.attachEvent('on'+type, f);
|
|
};
|
|
}
|
|
else
|
|
{
|
|
return function(el, type, fn) { element['on'+type] = fn; }
|
|
}
|
|
}();
|
|
|
|
// Cancel event bubbling and/or default action
|
|
var stopEvent = function(ev, cancelBubble, preventDefault)
|
|
{
|
|
if (cancelBubble)
|
|
{
|
|
if (ev.stopPropagation)
|
|
ev.stopPropagation();
|
|
else
|
|
ev.cancelBubble = true;
|
|
}
|
|
if (preventDefault && ev.preventDefault)
|
|
ev.preventDefault();
|
|
ev.returnValue = !preventDefault;
|
|
return !preventDefault;
|
|
};
|
|
|
|
// Set global mousedown listener
|
|
addListener(window, 'load', function() { addListener(document, 'mousedown', SimpleAutocomplete.GlobalMouseDown) });
|