/**
* A split button that provides a built-in dropdown arrow that can fire an event separately
* from the default click event of the button. Typically this would be used to display a dropdown
* menu that provides additional options to the primary button action, but any custom handler
* can provide the arrowclick implementation.
* Example usage:
*
* @example
* // display a dropdown menu:
* Ext.create('Ext.button.Split', {
* renderTo: Ext.getBody(),
* text: 'Options',
* // handle a click on the button itself
* handler: function() {
* alert("The button was clicked");
* },
* menu: new Ext.menu.Menu({
* items: [
* // these will render as dropdown menu items when the arrow is clicked:
* {text: 'Item 1', handler: function(){ alert("Item 1 clicked"); }},
* {text: 'Item 2', handler: function(){ alert("Item 2 clicked"); }}
* ]
* })
* });
*
* Instead of showing a menu, you can provide any type of custom functionality you want when
* the dropdown arrow is clicked:
*
* Ext.create('Ext.button.Split', {
* renderTo: 'button-ct',
* text: 'Options',
* handler: optionsHandler,
* arrowHandler: myCustomHandler
* });
*
*/
Ext.define('Ext.button.Split', {
extend: 'Ext.button.Button',
alternateClassName: 'Ext.SplitButton',
alias: 'widget.splitbutton',
isSplitButton: true,
/**
* @cfg {Function/String} arrowHandler
* A function called when the arrow button is clicked (can be used instead of click event)
* @cfg {Ext.button.Split} arrowHandler.this
* @cfg {Event} arrowHandler.e The click event.
* @controllable
*/
/**
* @cfg {String} arrowTooltip
* The title attribute of the arrow.
*/
/**
* @cfg {Boolean} [separateArrowStyling=false] If enabled, arrow element mouseover,
* click, and focus events will be handled separately from main element and
* corresponding hover, pressed, and focused states will be added separately
* to main element and arrow element, respectively.
*
* This requires theme support for extended states (see Graphite theme).
*
* @since 6.6.0
* @private
*/
separateArrowStyling: false,
/**
* @private
*/
arrowCls: 'split',
split: true,
componentCls: Ext.baseCSSPrefix + 'split-button',
/**
* @event arrowclick
* Fires when this button's arrow is clicked.
* @param {Ext.button.Split} this
* @param {Event} e The click event.
*/
// It is possible to use both menu and arrowHandler with a Split button, which is confusing
// and will clash with WAI-ARIA requirements. So we check that and warn if need be.
// Unfortunately this won't work with arrowclick event that can be subscribed to
// dynamically but we don't want to run these checks at run time so there's a limit
// to what we can do about it.
//<debug>
initComponent: function() {
var me = this;
// Don't warn if we're under the slicer. Only check hasListeners of the component
// instance; there could be listeners on the EventBus inherited via prototype.
if (me.menu && (me.arrowHandler || me.hasListeners.hasOwnProperty('arrowclick'))) {
Ext.ariaWarn(
me,
"Using both menu and arrowHandler config options in Split buttons " +
"leads to confusing user experience and conflicts with accessibility " +
"best practices. See WAI-ARIA 1.0 Authoring guide: " +
"http://www.w3.org/TR/wai-aria-practices/#menubutton"
);
}
me.callParent();
},
//</debug>
getTemplateArgs: function() {
var me = this,
ariaAttr, data;
data = me.callParent();
if (me.disabled) {
data.tabIndex = null;
}
ariaAttr = me.ariaArrowElAttributes || {};
ariaAttr['aria-hidden'] = !!me.hidden;
ariaAttr['aria-disabled'] = !!me.disabled;
if (me.arrowTooltip) {
ariaAttr['aria-label'] = me.arrowTooltip;
}
else {
ariaAttr['aria-labelledby'] = me.id;
}
data.arrowElAttributes = ariaAttr;
return data;
},
onRender: function() {
var me = this,
el;
me.callParent();
el = me.getFocusEl();
if (el) {
el.on({
scope: me,
focus: me.onMainElFocus,
blur: me.onMainElBlur
});
}
el = me.arrowEl;
if (el) {
el.dom.setAttribute('data-componentid', me.id);
el.setVisibilityMode(Ext.dom.Element.DISPLAY);
el.on({
scope: me,
focus: me.onArrowElFocus,
blur: me.onArrowElBlur
});
}
},
/**
* Sets this button's arrow click handler.
* @param {Function} handler The function to call when the arrow is clicked.
* @param {Object} scope (optional) Scope for the function passed above.
*/
setArrowHandler: function(handler, scope) {
this.arrowHandler = handler;
this.scope = scope;
},
onMouseDown: function(e) {
var me = this;
if (me.separateArrowStyling && !me.disabled && e.button === 0 &&
me.isWithinTrigger(e)) {
e.preventDefault();
me.arrowEl.focus();
Ext.button.Manager.onButtonMousedown(me, e);
me.addCls(me._arrowPressedCls);
}
else {
me.callParent([e]);
}
},
onMouseUp: function(e) {
var me = this;
if (me.separateArrowStyling && !me.destroyed && e.button === 0 &&
me.isWithinTrigger(e)) {
me.removeCls(me._arrowPressedCls);
}
else {
me.callParent([e]);
}
},
onMenuTriggerOver: function(e) {
var me = this;
if (me.separateArrowStyling && !me.disabled) {
me.addCls(me._arrowOverCls);
}
me.callParent([e]);
},
onMenuTriggerOut: function(e) {
var me = this;
if (me.separateArrowStyling && !me.disabled) {
me.removeCls(me._arrowOverCls);
}
me.callParent([e]);
},
/**
* @private
*/
onClick: function(e) {
var me = this,
arrowKeydown = e.type === 'keydown' && e.target === me.arrowEl.dom;
me.doPreventDefault(e);
if (!me.disabled) {
if (arrowKeydown || me.isWithinTrigger(e)) {
// Force prevent default here, if we click on the arrow part
// we want to trigger the menu, not any link if we have it
e.preventDefault();
me.maybeShowMenu(e);
me.fireEvent("arrowclick", me, e);
if (me.arrowHandler) {
me.arrowHandler.call(me.scope || me, me, e);
}
}
else {
me.doToggle();
me.fireHandler(e);
}
}
},
enable: function(silent) {
var me = this,
arrowEl = me.arrowEl;
me.callParent([silent]);
// May not be rendered yet
if (arrowEl) {
arrowEl.dom.setAttribute('tabIndex', me.tabIndex);
arrowEl.dom.setAttribute('aria-disabled', 'false');
}
},
disable: function(silent) {
var me = this,
arrowEl = me.arrowEl;
me.callParent([silent]);
// May not be rendered yet
if (arrowEl) {
arrowEl.dom.removeAttribute('tabIndex');
arrowEl.dom.setAttribute('aria-disabled', 'true');
}
},
afterHide: function(cb, scope) {
this.callParent([cb, scope]);
this.arrowEl.dom.setAttribute('aria-hidden', 'true');
},
afterShow: function(animateTarget, cb, scope) {
this.callParent([animateTarget, cb, scope]);
this.arrowEl.dom.setAttribute('aria-hidden', 'false');
},
privates: {
isFocusing: function(e) {
var me = this,
from = e.fromElement,
to = e.toElement,
focusEl = me.focusEl && me.focusEl.dom,
arrowEl = me.arrowEl && me.arrowEl.dom;
if (me.focusable) {
if (to === focusEl) {
return from === arrowEl ? false : true;
}
else if (to === arrowEl) {
return from === focusEl ? false : true;
}
return true;
}
return false;
},
isBlurring: function(e) {
var me = this,
from = e.fromElement,
to = e.toElement,
focusEl = me.focusEl && me.focusEl.dom,
arrowEl = me.arrowEl && me.arrowEl.dom;
if (me.focusable) {
if (from === focusEl) {
return to === arrowEl ? false : true;
}
else if (from === arrowEl) {
return to === focusEl ? false : true;
}
return true;
}
return false;
},
// We roll our own focus style handling for Split button, see below
getFocusClsEl: Ext.privateFn,
onMainElFocus: function(e) {
this.el.addCls(this._focusCls);
},
onMainElBlur: function(e) {
this.el.removeCls(this._focusCls);
},
onArrowElFocus: function(e) {
this.el.addCls(this._arrowFocusCls);
},
onArrowElBlur: function() {
this.el.removeCls(this._arrowFocusCls);
},
setTabIndex: function(newTabIndex) {
this.callParent([newTabIndex]);
// May not be rendered yet
if (this.arrowEl) {
this.arrowEl.set({ tabIndex: newTabIndex });
}
},
// This and below are called by the setMenu method in the parent class.
_addSplitCls: function() {
var arrowEl = this.arrowEl;
this.callParent();
arrowEl.dom.setAttribute('tabIndex', this.tabIndex);
arrowEl.setVisible(true);
},
_removeSplitCls: function() {
var arrowEl = this.arrowEl;
this.callParent();
arrowEl.dom.removeAttribute('tabIndex');
arrowEl.setVisible(false);
}
}
});