PDA

View Full Version : MultiSelect for Combo



dsonet
26 Mar 2009, 8:29 PM
It's will works only need you set the config "multiSelect" to true,try it!
now, introduced the config "checkedClass",so that you can control the style different from others when the item selected.
Here is the code.
(No extend, only do a small change in original ComboBox, so you can get the best performance)


/*
* Ext JS Library 2.2
* Copyright(c) 2006-2008, Ext JS, LLC.
* licensing@extjs.com
*
* http://extjs.com/license
*/
/**
* @class Ext.form.ComboBox
* @extends Ext.form.TriggerField
* A combobox control with support for autocomplete, remote-loading, paging and many other features.
* @constructor
* Create a new ComboBox.
* @param {Object} config Configuration options
*/
var ComboBox = Ext.form.ComboBox = Ext.extend(Ext.form.TriggerField, {
/**
* @cfg {Mixed} transform The id, DOM node or element of an existing HTML SELECT to convert to a ComboBox.
* Note that if you specify this and the combo is going to be in a {@link Ext.form.BasicForm} or
* {@link Ext.form.FormPanel}, you must also set {@link #lazyRender} = true.
*/
/**
* @cfg {Boolean} lazyRender True to prevent the ComboBox from rendering until requested (should always be used when
* rendering into an Ext.Editor, defaults to false).
*/
/**
* @cfg {Boolean/Object} autoCreate A DomHelper element spec, or true for a default element spec (defaults to:
* {tag: "input", type: "text", size: "24", autocomplete: "off"})
*/
/**
* @cfg {Ext.data.Store/Array} store The data source to which this combo is bound (defaults to undefined). This can be
* any {@link Ext.data.Store} subclass, a 1-dimensional array (e.g., ['Foo','Bar']) or a 2-dimensional array (e.g.,
* [['f','Foo'],['b','Bar']]). Arrays will be converted to a {@link Ext.data.SimpleStore} internally.
* 1-dimensional arrays will automatically be expanded (each array item will be the combo value and text) and
* for multi-dimensional arrays, the value in index 0 of each item will be assumed to be the combo value, while
* the value at index 1 is assumed to be the combo text.
*/
/**
* @cfg {String} title If supplied, a header element is created containing this text and added into the top of
* the dropdown list (defaults to undefined, with no header element)
*/
// private
defaultAutoCreate : {tag: "input", type: "text", size: "24", autocomplete: "off"},
/**
* @cfg {Number} listWidth The minWidth in pixels of the dropdown list (defaults to the width of the ComboBox field)
*/
/**
* @cfg {String} multiSelect whether can select multi values (defaults to undefined[false])
*/
/**
* @cfg {String} checkField Name of field used to store checked state.
* It is automatically added to existing fields.
* (defaults to "checked" - change it only if it collides with your normal field)
*/
checkField:'checked',
/**
* @cfg {String} separator Separator to use between values and texts (defaults to "," (comma))
*/
separator:',',
/**
* @cfg {String} displayField The underlying data field name to bind to this ComboBox (defaults to undefined if
* mode = 'remote' or 'text' if transforming a select)
*/
/**
* @cfg {String} valueField The underlying data value name to bind to this ComboBox (defaults to undefined if
* mode = 'remote' or 'value' if transforming a select) Note: use of a valueField requires the user to make a selection
* in order for a value to be mapped.
*/
/**
* @cfg {String} hiddenName If specified, a hidden form field with this name is dynamically generated to store the
* field's data value (defaults to the underlying DOM element's name). Required for the combo's value to automatically
* post during a form submission. Note that the hidden field's id will also default to this name if {@link #hiddenId}
* is not specified. The combo's id and the hidden field's ids should be different, since no two DOM nodes should
* share the same id, so if the combo and hidden names are the same, you should specify a unique hiddenId.
*/
/**
* @cfg {String} hiddenId If {@link #hiddenName} is specified, hiddenId can also be provided to give the hidden field
* a unique id (defaults to the hiddenName). The hiddenId and combo {@link #id} should be different, since no two DOM
* nodes should share the same id.
*/
/**
* @cfg {String} listClass CSS class to apply to the dropdown list element (defaults to '')
*/
listClass: '',
/**
* @cfg {String} checkedClass CSS class to apply to the checked item in the dropdown list (defaults to 'x-combo-checked')
*/
checkedClass: 'x-combo-checked',
/**
* @cfg {String} selectedClass CSS class to apply to the selected item in the dropdown list (defaults to 'x-combo-selected')
*/
selectedClass: 'x-combo-selected',
/**
* @cfg {String} triggerClass An additional CSS class used to style the trigger button. The trigger will always get the
* class 'x-form-trigger' and triggerClass will be <b>appended</b> if specified (defaults to 'x-form-arrow-trigger'
* which displays a downward arrow icon).
*/
triggerClass : 'x-form-arrow-trigger',
/**
* @cfg {Boolean/String} shadow True or "sides" for the default effect, "frame" for 4-way shadow, and "drop" for bottom-right
*/
shadow:'sides',
/**
* @cfg {String} listAlign A valid anchor position value. See {@link Ext.Element#alignTo} for details on supported
* anchor positions (defaults to 'tl-bl')
*/
listAlign: 'tl-bl?',
/**
* @cfg {Number} maxHeight The maximum height in pixels of the dropdown list before scrollbars are shown (defaults to 300)
*/
maxHeight: 300,
/**
* @cfg {Number} minHeight The minimum height in pixels of the dropdown list when the list is constrained by its
* distance to the viewport edges (defaults to 90)
*/
minHeight: 90,
/**
* @cfg {String} triggerAction The action to execute when the trigger field is activated. Use 'all' to run the
* query specified by the allQuery config option (defaults to 'query')
*/
triggerAction: 'query',
/**
* @cfg {Number} minChars The minimum number of characters the user must type before autocomplete and typeahead activate
* (defaults to 4 if remote or 0 if local, does not apply if editable = false)
*/
minChars : 4,
/**
* @cfg {Boolean} typeAhead True to populate and autoselect the remainder of the text being typed after a configurable
* delay ({@link #typeAheadDelay}) if it matches a known value (defaults to false)
*/
typeAhead: false,
/**
* @cfg {Number} queryDelay The length of time in milliseconds to delay between the start of typing and sending the
* query to filter the dropdown list (defaults to 500 if mode = 'remote' or 10 if mode = 'local')
*/
queryDelay: 500,
/**
* @cfg {Number} pageSize If greater than 0, a paging toolbar is displayed in the footer of the dropdown list and the
* filter queries will execute with page start and limit parameters. Only applies when mode = 'remote' (defaults to 0)
*/
pageSize: 0,
/**
* @cfg {Boolean} selectOnFocus True to select any existing text in the field immediately on focus. Only applies
* when editable = true (defaults to false)
*/
selectOnFocus:false,
/**
* @cfg {String} queryParam Name of the query as it will be passed on the querystring (defaults to 'query')
*/
queryParam: 'query',
/**
* @cfg {String} loadingText The text to display in the dropdown list while data is loading. Only applies
* when mode = 'remote' (defaults to 'Loading...')
*/
loadingText: 'Loading...',
/**
* @cfg {Boolean} resizable True to add a resize handle to the bottom of the dropdown list (defaults to false)
*/
resizable: false,
/**
* @cfg {Number} handleHeight The height in pixels of the dropdown list resize handle if resizable = true (defaults to 8)
*/
handleHeight : 8,
/**
* @cfg {Boolean} editable False to prevent the user from typing text directly into the field, just like a
* traditional select (defaults to true)
*/
editable: true,
/**
* @cfg {String} allQuery The text query to send to the server to return all records for the list with no filtering (defaults to '')
*/
allQuery: '',
/**
* @cfg {String} mode Set to 'local' if the ComboBox loads local data (defaults to 'remote' which loads from the server)
*/
mode: 'remote',
/**
* @cfg {Number} minListWidth The minimum width of the dropdown list in pixels (defaults to 70, will be ignored if
* listWidth has a higher value)
*/
minListWidth : 70,
/**
* @cfg {Boolean} forceSelection True to restrict the selected value to one of the values in the list, false to
* allow the user to set arbitrary text into the field (defaults to false)
*/
forceSelection:false,
/**
* @cfg {Number} typeAheadDelay The length of time in milliseconds to wait until the typeahead text is displayed
* if typeAhead = true (defaults to 250)
*/
typeAheadDelay : 250,
/**
* @cfg {String} valueNotFoundText When using a name/value combo, if the value passed to setValue is not found in
* the store, valueNotFoundText will be displayed as the field text if defined (defaults to undefined). If this
* defaut text is used, it means there is no value set and no validation will occur on this field.
*/
/**
* @cfg {Boolean} lazyInit True to not initialize the list for this combo until the field is focused (defaults to true)
*/
lazyInit : true,
// private
initComponent : function(){
ComboBoxSuperCls.initComponent.call(this);
//this.addEvents(
/**
* @event expand
* Fires when the dropdown list is expanded
* @param {Ext.form.ComboBox} combo This combo box
*/
//'expand',
/**
* @event collapse
* Fires when the dropdown list is collapsed
* @param {Ext.form.ComboBox} combo This combo box
*/
//'collapse',
/**
* @event beforeselect
* Fires before a list item is selected. Return false to cancel the selection.
* @param {Ext.form.ComboBox} combo This combo box
* @param {Ext.data.Record} record The data record returned from the underlying store
* @param {Number} index The index of the selected item in the dropdown list
*/
//'beforeselect',
/**
* @event select
* Fires when a list item is selected
* @param {Ext.form.ComboBox} combo This combo box
* @param {Ext.data.Record} record The data record returned from the underlying store
* @param {Number} index The index of the selected item in the dropdown list
*/
//'select',
/**
* @event beforequery
* Fires before all queries are processed. Return false to cancel the query or set the queryEvent's
* cancel property to true.
* @param {Object} queryEvent An object that has these properties:<ul>
* <li><code>combo</code> : Ext.form.ComboBox <div class="sub-desc">This combo box</div></li>
* <li><code>query</code> : String <div class="sub-desc">The query</div></li>
* <li><code>forceAll</code> : Boolean <div class="sub-desc">True to force "all" query</div></li>
* <li><code>cancel</code> : Boolean <div class="sub-desc">Set to true to cancel the query</div></li>
* </ul>
*/
//'beforequery'
//);
if(this.transform){
this.allowDomMove = false;
var s = Ext.getDom(this.transform);
if(!this.hiddenName){
this.hiddenName = s.name;
}
if(!this.store){
this.mode = 'local';
var d = [], opts = s.options;
for(var i = 0, len = opts.length;i < len; i++){
var o = opts[i];
var value = (Ext.isIE ? o.getAttributeNode('value').specified : o.hasAttribute('value')) ? o.value : o.text;
if(o.selected) {
this.value = value;
}
d.push([value, o.text]);
}
this.store = new Ext.data.SimpleStore({
'id': 0,
fields: ['value', 'text'],
data : d
});
this.valueField = 'value';
this.displayField = 'text';
}
s.name = Ext.id(); // wipe out the name in case somewhere else they have a reference
if(!this.lazyRender){
this.target = true;
this.el = Ext.DomHelper.insertBefore(s, this.autoCreate || this.defaultAutoCreate);
Ext.removeNode(s); // remove it
this.render(this.el.parentNode);
}else{
Ext.removeNode(s); // remove it
}
}
//auto-configure store from local array data
else if(Ext.isArray(this.store)){
if (Ext.isArray(this.store[0])){
this.store = new Ext.data.SimpleStore({
fields: ['value','text'],
data: this.store
});
this.valueField = 'value';
}else{
this.store = new Ext.data.SimpleStore({
fields: ['text'],
data: this.store,
expandData: true
});
this.valueField = 'text';
}
this.displayField = 'text';
this.mode = 'local';
}
this.selectedIndex = -1;
if(this.mode == 'local'){
if(this.initialConfig.queryDelay === undefined){
this.queryDelay = 10;
}
if(this.initialConfig.minChars === undefined){
this.minChars = 0;
}
}
},
// private
onRender : function(ct, position){
ComboBoxSuperCls.onRender.call(this, ct, position);
if(this.hiddenName){
this.hiddenField = this.el.insertSibling({tag:'input', type:'hidden', name: this.hiddenName,
id: (this.hiddenId||this.hiddenName)}, 'before', true);
// prevent input submission
this.el.dom.removeAttribute('name');
}
if(Ext.isGecko){
this.el.dom.setAttribute('autocomplete', 'off');
}
if(!this.lazyInit){
this.initList();
}else{
this.on('focus', this.initList, this, {single: true});
}
if(!this.editable){
this.editable = true;
this.setEditable(false);
}
},
// private
initValue : function(){
ComboBoxSuperCls.initValue.call(this);
if(this.hiddenField){
this.hiddenField.value =
this.hiddenValue !== undefined ? this.hiddenValue :
this.value !== undefined ? this.value : '';
}
},
// private
initList : function(){
if(!this.list){
var cls = 'x-combo-list';
this.list = new Ext.Layer({
shadow: this.shadow, cls: [cls, this.listClass].join(' '), constrain:false
});
var lw = this.listWidth || Math.max(this.wrap.getWidth(), this.minListWidth);
//this.list.setWidth(lw);
this.list.setSize(lw, 0);
this.list.swallowEvent('mousewheel');
this.assetHeight = 0;
if(this.title){
this.header = this.list.createChild({cls:cls+'-hd', html: this.title});
this.assetHeight += this.header.getHeight();
}
this.innerList = this.list.createChild({cls:cls+'-inner'});
this.innerList.on({
'mouseover': this.onViewOver,
'mousemove': this.onViewMove,
scope: this
});
this.innerList.setWidth(lw - this.list.getFrameWidth('lr'));
if(this.pageSize){
this.footer = this.list.createChild({cls:cls+'-ft'});
this.pageTb = new Ext.PagingToolbar({
store:this.store,
pageSize: this.pageSize,
renderTo:this.footer
});
this.assetHeight += this.footer.getHeight();
}
if(!this.tpl){
/**
* @cfg {String/Ext.XTemplate} tpl The template string, or {@link Ext.XTemplate}
* instance to use to display each item in the dropdown list. Use
* this to create custom UI layouts for items in the list.
* <p>
* If you wish to preserve the default visual look of list items, add the CSS
* class name <pre>x-combo-list-item</pre> to the template's container element.
* <p>
* <b>The template must contain one or more substitution parameters using field
* names from the Combo's</b> {@link #store Store}. An example of a custom template
* would be adding an <pre>ext:qtip</pre> attribute which might display other fields
* from the Store.
* <p>
* The dropdown list is displayed in a DataView. See {@link Ext.DataView} for details.
*/
this.tpl = '<tpl for="."><div class="'+cls+'-item'+ (this.multiSelect ? '{[values.'+ this.checkField +'?" '+ this.checkedClass +'":""]}' : "") +'">'+ (this.multiSelect ? '<img src="' + Ext.BLANK_IMAGE_URL + '" class="x-form-check{[values.'+ this.checkField +'?" x-form-check-checked":""]}" />': "") +'{' + this.displayField + '}</div></tpl>';
/**
* @cfg {String} itemSelector
* <b>This setting is required if a custom XTemplate has been specified in {@link #tpl}
* which assigns a class other than <pre>'x-combo-list-item'</pre> to dropdown list items</b>.
* A simple CSS selector (e.g. div.some-class or span:first-child) that will be
* used to determine what nodes the DataView which handles the dropdown display will
* be working with.
*/
}
/**
* The {@link Ext.DataView DataView} used to display the ComboBox's options.
* @type Ext.DataView
*/
this.view = new Ext.DataView({
applyTo: this.innerList,
tpl: this.tpl,
singleSelect: true,
selectedClass: this.selectedClass,
itemSelector: this.itemSelector || '.' + cls + '-item'
});
this.view.on('click', this.onViewClick, this);
this.bindStore(this.store, true);
if(this.resizable){
this.resizer = new Ext.Resizable(this.list, {
pinned:true, handles:'se'
});
this.resizer.on('resize', function(r, w, h){
this.maxHeight = h-this.handleHeight-this.list.getFrameWidth('tb')-this.assetHeight;
this.listWidth = w;
this.innerList.setWidth(w - this.list.getFrameWidth('lr'));
this.restrictHeight();
}, this);
this[this.pageSize?'footer':'innerList'].setStyle('margin-bottom', this.handleHeight+'px');
}
}
},
/**
* Returns the store associated with this combo.
* @return {Ext.data.Store} The store
*/
getStore : function(){
return this.store;
},
// private
bindStore : function(store, initial){
var dsLs = {
'beforeload': this.onBeforeLoad,
'load': this.onLoad,
'loadexception': this.collapse,
scope: this
};
if(this.store && !initial){
this.store.un(dsLs);
if(!store){
this.store = null;
if(this.view){
this.view.setStore(null);
}
}
}
if(store){
this.store = Ext.StoreMgr.lookup(store);
this.store.on(dsLs);
if(this.view){
this.view.setStore(store);
}
}
},
// private
initEvents : function(){
ComboBoxSuperCls.initEvents.call(this);
this.keyNav = new Ext.KeyNav(this.el, {
"up" : function(e){
this.inKeyMode = true;
this.selectPrev();
},
"down" : function(e){
if(!this.isExpanded()){
this.onTriggerClick();
}else{
this.inKeyMode = true;
this.selectNext();
}
},
"enter" : function(e){
this.onViewClick();
this.delayedCheck = true;
this.unsetDelayCheck.defer(10, this);
},
"esc" : function(e){
this.collapse();
},
"tab" : function(e){
this.onViewClick(false);
return true;
},
scope : this,
doRelay : function(foo, bar, hname){
if(hname == 'down' || this.scope.isExpanded()){
return Ext.KeyNav.prototype.doRelay.apply(this, arguments);
}
return true;
},
forceKeyDown : true
});
this.queryDelay = Math.max(this.queryDelay || 10,
this.mode == 'local' ? 10 : 250);
this.dqTask = new Ext.util.DelayedTask(this.initQuery, this);
if(this.typeAhead){
this.taTask = new Ext.util.DelayedTask(this.onTypeAhead, this);
}
if(this.editable !== false && !this.enableKeyEvents){
this.el.on("keyup", this.onKeyUp, this);
}
if(this.forceSelection){
this.on('blur', this.doForce, this);
}
},
// private
onDestroy : function(){
if(this.isExpanded())
{
Ext.getDoc().un({
'mousewheel': this.collapseIf,
'mousedown': this.collapseIf,
scope: this
});
}
if(this.view){
Ext.destroy(this.view);//need run function "destroy" for destroy element. //chang by guig
/*this.view.el.removeAllListeners();
this.view.el.remove();
this.view.purgeListeners();*/
}
if(this.list){
if (this.innerList) {
this.innerList.un({
'mouseover': this.onViewOver,
'mousemove': this.onViewMove,
scope: this
});
}
this.list.destroy();
}
if (this.dqTask){
this.dqTask.cancel();
this.dqTask = null;
}//http://www.extjs.com/forum/showthread.php?t=49426/*A simple way to trigger the bug is to have type something quickly (in a remote combo) then blur the box. If you destroy the combo on blur, the dqTask will still trigger a remote fetch and fail because this.store is missing (since destroy has called bindStores(null))*/
this.bindStore(null);
ComboBoxSuperCls.onDestroy.call(this);
},
// private
unsetDelayCheck : function(){
delete this.delayedCheck;
},
// private
fireKey : function(e){
if(e.isNavKeyPress() && !this.isExpanded() && !this.delayedCheck){
this.fireEvent("specialkey", this, e);
}
},
// private
onResize: function(w, h){
ComboBoxSuperCls.onResize.apply(this, arguments);
if(this.list && this.listWidth === undefined){
var lw = Math.max(w, this.minListWidth);
this.list.setWidth(lw);
this.innerList.setWidth(lw - this.list.getFrameWidth('lr'));
}
},
// private
onEnable: function(){
ComboBoxSuperCls.onEnable.apply(this, arguments);
if(this.hiddenField){
this.hiddenField.disabled = false;
}
},
// private
onDisable: function(){
ComboBoxSuperCls.onDisable.apply(this, arguments);
if(this.hiddenField){
this.hiddenField.disabled = true;
}
},
/**
* Allow or prevent the user from directly editing the field text. If false is passed,
* the user will only be able to select from the items defined in the dropdown list. This method
* is the runtime equivalent of setting the 'editable' config option at config time.
* @param {Boolean} value True to allow the user to directly edit the field text
*/
setEditable : function(value){
if(value == this.editable){
return;
}
this.editable = value;
if(!value){
this.el.dom.setAttribute('readOnly', true);
this.el.on('mousedown', this.onTriggerClick, this);
this.el.addClass('x-combo-noedit');
}else{
this.el.dom.removeAttribute('readOnly');
this.el.un('mousedown', this.onTriggerClick, this);
this.el.removeClass('x-combo-noedit');
}
},
// private
onBeforeLoad : function(){
if(!this.hasFocus){
return;
}
this.innerList.update(this.loadingText ?
'<div class="loading-indicator">'+this.loadingText+'</div>' : '');
this.restrictHeight();
this.selectedIndex = -1;
},
// private
onLoad : function(){
if(!this.hasFocus){
return;
}
if(this.store.getCount() > 0){
this.expand();
this.restrictHeight();
if(this.lastQuery == this.allQuery){
if(this.editable){
this.multiSelect ? this.selectText(this.getRawValue().length) : this.el.dom.select();
}
if(!this.selectByValue(this.value, true)){
this.select(0, true);
}
}else{
this.selectNext();
if(this.typeAhead && this.lastKey != Ext.EventObject.BACKSPACE && this.lastKey != Ext.EventObject.DELETE){
this.taTask.delay(this.typeAheadDelay);
}
}
}else{
this.onEmptyResults();
}
//this.el.focus();
},
// private
onTypeAhead : function(){
if(this.store.getCount() > 0){
var r = this.store.getAt(0);
var newValue = r.data[this.displayField];
var len = newValue.length;
var oldValue = this.getRawValue(), selStart = oldValue.length;
if(selStart != len){
this.setRawValue(newValue);
this.selectText(selStart, len);
}
}
},
// private
onSelect : function(record, index){
if(this.fireEvent('beforeselect', this, record, index) !== false){
var field = this.valueField || this.displayField;
if(!this.multiSelect)
{
this.setValue(record.data[field]);
this.collapse();
}
else
{
record.set(this.checkField, !record.get(this.checkField));
var c = [];
var snapshot = this.store.snapshot || this.store.data;

snapshot.each(function(r) {
if(r.get(this.checkField)) {
c.push(r.get(field));
}
}, this);
this.setValue(c);
// display full list
if(this.store.isFiltered()) {
this.doQuery(this.allQuery);
}
}
this.fireEvent('select', this, record, index);
}
},
/**
* Returns the currently selected field value or empty string if no value is set.
* @return {String} value The selected value
*/
getValue : function(){
if(this.valueField){
return typeof this.value != 'undefined' ? this.value : '';
}else{
return ComboBoxSuperCls.getValue.call(this);
}
},
/**
* Clears any text/value currently set in the field
*/
clearValue : function(){
if(this.hiddenField){
this.hiddenField.value = '';
}
this.setRawValue('');
this.lastSelectionText = '';
this.applyEmptyText();
this.value = '';
},
/**
* Sets the specified value into the field. If the value finds a match, the corresponding record text
* will be displayed in the field. If the value does not match the data value of an existing item,
* and the valueNotFoundText config option is defined, it will be displayed as the default field text.
* Otherwise the field will be blank (although the value will still be set).
* @param {String} value The value to match
*/
setValue : function(v){
var text = v;
if(this.valueField){
if(!this.multiSelect)
{
var r = this.findRecord(this.valueField, v);
if(r){
text = r.data[this.displayField];
}else if(this.valueNotFoundText !== undefined){
text = this.valueNotFoundText;
}
}
else
{
this.store.clearFilter();
if(!Ext.isArray(v))
v = v.split(this.separator);
text = [];
Ext.each(v, function(item, index, array){
var r = this.findRecord(this.valueField, item);
if(r == null)
{
array.remove(item);
}
else
{
r.set(this.checkField, true);
text.push(r.data[this.displayField]);
}
}, this);
text = text.join(this.separator);
v = v.join(this.separator);
}
}
else if(this.multiSelect)
{
this.store.clearFilter();
if(Ext.isArray(v))
text = v = v.join(this.separator);
}
this.lastSelectionText = text;
if(this.hiddenField){
this.hiddenField.value = v;
}
ComboBoxSuperCls.setValue.call(this, text);
this.value = v;
},
// private
findRecord : function(prop, value){
var record;
if(this.store.getCount() > 0){
this.store.each(function(r){
if(r.data[prop] == value){
record = r;
return false;
}
});
}
return record;
},
// private
onViewMove : function(e, t){
this.inKeyMode = false;
},
// private
onViewOver : function(e, t){
if(this.inKeyMode){ // prevent key nav and mouse over conflicts
return;
}
var item = this.view.findItemFromChild(t);
if(item){
var index = this.view.indexOf(item);
this.select(index, false);
}
},
// private
onViewClick : function(doFocus){
var index = this.view.getSelectedIndexes()[0];
var r = this.store.getAt(index);
if(r){
this.onSelect(r, index);
}
if(doFocus !== false){
this.el.focus();
}
},
// private
restrictHeight : function(){
this.innerList.dom.style.height = '';
var inner = this.innerList.dom;
var pad = this.list.getFrameWidth('tb')+(this.resizable?this.handleHeight:0)+this.assetHeight;
var h = Math.max(inner.clientHeight, inner.offsetHeight, inner.scrollHeight);
var ha = this.getPosition()[1]-Ext.getBody().getScroll().top;
var hb = Ext.lib.Dom.getViewHeight()-ha-this.getSize().height;
var space = Math.max(ha, hb, this.minHeight || 0)-this.list.shadowOffset-pad-5;
h = Math.min(h, space, this.maxHeight);
this.innerList.setHeight(h);
this.list.beginUpdate();
this.list.setHeight(h+pad);
this.list.alignTo(this.wrap, this.listAlign);
this.list.endUpdate();
},
// private
onEmptyResults : function(){
this.collapse();
},
/**
* Returns true if the dropdown list is expanded, else false.
*/
isExpanded : function(){
return this.list && this.list.isVisible();
},
/**
* Select an item in the dropdown list by its data value. This function does NOT cause the select event to fire.
* The store must be loaded and the list expanded for this function to work, otherwise use setValue.
* @param {String} value The data value of the item to select
* @param {Boolean} scrollIntoView False to prevent the dropdown list from autoscrolling to display the
* selected item if it is not currently in view (defaults to true)
* @return {Boolean} True if the value matched an item in the list, else false
*/
selectByValue : function(v, scrollIntoView){
if(v !== undefined && v !== null){
var field = this.valueField || this.displayField, r;
if(!this.multiSelect)
{
r = this.findRecord(field, v);
}
else
{
if(!Ext.isArray(v))
v = v.split(this.separator);

Ext.each(v, function(item, index, array){
var rr = this.findRecord(field, item);
if(rr)
{
rr.set(this.checkField, true);
r = rr;
}
}, this);
}
if(r){
this.select(this.store.indexOf(r), scrollIntoView);
return true;
}
}
return false;
},
/**
* Select an item in the dropdown list by its numeric index in the list. This function does NOT cause the select event to fire.
* The store must be loaded and the list expanded for this function to work, otherwise use setValue.
* @param {Number} index The zero-based index of the list item to select
* @param {Boolean} scrollIntoView False to prevent the dropdown list from autoscrolling to display the
* selected item if it is not currently in view (defaults to true)
*/
select : function(index, scrollIntoView){
this.selectedIndex = index;
this.view.select(index);
if(scrollIntoView !== false){
var el = this.view.getNode(index);
if(el){
this.innerList.scrollChildIntoView(el, false);
}
}
},
// private
selectNext : function(){
var ct = this.store.getCount();
if(ct > 0){
if(this.selectedIndex == -1){
this.select(0);
}else if(this.selectedIndex < ct-1){
this.select(this.selectedIndex+1);
}
}
},
// private
selectPrev : function(){
var ct = this.store.getCount();
if(ct > 0){
if(this.selectedIndex == -1){
this.select(0);
}else if(this.selectedIndex != 0){
this.select(this.selectedIndex-1);
}
}
},
// private
onKeyUp : function(e){
if(this.editable !== false && !e.isSpecialKey()){
this.lastKey = e.getKey();
this.dqTask.delay(this.queryDelay);
}
ComboBoxSuperCls.onKeyUp.call(this, e);
},
// private
validateBlur : function(){
return !this.list || !this.list.isVisible();
},
// private
initQuery : function(){
this.doQuery(this.getRawValue());
},
// private
doForce : function(){
if(this.el.dom.value.length > 0){
this.el.dom.value =
this.lastSelectionText === undefined ? '' : this.lastSelectionText;
this.applyEmptyText();
}
},
/**
* Execute a query to filter the dropdown list. Fires the {@link #beforequery} event prior to performing the
* query allowing the query action to be canceled if needed.
* @param {String} query The SQL query to execute
* @param {Boolean} forceAll True to force the query to execute even if there are currently fewer characters
* in the field than the minimum specified by the minChars config option. It also clears any filter previously
* saved in the current store (defaults to false)
*/
doQuery : function(q, forceAll){
if(q != '' && this.multiSelect)
{
q = q.split(this.separator);
q = q[q.length - 1];
}
if(q === undefined || q === null){
q = '';
}
var qe = {
query: q,
forceAll: forceAll,
combo: this,
cancel:false
};
if(this.fireEvent('beforequery', qe)===false || qe.cancel){
return false;
}
q = qe.query;
forceAll = qe.forceAll;
if(forceAll === true || (q.length >= this.minChars)){
if(this.lastQuery !== q){
this.lastQuery = q;
if(this.mode == 'local'){
this.selectedIndex = -1;
if(forceAll){
this.store.clearFilter();
}else{
this.store.filter(this.displayField, q);
}
this.onLoad();
}else{
this.store.baseParams[this.queryParam] = q;
this.store.load({
params: this.getParams(q)
});
this.expand();
}
}else{
this.selectedIndex = -1;
this.onLoad();
}
}
},
// private
getParams : function(q){
var p = {};
//p[this.queryParam] = q;
if(this.pageSize){
p.start = 0;
p.limit = this.pageSize;
}
return p;
},
/**
* Hides the dropdown list if it is currently expanded. Fires the {@link #collapse} event on completion.
*/
collapse : function(){
if(!this.isExpanded()){
return;
}
this.list.hide();
Ext.getDoc().un({
'mousewheel': this.collapseIf,
'mousedown': this.collapseIf,
scope: this
});
this.fireEvent('collapse', this);
},
// private
collapseIf : function(e){
if(!e.within(this.wrap) && !e.within(this.list)){
this.collapse();
}
},
/**
* Expands the dropdown list if it is currently hidden. Fires the {@link #expand} event on completion.
*/
expand : function(){
if(this.isExpanded() || !this.hasFocus){
return;
}
this.list.alignTo(this.wrap, this.listAlign);
this.list.show();
this.innerList.setOverflow('auto'); // necessary for FF 2.0/Mac
Ext.getDoc().on({
'mousewheel': this.collapseIf,
'mousedown': this.collapseIf,
scope: this
});
this.fireEvent('expand', this);
},
/**
* @method onTriggerClick
* @hide
*/
// private
// Implements the default empty TriggerField.onTriggerClick function
onTriggerClick : function(){
if(this.disabled){
return;
}
if(this.isExpanded()){
this.collapse();
}else {
this.onFocus({});
if(this.triggerAction == 'all') {
this.doQuery(this.allQuery, true);
} else {
this.doQuery(this.getRawValue());
}
}
this.el.focus();
}
/**
* @hide
* @method autoSize
*/
/**
* @cfg {Boolean} grow @hide
*/
/**
* @cfg {Number} growMin @hide
*/
/**
* @cfg {Number} growMax @hide
*/
});
Ext.reg('combo', ComboBox);
var ComboBoxSuperCls = ComboBox.superclass;

