Upgrade to ExtJS 3.0.0 - Released 07/06/2009
[extjs.git] / examples / image-organizer / imgorg / MultiCombo.js
1 /*!
2  * Ext JS Library 3.0.0
3  * Copyright(c) 2006-2009 Ext JS, LLC
4  * licensing@extjs.com
5  * http://www.extjs.com/license
6  */
7 Ext.ns('Ext.ux');
8
9 /**
10  * Ext.ux.MultiCombo
11  */
12 Ext.ux.MultiCombo = Ext.extend(Ext.form.ComboBox, {
13
14         /**
15          * @cfg {String} overClass [x-grid3-row-over]
16          */
17         overClass : 'x-grid3-row-over',
18         /**
19          * @cfg {Boolean} enableKeyEvents for typeAhead
20          */
21         enableKeyEvents: true,
22         /**
23          * @cfg {String} selectedClass [x-grid3-row-selected]
24          */
25         selectedClass: 'x-grid3-row-selected',
26         /**
27          * @cfg {String} highlightClass The css class applied to rows which are hovered with mouse
28          * selected via key-nav, or highlighted when a text-query matches a single item.
29          */
30         highlightClass: 'x-grid3-row-over',
31         /**
32          * @cfg {Number} autoSelectKey [44] COMMA Sets the key used to auto-select an auto-suggest
33          * highlighted query.  When pressed, the highlighted text-item will be selected as if the user
34          * selected the row with a mouse click.
35          */
36         autoSelectKey : 44,
37         /**
38          * @cfg {String} allSelectedText Text to display when all items are selected
39          */
40         allSelectedText : 'All selected',
41         /**
42          * @cfg {Number} maxDisplayRows The maximum number of rows to show before applying vscroll
43          */
44         maxDisplayRows: null,
45
46         mode: 'local',
47         triggerAction: 'all',
48         typeAhead: true,
49
50         // private
51         highlightIndex : null,
52         highlightIndexPrev : null,
53
54         query : null,
55
56
57         /**
58          * @cfg {Array} value CheckboxCombo expresses its value as an array.
59          */
60         value: [],
61
62         /**
63          * @cfg {Integer} minChars [0]
64          */
65         minChars: 0,
66
67         initComponent : function() {
68                 var cls = 'x-combo-list';
69
70                 // when blurring out of field, ensure that rawValue contains ONLY items contained in Store.
71                 this.on('blur', this.validateSelections.createDelegate(this));
72
73                 // create an auto-select key handler, like *nix-based console [tab] key behaviour
74                 this.on('keypress', function(field, ev) {
75                         if (ev.getKey() == this.autoSelectKey) {        // COMMA
76                                 this.onAutoSelect();
77                         }
78                 },this);
79
80                 this.addEvents(
81                         /**
82                          * @event initview Fires when Combo#initView is called.
83                          * gives plugins a chance to interact with DataView
84                          * @author Chris Scott
85                          * @param {Combo} this
86                          * @param {DataView} dv
87                          */
88                         'initview',
89             'clearall'
90                 );
91
92                 // when list expands, constrain the height with @cfg maxDisplayRows
93                 if (this.maxDisplayRows) {
94                         this.on('expand', function(){
95                                 var cnt = this.store.getCount();
96                                 if (cnt > this.maxDisplayRows) {
97                                         var children = this.view.getNodes();
98                                         var h = 0;
99                                         for (var n = 0; n < this.maxDisplayRows; n++) {
100                                                 h += Ext.fly(children[n]).getHeight();
101                                         }
102                                         this.maxHeight = h;
103                                 }
104                         }, this, {
105                                 single: true
106                         });
107                 }
108
109                 this.on('beforequery', this.onQuery, this);
110
111                 // Enforce that plugins is an Array.
112                 if (typeof(this.plugins) == 'undefined'){
113                         this.plugins = [];
114                 }
115                 else if (!Ext.isArray(this.plugins)) {
116                         this.plugins = [this.plugins];
117                 }
118
119                 var tmp = this.value;   // for case where transform is set.
120                 Ext.ux.MultiCombo.superclass.initComponent.call(this);
121                 if (this.transform) {
122                         if (typeof(tmp) == 'undefined') {
123                                 tmp = [];
124                         }
125                         this.setValue(tmp);
126                 }
127         },
128
129         // private
130     onViewClick : function(dv, index, node, ev){
131                 var rec = this.store.getAt(index);
132                 this.onSelect(rec, index);
133                 this.el.focus();
134                 /*
135         if(doFocus !== false){
136             this.el.focus();
137         }
138         */
139     },
140
141         // onTriggerClick, overrides Ext.form.ComboBox#onTriggerClick
142         onTriggerClick: function() {
143                 if (this.highlightIndex != -1) {
144                         this.clearHighlight();
145                 }
146                 this.highlightIndex = -1;
147
148                 if(this.disabled){
149             return;
150         }
151                 if(this.isExpanded()){
152             this.collapse();
153             this.el.focus();
154         }else {
155             this.onFocus({});
156                         if(this.triggerAction == 'all') {
157                                 this.doQuery(this.getRawValue(), true);
158                                 var vlen = this.getValue().length, slen = this.view.getSelectedRecords().length;
159                                 if (vlen != slen || vlen == 0) {
160                                         this.selectByValue(this.value, true);
161                                 }
162             } else {
163                 this.expand();
164                                 this.doQuery(this.getRawValue());
165             }
166
167                         this.highlightIndex = -1
168                         this.highlightIndexPrev = null;
169                         this.selectNext();
170                         this.scrollIntoView();
171             this.el.focus();
172         }
173         },
174
175         // onQuery, beforequery listener, @return false
176         onQuery : function(qe) {
177                 q = qe.query;
178         forceAll = qe.forceAll;
179         if(forceAll === true || (q.length >= this.minChars)){
180             if(this.lastQuery !== q){
181                                 if (typeof(this.lastQuery) != 'undefined') {
182                                         if (q.match(new RegExp('^'+this.allSelectedText))) {
183                                                 this.query = this.store.data;
184                                         }
185                                         else if (this.lastQuery.length > q.length) {
186                                                 var items = q.replace(/\s+/g, '').split(',');
187                                                 if (items[items.length-1].length == 0) {
188                                                         items.pop();
189                                                 }
190                                                 this.query = this.store.data.filterBy(this.store.createFilterFn(this.displayField, new RegExp('^'+items.join('$|^')+'$', "i"), false, false));
191                                         }
192                                         else {
193                                                 this.query = null;
194                                         }
195                                 }
196                 this.lastQuery = q;
197                 if(this.mode == 'local'){
198                                         var raw = this.getRawValue();
199                                         if (raw == this.allSelectedText) {
200
201                                         }
202                                         var items = raw.replace(/\s+/g, '').split(',');
203                                         var last = items.pop();
204                                         this.matches = this.store.data.filterBy(this.store.createFilterFn(this.displayField, new RegExp('^'+last, "i"), false, false)).filterBy(this.createTypeAheadFilterFn(items));
205                                         if (this.matches.getCount() == 0) {
206                                                 this.clearHighlight();
207                                         }
208                                         if (q.length == 0) {
209                                                 this.view.clearSelections();
210                                                 this.updateValue([]);
211                                         }
212
213                     this.onLoad();
214                 } else {
215                     this.store.baseParams[this.queryParam] = q;
216                     this.store.load({
217                         params: this.getParams(q)
218                     });
219                     this.expand();
220                 }
221             }else{
222                 this.selectedIndex = -1;
223                 this.onLoad();
224             }
225         }
226
227                 return false;
228         },
229
230         // onLoad, overrides Ext.form.ComboBox#onLoad
231         onLoad : function(){
232
233         if(!this.hasFocus){
234             return;
235         }
236         if(this.store.getCount() > 0){
237             if (!this.isExpanded()) {
238                                 this.expand();
239                                 this.restrictHeight();
240                         }
241             if(this.lastQuery == this.allQuery){
242                 if(this.editable){
243                     this.el.dom.select();
244                 }
245             }else{
246                                 if (this.query != null) {
247                                         var values = [], indexes = [];
248                                         this.query.each(function(r){
249                                                 values.push(r.data[this.valueField]);
250                                                 indexes.push(this.store.indexOf(r));
251                                         }, this);
252                                         this.view.clearSelections();
253                                         this.updateValue(values, this.getRawValue());
254                                         this.view.select(indexes);
255                                 }
256                                 if (this.matches != null) {
257                                         if (this.matches.getCount() == 1) {
258                                                 this.highlight(this.store.indexOf(this.matches.first()));
259                                                 this.scrollIntoView();
260                                         }
261                                 }
262                                 else {
263                                         // @HACK: If store was configured with a proxy, set its mode to local now that its populated with data.
264                                         // Re-execute the query now.
265                                         this.mode = 'local';
266                                         this.lastQuery = undefined;
267                                         this.doQuery(this.getRawValue(), true);
268                                 }
269                 if(this.typeAhead && this.lastKey != Ext.EventObject.DOWN && this.lastKey != Ext.EventObject.BACKSPACE && this.lastKey != Ext.EventObject.DELETE){
270                                         this.taTask.delay(this.typeAheadDelay);
271                 }
272             }
273         }else{
274             this.onEmptyResults();
275         }
276     },
277
278         onSelect : function(record, index) {
279                 if (index == -1) {
280                         throw new Error('MultiCombo#onSelect did not receive a valid index');
281                 }
282
283                 // select only when user clicks [apply] button
284                 if (this.selectOnApply == true) {
285                         return;
286                 }
287
288                 if (this.fireEvent('beforeselect', this, record, index) !== false) {
289                         var text = [];
290                         var value = [];
291                         var rs = this.view.getSelectedRecords();
292                         for (var n = 0, len = rs.length; n < len; n++) {
293                                 text.push(rs[n].data[this.displayField]);
294                                 value.push(rs[n].data[this.valueField]);
295                         }
296                         this.updateValue(value, (value.length != this.store.getCount()) ? text.join(', ') : this.allSelectedText);
297                         var node = this.view.getNode(index);
298                         this.innerList.scrollChildIntoView(node, false);
299                         this.fireEvent('select', this, record, index);
300                 }
301         },
302
303         // private
304     onViewOver : function(ev, node){
305                 var t = ev.getTarget(this.view.itemSelector);
306                 if (t == null) {
307                         return;
308                 }
309                 this.highlightIndex = this.store.indexOf(this.view.getRecord(t));
310                 this.clearHighlight();
311                 this.highlight(this.highlightIndex);
312         if(this.inKeyMode){ // prevent key nav and mouse over conflicts
313             return;null
314         }
315         return;
316     },
317
318         // private
319     onTypeAhead : function(){
320                 if(this.store.getCount() > 0){
321                         this.inKeyMode = false;
322             var raw = this.getRawValue();
323                         var pos = this.getCaretPosition(raw);
324                         var items = [];
325                         var query = '';
326                         if (pos !== false && pos < raw.length) {
327                                 items = raw.substr(0, pos).replace(/\s+/g, '').split(',');
328                                 query = items.pop();
329                         } else {
330                                 items = raw.replace(/\s+/g, '').split(',');
331                                 query = items.pop();
332                         }
333                         var rs = this.store.data.filterBy(this.store.createFilterFn(this.displayField, new RegExp(query, "i"), false, false)).filterBy(this.createTypeAheadFilterFn(items));
334
335                         if (rs.getCount() == 1) {
336                                 var r = rs.first();
337                                 var rindex = this.store.indexOf(r)
338                                 if (!this.view.isSelected(rindex)) {
339                             this.typeAheadSelected = true;
340                                         var selStart = raw.length;
341                                         var len = items.join(',').length;
342                                         var selEnd = null;
343                                         var newValue = r.data[this.displayField];
344                                         if (pos !== false && pos < raw.length) {
345                                                 var insertIdx = items.length;
346                                                 var selStart = pos;
347                                                 items = raw.replace(/\s+/g, '').split(',');
348                                                 items.splice(insertIdx, 1, newValue);
349                                                 selEnd = items.slice(0, insertIdx+1).join(', ').length;
350                                                 this.highlight(rindex);
351                                                 this.scrollIntoView();
352
353                                         }
354                                         else {
355                                                 items.push(newValue);
356                                         }
357                                         var len = items.join(',').length;
358                             if(selStart != len){
359                                                 var lastWord = raw.split(',').pop();
360                                                 if (items.length >1 && lastWord.match(/^\s+/) == null) {
361                                                         selStart++;
362                                                 }
363                                                 this.setRawValue(items.join(', '));
364                                 this.selectText(selStart, (selEnd!=null) ? selEnd : this.getRawValue().length);
365                             }
366                                 }
367                         }
368         }
369     },
370
371         apply : function() {
372                 var selected =  this.view.getSelectedRecords();
373                 var value = [];
374                 for (var n=0,len=selected.length;n<len;n++) {
375                         value.push(selected[n].data[this.valueField]);
376                 }
377                 this.setValue(value);
378         },
379
380         getCaretPosition : function(raw) {
381                 raw = raw || this.getRawValue();
382                 if(document.selection) {        // <-- IE, ugh:  http://parentnode.org/javascript/working-with-the-cursor-position/
383                 var range = document.selection.createRange();
384                         //Save the current value. We will need this value later to find out, where the text has been changed
385                         var orig = obj.value.replace(/rn/g, "n");
386                         // replace the text
387                         range.text = text;
388                         // Now get the new content and save it into a temporary variable
389                         var actual = tmp = obj.value.replace(/rn/g, "n");
390                         /* Find the first occurance, where the original differs
391                            from the actual content. This could be the startposition
392                            of our text selection, but it has not to be. Think of the
393                            selection "ab" and replacing it with "ac". The first
394                            difference would be the "c", while the start position
395                            is the "a"
396                         */
397                         for(var diff = 0; diff < orig.length; diff++) {
398                             if(orig.charAt(diff) != actual.charAt(diff)) break;
399                         }
400
401                         /* To get the real start position, we iterate through
402                            the string searching for the whole replacement
403                            text - "abc", as long as the first difference is not
404                            reached. If you do not understand that logic - no
405                            blame to you, just copy & paste it ;)
406                         */
407                         for(var index = 0, start = 0; tmp.match(text) && (tmp = tmp.replace(text, "")) && index <= diff; index = start + text.length) {
408                             start = actual.indexOf(text, index);
409                         }
410             } else if(this.el.dom.selectionStart) {     // <-- Go the Gecko way
411                         return this.el.dom.selectionStart;
412             } else {
413                 // Fallback for any other browser
414                         return false;
415             }
416         },
417
418         onAutoSelect : function() {
419                 if (!this.isExpanded()) {
420                         var vlen = this.getValue().length, slen = this.view.getSelectedRecords().length;
421                         if (vlen != slen || vlen == 0) {
422                                 this.selectByValue(this.value, true);
423                         }
424                 }
425                 var raw = this.getRawValue();
426                 this.selectText(raw.length, raw.length);
427
428                 var pos = this.getCaretPosition(raw);
429                 var word = '';
430                 if (pos !== false && pos < raw.length) {
431                         word = Ext.util.Format.trim(raw.substr(0, pos).split(',').pop());
432                 } else {
433                         word = Ext.util.Format.trim(raw.split(',').pop());
434                 }
435                 var idx = this.store.find(this.displayField, word);
436                 if (idx > -1 && !this.view.isSelected(idx)) {
437                         var rec = this.store.getAt(idx);
438                         this.select(idx);
439                 }
440         },
441         // filters-out already-selected items from type-ahead queries.
442         // e.g.: if store contains: "betty, barney, bart" and betty is already selected,
443         // when user types "b", only "bart" and "barney" should be returned as possible matches,
444         // since betty is *already* selected
445         createTypeAheadFilterFn : function(items) {
446                 var key = this.displayField;
447                 return function(rec) {
448                         var re = new RegExp(rec.data[key], "i");
449                         var add = true;
450                         for (var n=0,len=items.length;n<len;n++) {
451                                 if (re.test(items[n])) {
452                                         add = false;
453                                         break;
454                                 }
455                         }
456                         return add;
457                 }
458         },
459
460         updateValue : function(value, text) {
461                 this.value = value;
462                 if(this.hiddenField){
463                         this.hiddenField.value = value.join(',');
464                 }
465                 if (typeof(text) == 'string') {
466                         this.setRawValue(text);
467                 }
468
469         },
470
471         /**
472          * setValue
473          * Accepts a comma-separated list of ids or an array.  if given a string, will conver to Array.
474          * @param {Array, String} v
475          */
476         setValue : function(v) {
477                 var text = [];
478                 var value = [];
479
480                 if (typeof(v) == 'string') {    // <-- "1,2,3"
481                         value = v.match(/\d+/g); // <-- strip multiple spaces and split on ","
482             if(value){
483                             for (var n=0,len=value.length;n<len;n++) {
484                                     value[n] = parseInt(value[n]);
485                             }
486             }
487                 }
488                 else if (Ext.isArray(v)) {                      // <-- [1,2,3]
489                         value = v;
490                 }
491                 if (value && value.length) {
492                         if (this.mode == 'local') {
493                                 this.updateValue(value);
494                                 this.setRawValue(this.getTextValue());
495                         }
496                         else {
497                                 this.updateValue(value);
498                                 this.store.load({
499                                         callback: function() {
500                                                 this.setRawValue(this.getTextValue());
501                                         },
502                                         scope: this
503                                 });
504                                 this.mode = 'local';
505                         }
506                 }
507         },
508
509         getTextValue : function() {
510                 if (this.value.length == this.store.getCount()) {
511                         return this.allSelectedText;
512                 }
513                 else {
514                         var text = [];
515                         this.store.data.filterBy(this.store.createFilterFn(this.valueField, new RegExp(this.value.join('|'), "i"), false, false)).each(function(r){
516                                 text.push(r.data[this.displayField]);
517                         }, this);
518                         return text.join(', ');
519                 }
520         },
521
522         /**
523      * Select an item in the dropdown list by its numeric index in the list. This function does NOT cause the select event to fire.
524      * The store must be loaded and the list expanded for this function to work, otherwise use setValue.
525      * @param {Number} index The zero-based index of the list item to select
526      * @param {Boolean} scrollIntoView False to prevent the dropdown list from autoscrolling to display the
527      * selected item if it is not currently in view (defaults to true)
528      */
529     select : function(index, scrollIntoView){
530                 if (!typeof(index) == 'number') {
531                         throw new Error('MultiCombo#select expected @param {Number} index but got: ' + typeof(index));
532                 }
533         this.view.isSelected(index) ? this.view.deselect(index, true) : this.view.select(index, true);
534                 this.onSelect(this.store.getAt(index), index);
535
536                 this.matches = null;
537         if(scrollIntoView !== false){
538             var el = this.view.getNode(index);
539             if(el){
540                 this.innerList.scrollChildIntoView(el, false);
541             }
542         }
543
544     },
545
546         getLastValue : function() {
547                 return Ext.util.Format.trim(this.getRawValue().split(',').pop());
548         },
549
550         /**
551      * Select an item in the dropdown list by its data value. This function does NOT cause the select event to fire.
552      * The store must be loaded and the list expanded for this function to work, otherwise use setValue.
553      * @param {String} value The data value of the item to select
554      * @param {Boolean} scrollIntoView False to prevent the dropdown list from autoscrolling to display the
555      * selected item if it is not currently in view (defaults to true)
556      * @return {Boolean} True if the value matched an item in the list, else false
557      */
558     selectByValue : function(v, scrollIntoView){
559                 if (v.length) {
560                         var indexes = [];
561                         var rs = this.store.data.filterBy(this.store.createFilterFn(this.valueField, new RegExp(v.join('|'), "i"))).each(function(r){
562                                 indexes.push(this.store.indexOf(r));
563                         }, this);
564                         if (indexes.length) {
565                                 this.view.select(indexes);
566                                 return true;
567                         }
568                 }
569                 else {
570                         this.view.clearSelections();
571                         this.setRawValue('');
572                         return false;
573                 }
574     },
575
576         // private
577     initEvents : function(){
578         Ext.form.ComboBox.superclass.initEvents.call(this);
579         this.keyNav = new Ext.KeyNav(this.el, {
580             "up" : function(e){
581                                 this.lastKey = Ext.EventObject.UP;
582                 this.inKeyMode = true;
583                 this.selectPrev();
584                                 this.scrollIntoView();
585             },
586
587             "down" : function(e){
588                 this.inKeyMode = true;
589                                 if(!this.isExpanded()){
590                                         this.lastKey = Ext.EventObject.DOWN;
591                     this.onTriggerClick();
592                 }else{
593                     this.selectNext();
594                                         this.scrollIntoView();
595                 }
596
597             },
598
599             "enter" : function(e){
600                                 var idx = this.highlightIndex;
601                                 if (this.inKeyMode === true) {
602                                         if (this.plugins.length && (idx <= -1)) {
603                                                 if (this.plugins[idx + 1]) {
604                                                         this.plugins[idx + 1].onEnter(this);
605                                                 }
606                                         }
607                                         else
608                                                 if (this.plugins.length && this.highlightIndex == 0 && this.highlightIndexPrev == -1) {
609                                                         if (this.plugins[idx]) {
610                                                                 this.plugins[idx].onEnter(this);
611                                                         }
612                                                 }
613                                                 else {
614                                                         var idx = this.getHighlightedIndex() || 0;
615                                                         if (this.highlightIndex != null && idx != null) {
616                                                                 this.select(idx, true);
617                                                                 //this.delayedCheck = true;
618                                                                 //this.unsetDelayCheck.defer(10, this);
619
620                                                         }
621                                                 }
622                                 }
623                                 else {
624                                         var v = this.getLastValue();
625                                         var raw = this.getRawValue();
626
627                                         /** this block should be moved to method getCurrentWord
628                                          *
629                                          */
630                                         var pos = this.getCaretPosition(raw);
631                                         var word = '';
632                                         if (pos !== false && pos < raw.length) {
633                                                 word = Ext.util.Format.trim(raw.substr(0, pos).split(',').pop());
634                                         } else {
635                                                 word = Ext.util.Format.trim(raw.split(',').pop());
636                                         }
637                                         /*******************************************************/
638
639                                         var idx = this.store.find(this.displayField, word);
640                                         if (idx != -1) {
641                                                 var rec = this.store.getAt(idx);
642                                                 this.select(idx, true);
643                                         }
644                                         raw = this.getRawValue();
645                                         this.selectText(raw.length, raw.length);
646                                         this.collapse();
647                                 }
648             },
649
650             "esc" : function(e){
651                 this.collapse();
652             },
653
654             "tab" : function(e){
655                                 if (this.matches != null && this.matches.getCount() == 1) {
656                                         var idx = this.store.indexOf(this.matches.first());
657                                         if (!this.view.isSelected(idx)) {
658                                                 this.select(this.store.indexOf(this.matches.first()), true);
659                                         }
660                                 }
661                                 else if (this.value.length == 0 && this.getRawValue().length > 0) {
662                                         this.setRawValue('');
663                                 }
664                                 this.collapse();
665                 return true;
666             },
667
668             scope : this,
669
670             doRelay : function(foo, bar, hname){
671                 if(hname == 'down' || this.scope.isExpanded()){
672                    return Ext.KeyNav.prototype.doRelay.apply(this, arguments);
673                 }
674                 return true;
675             },
676
677             forceKeyDown : true
678         });
679         this.queryDelay = Math.max(this.queryDelay || 10,
680                 this.mode == 'local' ? 10 : 250);
681         this.dqTask = new Ext.util.DelayedTask(this.initQuery, this);
682         if(this.typeAhead){
683             this.taTask = new Ext.util.DelayedTask(this.onTypeAhead, this);
684         }
685         if(this.editable !== false){
686             this.el.on("keyup", this.onKeyUp, this);
687         }
688         if(this.forceSelection){
689             this.on('blur', this.doForce, this);
690         }
691     },
692
693         // private, blur-handler to ensure that rawValue contains only values from selections, in the same order as selected
694         validateSelections : function(field) {
695                 var v = this.getValue();
696                 var text = [];
697                 for (var i=0,len=v.length;i<len;i++) {
698                         var idx = this.store.find(this.valueField, v[i]);
699                         if (idx >=0) {
700                                 text.push(this.store.getAt(idx).data[this.displayField]);
701                         }
702                 }
703                 this.setRawValue(text.join(', '));
704         },
705
706         scrollIntoView : function() {
707                 var el = this.getHighlightedNode();
708                 if (el) {
709                         this.innerList.scrollChildIntoView(el);
710                 }
711         },
712
713         // private
714     selectNext : function(){
715                 this.clearHighlight();
716                 if (this.highlightIndex == null) {
717                         this.highlightIndex = -1;
718                 }
719                 if (this.highlightIndex <= -1 && this.highlightIndexPrev != -1) {
720                         if (this.plugins.length > 0) {
721                                 var idx = Math.abs(this.highlightIndex)-1;
722                                 if (this.plugins.length >= Math.abs(this.highlightIndex)) {
723                                         this.plugins[idx].selectNext(this);
724                                         this.highlightIndexPrev = this.highlightIndex;
725                                         this.highlightIndex++;
726                                         return false;
727                                 }
728                         }
729                 }
730                 if (this.highlightIndexPrev == -1 && this.highlightIndex == 0) {
731                         this.highlightIndex = -1;
732                 }
733                 var ct = this.store.getCount();
734                 if(ct > 0){
735             if (this.highlightIndex == -1 || this.highlightIndex+1 < ct) {
736                                 if (this.highlightIndex == -1) {
737                                         this.highlightIndexPrev = 0;
738                                 }
739                                 else {
740                                         this.highlightIndexPrev = this.highlightIndex -1;
741                                 }
742                                 this.highlight(++this.highlightIndex);
743
744                         }
745                         else {
746                                 this.highlight(ct-1);
747                         }
748         }
749     },
750
751     // private
752     selectPrev : function(){
753                 this.clearHighlight();
754                 if (this.highlightIndex <= 0) {
755                         var idx = Math.abs(this.highlightIndex);
756                         if (this.plugins.length >= idx+1 && this.highlightIndexPrev >= 0) {
757                                 this.clearHighlight();
758                                 this.plugins[idx].selectPrev(this);
759                                 this.highlightIndexPrev = this.highlightIndex;
760                                 this.highlightIndex--;
761                                 if (this.highlightIndex == -1) {
762                                         this.highlightIndexPrev = -1;
763                                 }
764                                 return false;
765                         }
766                         else {
767                                 this.highlightIndex = -1;
768                                 this.highlightIndexPrev = -1;
769                                 this.collapse();
770                                 return;
771                         }
772                 }
773
774                 this.highlightIndexPrev = this.highlightIndex;
775         var ct = this.store.getCount();
776         if(ct > 0){
777                         if (this.highlighIndex == -1) {
778                                 this.highlightIndex = 0;
779                         }
780                         else if (this.highlightIndex != 0) {
781                                 this.highlightIndex--;
782                         }
783                         else if (this.highlightIndex == 0) {
784                                 this.collapse();
785                         }
786                         this.highlight(this.highlightIndex);
787         }
788     },
789
790         collapse : function() {
791                 if (this.isExpanded()) {
792                         this.highlightIndex = null;
793                         this.highlightIndexPrev = null;
794                 }
795                 Ext.ux.MultiCombo.superclass.collapse.call(this);
796         },
797
798         highlight : function(index) {
799                 this.view.el.select('.'+this.highlightClass).removeClass(this.highlightClass);
800                 var node = Ext.fly(this.view.getNode(index));
801                 if (node) {
802                         node.addClass(this.highlightClass);
803                 }
804         },
805
806         getHighlightedIndex : function() {
807                 var node = this.view.el.child('.' + this.highlightClass, true);
808                 return (node) ? this.store.indexOf(this.view.getRecord(node)) : this.highlightIndex;
809         },
810         getHighlightedNode : function() {
811                 return this.view.el.child('.'+this.highlightClass, true);
812         },
813
814         clearHighlight : function() {
815                 if (typeof(this.view) != 'object') { return false; }
816                 var el = this.view.el.select('.'+this.highlightClass);
817                 if (el) {
818                         el.removeClass(this.highlightClass);
819                 }
820         },
821
822     // private
823     initList : function(){
824         if(!this.list){
825             var cls = 'x-combo-list';
826
827             this.list = new Ext.Layer({
828                 shadow: this.shadow, cls: [cls, this.listClass].join(' '), constrain:false
829             });
830
831             var lw = this.listWidth || Math.max(this.wrap.getWidth(), this.minListWidth);
832             this.list.setWidth(lw);
833             this.list.swallowEvent('mousewheel');
834             this.assetHeight = 0;
835             if(this.syncFont !== false){
836                 this.list.setStyle('font-size', this.el.getStyle('font-size'));
837             }
838             if(this.title){
839                 this.header = this.list.createChild({cls:cls+'-hd', html: this.title});
840                 this.assetHeight += this.header.getHeight();
841             }
842
843             this.innerList = this.list.createChild({cls:cls+'-inner'});
844             this.innerList.on('mouseover', this.onViewOver, this);
845             this.innerList.on('mousemove', this.onViewMove, this);
846             this.innerList.setWidth(lw - this.list.getFrameWidth('lr'));
847
848             if(this.pageSize){
849                 this.footer = this.list.createChild({cls:cls+'-ft'});
850                 this.pageTb = new Ext.PagingToolbar({
851                     store:this.store,
852                     pageSize: this.pageSize,
853                     renderTo:this.footer
854                 });
855                 this.assetHeight += this.footer.getHeight();
856             }
857
858             if(!this.tpl){
859                 /**
860                 * @cfg {String/Ext.XTemplate} tpl The template string, or {@link Ext.XTemplate}
861                 * instance to use to display each item in the dropdown list. Use
862                 * this to create custom UI layouts for items in the list.
863                 * <p>
864                 * If you wish to preserve the default visual look of list items, add the CSS
865                 * class name <pre>x-combo-list-item</pre> to the template's container element.
866                 * <p>
867                 * <b>The template must contain one or more substitution parameters using field
868                 * names from the Combo's</b> {@link #store Store}. An example of a custom template
869                 * would be adding an <pre>ext:qtip</pre> attribute which might display other fields
870                 * from the Store.
871                 * <p>
872                 * The dropdown list is displayed in a DataView. See {@link Ext.DataView} for details.
873                 */
874                 this.tpl = '<tpl for="."><div class="'+cls+'-item">{' + this.displayField + '}</div></tpl>';
875                 /**
876                  * @cfg {String} itemSelector
877                  * <b>This setting is required if a custom XTemplate has been specified in {@link #tpl}
878                  * which assigns a class other than <pre>'x-combo-list-item'</pre> to dropdown list items</b>.
879                  * A simple CSS selector (e.g. div.some-class or span:first-child) that will be
880                  * used to determine what nodes the DataView which handles the dropdown display will
881                  * be working with.
882                  */
883             }
884
885             /**
886             * The {@link Ext.DataView DataView} used to display the ComboBox's options.
887             * @type Ext.DataView
888             */
889             this.view = new Ext.DataView({
890                 applyTo: this.innerList,
891                 tpl: this.tpl,
892                                 simpleSelect: true,
893                 multiSelect: true,
894                                 overClass: this.overClass,
895                 selectedClass: this.selectedClass,
896                 itemSelector: this.itemSelector || '.' + cls + '-item'
897             });
898             this.view.on('click', this.onViewClick, this);
899                         this.fireEvent('initview', this, this.view);
900             this.bindStore(this.store, true);
901
902             if(this.resizable){
903                 this.resizer = new Ext.Resizable(this.list,  {
904                    pinned:true, handles:'se'
905                 });
906                 this.resizer.on('resize', function(r, w, h){
907                     this.maxHeight = h-this.handleHeight-this.list.getFrameWidth('tb')-this.assetHeight;
908                     this.listWidth = w;
909                     this.innerList.setWidth(w - this.list.getFrameWidth('lr'));
910                     this.restrictHeight();
911                 }, this);
912                 this[this.pageSize?'footer':'innerList'].setStyle('margin-bottom', this.handleHeight+'px');
913             }
914         }
915     }
916 });
917
918
919 Ext.reg('multicombo', Ext.ux.MultiCombo);