/* Interactive drop-down search widget. Usage: * * dsInit(obj, url, trfunc, serfunc, retfunc); * * obj: An object. * * url: The base URL of the XML API, e.g. "/xml/tags.xml?q=", the search query is appended to this URL. * The resource at the URL should return an XML document with a * .. * element for each result. * * trfunc(item, tr): Function that is given an object given by the XML * document and an empty object. The function should format the data of * the item to be shown in the tr. * * serfunc(item, obj): Called whenever a user selects an item from the search * results. Should return a string, which will be used as the new value of the * input object. * * retfunc(obj): Called whenever the user selects an item from the search * results (after setfunc()) or when enter is pressed (even if nothing is * selected). * * setfunc and retfunc can be null. * * TODO: Some users of this widget consider serfunc() as their final "apply * this selection" function, whereas others use retfunc() for this. Might be * worth investigating whether the additional flexibility offered by * retfunc() is actually necessary, and remove the callback if not. */ var boxobj; function box() { if(!boxobj) { boxobj = tag('div', {id: 'ds_box', 'class':'hidden'}, tag('b', mt('_js_loading'))); addBody(boxobj); } return boxobj; } function init(obj, url, trfunc, serfunc, retfunc) { obj.setAttribute('autocomplete', 'off'); obj.onkeydown = keydown; obj.onclick = obj.onchange = obj.oninput = function() { return textchanged(obj); }; obj.onblur = blur; obj.ds_returnFunc = retfunc; obj.ds_trFunc = trfunc; obj.ds_serFunc = serfunc; obj.ds_searchURL = url; obj.ds_selectedId = 0; obj.ds_dosearch = null; obj.ds_lastVal = obj.value; } function blur() { setTimeout(function () { setClass(box(), 'hidden', true) }, 500) } function setselected(obj, id) { obj.ds_selectedId = id; var l = byName(box(), 'tr'); for(var i=0; i0 ? i-1 : l.length-1) : (l[i+1] ? i+1 : 0); setselected(obj, l[sel].id.substr(7)); return false; } function textchanged(obj) { // Ignore this event if the text hasn't actually changed. if(obj.ds_lastVal == obj.value) return true; obj.ds_lastVal = obj.value; // perform search after a timeout if(obj.ds_dosearch) clearTimeout(obj.ds_dosearch); obj.ds_dosearch = setTimeout(function() { search(obj); }, 500); return true; } function keydown(ev) { var c = document.layers ? ev.which : document.all ? event.keyCode : ev.keyCode; var obj = this; if(c == 9) // tab return true; if(c == 13) // enter return enter(obj); if(c == 38 || c == 40) // up / down return updown(obj, c == 38); return textchanged(obj); } function search(obj) { var b = box(); var val = obj.value; clearTimeout(obj.ds_dosearch); obj.ds_dosearch = null; // hide the ds_box div if the search string is too short if(val.length < 2) { setClass(b, 'hidden', true); setContent(b, tag('b', mt('_js_loading'))); setselected(obj, 0); return; } // position the div var ddx=0; var ddy=obj.offsetHeight; var o = obj; do { ddx += o.offsetLeft; ddy += o.offsetTop; } while(o = o.offsetParent); b.style.position = 'absolute'; b.style.left = ddx+'px'; b.style.top = ddy+'px'; b.style.width = obj.offsetWidth+'px'; setClass(b, 'hidden', false); // perform search ajax(obj.ds_searchURL + encodeURIComponent(val), function(hr) { results(hr, obj) }); } function results(hr, obj) { var lst = hr.responseXML.getElementsByTagName('item'); var b = box(); if(lst.length < 1) { setContent(b, tag('b', mt('_js_ds_noresults'))); setselected(obj, 0); return; } var tb = tag('tbody', null); for(var i=0; i