PDA

View Full Version : Multiselect 'CheckboxSelect' control



prometheus
24 Apr 2009, 12:22 PM
Sorry for double post (this post originaly created on Ext 2 subforum, but control based on Ext 3 - I think that should works on ver 2 but never tested), it's my mistake - that topic being unmonitored in the future.

Hi, this is a composite control using a combobox for selecting items and checkboxes for displaying and unselecting preivously selected items. This is great for users because their selections are visible but not reserve a lot of space (like a list) and very easy to use (not need any knowledge to handle). Component handled like any other form fields and able to load initial values from remote server in a practical way using a simple array.
Why this component created? Because most of my useres have a lot of problem with Ctrl-click selection and default auto multiselection. Another reason that I don`t like to see list controls in a complex layout. My goal is a practial, intuitive and easily oversightable solution.

Warning! This component isn`t a well-tested, it`s under developed and seems it`s works. I`m only tested on FF3. Code documentation is actual but has some formal bug which will be fixed if I have enough time to it, sorry.

Usability tips:
Select tags - a very visible implementation, user could type one of the available tags, tag added if return pressed, checkbox with tag appears.
Any case where there are so many items available, list seems like unlimited, and we do not want to force the user to "scroll and scroll and select one or some" - this control should be a very useful solution because user colud select items very easily and will alvays see what he/she choosed.
etc..
Changelog after first revision shared:

[2009 May, 16]
config setting 'checkboxArrayName' becomes 'name' to better fit to the fields standard.
added 'hiddenName' config which if specified, control creates a hidden field with that name to store the field`s raw value to able to submit that to server-side.
added more of standard field config settings: cls, fieldClass, focusClass, invalidClass, invalidText, msgFx, msgTarget, readOnly (not implemented yet), tabIndex, validateOnBlur, validationEvent, value.
added standard field methods: clearInvalid, getName, getRawValue, getValue, isDirty, isValid, markInvalid, reset, setValue, setRawValue, validate.
updated example code (you can see that highlighted at end of this post).
bugfix deleted two IE-killer commas. :)
example code extracted to here from code
bugfix added an extra checking to setRawValue() method
feature added mode=local to default combo config for usability reasons[2009 May, 23]
comboConfig now supports user specified xtype for specifing any ComboBox-derived control in there (for example my ComboGrid (http://extjs.com/forum/showthread.php?t=68618) solution :) ).
Actual code base:

/**
* Ext.sm.Form.CheckboxSelect - ExtJS Library for sYs-mini SDK
*
* For any licensing informations ask licensing@extjs.com or visit
* http://extjs.com/license
*
* @author Csaba Dobai (prometheus) <fejlesztes@php-sparcle.hu>
* @copyright All rights reserved by author, based on ExtJS 3.0 licensing!
*/

Ext.namespace('Ext.sm.Form');

/**
* @class Ext.sm.Form.CheckboxSelect
* @extends Ext.Panel
* <p><b>Represent a composite control to realize special multiselection.</b></p>
* <p>This control displays an Ext.form.ComboBox, and a panel below the combobox.
* If you select an item from the combobox, a checked checkbox appears within the
* panel. User can uncheck a checkbox, then checkbox disappears (going to remove).</p>
* <p>You can configure fully the ComboBox as usual within the {@link #comboConfig}.
* Checkboxes are displayed in columns, next to each other. On default the control
* has three columns for that, but you can override this by set the {@link #checkboxColumns}
* configuration setting. Checkbox values parsed as array of values. For example you
* have a ComboBox with items ['first'='One', 'second'='Two'] (value=text) and {@link name}
* has the value 'numbers' - now if you select all items, you have two checkboxes on
* the form, the parsed result is similar: <i>...numbersItem[]=first&numbersItem[]=second</i>.</p>
* <p>The code below illustrates how the component works:</p><pre><code>
var win = new Ext.Window({
title : 'Test',
items : [{
xtype : 'form',
autoHeight : true,
ctCls: 'testForm',
items : [{
xtype : 'checkboxselect',
name : 'numbers',
hiddenName: 'numbersRaw',
fieldLabel : 'Test',
comboConfig : {
store : [['1', 'One'], ['2', 'Two'], ['3', 'Three'], ['4', 'Four']]
}
}],
buttons: [{
text: 'Save',
handler: function()
{
var fp = this.findParentByType('form');

if(fp.getForm().isValid()){
Ext.Msg.alert('Submitted Values', 'The following values could get by the server: <br />'+
fp.getForm().getValues(true));
}
}
}]
}],
listeners: {
afterrender: function(self)
{
self.find('ctCls', 'testForm')[0].load({
url: '/cstest.php'
});
}
}
});

win.show();
</code></pre></p>
*/
Ext.sm.Form.CheckboxSelect = Ext.extend(Ext.Panel, {
/**
* @cfg {String} cls A custom CSS class to apply to the field's underlying
* element (defaults to ""). Mapped to ComboBox.
*/

/**
* @cfg {String} fieldClass The default CSS class for the field (defaults
* to "x-form-field"). Mapped to ComboBox.
*/

/**
* @cfg {String} focusClass The CSS class to use when the field receives
* focus (defaults to "x-form-focus"). Mapped to ComboBox.
*/

/**
* @cfg {String} invalidClass The CSS class to use when marking a field
* invalid (defaults to "x-form-invalid").
*/
invalidClass: 'x-form-invalid',

/**
* @cfg {String} invalidText The error text to use when marking a field
* invalid and no message is provided (defaults to "The value in this field
* is invalid"). Mapped to ComboBox.
*/

/**
* @cfg {String} msgFx <b>Experimental</b> The effect used when displaying a
* validation message under the field (defaults to 'normal').
*/
msgFx: 'normal',

/**
* @cfg {String} msgTarget The location where error text should display.
* Should be one of the following values (defaults to 'qtip'):
* <pre>
Value Description
----------- ----------------------------------------------------------------------
qtip Display a quick tip when the user hovers over the field
title Display a default browser title attribute popup
under Add a block div beneath the field containing the error text
side Add an error icon to the right of the field with a popup on hover
[element id] Add the error text directly to the innerHTML of the specified element
</pre>
*/
msgTarget : 'qtip',

/**
* @cfg {String} name Name of Ext.form.Checkbox components. A
* default 'unnamed' set, but you must change this if you want a normal result
* on the server-side, or use this field by form load.
*/
name: 'unnamed',

/**
* @cfg {Boolean} readOnly True to mark the field as readOnly in HTML
* (defaults to false) -- Note: this only sets the element's readOnly DOM
* attribute. <b>This is mapped to all subfields!</b>
*/
readOnly : false,

/**
* @cfg {Number} tabIndex The tabIndex for this field. Note this only
* applies to fields that are rendered, not those which are built via
* applyTo (defaults to undefined). Mapped to ComboBox.
*/

/**
* @cfg {Boolean} validateOnBlur Whether the field should validate when
* it loses focus (defaults to true). Mapped to ComboBox.
*/

/**
* @cfg {String/Boolean} validationEvent The event that should initiate
* field validation. Set to false to disable automatic validation (defaults
* to "keyup"). Mapped to ComboBox.
*/

/**
* @cfg {Mixed} value A value to initialize this field with (defaults to
* undefined). Mapped to ComboBox.
*/

// private - indicates that this is a form field for BasicForm.
isFormField: true,

// private
originalValue : null,

// private
comboConfigDefault : {
xtype: 'combo',
typeAhead : true,
forceSelection : true,
triggerAction : "all",
selectOnFocus :true,
mode: 'local'
},

// private
cbpanelConfigDefault : {
xtype : "panel",
border : false,
layout : 'column'
},

// private
cbpanelColConfig : {
defaultType : 'checkbox',
layout : 'form',
border : false,
defaults : {
hideLabel : true,
anchor : '100%'
}
},

// private
combo : null,

// private
checkboxPanel : null,

// private
privateValueField : null,

/**
* @cfg {String} hiddenName
* This is the name of used hidden field for storing raw value for correctly
* submitting this field. If this has an empty (null, undefined, etc) value,
* hidden field will not created. Default is undefined.
*/

/**
* @cfg {Object} comboConfig
* This is the config of component`s ComboBox, here you can set the fieldLabel and
* store etc on it. Default settings are: typeAhead=true, forceSelection=true.
*/
comboConfig : {},

/**
* @cfg {Integer} checkboxColumns
* Number of columns in checkbox display panel. Default is 3.
*/
checkboxColumns : 3,
/**
* @cfg {String/Object} layout
* Layout is fixed to 'form', because this panel is going to a FormPanel
* component now.
*/

// private
lastColIdx : 0,

// private
initComponent : function()
{
var comboConfig = (this.comboConfig == undefined ? {} : this.comboConfig);
var colCfg;
var columns = [];

Ext.sm.Form.CheckboxSelect.superclass.initComponent.call(this);
this.border = false;

// combo
this.mapToCombo();
comboConfig = Ext.applyIf(comboConfig, this.comboConfigDefault);
this.combo = new Ext.ComponentMgr.create(comboConfig);

// checkbox panel
for (var i=0; i<this.checkboxColumns; i++)
{
colCfg = Ext.apply({}, this.cbpanelColConfig);
columns.push(colCfg);
}
Ext.apply(this.cbpanelConfigDefault, {
layoutConfig : {columns : this.checkboxColumns},
items : columns
});
this.checkboxPanel = new Ext.Panel(this.cbpanelConfigDefault);

// listener
this.combo.on('select', function(self, record, index){this.onComboSelect(self, record, index)}.createDelegate(this));

// add to me
this.add(this.combo);
this.add(this.checkboxPanel);
},

// private
onComboSelect : function(self, record, index)
{
var vf = this.combo.valueField;
var df = this.combo.displayField;

this.addCheckbox(record.data[df], record.data[vf]);

return true;
},

// private
addCheckbox : function(text, value)
{
var cb;
var p;

if (this.checkboxPanel.find('value', value).length == 0)
{
p = this.getColumnNext();
cb = new Ext.form.Checkbox({
boxLabel : text,
name : this.name + '[]',
value: value,
inputValue : value,
checked : true
});
cb.on('check', function(self, checked){this.onCheckboxCheck(self, checked)}.createDelegate(this));
p.add(cb);
p.bubble(function(){this.render(); return true;});
}
},

// private
onCheckboxCheck : function(self, checked)
{
if (!checked)
{
this.removeCheckbox(self);
}
},

/**
* Removes the passed checkbox from the panel and reorders the other.
* @param {Component} cb The checkbox to remove.
*/
removeCheckbox : function(cb)
{
var cbColList = [];
var cbConfigList = [];
var p;
var i, j = 0;
var maxLength = 0;
var cbi;

// Clone all checkbox components in order, except the removed one.
for (var i=0; i<this.checkboxColumns; i++)
{
cbColList.push(this.checkboxPanel.get(i).findByType('checkbox'));
}
for (var i=0; i<cbColList.length; i++)
{
if (maxLength < cbColList[i].length) maxLength = cbColList[i].length;
}
for (var i=0; i<maxLength; i++)
{
for (var j=0; j<this.checkboxColumns; j++)
{
cbi = this.checkboxPanel.get(j).get(i);
if (cbi !== undefined && cbi.getId() != cb.getId())
{
cbConfigList.push(cbi.cloneConfig());
}
}
}

// Remove checkboxes from all columns.
this.removeAllCheckboxes();

// recreate all checkboxes
this.lastColIdx = 0;
for (var i=0; i<cbConfigList.length; i++)
{
p = this.getColumnNext();
cbConfigList[i].on('check', function(self, checked){this.onCheckboxCheck(self, checked)}.createDelegate(this));
p.add(cbConfigList[i]);
p.bubble(function(){this.render(); return true;});
}
},

// private
removeAllCheckboxes: function()
{
for (var i=0; i<this.checkboxColumns; i++)
{
this.checkboxPanel.get(i).removeAll();
}
},

// private
getColumnNext : function()
{
var key = this.lastColIdx;

this.lastColIdx++;
if (this.lastColIdx >= this.checkboxColumns) this.lastColIdx = 0;

return this.checkboxPanel.get(key);
},

/**
* Returns selected items` values in array.
* @return {Array}
*/
getSelectedValues : function()
{
var result = [];
var chs;

chs = this.checkboxPanel.findByType('checkbox');
for (var i=0; i<chs.length; i++)
{
result.push(chs[i].inputValue);
}

return result;
},

//***
// Now implementing necessary Ext.form.Field methods.
//

/**
* Clear any invalid styles/messages for this field. Mapped to ComboBox.
*/
clearInvalid : function()
{
this.combo.clearInvalid();
},

/**
* Returns the name attribute of the field if available.
* @return {String} name The field {@link Ext.form.Field#name name} or {@link Ext.form.ComboBox#hiddenName hiddenName}
*/
getName : function()
{
return (Ext.isEmpty(this.name)? null : this.name);
},

/**
* Returns the raw data value which may or may not be a valid, defined value.
* To return a normalized value see {@link #getValue}. This value on this field
* is a dynamic set of values like 'value[]=1&value[]=2&value[]=3'.
* @return {String} value The field value
*/
getRawValue : function()
{
var v = this.getSelectedValues();
var ra = [];
var result;

if (this.rendered)
{
for (var i=0; i<v.length; i++)
{
ra.push(this.name + '[]=' + v[i]);
}
}

result = (ra.length == 0? '' : ra.join('&'));

return result;
},

/**
* Returns the normalized data value (undefined or emptyText will be returned
* as ''). To return the raw value see {@link #getRawValue}.
* @return {String} value The field value
*/
getValue : function()
{
return this.getRawValue();
},

/**
* <p>Returns true if the value of this Field has been changed from its original value,
* and is not disabled.</p>
* <p>Note that if the owning {@link Ext.form.BasicForm form} was configured with
* {@link Ext.form.BasicForm}.{@link Ext.form.BasicForm#trackResetOnLoad trackResetOnLoad}
* then the <i>original value</i> is updated when the values are loaded by
* {@link Ext.form.BasicForm}.{@link Ext.form.BasicForm#setValues setValues}.</p>
* @return {Boolean} True if this field has been changed from its original value (and
* is not disabled), false otherwise.
*/
isDirty : function()
{
if (this.disabled)
{
return false;
}

return String(this.getValue()) !== String(this.originalValue);
},

/**
* Returns whether or not the field value is currently valid. Mapped to ComboBox.
* @param {Boolean} preventMark True to disable marking the field invalid
* @return {Boolean} True if the value is valid, else false
*/
isValid : function(preventMark)
{
return this.combo.isValid(preventMark);
},

/**
* Mark this field as invalid, using {@link #msgTarget} to determine how to
* display the error and applying {@link #invalidClass} to the field's element.
* @param {String} msg (optional) The validation message (defaults to {@link #invalidText})
*/
markInvalid : function(msg)
{
var mt, t;

if (!this.rendered || this.preventMark) // not rendered
{
return;
}

msg = msg || this.invalidText;
mt = this.getMessageHandler();
if (mt)
{
mt.mark(this, msg);
}
else if (this.msgTarget)
{
this.el.addClass(this.invalidClass);
t = Ext.getDom(this.msgTarget);
if (t)
{
t.innerHTML = msg;
t.style.display = this.msgDisplay;
}
}

// TODO: events implementation.
//this.fireEvent('invalid', this, msg);
},

/**
* Resets the current field value to the originally loaded value and clears
* any validation messages.
* See {@link Ext.form.BasicForm}.{@link Ext.form.BasicForm#trackResetOnLoad trackResetOnLoad}
*/
reset : function()
{
this.clearInvalid();
this.combo.reset();
this.removeAllCheckboxes();
this.setValue(this.originalValue);
},

/**
* Sets a data value into the field and validates it. To set the value directly
* without validation see {@link #setRawValue}. Applied values are type of
* String and Array.
* @param {String/Array} value The value to set
* @return {Ext.form.Field} this
*/
setValue : function(value)
{
this.value = '';

if (!Ext.isEmpty(value))
{
this.value = this.valueIfArray(value);
}

if (this.rendered)
{
this.setRawValue(this.value);
this.validate();
}

return this;
},

/**
* Sets the values by creating all checkboxes that are existed by combo`s
* store, bypassing validation. To set the value with validation see {@link #setValue}.
* @param {String/Array} value The value to set
* @return {String} value The field value that is set
*/
setRawValue : function(value)
{
var va = [];
var vi, v, dr;
var vf = this.combo.valueField;
var df = this.combo.displayField;
var result;

if (!Ext.isEmpty(value))
{
va = this.valueIfArray(value).split('&');
}

if (this.combo.store && this.combo.store.getCount() == 0) this.combo.store.load();

for (var i=0; i<va.length; i++)
{
vi = va[i].split('=');
v = (vi.length == 1? '' : vi[1]); // if has no value for this item.
dr = this.combo.findRecord(vf, v);

this.addCheckbox(dr.data[df], v);
}

result = this.getValue();

if (!Ext.isEmpty(this.hiddenName))
{
if (Ext.isEmpty(this.privateValueField))
{
this.privateValueField = new Ext.form.Hidden({
name: this.hiddenName,
value: result,
inputValue: result
});
this.add(this.privateValueField);
}
else
{
this.privateValueField.setValue(result);
}
}

return result;
},

/**
* Validates the field value. Mapped to ComboBox.
* @return {Boolean} True if the value is valid, else false
*/
validate : function()
{
return this.combo.validate();
},

// private
afterRender : function()
{
Ext.sm.Form.CheckboxSelect.superclass.afterRender.call(this);
// TODO: This is the place for initEvents() if needed.
//this.initEvents();
this.initValue();
},

// private
initValue : function()
{
if (this.value !== undefined)
{
this.setValue(this.value);
}
else
{
this.setValue(this.getValue());
}
// reference to original value for reset
this.originalValue = this.getValue();
},

// private
getMessageHandler : function()
{
return Ext.form.MessageTargets[this.msgTarget];
},

// private
valueIfArray : function(value)
{
var result = value;
var ri;
var ra = [];

if (Ext.isArray(value) && !Ext.isEmpty(value[0]))
{
for (var i=0; i<value.length; i++)
{
ri = this.name + '[]=' + value[i];
ra.push(ri);
}
result = ra.join('&');
}

return result;
},

// private
mapToCombo : function()
{
var toMap = [
'cls',
'fieldClass',
'focusClass',
'invalidText',
'tabIndex',
'validateOnBlur',
'validationEvent',
];

for (var i=0; i<toMap.length; i++)
{
if (this[toMap[i]] !== undefined) this.comboConfigDefault[toMap[i]] = this[toMap[i]];
}
}
});

Ext.reg('checkboxselect', Ext.sm.Form.CheckboxSelect);Example that demonstrated the magic of using:

var win = new Ext.Window({
title : 'Test',
items : [{
xtype : 'form',
autoHeight : true,
ctCls: 'testForm',
items : [{
xtype : 'checkboxselect',
name : 'numbers',
hiddenName: 'numbersRaw',
fieldLabel : 'Test',
comboConfig : {
store : [['1', 'One'], ['2', 'Two'], ['3', 'Three'], ['4', 'Four']]
}
}],
buttons: [{
text: 'Save',
handler: function()
{
var fp = this.findParentByType('form');

if(fp.getForm().isValid()){
Ext.Msg.alert('Submitted Values', 'The following values could get by the server: <br />'+
fp.getForm().getValues(true));
}
}
}]
}],
listeners: {
afterrender: function(self)
{
self.find('ctCls', 'testForm')[0].load({
url: '/cstest.json'
});
}
}
});

win.show();Content of cstest.json is:

{"success":true,"data":{"numbers":[1,3]}}Any bugreports, replies or comments are welcomed!


Regards,