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