Форк Rambox
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

513 lines
16 KiB

/**
* SegmentedButton is a container for a group of {@link Ext.button.Button Button}s. You
* may populate the segmented button's children by adding buttons to the items config. The segmented
* button's children enjoy the same customizations as regular buttons, such as
* menu, tooltip, etc. You can see usages of the various configuration
* possibilities in the example below.
*
* @example @preview
* Ext.create('Ext.button.Segmented', {
* renderTo: Ext.getBody(),
* allowMultiple: true,
* items: [{
* text: 'Segment Item 1',
* menu: [{
* text: 'Menu Item 1'
* }]
* },{
* text: 'Segment Item 2',
* tooltip: 'My custom tooltip'
* },{
* text: 'Segment Item 3'
* }],
* listeners: {
* toggle: function(container, button, pressed) {
* console.log("User toggled the '" + button.text + "' button: " + (pressed ? 'on' : 'off'));
* }
* }
* });
*
*/
Ext.define('Ext.button.Segmented', {
extend: 'Ext.container.Container',
xtype: 'segmentedbutton',
requires: [ 'Ext.button.Button' ],
config: {
/**
* @cfg {Boolean}
* Allow toggling the pressed state of each button.
* Only applicable when {@link #allowMultiple} is `false`.
*/
allowDepress: false,
/**
* @cfg {Boolean}
* Allow multiple pressed buttons.
*/
allowMultiple: false,
/**
* @cfg {Boolean}
* True to enable pressed/not pressed toggling.
*/
allowToggle: true,
/**
* @cfg {Boolean}
* True to align the buttons vertically
*/
vertical: false,
/**
* @cfg {String}
* Default {@link Ext.Component#ui ui} to use for buttons in this segmented button.
* Buttons can override this default by specifying their own UI
*/
defaultUI: 'default'
},
beforeRenderConfig: {
/**
* @cfg {String/Number/String[]/Number[]}
* The value of this button. When {@link #allowMultiple} is `false`, value is a
* String or Number. When {@link #allowMultiple is `true`, value is an array
* of values. A value corresponds to a child button's {@link Ext.button.Button#value
* value}, or its index if no child button values match the given value.
*
* Using the `value` config of the child buttons with single toggle:
*
* @example
* var button = Ext.create('Ext.button.Segmented', {
* renderTo: Ext.getBody(),
* value: 'optTwo', // begin with "Option Two" selected
* items: [{
* text: 'Option One',
* value: 'optOne'
* }, {
* text: 'Option Two',
* value: 'optTwo'
* }, {
* text: 'Option Three',
* value: 'optThree'
* }]
* });
*
* console.log(button.getValue()); // optTwo
*
* // Sets the value to optOne, and sets the pressed state of the "Option One" button
* button.setValue('optOne');
*
* console.log(button.getValue()); // optOne
*
* Using multiple toggle, and index-based values:
*
* @example
* var button = Ext.create('Ext.button.Segmented', {
* renderTo: Ext.getBody(),
* allowMultiple: true
* value: [1, 2], // begin with "Option Two" and "Option Three" selected
* items: [{
* text: 'Option One'
* }, {
* text: 'Option Two'
* }, {
* text: 'Option Three'
* }]
* });
*
* // Sets value to [0, 2], and sets pressed state of "Option One" and "Option Three"
* button.setValue([0, 2]);
*
* console.log(button.getValue()); // [0, 2]
*
* // Remove all pressed buttons, and set value to null
* button.setValue(null);
*/
value: undefined
},
/**
* @inheritdoc
*/
defaultBindProperty: 'value',
publishes: ['value'],
twoWayBindable: ['value'],
layout: 'segmentedbutton',
defaultType: 'button',
maskOnDisable: false,
isSegmentedButton: true,
baseCls: Ext.baseCSSPrefix + 'segmented-button',
itemCls: Ext.baseCSSPrefix + 'segmented-button-item',
// private
_firstCls: Ext.baseCSSPrefix + 'segmented-button-first',
// private
_lastCls: Ext.baseCSSPrefix + 'segmented-button-last',
// private
_middleCls: Ext.baseCSSPrefix + 'segmented-button-middle',
/**
* @event toggle
* Fires when any child button's pressed state has changed.
* @param {Ext.button.Segmented} this
* @param {Ext.button.Button} button The toggled button.
* @param {Boolean} isPressed `true` to indicate if the button was pressed.
*/
applyValue: function(value, oldValue) {
var me = this,
allowMultiple = me.getAllowMultiple(),
buttonValue, button, values, oldValues, items, i, ln;
values = (value instanceof Array) ? value : (value == null) ? [] : [value];
oldValues = (oldValue instanceof Array) ? oldValue :
(oldValue == null) ? [] : [oldValue];
// Set a flag to tell our toggle listener not to respond to the buttons' toggle
// events while we are applying the value.
me._isApplyingValue = true;
if (!me.rendered) {
// first time - add values of buttons with an initial config of pressed:true
items = me.items.items;
for (i = 0, ln = items.length; i < ln; i++) {
button = items[i];
if (button.pressed) {
buttonValue = button.value;
if (buttonValue == null) {
buttonValue = me.items.indexOf(button);
}
if (!Ext.Array.contains(values, buttonValue)) {
values.push(buttonValue);
}
}
}
}
ln = values.length;
//<debug>
if (ln > 1 && !allowMultiple) {
Ext.Error.raise('Cannot set multiple values when allowMultiple is false');
}
//</debug>
// press all buttons corresponding to the values
for (i = 0; i < ln; i++) {
value = values[i];
button = me._lookupButtonByValue(value);
if (button) {
buttonValue = button.value;
if ((buttonValue != null) && buttonValue !== value) {
// button has a value, but it was matched by index.
// transform the index into the button value
values[i] = buttonValue;
}
if (!button.pressed) {
button.setPressed(true);
}
}
//<debug>
else {
// no matched button. fail.
Ext.Error.raise("Invalid value '" + value + "' for segmented button: '" + me.id + "'");
}
//</debug>
}
value = allowMultiple ? values : ln ? values[0] : null;
// unpress buttons for the old values, if they do not exist in the new values array
for (i = 0, ln = oldValues.length; i < ln; i++) {
oldValue = oldValues[i];
if (!Ext.Array.contains(values, oldValue)) {
me._lookupButtonByValue(oldValue).setPressed(false);
}
}
me._isApplyingValue = false;
return value;
},
beforeRender: function() {
var me = this;
me.addCls(me.baseCls + me._getClsSuffix());
me._syncItemClasses(true);
me.callParent();
},
onAdd: function(item) {
var me = this,
syncItemClasses = '_syncItemClasses';
//<debug>
var items = me.items.items,
ln = items.length,
i = 0,
value, defaultUI;
if (item.ui === 'default' && !item.hasOwnProperty('ui')) {
defaultUI = me.getDefaultUI();
if (defaultUI !== 'default') {
item.ui = defaultUI;
}
}
for(; i < ln; i++) {
if (items[i] !== item) {
value = items[i].value;
if (value != null && value === item.value) {
Ext.Error.raise("Segmented button '" + me.id +
"' cannot contain multiple items with value: '" + value + "'");
}
}
}
//</debug>
me.mon(item, {
hide: syncItemClasses,
show: syncItemClasses,
toggle: '_onItemToggle',
scope: me
});
if (me.getAllowToggle()) {
item.enableToggle = true;
if (!me.getAllowMultiple()) {
item.toggleGroup = me.getId();
item.allowDepress = me.getAllowDepress();
}
}
item.addCls(me.itemCls + me._getClsSuffix());
me._syncItemClasses();
me.callParent([item]);
},
onRemove: function(item) {
var me = this;
item.removeCls(me.itemCls + me._getClsSuffix());
me._syncItemClasses();
me.callParent([item]);
},
beforeLayout: function() {
if (Ext.isChrome) {
// workaround for a chrome bug with table-layout:fixed elements where the element
// is layed out with 0 width, for example, in the following test case, without
// this workaround the segmented button has 0 width in chrome:
//
// Ext.create({
// renderTo: document.body,
// xtype: 'toolbar',
// items: [{
// xtype: 'segmentedbutton',
// items: [{
// text: 'Foo'
// }]
// }]
// });
//
// reading offsetWidth corrects the issue.
this.el.dom.offsetWidth; // jshint ignore:line
}
this.callParent();
},
updateDefaultUI: function(defaultUI) {
var items = this.items,
item, i, ln;
if (this.rendered) {
Ext.Error.raise("Changing the ui config of a segmented button after render is not supported.");
} else if (items) {
if (items.items) { // Mixed collection already created
items = items.items;
}
for (i = 0, ln = items.length; i < ln; i++) {
item = items[i];
if (item.ui === 'default' && defaultUI !== 'default' && !item.hasOwnProperty('ui') ) {
items[i].ui = defaultUI;
}
}
}
},
//<debug>
updateAllowDepress: function(newAllowDepress, oldAllowDepress) {
if (this.rendered && (newAllowDepress !== oldAllowDepress)) {
Ext.Error.raise("Changing the allowDepress config of a segmented button after render is not supported.");
}
},
updateAllowMultiple: function(newAllowMultiple, oldAllowMultiple) {
if (this.rendered && (newAllowMultiple !== oldAllowMultiple)) {
Ext.Error.raise("Changing the allowMultiple config of a segmented button after render is not supported.");
}
},
updateAllowToggle: function(newAllowToggle, oldAllowToggle) {
if (this.rendered && (newAllowToggle !== oldAllowToggle)) {
Ext.Error.raise("Changing the allowToggle config of a segmented button after render is not supported.");
}
},
updateVertical: function(newVertical, oldVertical) {
if (this.rendered && (newVertical !== oldVertical)) {
Ext.Error.raise("Changing the orientation of a segmented button after render is not supported.");
}
},
//</debug>
privates: {
_getClsSuffix: function() {
return this.getVertical() ? '-vertical' : '-horizontal';
},
// rtl hook
_getFirstCls: function() {
return this._firstCls;
},
// rtl hook
_getLastCls: function() {
return this._lastCls;
},
/**
* Looks up a child button by its value
* @private
* @param {String/Number} value The button's value or index
* @return {Ext.button.Button}
*/
_lookupButtonByValue: function(value) {
var items = this.items.items,
ln = items.length,
i = 0,
button = null,
buttonValue, btn;
for (; i < ln; i++) {
btn = items[i];
buttonValue = btn.value;
if ((buttonValue != null) && buttonValue === value) {
button = btn;
break;
}
}
if (!button && typeof value === 'number') {
// no button matched by value, assume value is an index
button = items[value];
}
return button;
},
/**
* Handles the "toggle" event of the child buttons.
* @private
* @param {Ext.button.Button} button
* @param {Boolean} pressed
*/
_onItemToggle: function(button, pressed) {
if (this._isApplyingValue) {
return;
}
var me = this,
Array = Ext.Array,
allowMultiple = me.allowMultiple,
buttonValue = (button.value != null) ? button.value : me.items.indexOf(button),
value = me.getValue(),
valueIndex;
if (allowMultiple) {
valueIndex = Array.indexOf(value, buttonValue);
}
if (pressed) {
if (allowMultiple) {
if (valueIndex === -1) {
value.push(buttonValue);
}
} else {
value = buttonValue;
}
} else {
if (allowMultiple) {
if (valueIndex > -1) {
value.splice(valueIndex, 1);
}
} else if (value === buttonValue) {
value = null;
}
}
me.setValue(value);
me.fireEvent('toggle', me, button, pressed);
},
/**
* Synchronizes the "first", "last", and "middle" css classes when buttons are
* added, removed, shown, or hidden
* @private
* @param {Boolean} force force sync even if not rendered.
*/
_syncItemClasses: function(force) {
var me = this,
firstCls, middleCls, lastCls, items, ln, visibleItems, item, i;
if (!force && !me.rendered) {
return;
}
firstCls = me._getFirstCls();
middleCls = me._middleCls;
lastCls = me._getLastCls();
items = me.items.items;
ln = items.length;
visibleItems = [];
for (i = 0; i < ln; i++) {
item = items[i];
if (!item.hidden) {
visibleItems.push(item);
}
}
ln = visibleItems.length;
//remove all existing classes from visible items
for (i = 0; i < ln; i++) {
visibleItems[i].removeCls([ firstCls, middleCls, lastCls ]);
}
// do not add any classes if there is only one item (no border removal needed)
if (ln > 1) {
//add firstCls to the first visible button
visibleItems[0].addCls(firstCls);
//add middleCls to all visible buttons in between
for (i = 1; i < ln - 1; i++) {
visibleItems[i].addCls(middleCls);
}
//add lastCls to the first visible button
visibleItems[ln - 1].addCls(lastCls);
}
}
}
});