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