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);