1 (function ($, rf) { 2 3 /* 4 * TODO: add user's event handlers call from options 5 * TODO: add fire events 6 */ 7 8 rf.ui = rf.ui || {}; 9 // Constructor definition 10 rf.ui.Autocomplete = function(componentId, fieldId, options) { 11 this.namespace = "." + rf.Event.createNamespace(this.name, componentId); 12 this.options = {}; 13 // call constructor of parent class 14 $super.constructor.call(this, componentId, componentId + ID.SELECT, fieldId, options); 15 this.attachToDom(); 16 this.options = $.extend(this.options, defaultOptions, options); 17 this.value = ""; 18 this.index = null; 19 this.isFirstAjax = true; 20 updateTokenOptions.call(this); 21 bindEventHandlers.call(this); 22 updateItemsList.call(this, ""); 23 }; 24 25 // Extend component class and add protected methods from parent class to our container 26 rf.ui.AutocompleteBase.extend(rf.ui.Autocomplete); 27 28 // define super class link 29 var $super = rf.ui.Autocomplete.$super; 30 31 var defaultOptions = { 32 itemClass:'rf-au-itm', 33 selectedItemClass:'rf-au-itm-sel', 34 subItemClass:'rf-au-opt', 35 selectedSubItemClass:'rf-au-opt-sel', 36 autofill:true, 37 minChars:1, 38 selectFirst:true, 39 ajaxMode:true, 40 lazyClientMode:false, 41 isCachedAjax:true, 42 tokens: "", 43 attachToBody:true, 44 filterFunction: undefined 45 //nothingLabel = "Nothing"; 46 }; 47 48 var ID = { 49 SELECT:'List', 50 ITEMS:'Items', 51 VALUE:'Value' 52 }; 53 54 var REGEXP_TRIM = /^[\n\s]*(.*)[\n\s]*$/; 55 56 var getData = function (nodeList) { 57 var data = []; 58 nodeList.each(function () { 59 data.push($(this).text().replace(REGEXP_TRIM, "$1")); 60 }); 61 return data; 62 } 63 64 var updateTokenOptions = function () { 65 this.useTokens = (typeof this.options.tokens == "string" && this.options.tokens.length > 0); 66 if (this.useTokens) { 67 var escapedTokens = this.options.tokens.split('').join("\\"); 68 this.REGEXP_TOKEN_RIGHT = new RegExp('[' + escapedTokens + ']', 'i'); 69 this.getLastTokenIndex = function(value) { 70 return RichFaces.ui.Autocomplete.__getLastTokenIndex(escapedTokens, value); 71 } 72 } 73 }; 74 75 var bindEventHandlers = function () { 76 var list = $(rf.getDomElement(this.id + ID.ITEMS).parentNode); 77 list.on("click" + this.namespace, "."+this.options.itemClass, $.proxy(onMouseClick, this)); 78 // The mouseenter event is available only natively in Internet Explorer, however jQuery emulates it in other browsers 79 list.on("mouseenter" + this.namespace, "."+this.options.itemClass, $.proxy(onMouseEnter, this)); 80 }; 81 82 var onMouseEnter = function(event) { 83 var element = $(event.target); 84 85 if (element && !element.hasClass(this.options.itemClass)) { 86 element = element.parents("." + this.options.itemClass).get(0); 87 } 88 89 if (element) { 90 var index = this.items.index(element); 91 selectItem.call(this, event, index); 92 } 93 }; 94 95 var onMouseClick = function(event) { 96 var element = $(event.target); 97 98 if (element && !element.hasClass(this.options.itemClass)) { 99 element = element.parents("." + this.options.itemClass).get(0); 100 } 101 102 if (element) { 103 this.__onEnter(event); 104 rf.Selection.setCaretTo(rf.getDomElement(this.fieldId)); 105 this.__hide(event); 106 } 107 }; 108 109 var updateItemsList = function (value, fetchValues) { 110 var itemsContainer = $(rf.getDomElement(this.id + ID.ITEMS)); 111 this.items = itemsContainer.find("." + this.options.itemClass); 112 var componentData = itemsContainer.data("componentData"); 113 itemsContainer.removeData("componentData"); 114 if (this.items.length > 0) { 115 this.cache = new rf.utils.Cache((this.options.ajaxMode ? value : ""), this.items, fetchValues || componentData || getData, !this.options.ajaxMode); 116 } 117 }; 118 119 var scrollToSelectedItem = function() { 120 var offset = 0; 121 this.items.slice(0, this.index).each(function() { 122 offset += this.offsetHeight; 123 }); 124 var parentContainer = $(rf.getDomElement(this.id + ID.ITEMS)).parent(); 125 if (offset < parentContainer.scrollTop()) { 126 parentContainer.scrollTop(offset); 127 } else { 128 offset += this.items.eq(this.index).outerHeight(); 129 if (offset - parentContainer.scrollTop() > parentContainer.innerHeight()) { 130 parentContainer.scrollTop(offset - parentContainer.innerHeight()); 131 } 132 } 133 }; 134 135 var autoFill = function (inputValue, value) { 136 if (this.options.autofill && value.toLowerCase().indexOf(inputValue) == 0) { 137 var field = rf.getDomElement(this.fieldId); 138 var start = rf.Selection.getStart(field); 139 this.__setInputValue(inputValue + value.substring(inputValue.length)); 140 var end = start + value.length - inputValue.length; 141 rf.Selection.set(field, start, end); 142 } 143 }; 144 145 var callAjax = function(event, callback) { 146 147 rf.getDomElement(this.id + ID.VALUE).value = this.value; 148 149 var _this = this; 150 var _event = event; 151 var ajaxSuccess = function (event) { 152 updateItemsList.call(_this, _this.value, event.componentData && event.componentData[_this.id]); 153 if (_this.options.lazyClientMode && _this.value.length != 0) { 154 updateItemsFromCache.call(_this, _this.value); 155 } 156 if (_this.items.length != 0) { 157 if (callback) { 158 (_this.focused || _this.isMouseDown) && callback.call(_this, _event); 159 } else { 160 _this.isVisible && _this.options.selectFirst && selectItem.call(_this, _event, 0); 161 } 162 } else { 163 _this.__hide(_event); 164 } 165 }; 166 167 var ajaxError = function (event) { 168 _this.__hide(_event); 169 clearItems.call(_this); 170 }; 171 172 this.isFirstAjax = false; 173 //caution: JSF submits inputs with empty names causing "WARNING: Parameters: Invalid chunk ignored." in Tomcat log 174 var params = {}; 175 params[this.id + ".ajax"] = "1"; 176 rf.ajax(this.id, event, {parameters: params, error: ajaxError, complete:ajaxSuccess}); 177 }; 178 179 var clearSelection = function () { 180 if (this.index != null) { 181 var element = this.items.eq(this.index); 182 if (element.removeClass(this.options.selectedItemClass).hasClass(this.options.subItemClass)) { 183 element.removeClass(this.options.selectedSubItemClass); 184 } 185 this.index = null; 186 } 187 }; 188 189 var selectItem = function(event, index, isOffset) { 190 if (this.items.length == 0 || (!isOffset && index == this.index)) return; 191 192 if (index == null || index == undefined) { 193 clearSelection.call(this); 194 return; 195 } 196 197 if (isOffset) { 198 if (this.index == null) { 199 index = 0; 200 } else { 201 index = this.index + index; 202 } 203 } 204 if (index < 0) { 205 index = 0; 206 } else if (index >= this.items.length) { 207 index = this.items.length - 1; 208 } 209 if (index == this.index) return; 210 211 clearSelection.call(this); 212 this.index = index; 213 214 var item = this.items.eq(this.index); 215 if (item.addClass(this.options.selectedItemClass).hasClass(this.options.subItemClass)) { 216 item.addClass(this.options.selectedSubItemClass); 217 } 218 scrollToSelectedItem.call(this); 219 if (event && 220 event.keyCode != rf.KEYS.BACKSPACE && 221 event.keyCode != rf.KEYS.DEL && 222 event.keyCode != rf.KEYS.LEFT && 223 event.keyCode != rf.KEYS.RIGHT) { 224 autoFill.call(this, this.value, getSelectedItemValue.call(this)); 225 } 226 }; 227 228 var updateItemsFromCache = function (value) { 229 var newItems = this.cache.getItems(value, this.options.filterFunction); 230 this.items = $(newItems); 231 //TODO: works only with simple markup, not with <tr> 232 $(rf.getDomElement(this.id + ID.ITEMS)).empty().append(this.items); 233 }; 234 235 var clearItems = function () { 236 $(rf.getDomElement(this.id + ID.ITEMS)).removeData().empty(); 237 this.items = []; 238 }; 239 240 var onChangeValue = function (event, value, callback) { 241 selectItem.call(this, event); 242 243 // value is undefined if called from AutocompleteBase onChange 244 var subValue = (typeof value == "undefined") ? this.__getSubValue() : value; 245 var oldValue = this.value; 246 this.value = subValue; 247 248 if ((this.options.isCachedAjax || !this.options.ajaxMode) && 249 this.cache && this.cache.isCached(subValue)) { 250 if (oldValue != subValue) { 251 updateItemsFromCache.call(this, subValue); 252 } 253 if (this.items.length != 0) { 254 callback && callback.call(this, event); 255 } else { 256 this.__hide(event); 257 } 258 if (event.keyCode == rf.KEYS.RETURN || event.type == "click") { 259 this.__setInputValue(subValue); 260 } else if (this.options.selectFirst) { 261 selectItem.call(this, event, 0); 262 } 263 } else { 264 if (event.keyCode == rf.KEYS.RETURN || event.type == "click") { 265 this.__setInputValue(subValue); 266 } 267 if (subValue.length >= this.options.minChars) { 268 if ((this.options.ajaxMode || this.options.lazyClientMode) && (oldValue != subValue || (oldValue === '' && subValue === ''))) { 269 callAjax.call(this, event, callback); 270 } 271 } else { 272 if (this.options.ajaxMode) { 273 clearItems.call(this); 274 this.__hide(event); 275 } 276 } 277 } 278 279 }; 280 281 var getSelectedItemValue = function () { 282 if (this.index != null) { 283 var element = this.items.eq(this.index); 284 return this.cache.getItemValue(element); 285 } 286 return undefined; 287 }; 288 289 var getSubValue = function () { 290 //TODO: add posibility to use space chars before and after tokens if space not a token char 291 if (this.useTokens) { 292 var field = rf.getDomElement(this.fieldId); 293 var value = field.value; 294 295 var cursorPosition = rf.Selection.getStart(field); 296 var beforeCursorStr = value.substring(0, cursorPosition); 297 var afterCursorStr = value.substring(cursorPosition); 298 var result = beforeCursorStr.substring(this.getLastTokenIndex(beforeCursorStr)); 299 r = afterCursorStr.search(this.REGEXP_TOKEN_RIGHT); 300 if (r == -1) r = afterCursorStr.length; 301 result += afterCursorStr.substring(0, r); 302 303 return result; 304 } else { 305 return this.getValue(); 306 } 307 }; 308 309 var getCursorPosition = function(field) { 310 var pos = rf.Selection.getStart(field); 311 if (pos <= 0) { 312 // when cursorPosition is not determined (input is not focused), 313 // use position of last token occurence) 314 pos = this.getLastTokenIndex(field.value); 315 } 316 return pos; 317 } 318 319 var updateInputValue = function (value) { 320 var field = rf.getDomElement(this.fieldId); 321 var inputValue = field.value; 322 323 var cursorPosition = this.__getCursorPosition(field); 324 var beforeCursorStr = inputValue.substring(0, cursorPosition); 325 var afterCursorStr = inputValue.substring(cursorPosition); 326 var pos = this.getLastTokenIndex(beforeCursorStr); 327 var startPos = pos != -1 ? pos : beforeCursorStr.length; 328 pos = afterCursorStr.search(this.REGEXP_TOKEN_RIGHT); 329 var endPos = pos != -1 ? pos : afterCursorStr.length; 330 331 var beginNewValue = inputValue.substring(0, startPos) + value; 332 cursorPosition = beginNewValue.length; 333 field.value = beginNewValue + afterCursorStr.substring(endPos); 334 field.focus(); 335 rf.Selection.setCaretTo(field, cursorPosition); 336 return field.value; 337 }; 338 339 var getPageLastItem = function() { 340 if (this.items.length == 0) return -1; 341 var parentContainer = $(rf.getDomElement(this.id + ID.ITEMS)).parent(); 342 var h = parentContainer.scrollTop() + parentContainer.innerHeight() + this.items[0].offsetTop; 343 var item; 344 var i = (this.index != null && this.items[this.index].offsetTop <= h) ? this.index : 0; 345 for (i; i < this.items.length; i++) { 346 item = this.items[i]; 347 if (item.offsetTop + item.offsetHeight > h) { 348 i--; 349 break; 350 } 351 } 352 if (i != this.items.length - 1 && i == this.index) { 353 h += this.items[i].offsetTop - parentContainer.scrollTop(); 354 for (++i; i < this.items.length; i++) { 355 item = this.items[i]; 356 if (item.offsetTop + item.offsetHeight > h) { 357 break; 358 } 359 } 360 } 361 return i; 362 }; 363 364 var getPageFirstItem = function() { 365 if (this.items.length == 0) return -1; 366 var parentContainer = $(rf.getDomElement(this.id + ID.ITEMS)).parent(); 367 var h = parentContainer.scrollTop() + this.items[0].offsetTop; 368 var item; 369 var i = (this.index != null && this.items[this.index].offsetTop >= h) ? this.index - 1 : this.items.length - 1; 370 for (i; i >= 0; i--) { 371 item = this.items[i]; 372 if (item.offsetTop < h) { 373 i++; 374 break; 375 } 376 } 377 if (i != 0 && i == this.index) { 378 h = this.items[i].offsetTop - parentContainer.innerHeight(); 379 if (h < this.items[0].offsetTop) h = this.items[0].offsetTop; 380 for (--i; i >= 0; i--) { 381 item = this.items[i]; 382 if (item.offsetTop < h) { 383 i++; 384 break; 385 } 386 } 387 } 388 return i; 389 }; 390 391 /* 392 * Prototype definition 393 */ 394 $.extend(rf.ui.Autocomplete.prototype, (function () { 395 return { 396 /* 397 * public API functions 398 */ 399 name:"Autocomplete", 400 /* 401 * Protected methods 402 */ 403 __updateState: function (event) { 404 var subValue = this.__getSubValue(); 405 // called from AutocompleteBase when not actually value changed 406 if (this.items.length == 0 && this.isFirstAjax) { 407 if ((this.options.ajaxMode && subValue.length >= this.options.minChars) || this.options.lazyClientMode) { 408 this.value = subValue; 409 callAjax.call(this, event, this.__show); 410 return true; 411 } 412 } 413 return false; 414 }, 415 __getSubValue: getSubValue, 416 __getCursorPosition: getCursorPosition, 417 __updateInputValue: function (value) { 418 if (this.useTokens) { 419 return updateInputValue.call(this, value); 420 } else { 421 return $super.__updateInputValue.call(this, value); 422 } 423 }, 424 __setInputValue: function (value) { 425 this.currentValue = this.__updateInputValue(value); 426 }, 427 __onChangeValue: onChangeValue, 428 /* 429 * Override abstract protected methods 430 */ 431 __onKeyUp: function (event) { 432 selectItem.call(this, event, -1, true); 433 }, 434 __onKeyDown: function (event) { 435 selectItem.call(this, event, 1, true); 436 }, 437 __onPageUp: function (event) { 438 selectItem.call(this, event, getPageFirstItem.call(this)); 439 }, 440 __onPageDown: function (event) { 441 selectItem.call(this, event, getPageLastItem.call(this)); 442 }, 443 __onKeyHome: function (event) { 444 selectItem.call(this, event, 0); 445 }, 446 __onKeyEnd: function (event) { 447 selectItem.call(this, event, this.items.length - 1); 448 }, 449 __onBeforeShow: function (event) { 450 }, 451 __onEnter: function (event) { 452 var value = getSelectedItemValue.call(this); 453 this.__onChangeValue(event, value); 454 this.invokeEvent("selectitem", rf.getDomElement(this.fieldId), event, value); 455 }, 456 __onShow: function (event) { 457 if (this.options.selectFirst) { 458 selectItem.call(this, event, 0); 459 } 460 }, 461 __onHide: function (event) { 462 selectItem.call(this, event); 463 }, 464 /* 465 * Destructor 466 */ 467 destroy: function () { 468 //TODO: add all unbind 469 this.items = null; 470 this.cache = null; 471 var itemsContainer = rf.getDomElement(this.id + ID.ITEMS); 472 $(itemsContainer).removeData(); 473 rf.Event.unbind(itemsContainer.parentNode, this.namespace); 474 this.__conceal(); 475 $super.destroy.call(this); 476 } 477 }; 478 })()); 479 480 $.extend(rf.ui.Autocomplete, { 481 setData: function (id, data) { 482 $(rf.getDomElement(id)).data("componentData", data); 483 }, 484 485 __getLastTokenIndex: function (tokens, value) { 486 var LAST_TOKEN_OCCURENCE = new RegExp("[" + tokens + "][^" + tokens + "]*$", "i"); 487 var AFTER_LAST_TOKEN_WITH_SPACES = new RegExp("[^" + tokens + " ]", "i"); 488 489 var value = value || ""; 490 491 var lastTokenIndex = value.search(LAST_TOKEN_OCCURENCE); 492 if (lastTokenIndex < 0) { 493 return 0; 494 } 495 var beforeToken = value.substring(lastTokenIndex); 496 var afterLastTokenIndex = beforeToken.search(AFTER_LAST_TOKEN_WITH_SPACES); 497 if (afterLastTokenIndex <= 0) { 498 afterLastTokenIndex = beforeToken.length; 499 } 500 501 return lastTokenIndex + afterLastTokenIndex; 502 } 503 }); 504 505 })(RichFaces.jQuery, RichFaces);