mystix
26 Mar 2009, 9:56 PM
could you highlight your code changes in red so everyone can see what differs from the original combo code? thanks

dsonet
27 Mar 2009, 1:19 AM
as you can see, I improved the Ext.util.Observable,and Ext.EventManager, you can see they don't need you call the addEvents now,that's for performance,also you can call removeListener(un) as addEventListener(on).

I'll post those in another topic.

dsonet
27 Mar 2009, 5:40 AM
the new version of this widget,it's rebuild for high performance.
you can use the file differ to see the difference from the previous version.
Not tested yet,but I think maybe it's support paging data now?
sorry for the version at last night,it's had many bugs.Now,I fixed all of them.
Thanks for your feedback.

/*
* Ext JS Library 2.2
* Copyright(c) 2006-2008, Ext JS, LLC.
* licensing@extjs.com
*
* http://extjs.com/license
*/
/**
* @class Ext.form.ComboBox
* @modifier: DSONet (dsonet@msn.com)
* @extends Ext.form.TriggerField
* A combobox control with support for autocomplete, remote-loading, paging and many other features.
* @constructor
* Create a new ComboBox.
* @param {Object} config Configuration options
*/
var ComboBox = Ext.form.ComboBox = Ext.extend(Ext.form.TriggerField, {
/**
* @cfg {Mixed} transform The id, DOM node or element of an existing HTML SELECT to convert to a ComboBox.
* Note that if you specify this and the combo is going to be in a {@link Ext.form.BasicForm} or
* {@link Ext.form.FormPanel}, you must also set {@link #lazyRender} = true.
*/
/**
* @cfg {Boolean} lazyRender True to prevent the ComboBox from rendering until requested (should always be used when
* rendering into an Ext.Editor, defaults to false).
*/
/**
* @cfg {Boolean/Object} autoCreate A DomHelper element spec, or true for a default element spec (defaults to:
* {tag: "input", type: "text", size: "24", autocomplete: "off"})
*/
/**
* @cfg {Ext.data.Store/Array} store The data source to which this combo is bound (defaults to undefined). This can be
* any {@link Ext.data.Store} subclass, a 1-dimensional array (e.g., ['Foo','Bar']) or a 2-dimensional array (e.g.,
* [['f','Foo'],['b','Bar']]). Arrays will be converted to a {@link Ext.data.SimpleStore} internally.
* 1-dimensional arrays will automatically be expanded (each array item will be the combo value and text) and
* for multi-dimensional arrays, the value in index 0 of each item will be assumed to be the combo value, while
* the value at index 1 is assumed to be the combo text.
*/
/**
* @cfg {String} title If supplied, a header element is created containing this text and added into the top of
* the dropdown list (defaults to undefined, with no header element)
*/
// private
defaultAutoCreate : {tag: "input", type: "text", size: "24", autocomplete: "off"},
/**
* @cfg {Number} listWidth The minWidth in pixels of the dropdown list (defaults to the width of the ComboBox field)
*/
/**
* @cfg {String} multiSelect whether can select multi values (defaults to undefined[false])
*/
/**
* @cfg {String} checkField Name of field used to store checked state.
* It is automatically added to existing fields.
* (defaults to "checked" - change it only if it collides with your normal field)
*/
checkField:'checked',
/**
* @cfg {String} separator Separator to use between values and texts (defaults to "," (comma))
*/
separator:',',
/**
* @cfg {String} displayField The underlying data field name to bind to this ComboBox (defaults to undefined if
* mode = 'remote' or 'text' if transforming a select)
*/
/**
* @cfg {String} valueField The underlying data value name to bind to this ComboBox (defaults to undefined if
* mode = 'remote' or 'value' if transforming a select) Note: use of a valueField requires the user to make a selection
* in order for a value to be mapped.
*/
/**
* @cfg {String} hiddenName If specified, a hidden form field with this name is dynamically generated to store the
* field's data value (defaults to the underlying DOM element's name). Required for the combo's value to automatically
* post during a form submission. Note that the hidden field's id will also default to this name if {@link #hiddenId}
* is not specified. The combo's id and the hidden field's ids should be different, since no two DOM nodes should
* share the same id, so if the combo and hidden names are the same, you should specify a unique hiddenId.
*/
/**
* @cfg {String} hiddenId If {@link #hiddenName} is specified, hiddenId can also be provided to give the hidden field
* a unique id (defaults to the hiddenName). The hiddenId and combo {@link #id} should be different, since no two DOM
* nodes should share the same id.
*/
/**
* @cfg {String} listClass CSS class to apply to the dropdown list element (defaults to '')
*/
listClass: '',
/**
* @cfg {String} checkedClass CSS class to apply to the checked item in the dropdown list (defaults to 'x-combo-checked')
*/
checkedClass: 'x-combo-checked',
/**
* @cfg {String} selectedClass CSS class to apply to the selected item in the dropdown list (defaults to 'x-combo-selected')
*/
selectedClass: 'x-combo-selected',
/**
* @cfg {String} triggerClass An additional CSS class used to style the trigger button. The trigger will always get the
* class 'x-form-trigger' and triggerClass will be <b>appended</b> if specified (defaults to 'x-form-arrow-trigger'
* which displays a downward arrow icon).
*/
triggerClass : 'x-form-arrow-trigger',
/**
* @cfg {Boolean/String} shadow True or "sides" for the default effect, "frame" for 4-way shadow, and "drop" for bottom-right
*/
shadow:'sides',
/**
* @cfg {String} listAlign A valid anchor position value. See {@link Ext.Element#alignTo} for details on supported
* anchor positions (defaults to 'tl-bl')
*/
listAlign: 'tl-bl?',
/**
* @cfg {Number} maxHeight The maximum height in pixels of the dropdown list before scrollbars are shown (defaults to 300)
*/
maxHeight: 300,
/**
* @cfg {Number} minHeight The minimum height in pixels of the dropdown list when the list is constrained by its
* distance to the viewport edges (defaults to 90)
*/
minHeight: 90,
/**
* @cfg {String} triggerAction The action to execute when the trigger field is activated. Use 'all' to run the
* query specified by the allQuery config option (defaults to 'query')
*/
triggerAction: 'query',
/**
* @cfg {Number} minChars The minimum number of characters the user must type before autocomplete and typeahead activate
* (defaults to 4 if remote or 0 if local, does not apply if editable = false)
*/
minChars : 4,
/**
* @cfg {Boolean} typeAhead True to populate and autoselect the remainder of the text being typed after a configurable
* delay ({@link #typeAheadDelay}) if it matches a known value (defaults to false)
*/
typeAhead: false,
/**
* @cfg {Number} queryDelay The length of time in milliseconds to delay between the start of typing and sending the
* query to filter the dropdown list (defaults to 500 if mode = 'remote' or 10 if mode = 'local')
*/
queryDelay: 500,
/**
* @cfg {Number} pageSize If greater than 0, a paging toolbar is displayed in the footer of the dropdown list and the
* filter queries will execute with page start and limit parameters. Only applies when mode = 'remote' (defaults to 0)
*/
pageSize: 0,
/**
* @cfg {Boolean} selectOnFocus True to select any existing text in the field immediately on focus. Only applies
* when editable = true (defaults to false)
*/
selectOnFocus:false,
/**
* @cfg {String} queryParam Name of the query as it will be passed on the querystring (defaults to 'query')
*/
queryParam: 'query',
/**
* @cfg {String} loadingText The text to display in the dropdown list while data is loading. Only applies
* when mode = 'remote' (defaults to 'Loading...')
*/
loadingText: 'Loading...',
/**
* @cfg {Boolean} resizable True to add a resize handle to the bottom of the dropdown list (defaults to false)
*/
resizable: false,
/**
* @cfg {Number} handleHeight The height in pixels of the dropdown list resize handle if resizable = true (defaults to 8)
*/
handleHeight : 8,
/**
* @cfg {Boolean} editable False to prevent the user from typing text directly into the field, just like a
* traditional select (defaults to true)
*/
editable: true,
/**
* @cfg {String} allQuery The text query to send to the server to return all records for the list with no filtering (defaults to '')
*/
allQuery: '',
/**
* @cfg {String} mode Set to 'local' if the ComboBox loads local data (defaults to 'remote' which loads from the server)
*/
mode: 'remote',
/**
* @cfg {Number} minListWidth The minimum width of the dropdown list in pixels (defaults to 70, will be ignored if
* listWidth has a higher value)
*/
minListWidth : 70,
/**
* @cfg {Boolean} forceSelection True to restrict the selected value to one of the values in the list, false to
* allow the user to set arbitrary text into the field (defaults to false)
*/
forceSelection:false,
/**
* @cfg {Number} typeAheadDelay The length of time in milliseconds to wait until the typeahead text is displayed
* if typeAhead = true (defaults to 250)
*/
typeAheadDelay : 250,
/**
* @cfg {String} valueNotFoundText When using a name/value combo, if the value passed to setValue is not found in
* the store, valueNotFoundText will be displayed as the field text if defined (defaults to undefined). If this
* defaut text is used, it means there is no value set and no validation will occur on this field.
*/
/**
* @cfg {Boolean} lazyInit True to not initialize the list for this combo until the field is focused (defaults to true)
*/
lazyInit : true,
// private
initComponent : function(){
ComboBoxSuperCls.initComponent.call(this);
//this.addEvents(
/**
* @event expand
* Fires when the dropdown list is expanded
* @param {Ext.form.ComboBox} combo This combo box
*/
//'expand',
/**
* @event collapse
* Fires when the dropdown list is collapsed
* @param {Ext.form.ComboBox} combo This combo box
*/
//'collapse',
/**
* @event beforeselect
* Fires before a list item is selected. Return false to cancel the selection.
* @param {Ext.form.ComboBox} combo This combo box
* @param {Ext.data.Record} record The data record returned from the underlying store
* @param {Number} index The index of the selected item in the dropdown list
*/
//'beforeselect',
/**
* @event select
* Fires when a list item is selected
* @param {Ext.form.ComboBox} combo This combo box
* @param {Ext.data.Record} record The data record returned from the underlying store
* @param {Number} index The index of the selected item in the dropdown list
*/
//'select',
/**
* @event beforequery
* Fires before all queries are processed. Return false to cancel the query or set the queryEvent's
* cancel property to true.
* @param {Object} queryEvent An object that has these properties:<ul>
* <li><code>combo</code> : Ext.form.ComboBox <div class="sub-desc">This combo box</div></li>
* <li><code>query</code> : String <div class="sub-desc">The query</div></li>
* <li><code>forceAll</code> : Boolean <div class="sub-desc">True to force "all" query</div></li>
* <li><code>cancel</code> : Boolean <div class="sub-desc">Set to true to cancel the query</div></li>
* </ul>
*/
//'beforequery'
//);
if(this.transform){
this.allowDomMove = false;
var s = Ext.getDom(this.transform);
if(!this.hiddenName){
this.hiddenName = s.name;
}
if(!this.store){
this.mode = 'local';
var d = [], opts = s.options;
for(var i = 0, len = opts.length;i < len; i++){
var o = opts[i];
var value = (Ext.isIE ? o.getAttributeNode('value').specified : o.hasAttribute('value')) ? o.value : o.text;
if(o.selected) {
this.value = value;
}
d.push([value, o.text]);
}
this.store = new Ext.data.SimpleStore({
'id': 0,
fields: ['value', 'text'],
data : d
});
this.valueField = 'value';
this.displayField = 'text';
}
s.name = Ext.id(); // wipe out the name in case somewhere else they have a reference
if(!this.lazyRender){
this.target = true;
this.el = Ext.DomHelper.insertBefore(s, this.autoCreate || this.defaultAutoCreate);
Ext.removeNode(s); // remove it
this.render(this.el.parentNode);
}else{
Ext.removeNode(s); // remove it
}
}
//auto-configure store from local array data
else if(Ext.isArray(this.store)){
if (Ext.isArray(this.store[0])){
this.store = new Ext.data.SimpleStore({
fields: ['value','text'],
data: this.store
});
this.valueField = 'value';
}else{
this.store = new Ext.data.SimpleStore({
fields: ['text'],
data: this.store,
expandData: true
});
this.valueField = 'text';
}
this.displayField = 'text';
this.mode = 'local';
}
this.selectedIndex = -1;
if(this.mode == 'local'){
if(this.initialConfig.queryDelay === undefined){
this.queryDelay = 10;
}
if(this.initialConfig.minChars === undefined){
this.minChars = 0;
}
}
},
// private
onRender : function(ct, position){
ComboBoxSuperCls.onRender.call(this, ct, position);
if(this.hiddenName){
this.hiddenField = this.el.insertSibling({tag:'input', type:'hidden', name: this.hiddenName,
id: (this.hiddenId||this.hiddenName)}, 'before', true);
// prevent input submission
this.el.dom.removeAttribute('name');
}
if(Ext.isGecko){
this.el.dom.setAttribute('autocomplete', 'off');
}
if(!this.lazyInit){
this.initList();
}else{
this.on('focus', this.initList, this, {single: true});
}
if(!this.editable){
this.editable = true;
this.setEditable(false);
}
},
// private
initValue : function(){
ComboBoxSuperCls.initValue.call(this);
if(this.hiddenField){
this.hiddenField.value =
this.hiddenValue !== undefined ? this.hiddenValue :
this.value !== undefined ? this.value : '';
}
},
// private
initList : function(){
if(!this.list){
var cls = 'x-combo-list';
this.list = new Ext.Layer({
shadow: this.shadow, cls: [cls, this.listClass].join(' '), constrain:false
});
var lw = this.listWidth || Math.max(this.wrap.getWidth(), this.minListWidth);
//this.list.setWidth(lw);
this.list.setSize(lw, 0);
this.list.swallowEvent('mousewheel');
this.assetHeight = 0;
if(this.title){
this.header = this.list.createChild({cls:cls+'-hd', html: this.title});
this.assetHeight += this.header.getHeight();
}
this.innerList = this.list.createChild({cls:cls+'-inner'});
this.innerList.on({
'mouseover': this.onViewOver,
'mousemove': this.onViewMove,
scope: this
});
this.innerList.setWidth(lw - this.list.getFrameWidth('lr'));
if(this.pageSize){
this.footer = this.list.createChild({cls:cls+'-ft'});
this.pageTb = new Ext.PagingToolbar({
store:this.store,
pageSize: this.pageSize,
renderTo:this.footer
});
this.assetHeight += this.footer.getHeight();
}
if(!this.tpl){
/**
* @cfg {String/Ext.XTemplate} tpl The template string, or {@link Ext.XTemplate}
* instance to use to display each item in the dropdown list. Use
* this to create custom UI layouts for items in the list.
* <p>
* If you wish to preserve the default visual look of list items, add the CSS
* class name <pre>x-combo-list-item</pre> to the template's container element.
* <p>
* <b>The template must contain one or more substitution parameters using field
* names from the Combo's</b> {@link #store Store}. An example of a custom template
* would be adding an <pre>ext:qtip</pre> attribute which might display other fields
* from the Store.
* <p>
* The dropdown list is displayed in a DataView. See {@link Ext.DataView} for details.
*/
this.tpl = '<tpl for="."><div class="'+cls+'-item'+ (this.multiSelect ? '{[values.'+ this.checkField +'?" '+ this.checkedClass +'":""]}' : "") +'">'+ (this.multiSelect ? '<img src="' + Ext.BLANK_IMAGE_URL + '" class="x-form-check{[values.'+ this.checkField +'?" x-form-check-checked":""]}" />': "") +'{' + this.displayField + '}</div></tpl>';
/**
* @cfg {String} itemSelector
* <b>This setting is required if a custom XTemplate has been specified in {@link #tpl}
* which assigns a class other than <pre>'x-combo-list-item'</pre> to dropdown list items</b>.
* A simple CSS selector (e.g. div.some-class or span:first-child) that will be
* used to determine what nodes the DataView which handles the dropdown display will
* be working with.
*/
}
/**
* The {@link Ext.DataView DataView} used to display the ComboBox's options.
* @type Ext.DataView
*/
this.view = new Ext.DataView({
applyTo: this.innerList,
tpl: this.tpl,
singleSelect: true,
selectedClass: this.selectedClass,
itemSelector: this.itemSelector || '.' + cls + '-item'
});
this.view.on('click', this.onViewClick, this);
this.bindStore(this.store, true);
if(this.resizable){
this.resizer = new Ext.Resizable(this.list, {
pinned:true, handles:'se'
});
this.resizer.on('resize', function(r, w, h){
this.maxHeight = h-this.handleHeight-this.list.getFrameWidth('tb')-this.assetHeight;
this.listWidth = w;
this.innerList.setWidth(w - this.list.getFrameWidth('lr'));
this.restrictHeight();
}, this);
this[this.pageSize?'footer':'innerList'].setStyle('margin-bottom', this.handleHeight+'px');
}
}
},
/**
* Returns the store associated with this combo.
* @return {Ext.data.Store} The store
*/
getStore : function(){
return this.store;
},
// private
bindStore : function(store, initial){
var dsLs = {
'beforeload': this.onBeforeLoad,
'load': this.onLoad,
'loadexception': this.collapse,
scope: this
};
if(this.store && !initial){
this.store.un(dsLs);
if(!store){
this.store = null;
if(this.view){
this.view.setStore(null);
}
}
}
if(store){
this.store = Ext.StoreMgr.lookup(store);
this.store.on(dsLs);
if(this.view){
this.view.setStore(store);
}
}
},
// private
initEvents : function(){
ComboBoxSuperCls.initEvents.call(this);
this.keyNav = new Ext.KeyNav(this.el, {
"up" : function(e){
this.inKeyMode = true;
this.selectPrev();
},
"down" : function(e){
if(!this.isExpanded()){
this.onTriggerClick();
}else{
this.inKeyMode = true;
this.selectNext();
}
},
"enter" : function(e){
this.onViewClick();
this.delayedCheck = true;
this.unsetDelayCheck.defer(10, this);
},
"esc" : function(e){
this.collapse();
},
"tab" : function(e){
this.onViewClick(false);
return true;
},
scope : this,
doRelay : function(foo, bar, hname){
if(hname == 'down' || this.scope.isExpanded()){
return Ext.KeyNav.prototype.doRelay.apply(this, arguments);
}
return true;
},
forceKeyDown : true
});
this.queryDelay = Math.max(this.queryDelay || 10,
this.mode == 'local' ? 10 : 250);
this.dqTask = new Ext.util.DelayedTask(this.initQuery, this);
if(this.typeAhead){
this.taTask = new Ext.util.DelayedTask(this.onTypeAhead, this);
}
if(this.editable !== false && !this.enableKeyEvents){
this.el.on("keyup", this.onKeyUp, this);
}
if(this.forceSelection){
this.on('blur', this.doForce, this);
}
},
// private
onDestroy : function(){
if(this.isExpanded())
{
Ext.getDoc().un({
'mousewheel': this.collapseIf,
'mousedown': this.collapseIf,
scope: this
});
}
if(this.view){
Ext.destroy(this.view);//need run function "destroy" for destroy element. //chang by guig
/*this.view.el.removeAllListeners();
this.view.el.remove();
this.view.purgeListeners();*/
}
if(this.list){
if (this.innerList) {
this.innerList.un({
'mouseover': this.onViewOver,
'mousemove': this.onViewMove,
scope: this
});
}
this.list.destroy();
}
if (this.dqTask){
this.dqTask.cancel();
this.dqTask = null;
}//http://www.extjs.com/forum/showthread.php?t=49426/*A simple way to trigger the bug is to have type something quickly (in a remote combo) then blur the box. If you destroy the combo on blur, the dqTask will still trigger a remote fetch and fail because this.store is missing (since destroy has called bindStores(null))*/
this.bindStore(null);
ComboBoxSuperCls.onDestroy.call(this);
},
// private
unsetDelayCheck : function(){
delete this.delayedCheck;
},
// private
fireKey : function(e){
if(e.isNavKeyPress() && !this.isExpanded() && !this.delayedCheck){
this.fireEvent("specialkey", this, e);
}
},
// private
onResize: function(w, h){
ComboBoxSuperCls.onResize.apply(this, arguments);
if(this.list && this.listWidth === undefined){
var lw = Math.max(w, this.minListWidth);
this.list.setWidth(lw);
this.innerList.setWidth(lw - this.list.getFrameWidth('lr'));
}
},
// private
onEnable: function(){
ComboBoxSuperCls.onEnable.apply(this, arguments);
if(this.hiddenField){
this.hiddenField.disabled = false;
}
},
// private
onDisable: function(){
ComboBoxSuperCls.onDisable.apply(this, arguments);
if(this.hiddenField){
this.hiddenField.disabled = true;
}
},
/**
* Allow or prevent the user from directly editing the field text. If false is passed,
* the user will only be able to select from the items defined in the dropdown list. This method
* is the runtime equivalent of setting the 'editable' config option at config time.
* @param {Boolean} value True to allow the user to directly edit the field text
*/
setEditable : function(value){
if(value == this.editable){
return;
}
this.editable = value;
if(!value){
this.el.dom.setAttribute('readOnly', true);
this.el.on('mousedown', this.onTriggerClick, this);
this.el.addClass('x-combo-noedit');
}else{
this.el.dom.removeAttribute('readOnly');
this.el.un('mousedown', this.onTriggerClick, this);
this.el.removeClass('x-combo-noedit');
}
},
// private
onBeforeLoad : function(){
if(!this.hasFocus){
return;
}
this.innerList.update(this.loadingText ?
'<div class="loading-indicator">'+this.loadingText+'</div>' : '');
this.restrictHeight();
this.selectedIndex = -1;
},
// private
onLoad : function(){
if(!this.hasFocus){
return;
}
if(this.store.getCount() > 0){
this.expand();
this.restrictHeight();
if(this.lastQuery == this.allQuery){
if(this.editable){
this.multiSelect ? this.selectText(this.getRawValue().length) : this.el.dom.select();
}
if(!this.selectByValue(this.value, true)){
this.select(0, true);
}
}else{
this.selectNext();
if(this.typeAhead && this.lastKey != Ext.EventObject.BACKSPACE && this.lastKey != Ext.EventObject.DELETE){
this.taTask.delay(this.typeAheadDelay);
}
}
}else{
this.onEmptyResults();
}
//this.el.focus();
},
// private
onTypeAhead : function(){
if(this.store.getCount() > 0){
var r = this.store.getAt(0);
var newValue = r.data[this.displayField];
var len = newValue.length;
var oldValue = this.getRawValue(), selStart = oldValue.length;
if(selStart != len){
this.setRawValue(newValue);
this.selectText(selStart, len);
}
}
},
// private
onSelect : function(record, index){
if(this.fireEvent('beforeselect', this, record, index) !== false){
var field = this.valueField || this.displayField;
if(this.multiSelect)
{
var val = !record.get(this.checkField);
record.set(this.checkField, val);
this.setValue(record.data[field], val);
// display full list
if(this.store.isFiltered()) {
this.doQuery(this.allQuery);
}
}
else
{
this.setValue(record.data[field]);
this.collapse();
}
this.fireEvent('select', this, record, index);
}
},
/**
* Returns the currently selected field value or empty string if no value is set.
* @return {String} value The selected value
*/
getValue : function(){
if(this.valueField){
return typeof this.value != 'undefined' ? this.value : '';
}else{
return ComboBoxSuperCls.getValue.call(this);
}
},
/**
* Clears any text/value currently set in the field
*/
clearValue : function(){
if(this.multiSelect)
{
var v = this.value;
var field = this.valueField || this.displayField;
v = v && v.split(this.separator) || [];
this.store.clearFilter();
v.length && this.store.each(function(r) {
var idx;
if((idx = v.indexOf(r.data[field])) != -1){
r.set(this.checkField, false);
v.splice(idx, 1);
if(!v.length)
{
return false;
}
}
}, this);
}
if(this.hiddenField){
this.hiddenField.value = '';
}
this.setRawValue('');
this.lastSelectionText = '';
this.applyEmptyText();
this.value = '';
},
/**
* Sets the specified value into the field. If the value finds a match, the corresponding record text
* will be displayed in the field. If the value does not match the data value of an existing item,
* and the valueNotFoundText config option is defined, it will be displayed as the default field text.
* Otherwise the field will be blank (although the value will still be set).
* @param {String} value The value to match
* @param {Boolean} action internal use only
*/
setValue : function(v, action){
var text = v;
var valueField = this.valueField;
var displayField = this.displayField;
var separator = this.separator;
if(valueField){
if(this.multiSelect)
{
this.store.clearFilter();
if((v || this.value) && this.store.getCount() > 0)
{
text = [];
var vAdd = v && v.split(separator) || [];
var vRemove = this.value && this.value.split(separator) || [];
var textFiltered = this.lastSelectionText;
var checkField = this.checkField;
var a1, a2;
textFiltered = textFiltered && textFiltered.split(separator) || [];
if(textFiltered.length && typeof action != "boolean")
{
//vRemove.sort();vAdd.sort();
if(vRemove.length > vAdd.length)
{
a1 = vAdd;a2 = vRemove;
}
else
{
a2 = vAdd;a1 = vRemove;
}
var l = a1.length, i = -1, idx;
while(++i < l)
{
if((idx = a2.indexOf(a1[i])) != -1)
{
a1.splice(i, 1);
a2.splice(idx, 1);
--i;--l;
}
}
}
else if(action === true)
{
vRemove.push(v);
v = vRemove.join(separator);vRemove = [];
}
else if (action === false)
{
vRemove.remove(v);
v = vRemove.join(separator);vRemove = vAdd;vAdd = [];
}
this.store.each(function(r){
var idx, v = r.data[valueField], d = r.data[displayField];
if(vRemove.length && (idx = vRemove.indexOf(v)) != -1){
r.set(checkField, false);
vRemove.splice(idx, 1);
if((idx = textFiltered.indexOf(d)) != -1){
textFiltered.splice(idx, 1);
}
}
if(vAdd.length && (idx = vAdd.indexOf(v)) != -1){
r.set(checkField, true);
text.push(d);
vAdd.splice(idx, 1);
}
if(!(vRemove.length || vAdd.length))
{
return false;
}
});
text = textFiltered.concat(text).join(separator);//.sort()
}
}
else
{
var r = this.findRecord(valueField, v);
if(r){
text = r.data[displayField];
}else if(this.valueNotFoundText !== undefined){
text = this.valueNotFoundText;
}
}
}
else if(this.multiSelect)
{
if(action === true)
{
text = v = (this.value || "") + separator + v;
}
else if(action === false)
{
var tmp = (this.value || "").split(separator);
tmp.remove(v);
text = v = tmp.join(separator);
}
}
this.lastSelectionText = text;
if(this.hiddenField){
this.hiddenField.value = v;
}
ComboBoxSuperCls.setValue.call(this, text);
this.value = v;
},
// private
findRecord : function(prop, value){
var record;
if(this.store.getCount() > 0){
this.store.each(function(r){
if(r.data[prop] == value){
record = r;
return false;
}
});
}
return record;
},
// private
onViewMove : function(e, t){
this.inKeyMode = false;
},
// private
onViewOver : function(e, t){
if(this.inKeyMode){ // prevent key nav and mouse over conflicts
return;
}
var item = this.view.findItemFromChild(t);
if(item){
var index = this.view.indexOf(item);
this.select(index, false);
}
},
// private
onViewClick : function(doFocus){
var index = this.view.getSelectedIndexes()[0];
var r = this.store.getAt(index);
if(r){
this.onSelect(r, index);
}
if(doFocus !== false){
this.el.focus();
}
},
// private
restrictHeight : function(){
this.innerList.dom.style.height = '';
var inner = this.innerList.dom;
var pad = this.list.getFrameWidth('tb')+(this.resizable?this.handleHeight:0)+this.assetHeight;
var h = Math.max(inner.clientHeight, inner.offsetHeight, inner.scrollHeight);
var ha = this.getPosition()[1]-Ext.getBody().getScroll().top;
var hb = Ext.lib.Dom.getViewHeight()-ha-this.getSize().height;
var space = Math.max(ha, hb, this.minHeight || 0)-this.list.shadowOffset-pad-5;
h = Math.min(h, space, this.maxHeight);
this.innerList.setHeight(h);
this.list.beginUpdate();
this.list.setHeight(h+pad);
this.list.alignTo(this.wrap, this.listAlign);
this.list.endUpdate();
},
// private
onEmptyResults : function(){
this.collapse();
},
/**
* Returns true if the dropdown list is expanded, else false.
*/
isExpanded : function(){
return this.list && this.list.isVisible();
},
/**
* Select an item in the dropdown list by its data value. This function does NOT cause the select event to fire.
* The store must be loaded and the list expanded for this function to work, otherwise use setValue.
* @param {String} value The data value of the item to select
* @param {Boolean} scrollIntoView False to prevent the dropdown list from autoscrolling to display the
* selected item if it is not currently in view (defaults to true)
* @return {Boolean} True if the value matched an item in the list, else false
*/
selectByValue : function(v, scrollIntoView){
if(v !== undefined && v !== null){
var field = this.valueField || this.displayField, r;
if(this.multiSelect)
{
v = v && v.split(this.separator) || "";
if(v.length && this.store.getCount() > 0){
var checkField = this.checkField;
this.store.each(function(rr){
var idx;
if(v.length && (idx = v.indexOf(rr.data[field])) != -1){
rr.set(checkField, true);
r = rr;
v.splice(idx, 1);
}
else
{
rr.set(checkField, false);
}
});
}
}
else
{
r = this.findRecord(field, v);
}
if(r){
this.select(this.store.indexOf(r), scrollIntoView);
return true;
}
}
return false;
},
/**
* Select an item in the dropdown list by its numeric index in the list. This function does NOT cause the select event to fire.
* The store must be loaded and the list expanded for this function to work, otherwise use setValue.
* @param {Number} index The zero-based index of the list item to select
* @param {Boolean} scrollIntoView False to prevent the dropdown list from autoscrolling to display the
* selected item if it is not currently in view (defaults to true)
*/
select : function(index, scrollIntoView){
this.selectedIndex = index;
this.view.select(index);
if(scrollIntoView !== false){
var el = this.view.getNode(index);
if(el){
this.innerList.scrollChildIntoView(el, false);
}
}
},
// private
selectNext : function(){
var ct = this.store.getCount();
if(ct > 0){
if(this.selectedIndex == -1){
this.select(0);
}else if(this.selectedIndex < ct-1){
this.select(this.selectedIndex+1);
}
}
},
// private
selectPrev : function(){
var ct = this.store.getCount();
if(ct > 0){
if(this.selectedIndex == -1){
this.select(0);
}else if(this.selectedIndex != 0){
this.select(this.selectedIndex-1);
}
}
},
// private
onKeyUp : function(e){
if(this.editable !== false && !e.isSpecialKey()){
this.lastKey = e.getKey();
this.dqTask.delay(this.queryDelay);
}
ComboBoxSuperCls.onKeyUp.call(this, e);
},
// private
validateBlur : function(){
return !this.list || !this.list.isVisible();
},
// private
initQuery : function(){
this.doQuery(this.getRawValue());
},
// private
doForce : function(){
if(this.el.dom.value.length > 0){
this.el.dom.value =
this.lastSelectionText === undefined ? '' : this.lastSelectionText;
this.applyEmptyText();
}
},
/**
* Execute a query to filter the dropdown list. Fires the {@link #beforequery} event prior to performing the
* query allowing the query action to be canceled if needed.
* @param {String} query The SQL query to execute
* @param {Boolean} forceAll True to force the query to execute even if there are currently fewer characters
* in the field than the minimum specified by the minChars config option. It also clears any filter previously
* saved in the current store (defaults to false)
*/
doQuery : function(q, forceAll){
if(q != '' && this.multiSelect)
{
q = q.split(this.separator);
q = q[q.length - 1];
}
if(q === undefined || q === null){
q = '';
}
var qe = {
query: q,
forceAll: forceAll,
combo: this,
cancel:false
};
if(this.fireEvent('beforequery', qe)===false || qe.cancel){
return false;
}
q = qe.query;
forceAll = qe.forceAll;
if(forceAll === true || (q.length >= this.minChars)){
if(this.lastQuery !== q){
this.lastQuery = q;
if(this.mode == 'local'){
this.selectedIndex = -1;
if(forceAll){
this.store.clearFilter();
}else{
this.store.filter(this.displayField, q);
}
this.onLoad();
}else{
this.store.baseParams[this.queryParam] = q;
this.store.load({
params: this.getParams(q)
});
this.expand();
}
}else{
this.selectedIndex = -1;
this.onLoad();
}
}
},
// private
getParams : function(q){
var p = {};
//p[this.queryParam] = q;
if(this.pageSize){
p.start = 0;
p.limit = this.pageSize;
}
return p;
},
/**
* Hides the dropdown list if it is currently expanded. Fires the {@link #collapse} event on completion.
*/
collapse : function(){
if(!this.isExpanded()){
return;
}
this.list.hide();
Ext.getDoc().un({
'mousewheel': this.collapseIf,
'mousedown': this.collapseIf,
scope: this
});
this.fireEvent('collapse', this);
},
// private
collapseIf : function(e){
if(!e.within(this.wrap) && !e.within(this.list)){
this.collapse();
}
},
/**
* Expands the dropdown list if it is currently hidden. Fires the {@link #expand} event on completion.
*/
expand : function(){
if(this.isExpanded() || !this.hasFocus){
return;
}
this.list.alignTo(this.wrap, this.listAlign);
this.list.show();
this.innerList.setOverflow('auto'); // necessary for FF 2.0/Mac
Ext.getDoc().on({
'mousewheel': this.collapseIf,
'mousedown': this.collapseIf,
scope: this
});
this.fireEvent('expand', this);
},
/**
* @method onTriggerClick
* @hide
*/
// private
// Implements the default empty TriggerField.onTriggerClick function
onTriggerClick : function(){
if(this.disabled){
return;
}
if(this.isExpanded()){
this.collapse();
}else {
this.onFocus({});
if(this.triggerAction == 'all') {
this.doQuery(this.allQuery, true);
} else {
this.doQuery(this.getRawValue());
}
}
this.el.focus();
}
/**
* @hide
* @method autoSize
*/
/**
* @cfg {Boolean} grow @hide
*/
/**
* @cfg {Number} growMin @hide
*/
/**
* @cfg {Number} growMax @hide
*/
});
Ext.reg('combo', ComboBox);
var ComboBoxSuperCls = ComboBox.superclass;

galdaka
28 Mar 2009, 2:00 AM
Use lovcombo: http://extjs.com/forum/showthread.php?p=174214

Demo: http://lovcombo.extjs.eu/

Greetings,