PDA

View Full Version : ExtJs4 - Checkbox List Combo



wiz61
17 Jul 2011, 12:47 PM
As part of a UI redesign project, I have implemented a CheckboxListCombo control for ExtJs4, the code is shown below. I've added some explanation afterward. In addition, there's an issue that might be an ExtJs bug that I could use some help with (though it does work as is).



// 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.

The implementation ended up being fairly straight-forward. I had a 3.3 version that was loosely based on Saki's lovcombo, but it turned out that the changes in ExtJs4 made moving that over more trouble than it was worth. The first 40% or so of the code implements the simple case, mainly using css styles. The getDisplayText() function determines what to display in the combo text field. It does a fair amount of stuff in order to provide some flexibility; i.e., it will build the "brief" list even if it only shows the summary, so you could access the list for use in a panel (I needed it for a grid).

In addition to a simple checklist, I also added the ability to have the first item control the check/uncheck of all the rest, as well as provide an indicator of the all/some/none state. It uses a gray box if some are checked, unchecked if none and checked if all are checked. Getting the check/uncheck all to work was a bit trickier than I expected, as I am having some issues related to suspending events when doing a selectAll or deslectAll, as the pamater to not fire any events is ignored (just trace through and you'll see), so a little help here should help me fix that piece. As it is the combo will fire several events when you check or uncheck the first item.

In any event, I hope you find this to be useful.

-Gary Skiba (Austin, TX)

jwalters3
13 Sep 2011, 4:41 AM
I have three items that I need checkboxes for, and need to put them in a drop down combo box inside a grid. But, I have a bitwise value in the store. How could I implement a checkbox list combo to read from the bitwise store value? Thanks!

crysfel
14 Sep 2011, 9:39 AM
Hi @wiz61, do you have an online demo?

Cheers

andong
3 Jun 2012, 5:22 PM
Are there any demos or screenshot?