diff options
Diffstat (limited to 'misc/autocomplete.js')
-rw-r--r-- | misc/autocomplete.js | 263 |
1 files changed, 263 insertions, 0 deletions
diff --git a/misc/autocomplete.js b/misc/autocomplete.js new file mode 100644 index 000000000..534b2fe4f --- /dev/null +++ b/misc/autocomplete.js @@ -0,0 +1,263 @@ +// Global Killswitch +if (isJsEnabled()) { + addLoadEvent(autocompleteAutoAttach); +} + +/** + * Attaches the autocomplete behaviour to all required fields + */ +function autocompleteAutoAttach() { + var acdb = []; + var inputs = document.getElementsByTagName('input'); + for (i = 0; input = inputs[i]; i++) { + if (input && hasClass(input, 'autocomplete')) { + uri = input.value; + if (!acdb[uri]) { + acdb[uri] = new ACDB(uri); + } + id = input.id.substr(0, input.id.length - 13); + input = document.getElementById(id); + input.setAttribute('autocomplete', 'OFF'); + input.form.onsubmit = autocompleteSubmit; + new jsAC(input, acdb[uri]); + } + } +} + +/** + * Prevents the form from submitting if the suggestions popup is open + */ +function autocompleteSubmit() { + var popup = document.getElementById('autocomplete'); + if (popup) { + popup.owner.hidePopup(); + return false; + } + return true; +} + + +/** + * An AutoComplete object + */ +function jsAC(input, db) { + var ac = this; + this.input = input; + this.db = db; + this.db.owner = this; + this.input.onkeydown = function (event) { return ac.onkeydown(this, event); }; + this.input.onkeyup = function (event) { ac.onkeyup(this, event) }; + this.input.onblur = function () { ac.hidePopup() }; + this.popup = document.createElement('div'); + this.popup.id = 'autocomplete'; + this.popup.owner = this; +}; + +/** + * Hides the autocomplete suggestions + */ +jsAC.prototype.hidePopup = function (keycode) { + if (this.selected && ((keycode && keycode != 46 && keycode != 8 && keycode != 27) || !keycode)) { + this.input.value = this.selected.autocompleteValue; + } + if (this.popup.parentNode && this.popup.parentNode.tagName) { + removeNode(this.popup); + } + this.selected = false; +} + + +/** + * Handler for the "keydown" event + */ +jsAC.prototype.onkeydown = function (input, e) { + if (!e) { + e = window.event; + } + switch (e.keyCode) { + case 40: // down arrow + this.selectDown(); + return false; + case 38: // up arrow + this.selectUp(); + return false; + default: // all other keys + return true; + } +} + +/** + * Handler for the "keyup" event + */ +jsAC.prototype.onkeyup = function (input, e) { + if (!e) { + e = window.event; + } + switch (e.keyCode) { + case 16: // shift + case 17: // ctrl + case 18: // alt + case 20: // caps lock + case 33: // page up + case 34: // page down + case 35: // end + case 36: // home + case 37: // left arrow + case 38: // up arrow + case 39: // right arrow + case 40: // down arrow + return true; + + case 9: // tab + case 13: // enter + case 27: // esc + this.hidePopup(e.keyCode); + return true; + + default: // all other keys + if (input.value.length > 0) + this.populatePopup(); + else + this.hidePopup(e.keyCode); + return true; + } +} + +/** + * Puts the currently highlighted suggestion into the autocomplete field + */ +jsAC.prototype.select = function (node) { + this.input.value = node.autocompleteValue; +} + +/** + * Highlights the next suggestion + */ +jsAC.prototype.selectDown = function () { + if (this.selected && this.selected.nextSibling) { + this.highlight(this.selected.nextSibling); + } + else { + var lis = this.popup.getElementsByTagName('li'); + if (lis.length > 0) { + this.highlight(lis[0]); + } + } +} + +/** + * Highlights the previous suggestion + */ +jsAC.prototype.selectUp = function () { + if (this.selected && this.selected.previousSibling) { + this.highlight(this.selected.previousSibling); + } +} + +/** + * Highlights a suggestion + */ +jsAC.prototype.highlight = function (node) { + removeClass(this.selected, 'selected'); + addClass(node, 'selected'); + this.selected = node; +} + +/** + * Unhighlights a suggestion + */ +jsAC.prototype.unhighlight = function (node) { + removeClass(node, 'selected'); + this.selected = false; +} + +/** + * Positions the suggestions popup and starts a search + */ +jsAC.prototype.populatePopup = function () { + var ac = this; + var pos = absolutePosition(this.input); + this.selected = false; + this.popup.style.top = (pos.y + this.input.offsetHeight) +'px'; + this.popup.style.left = pos.x +'px'; + this.popup.style.width = (this.input.offsetWidth - 4) +'px'; + addClass(this.input, 'throbbing'); + this.db.search(this.input.value); +} + +/** + * Fills the suggestion popup with any matches received + */ +jsAC.prototype.found = function (matches) { + while (this.popup.hasChildNodes()) { + this.popup.removeChild(this.popup.childNodes[0]); + } + if (!this.popup.parentNode || !this.popup.parentNode.tagName) { + document.getElementsByTagName('body')[0].appendChild(this.popup); + } + var ul = document.createElement('ul'); + var ac = this; + if (matches.length > 0) { + for (i in matches) { + li = document.createElement('li'); + div = document.createElement('div'); + div.innerHTML = matches[i][1]; + li.appendChild(div); + li.autocompleteValue = matches[i][0]; + li.onmousedown = function() { ac.select(this); }; + li.onmouseover = function() { ac.highlight(this); }; + li.onmouseout = function() { ac.unhighlight(this); }; + ul.appendChild(li); + } + this.popup.appendChild(ul); + } + else { + this.hidePopup(); + } + removeClass(this.input, 'throbbing'); +} + +/** + * An AutoComplete DataBase object + */ +function ACDB(uri) { + this.uri = uri; + this.max = 15; + this.delay = 300; + this.cache = {}; +} + +/** + * Performs a cached and delayed search + */ +ACDB.prototype.search = function(searchString) { + if (this.docache) { + this.searchString = searchString; + if (this.cache[searchString]) { + return this.match(this.cache[searchString]); + } + } + if (this.timer) { + clearTimeout(this.timer); + } + var db = this; + this.timer = setTimeout(function() { HTTPGet(db.uri +'/'+ searchString +'/'+ db.max, db.receive, db); }, this.delay); +} + +/** + * HTTP callback function. Passes suggestions to the autocomplete object + */ +ACDB.prototype.receive = function(string, xmlhttp, acdb) { + if (xmlhttp.status != 200) { + return alert('An HTTP error '+ xmlhttp.status +' occured.\n'+ acdb.uri); + } + // Split into array of key->value pairs + var matches = string.length > 0 ? string.split('||') : []; + for (i in matches) { + matches[i] = matches[i].length > 0 ? matches[i].split('|') : []; + // Decode textfield pipes back to plain-text + matches[i][0] = eregReplace('|', '|', matches[i][0]); + } + acdb.cache[acdb.searchString] = matches; + acdb.owner.found(matches); +} |