Ещё более простая версия автокомплита (хотя может меньше), и сильно более читаемый код и нет зависимости от кривого exAttach

master
Vitaliy Filippov 2011-11-08 14:14:01 +00:00
commit 78a2118554
2 changed files with 392 additions and 0 deletions

28
hinter.css Normal file
View File

@ -0,0 +1,28 @@
.hintLayer {
border: 1px solid gray;
color: gray;
width: 200pt;
background-color: white;
font-size: 80%;
max-height: 300pt;
overflow-y: scroll;
overflow: -moz-scrollbars-vertical;
}
.hintEmptyText {
padding: 3px;
}
.hintItem {
color: black;
cursor: pointer;
padding: 1px 3px;
}
.hintActiveItem {
color: white;
background-color: #008;
cursor: pointer;
padding: 1px 3px;
}
.hintDisabledItem {
background-color: white;
color: gray;
}

364
hinter.js Normal file
View File

@ -0,0 +1,364 @@
/* 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) });