/** * 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); } }});