/** * $Id: autosuggest.js 147 2006-12-20 22:53:19Z wingedfox $ * $HeadURL: https://svn.debugger.ru/repos/jslibs/BrowserExtensions/tags/BrowserExtensions.003/dom/autosuggest.js $ * * Extends the selectbox interface * * @author Ilya Lebedev * @modified $Date: 2006-12-21 01:53:19 +0300 (Чтв, 21 Дек 2006) $ * @version $Rev: 147 $ * @title Selectbox * @license LGPL 2.1 or later */ /** * Autosuggestion control * * @constructor * @param {Object} target */ var Autosuggest = function (target) { this.$VERSION$ = " $Rev: 147 $ ".replace(/\D/g,""); var self = this; /** * CSS classes */ var cssClasses = { 'root' : 'Autosuggest' } /** * Stores selectbox with suggestions * * @type HTMLElement * @scope private */ var node = null; /** * Selectbox controller * * @type Selectbox * @scope private */ var controller = null; /** * Config settings * * @type Object * @access private */ var options = { 'minlength' : 2 // minimum filter length to show the box ,'delay' : 50 // delay before displaying the box ,'size' : 15 // max select size ,'list' : [] // array of suggestions ,'place' : 0 // 1: top side // 0: bottom side ,'match' : 'start'// how to filter user input } /** * Stores the open interval ID to clear it, if needed * * @type Number * @scope private */ var interval = null; //------------------------------------------------------------------------- // PUBLICS //----------------------------------------------------- // Setters //--------------------------------- /** * Defines the draw place * * @param {String} dir 'top' or 'bottom' * @return {Boolean} * @scope public */ this.setPlace = function (dir) { if (!isString(dir)) return false; switch (dir.toLowerCase()) { case 'top' : options.place = 1; break; case 'bottom' : options.place = 0; break; default : return false; } return true; } /** * Set the minimum number of the letters to show the box * * @return {Number} * @scope public */ this.setMinLength = function (d /* :Number */) /* Boolean */ { if (!isNumeric(parseInt(d))) return false; options.minlength = d; return true; } /** * Set the delay to show the box * * @param {Number, String} d delay * @return {Boolean} * @scope public */ this.setDelay = function (d /* :Number */) /* Boolean */ { if (!isNumeric(parseInt(d))) return false; options.delay = d; return true; } /** * Set suggestions * * @param {Array} list of the suggestions * @return {Boolean} * @scope public */ this.setSuggestions = function (list /* :Array */) /* :Boolean */ { if (!isArray(list)) return false; options.list = list; options.rebuildList = true; } /** * Adds the filter to the suggestions list * * @param {String} filter string to filter suggestions * @return {Boolean} * @scope public */ this.setFilter = function (filter /* :String */) /* :Boolean */ { if (!controller) return false; return controller.showOnlyMatchingOptions(filter, options.match); } /** * Adds the filter to the suggestions list * * @see Selectbox#showOnlyMatchingOptions * @param {String} match option to be sent to the selectbox controller * @scope public */ this.setFilterMatch = function (match /* :String */) { options.match = match; } //--------------------------------- // Getters //--------------------------------- /** * Retrieves the delay to show the box * * @return {Number} * @scope public */ this.getDelay = function () /* Boolean */ { return options.delay; } /** * Retrieves the minimum number of the letters to show the box * * @return {Number} * @scope public */ this.getMinLength = function () /* Boolean */ { return options.minlength; } //--------------------------------- // Misc functions //--------------------------------- /** * Shows the suggestions box * * @return {Boolean} * @scope public */ this.show = function () /* :Boolean */ { if (!node || self.isVisible()) return false; if (!node.parentNode || node.parentNode.nodeType==11) { document.body.appendChild(node); /* * where to place the box */ var xy = DOM.getOffset(target); node.style.left = xy.x+'px'; node.style.top = xy.y+target.offsetHeight+'px'; /* * attach selectbox controller */ controller = new Selectbox(node.firstChild.id); } /* * do things, only if controller is available */ if (controller) { if (options.rebuildList) { controller.addOptionsList(options.list); options.rebuildList = false; } controller.showOnlyMatchingOptions(target.value, options.match); var len = controller.getOptionsLength(); if (len == 0) return false; else node.firstChild.size = options.size; } else { return false; } setTimeout(doShow, options.delay); return true; } /** * Hides the suggestions box * * @return {Boolean} * @scope public */ this.hide = function (e) /* :Boolean */{ if (!node || self.isHidden()) return false; target.focus(); interval = setTimeout(function() {node.style.display = 'none'; interval=null}, options.delay); return true; } /** * Tells, if suggest box is visible * * @return {Booolean} * @scope public */ this.isVisible = function () /* :Boolean */ { return !node.style.display; } /** * Tells, if suggest box is hidden * * @return {Booolean} * @scope public */ this.isHidden = function () /* :Boolean */ { return !self.isVisible(); } //------------------------------------------------------------------------- // PRIVATES //----------------------------------------------------- // Misc functions //--------------------------------- /** * Actual method, showing the box * * @scope private */ var doShow = function () { node.style.visibility = 'hidden'; node.style.display = ''; var tsp = DOM.getOffset(target)['y']-DOM.getBodyScrollTop() ,ht = DOM.getClientHeight() ,bsp = ht - (DOM.getOffset(target)['y'] + target.offsetHeight-DOM.getBodyScrollTop()) ,ct = document.getElementById(controller.getId()) ,ttop = DOM.getOffset(target)['y'] ,tbot = ttop + target.offsetHeight+'px' if (tsp>ct.parentNode.offsetHeight && options.place) { /* * default place - top and it's enough space there */ ct.parentNode.style.top = tsp-ct.parentNode.offsetHeight+DOM.getBodyScrollTop()+'px'; } else if (bsp>ct.parentNode.offsetHeight && !options.place) { /* * default place - bottom and it's enough space there */ ct.parentNode.style.top = DOM.getOffset(target)['y'] + target.offsetHeight+'px'; } else { /* * calculate the needed height */ while (ct.size>2 && bsp=ct.parentNode.offsetHeight || bsp>=tsp) { ct.parentNode.style.top = DOM.getOffset(target)['y'] + target.offsetHeight+'px'; } else if (tsp>=ct.parentNode.offsetHeight) { ct.parentNode.style.top = tsp-ct.parentNode.offsetHeight+DOM.getBodyScrollTop()+'px'; } } node.style.visibility = ''; if (controller.getSelectedIndex()<0) controller.selectOption(0); interval=1; } //--------------------------------- // Event handlers //--------------------------------- /** * Keypress event handler, used to track the input * * @param {EventTarget} e * @scope protected */ var keypress = function (e) { el = e.target||e.srcElement; switch (e.keyCode) { case 33: //page up case 34: //page down case 36: //home case 35: //end case 38: //up arrow case 40: //down arrow self.show(); case 27: // escape if (self.isVisible() && !e.shiftKey) __preventDefault(e); break; case 9: case 13: // enter pasteSuggestion(e); break; case 37: // left arrow case 39: // right arrow break; default: if (target.value.length >= options.minlength) self.show() if (controller) { controller.showOnlyMatchingOptions(target.value, options.match); if (controller.getSelectedIndex()<0) controller.selectOption(0); } break; } } var pasteSuggestion = function (e) { var el = e.target||e.srcElement; /* * since we use this handler for both click and keypress events, emulate 'Enter' press */ if ('click' == e.type) e.keyCode = 13; switch (e.keyCode) { case 9: // tab case 13: // enter if (self.hide()) { target.value = node.firstChild.value; return __preventDefault(e); } default: } target.focus(); } /** * Prevents some keys to generate default actions * * @param {EventTarget} e * @scope protected */ var __preventKeyPress = function (e) { switch (e.keyCode) { case 27: //esc case 33: //page up case 34: //page down case 36: //home case 35: //end case 27 : // escape if (self.isHidden()) return; case 38: //up arrow case 40: //down arrow case 37: //left arrow case 39: //right arrow if (!e.shiftKey) return; // case 9: //tab case 13: //enter return __preventDefault(e); } } /** * Prevents default action for the target * * @param {EventTarget} el * @scope private */ var __preventDefault = function (e) { e.returnValue = false; if (e.preventDefault) e.preventDefault(); return false; } /** * constructor */ var __construct = function () { node = document.createElementExt('div', {'class' : cssClasses.root}); node.innerHTML = ''; if (!node) return false; node.style.display = 'none'; /* * init target */ if (isString(target)) target = document.getElementById(target); target.attachEvent('onkeyup',keypress); target.attachEvent('onkeypress',__preventKeyPress); target.attachEvent('onkeydown',function (e) { switch (e.keyCode) { case 9: pasteSuggestion(e); break; case 27: if (self.hide()) { return __preventDefault(e); } break; case 32: if (!e.ctrlKey) return; case 38: // key_up case 40: // key_down case 33: //page up case 34: //page down if (self.isHidden()) { self.show(); } else { if (38 == e.keyCode) controller.selectPrev(true); else if (40 == e.keyCode) controller.selectNext(true); else if (33 == e.keyCode) controller.selectOption(controller.getSelectedIndex()-node.firstChild.size, true); else if (34 == e.keyCode) controller.selectOption(controller.getSelectedIndex()-(-node.firstChild.size), true); } __preventDefault(e); break; case 36: //home case 35: //end if (self.isVisible()) { if (36 == e.keyCode) controller.selectOption(0,true); else if (35 == e.keyCode) controller.selectOption(controller.getOptionsLength(), true); __preventDefault(e); } break; } }); /* * keep things persistent, while clicking between the boxes */ target.attachEvent('onmousedown', function () {interval = 1;}); target.attachEvent('onblur', function(){interval==1?self.hide():interval=2}); node.firstChild.attachEvent('onmousedown', function () {interval = 2;}); node.firstChild.attachEvent('onkeydown', function () {interval = 1;}); node.firstChild.attachEvent('onblur', function(){interval==2?self.hide():interval=1}); /* * turn off damn autocomplete */ target.autocomplete = 'off'; target.setAttribute('autocomplete','off'); node.firstChild.attachEvent('onclick', pasteSuggestion); node.firstChild.attachEvent('onkeydown', pasteSuggestion); } return __construct(); }