summaryrefslogtreecommitdiff
path: root/misc/autocomplete.js
diff options
context:
space:
mode:
Diffstat (limited to 'misc/autocomplete.js')
-rw-r--r--misc/autocomplete.js263
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);
+}