1/** 2 * $Id: autosuggest.js 147 2006-12-20 22:53:19Z wingedfox $ 3 * $HeadURL: https://svn.debugger.ru/repos/jslibs/BrowserExtensions/tags/BrowserExtensions.003/dom/autosuggest.js $ 4 * 5 * Extends the selectbox interface 6 * 7 * @author Ilya Lebedev <ilya@lebedev.net> 8 * @modified $Date: 2006-12-21 01:53:19 +0300 (Чтв, 21 Дек 2006) $ 9 * @version $Rev: 147 $ 10 * @title Selectbox 11 * @license LGPL 2.1 or later 12 */ 13/** 14 * Autosuggestion control 15 * 16 * @constructor 17 * @param {Object} target 18 */ 19var Autosuggest = function (target) { 20 this.$VERSION$ = " $Rev: 147 $ ".replace(/\D/g,""); 21 var self = this; 22 /** 23 * CSS classes 24 */ 25 var cssClasses = { 26 'root' : 'Autosuggest' 27 } 28 /** 29 * Stores selectbox with suggestions 30 * 31 * @type HTMLElement 32 * @scope private 33 */ 34 var node = null; 35 /** 36 * Selectbox controller 37 * 38 * @type Selectbox 39 * @scope private 40 */ 41 var controller = null; 42 /** 43 * Config settings 44 * 45 * @type Object 46 * @access private 47 */ 48 var options = { 49 'minlength' : 2 // minimum filter length to show the box 50 ,'delay' : 50 // delay before displaying the box 51 ,'size' : 15 // max select size 52 ,'list' : [] // array of suggestions 53 ,'place' : 0 // 1: top side 54 // 0: bottom side 55 ,'match' : 'start'// how to filter user input 56 } 57 /** 58 * Stores the open interval ID to clear it, if needed 59 * 60 * @type Number 61 * @scope private 62 */ 63 var interval = null; 64 //------------------------------------------------------------------------- 65 // PUBLICS 66 //----------------------------------------------------- 67 // Setters 68 //--------------------------------- 69 /** 70 * Defines the draw place 71 * 72 * @param {String} dir 'top' or 'bottom' 73 * @return {Boolean} 74 * @scope public 75 */ 76 this.setPlace = function (dir) { 77 if (!isString(dir)) return false; 78 switch (dir.toLowerCase()) { 79 case 'top' : 80 options.place = 1; 81 break; 82 case 'bottom' : 83 options.place = 0; 84 break; 85 default : 86 return false; 87 } 88 return true; 89 } 90 /** 91 * Set the minimum number of the letters to show the box 92 * 93 * @return {Number} 94 * @scope public 95 */ 96 this.setMinLength = function (d /* :Number */) /* Boolean */ { 97 if (!isNumeric(parseInt(d))) return false; 98 options.minlength = d; 99 return true; 100 } 101 /** 102 * Set the delay to show the box 103 * 104 * @param {Number, String} d delay 105 * @return {Boolean} 106 * @scope public 107 */ 108 this.setDelay = function (d /* :Number */) /* Boolean */ { 109 if (!isNumeric(parseInt(d))) return false; 110 options.delay = d; 111 return true; 112 } 113 /** 114 * Set suggestions 115 * 116 * @param {Array} list of the suggestions 117 * @return {Boolean} 118 * @scope public 119 */ 120 this.setSuggestions = function (list /* :Array */) /* :Boolean */ { 121 if (!isArray(list)) return false; 122 options.list = list; 123 options.rebuildList = true; 124 } 125 /** 126 * Adds the filter to the suggestions list 127 * 128 * @param {String} filter string to filter suggestions 129 * @return {Boolean} 130 * @scope public 131 */ 132 this.setFilter = function (filter /* :String */) /* :Boolean */ { 133 if (!controller) return false; 134 return controller.showOnlyMatchingOptions(filter, options.match); 135 } 136 /** 137 * Adds the filter to the suggestions list 138 * 139 * @see Selectbox#showOnlyMatchingOptions 140 * @param {String} match option to be sent to the selectbox controller 141 * @scope public 142 */ 143 this.setFilterMatch = function (match /* :String */) { 144 options.match = match; 145 } 146 //--------------------------------- 147 // Getters 148 //--------------------------------- 149 /** 150 * Retrieves the delay to show the box 151 * 152 * @return {Number} 153 * @scope public 154 */ 155 this.getDelay = function () /* Boolean */ { 156 return options.delay; 157 } 158 /** 159 * Retrieves the minimum number of the letters to show the box 160 * 161 * @return {Number} 162 * @scope public 163 */ 164 this.getMinLength = function () /* Boolean */ { 165 return options.minlength; 166 } 167 //--------------------------------- 168 // Misc functions 169 //--------------------------------- 170 /** 171 * Shows the suggestions box 172 * 173 * @return {Boolean} 174 * @scope public 175 */ 176 this.show = function () /* :Boolean */ { 177 if (!node || self.isVisible()) return false; 178 if (!node.parentNode || node.parentNode.nodeType==11) { 179 document.body.appendChild(node); 180 /* 181 * where to place the box 182 */ 183 var xy = DOM.getOffset(target); 184 node.style.left = xy.x+'px'; 185 node.style.top = xy.y+target.offsetHeight+'px'; 186 /* 187 * attach selectbox controller 188 */ 189 controller = new Selectbox(node.firstChild.id); 190 } 191 /* 192 * do things, only if controller is available 193 */ 194 if (controller) { 195 if (options.rebuildList) { 196 controller.addOptionsList(options.list); 197 options.rebuildList = false; 198 } 199 controller.showOnlyMatchingOptions(target.value, options.match); 200 var len = controller.getOptionsLength(); 201 if (len == 0) return false; 202 else node.firstChild.size = options.size; 203 } else { 204 return false; 205 } 206 setTimeout(doShow, options.delay); 207 return true; 208 } 209 210 /** 211 * Hides the suggestions box 212 * 213 * @return {Boolean} 214 * @scope public 215 */ 216 this.hide = function (e) /* :Boolean */{ 217 if (!node || self.isHidden()) return false; 218 target.focus(); 219 interval = setTimeout(function() {node.style.display = 'none'; interval=null}, options.delay); 220 return true; 221 } 222 /** 223 * Tells, if suggest box is visible 224 * 225 * @return {Booolean} 226 * @scope public 227 */ 228 this.isVisible = function () /* :Boolean */ { 229 return !node.style.display; 230 } 231 /** 232 * Tells, if suggest box is hidden 233 * 234 * @return {Booolean} 235 * @scope public 236 */ 237 this.isHidden = function () /* :Boolean */ { 238 return !self.isVisible(); 239 } 240 //------------------------------------------------------------------------- 241 // PRIVATES 242 //----------------------------------------------------- 243 // Misc functions 244 //--------------------------------- 245 /** 246 * Actual method, showing the box 247 * 248 * @scope private 249 */ 250 var doShow = function () { 251 node.style.visibility = 'hidden'; 252 node.style.display = ''; 253 var tsp = DOM.getOffset(target)['y']-DOM.getBodyScrollTop() 254 ,ht = DOM.getClientHeight() 255 ,bsp = ht - (DOM.getOffset(target)['y'] + target.offsetHeight-DOM.getBodyScrollTop()) 256 ,ct = document.getElementById(controller.getId()) 257 ,ttop = DOM.getOffset(target)['y'] 258 ,tbot = ttop + target.offsetHeight+'px' 259 if (tsp>ct.parentNode.offsetHeight && options.place) { 260 /* 261 * default place - top and it's enough space there 262 */ 263 ct.parentNode.style.top = tsp-ct.parentNode.offsetHeight+DOM.getBodyScrollTop()+'px'; 264 } else if (bsp>ct.parentNode.offsetHeight && !options.place) { 265 /* 266 * default place - bottom and it's enough space there 267 */ 268 ct.parentNode.style.top = DOM.getOffset(target)['y'] + target.offsetHeight+'px'; 269 } else { 270 /* 271 * calculate the needed height 272 */ 273 while (ct.size>2 && bsp<ct.parentNode.offsetHeight && tsp<ct.parentNode.offsetHeight) { 274 ct.size--; 275 } 276 if (bsp>=ct.parentNode.offsetHeight || bsp>=tsp) { 277 ct.parentNode.style.top = DOM.getOffset(target)['y'] + target.offsetHeight+'px'; 278 } else if (tsp>=ct.parentNode.offsetHeight) { 279 ct.parentNode.style.top = tsp-ct.parentNode.offsetHeight+DOM.getBodyScrollTop()+'px'; 280 } 281 } 282 node.style.visibility = ''; 283 if (controller.getSelectedIndex()<0) controller.selectOption(0); 284 interval=1; 285 } 286 //--------------------------------- 287 // Event handlers 288 //--------------------------------- 289 /** 290 * Keypress event handler, used to track the input 291 * 292 * @param {EventTarget} e 293 * @scope protected 294 */ 295 var keypress = function (e) { 296 el = e.target||e.srcElement; 297 switch (e.keyCode) { 298 case 33: //page up 299 case 34: //page down 300 case 36: //home 301 case 35: //end 302 case 38: //up arrow 303 case 40: //down arrow 304 self.show(); 305 case 27: // escape 306 if (self.isVisible() && !e.shiftKey) __preventDefault(e); 307 break; 308 case 9: 309 case 13: // enter 310 pasteSuggestion(e); 311 break; 312 case 37: // left arrow 313 case 39: // right arrow 314 break; 315 default: 316 if (target.value.length >= options.minlength) self.show() 317 if (controller) { 318 controller.showOnlyMatchingOptions(target.value, options.match); 319 if (controller.getSelectedIndex()<0) controller.selectOption(0); 320 } 321 break; 322 } 323 } 324 var pasteSuggestion = function (e) { 325 var el = e.target||e.srcElement; 326 /* 327 * since we use this handler for both click and keypress events, emulate 'Enter' press 328 */ 329 if ('click' == e.type) e.keyCode = 13; 330 switch (e.keyCode) { 331 case 9: // tab 332 case 13: // enter 333 if (self.hide()) { 334 target.value = node.firstChild.value; 335 return __preventDefault(e); 336 } 337 default: 338 } 339 target.focus(); 340 } 341 /** 342 * Prevents some keys to generate default actions 343 * 344 * @param {EventTarget} e 345 * @scope protected 346 */ 347 var __preventKeyPress = function (e) { 348 switch (e.keyCode) { 349 case 27: //esc 350 case 33: //page up 351 case 34: //page down 352 case 36: //home 353 case 35: //end 354 case 27 : // escape 355 if (self.isHidden()) return; 356 case 38: //up arrow 357 case 40: //down arrow 358 case 37: //left arrow 359 case 39: //right arrow 360 if (!e.shiftKey) return; 361// case 9: //tab 362 case 13: //enter 363 return __preventDefault(e); 364 } 365 } 366 /** 367 * Prevents default action for the target 368 * 369 * @param {EventTarget} el 370 * @scope private 371 */ 372 var __preventDefault = function (e) { 373 e.returnValue = false; 374 if (e.preventDefault) e.preventDefault(); 375 return false; 376 } 377 /** 378 * constructor 379 */ 380 var __construct = function () { 381 node = document.createElementExt('div', {'class' : cssClasses.root}); 382 node.innerHTML = '<select id="Autosuggest'+(new Date).valueOf()+'" size="10" autocomplete="off"></select>'; 383 if (!node) return false; 384 node.style.display = 'none'; 385 386 /* 387 * init target 388 */ 389 if (isString(target)) target = document.getElementById(target); 390 target.attachEvent('onkeyup',keypress); 391 target.attachEvent('onkeypress',__preventKeyPress); 392 target.attachEvent('onkeydown',function (e) { 393 switch (e.keyCode) { 394 case 9: 395 pasteSuggestion(e); 396 break; 397 case 27: 398 if (self.hide()) { 399 return __preventDefault(e); 400 } 401 break; 402 case 32: 403 if (!e.ctrlKey) return; 404 case 38: // key_up 405 case 40: // key_down 406 case 33: //page up 407 case 34: //page down 408 if (self.isHidden()) { 409 self.show(); 410 } else { 411 if (38 == e.keyCode) controller.selectPrev(true); 412 else if (40 == e.keyCode) controller.selectNext(true); 413 else if (33 == e.keyCode) controller.selectOption(controller.getSelectedIndex()-node.firstChild.size, true); 414 else if (34 == e.keyCode) controller.selectOption(controller.getSelectedIndex()-(-node.firstChild.size), true); 415 } 416 __preventDefault(e); 417 break; 418 case 36: //home 419 case 35: //end 420 if (self.isVisible()) { 421 if (36 == e.keyCode) controller.selectOption(0,true); 422 else if (35 == e.keyCode) controller.selectOption(controller.getOptionsLength(), true); 423 __preventDefault(e); 424 } 425 break; 426 } 427 428 }); 429 /* 430 * keep things persistent, while clicking between the boxes 431 */ 432 target.attachEvent('onmousedown', function () {interval = 1;}); 433 target.attachEvent('onblur', function(){interval==1?self.hide():interval=2}); 434 node.firstChild.attachEvent('onmousedown', function () {interval = 2;}); 435 node.firstChild.attachEvent('onkeydown', function () {interval = 1;}); 436 node.firstChild.attachEvent('onblur', function(){interval==2?self.hide():interval=1}); 437 /* 438 * turn off damn autocomplete 439 */ 440 target.autocomplete = 'off'; 441 target.setAttribute('autocomplete','off'); 442 443 node.firstChild.attachEvent('onclick', pasteSuggestion); 444 node.firstChild.attachEvent('onkeydown', pasteSuggestion); 445 446 } 447 return __construct(); 448} 449