commit 78a2118554090100920caed22759a4fd822809a5 Author: Vitaliy Filippov Date: Tue Nov 8 14:14:01 2011 +0000 Ещё более простая версия автокомплита (хотя может меньше), и сильно более читаемый код и нет зависимости от кривого exAttach diff --git a/hinter.css b/hinter.css new file mode 100644 index 0000000..eb6ea4f --- /dev/null +++ b/hinter.css @@ -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; +} diff --git a/hinter.js b/hinter.js new file mode 100644 index 0000000..25bdc8f --- /dev/null +++ b/hinter.js @@ -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) });