Code:
// Note: add the styles shown in the comment at the end of this code block to your .css
Ext.ns('Ext.ux.form'); // create namespace
Ext.define('Ext.ux.form.CheckboxListCombo', {
extend: 'Ext.form.field.ComboBox',
alias: 'widget.checkboxlistcombo',
/**
* The following options were added to extend the ComboBox to provide additional functionality.
* The are now 4 possible display values in the text box of the combo:
* (1) .emptyText
* (2) .displayField - if only one item is selected
* (3) optional .briefDisplayField - if 2 or more items are selected
* (4) optional {nn} briefSummaryTitle - if more than a certain number of items are selected (and all won't fit)
* These features are only used if the values below are not falsy
*/
briefDisplayField: false, // store field to use if there are multiple items selected (if not false)
briefDisplayLimit: false, // max # of "briefDisplayField" items to display, after which "## {briefSummaryTitle}" is displayed
briefSummaryTitle: false, // string to display if there are too many selected items for the display box
displayList: "", // will hold the delimited list of all selections (may not be in the combo if too many, but can be used as desired)
firstItemChecksAll: false, // if true then the first item is ignored other than for this purpose (value must be falsy)
allSelectedTitle: false, // if not set, it will be set to the displayValue of the first item
constructor: function(config) {
Ext.ux.form.CheckboxListCombo.superclass.constructor.call(this, config);
},
initComponent: function () {
if (this.briefDisplayField) {
this.briefDisplayTpl = Ext.create('Ext.XTemplate', '<tpl for=".">{[typeof values === "string" ? values : values.' + this.briefDisplayField + ']}<tpl if="xindex < xcount">' + this.delimiter + '</tpl></tpl>');
}
Ext.ux.form.CheckboxListCombo.superclass.initComponent.apply(this, arguments);
this.listConfig.checkboxComboId = this.id; // for firstItem checking (see below)
},
getDisplayValue: function () {
this.displayList = this.displayTplData && this.displayTplData.length
? (this.briefDisplayTpl || this.displayTpl).apply(this.displayTplData)
: "";
var ttl = this.allSelected
? this.allSelectedTitle || "[" + this.emptyText + "]"
: (
(this.briefDisplayLimit && this.briefSummaryTitle && this.displayTplData && this.displayTplData.length > this.briefDisplayLimit)
? (this.displayTplData.length) + " " + this.briefSummaryTitle
: this.displayList
);
return ttl || this.emptyText;
},
lastQuery: '', // prevents clearing of the list after initial setValue
listConfig: {
getInnerTpl: function (displayField) {
return '<tpl for="."><div><img src="' + Ext.BLANK_IMAGE_URL + '" ' + 'class="ux-checkboxlistcombo-icon">{' + (displayField || 'text') + ':htmlEncode}</div></tpl>';
}
// from here down it's all about the first item checking/unchecking all others
,previousAllChecked: false,
previousCheckCount: 0,
listeners: {
beforeselect: function ( me, node, selections, options ) {
// since selectionChange does not provide info on which node changed,
// we need to determine whether the all item was selected...
var combo = Ext.getCmp(me.view.checkboxComboId);
me.view.somethingChecked = true;
me.view.allChecked = combo && combo.firstItemChecksAll && !node.data[combo.valueField];
return true;
},
selectionchange: function (dataViewModel, selections, options) {
var me = dataViewModel,combo = Ext.getCmp(me.view.checkboxComboId),
storeRecs, recs = [], d, i, j, nChecked, vField, dField, allState, allNodes, allItem,
grayCls = "ux-checkboxcombolist-tri",
somethingChecked = me.view.somethingChecked,
allChecked = me.view.allChecked;
me.view.somethingChecked = false;
me.view.allChecked = false; // beforeselect doesn't fire on deselect
if (combo && combo.firstItemChecksAll) {
allNodes = me.view.getNodes();
if (allNodes.length) {
allItem = Ext.get(allNodes[0]);
vField = combo.valueField;
dField = combo.displayField;
storeRecs = Ext.clone(me.store.getRange(0));
for (i=nChecked=0;i<storeRecs.length;i++) {
d = storeRecs[i].data;
d.checked = false;
for (j=0;selections && !d.checked && j<selections.length;j++) {
if (selections[j].data[vField] == d[vField]) {
d.checked = true;
if (i>0) {
nChecked++;
} else if (!combo.allSelectedTitle) {
combo.allSelectedTitle = d[dField];
}
}
}
recs.push(d);
}
allState = ( ( recs[0].checked && allChecked ) || (nChecked == recs.length-1 && somethingChecked))
? 1
: ( 0 < nChecked && nChecked < recs.length-1 ? 2 : 0);
me.view.suspendEvents();// suspend events, though selectAll & deselectAll send them anyway
me.suspendEvents();
switch (allState) {
case 0: // None
//me.deselectAll(true); // Nope, doesn't suspend events
combo.allSelected = false;
allItem.removeCls(grayCls);
setTimeout(function () { me.deselectAll(false); },1); // suspendEvent is ignored, so we hack using a timer
break;
case 1: // All
//me.selectAll(true); // Nope, doesn't suspend events
combo.allSelected = true;
allItem.removeCls(grayCls);
setTimeout(function () { me.selectAll(false); },1); // suspendEvent is ignored, so we hack using a timer
break;
case 2: // Some (gray out the ALL item)
allItem.addCls(grayCls);
combo.allSelected = false;
me.deselect(0,true); // in this case the suspendEvent flag works
break;
}
me.resumeEvents();// resume events, though selectAll & deselectAll send them anyway
me.view.resumeEvents();
console.log("CheckboxComboList.changed: " + allState + " / " + nChecked);
}
}
}
}
},
});
/* -- Add the following to your .css file --
.ux-checkboxlistcombo-icon {
float: left; width:16px; height:16px;
background-position: -1px -1px ! important; background-repeat: no-repeat ! important;
}
.x-boundlist-item>div>.ux-checkboxlistcombo-icon {
background: url('data:image/gif;base64,R0lGODlhEAAQAIcAAED/QI6Pj66zua+0urK3vLS5vbi7v7u+wby/wsHDxcLExsbHyMrLzMzNzcvP1c3R1s3R19TU1dTV1tDT2NDU2dLV2tTX29XY3Njb3tvb3Nrc39vd39zd3t3f4eDh4eDh4+Hi4uHi4+Lj5OPk5eTl5eXm5ubm5ubn5+jo6Onp6enp6urq6urr6+vr7Ovs7Ozs7O3t7e/v7/Dw8PLy8vT09PX19fb29gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAMAAAAALAAAAAAQABAAAAiKAAEIHEiwoMGDBQMoXMhw4cAANCJKnEgjwEMaAjJqHFDgQMWLAhyIdDDhwoYEHwVCDOngQQUMHUQsSAlgJQQKFjaEKIGiAU2IBC5o+IDiBQwYEn7SMNCBxIoYM6JyUIpgRAoYM2jUoAFCqYKrMmjYGGtCKYMIGTycYOGihQqlFClaVNmw7lyECAMCADs=');
}
.x-boundlist-item.x-boundlist-selected>div>.ux-checkboxlistcombo-icon {
background: url('data:image/gif;base64,R0lGODlhEAAQAIcAAENZkkRZkkZblEdclUhdlkhelklel0pfl0tgmFZpnV1woWBypGN1pWR2pWZ3p21+qkD/QHaGq46Pj4uZu4yZupCdva6zuZ2nwKOuyK+4z7C50be/1bm/0LrB1bzD1sbIysXJzcrLzMnM0MvP1c3P0cnP3czQ1s3R1tXV1tXY3NXZ3dra29vc3Nzd3s/U4tzf5N7g4d/i5d7h6d/i6eHi4uDi5ubm5ufn6Onp6uvr7Ozt7e3u7urs8O3u8fLz9PLz9vT09PX19fb29vj4+Pn5+fj5+gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAMAABAALAAAAAAQABAAAAicACEIHEiwoMGDBSUoXMhw4UAJQCJKBDKEiBAgEh4CscCRo4gLEV4EySgQooURKE30SACAgxCSEEyiPFEjQ4AGNzBqtKAiRgofCgR4YKGzJBAQJTDs6ECAwRAURWMCgfFAgAsHBzYIaREVIgkZAwocWFAECI2uQD7wmGAAgQaLNtCGWDGDQoUfOnLgQBsxCJEhQSbCbEhYIcLDEAICADs=');
}
.x-boundlist-item.ux-checkboxcombolist-tri>div>.ux-checkboxlistcombo-icon {
background: url('data:image/gif;base64,R0lGODlhEAAQAKU3AED/QI6Pj66zua+0urK3vLS5vbi7v7u+wby/wsHDxcLExsbHyMrLzMzNzcvP1c3R1s3R19TU1dTV1tDT2NDU2dLV2tTX29XY3Njb3tvb3Nrc39vd39zd3t3f4eDh4eDh4+Hi4uHi4+Lj5OPk5eTl5eXm5ubm5ubn5+jo6Onp6enp6urq6urr6+vr7Ovs7Ozs7O3t7e/v7/Dw8PLy8vT09PX19fb29v///////////////////////////////////yH5BAEAAD8ALAAAAAAQABAAAAZLwJ9wSCwaj8WAcslcDgO0qJRmq9ICT5pguzV4DTWsECr4mg028Y98/l6z5bbhPabJvfS1/Z6H3udqfnyBe3J9UjVVNVOBTY5qSEdBADs=');
}
*/
My implementation provides for "brief" display options to allow for more info in the displayField. For example, when dealing with US states it would make sense to use the full name in the list but a list of delimited abbreviations in the displayField. And you would want to control how many selected you would want to display before just saying something like "10 states". These options are in there.