/** * A menu item that contains a togglable checkbox by default, but that can also be * a part of a radio group. * * @example * Ext.create({ * xtype: 'menu', * renderTo: Ext.getBody(), * width: 100, * items: [{ * xtype: 'menucheckitem', * text: 'select all' * },{ * xtype: 'menucheckitem', * text: 'select specific' * },{ * iconCls: 'add16', * text: 'icon item' * },{ * text: 'regular item' * }] * }); * * @since 6.5.0 */Ext.define('Ext.menu.CheckItem', { extend: 'Ext.menu.Item', xtype: 'menucheckitem', /** * @property {Boolean} isMenuCheckItem * `true` in this class to identify an object as an instantiated Menu CheckItem, or subclass thereof. */ isMenuCheckItem: true, /** * @cfg {Boolean} hideOnClick * Whether to not to hide the owning menu when this item is clicked. * Defaults to `false` for checkbox items, and radio group items. */ hideOnClick: false, config: { /** * @cfg {Boolean} checked * True to render the menuitem initially checked. */ checked: false, /** * @cfg {Function/String} checkHandler * @param {Ext.menu.CheckItem} This menu CheckItem * @param {Boolean} checked The new checked state * Alternative for the {@link #checkchange} event. Gets called with the same parameters. * @controllable */ checkHandler: null, /** * @cfg {Object} scope * Scope for the {@link #checkHandler} callback. */ /** * @cfg {Boolean} checkChangeDisabled * True to prevent the checked item from being toggled. Any submenu will still be accessible. */ checkChangeDisabled: false, /** * @cfg {String} value * The value of this item when {@link #cfg!checked} is `true`. * If this is not specified, the value defaults to the {@link #cfg!text} value. */ value: null, showCheckbox: null }, /** * @cfg [publishes='checked'] * @inheritdoc Ext.mixin.Bindable#cfg-publishes */ classCls: Ext.baseCSSPrefix + 'menucheckitem', checkedCls: Ext.baseCSSPrefix + 'checked', checkboxIconElCls: Ext.baseCSSPrefix + 'checkbox-icon-el', ariaRole: 'menuitemcheckbox', defaultBindProperty: 'checked', /** * @cfg {String} submenuText * Text to be announced by screen readers when a check item submenu is focused. * @locale */ submenuText: '{0} submenu', /** * @hide * Not supported on CheckItems and RadioItems */ href: null, /** * @hide * Not supported on CheckItems and RadioItems */ target: null, /** * @event beforecheckchange * Fires before a a user invoked check change. This does *not* fire when a programmatic * check change is performed. * @param {Ext.menu.CheckItem} this CheckItem * @param {Boolean} checked */ /** * @event checkchange * Fires after a change event. * @param {Ext.menu.CheckItem} this CheckItem * @param {Boolean} checked */ element: { reference: 'element', tabindex: Ext.is.iOS ? -1: null, cls: Ext.baseCSSPrefix + 'unselectable ' + // The checkbox always occupies the "left" icon space Ext.baseCSSPrefix + 'has-left-icon' }, focusEl: 'checkboxElement', ariaEl: 'checkboxElement', getTemplate: function() { var template = this.callParent(), body = template[0]; body.tag = 'div'; body.href = null; body.children.push({ // An absolutely positioned transparent checkbox that acts as the focus/aria element tag: 'input', type: 'checkbox', reference: 'checkboxElement', cls: Ext.baseCSSPrefix + 'checkbox-el' }); return template; }, initialize: function() { var me = this; me.callParent(); me.element.on({ mousedown: 'onCheckboxMousedown', translate: false, scope: me }); me.checkboxElement.on({ change: 'onCheckboxChange', delegated: false, scope: me }); this.syncCheckboxCls(); }, enableFocusable: function() { this.mixins.focusable.enableFocusable(); // Menuitems only go readonly when disabled. this.checkboxElement.dom.readOnly = ''; }, disableFocusable: function() { this.mixins.focusable.disableFocusable(); // Menu items must be focusable, but not active when disabled. this.checkboxElement.dom.readOnly = 'readonly'; }, /** * Sets the checked state of the item * @param {Boolean} checked True to check, false to un-check * @param {Boolean} [suppressEvents=false] True to prevent firing the checkchange events. */ setChecked: function(checked, suppressEvents) { var me = this, isConfiguring = me.isConfiguring; // Events and handlers are suppressed during configuration if (suppressEvents) { me.isConfiguring = true; } me.callParent([checked]); if (suppressEvents) { me.isConfiguring = isConfiguring; } }, updateChecked: function (checked) { this.checkboxElement.dom.checked = checked; // We do not get an event on programmatic check change // so call it proramatically. this.onCheckChange(); }, updateCheckChangeDisabled: function (checkChangeDisabled) { this.checkboxElement.dom.readOnly = checkChangeDisabled; }, updateValue: function (value) { this.checkboxElement.dom.value = value; }, updateText: function (text) { var me = this, ariaDom = me.ariaEl.dom; me.callParent([text]); // Use text as an analog for value if user has not specified a value if (me.getValue() === null) { me.setValue(text); } if (ariaDom && me.getMenu()) { ariaDom.setAttribute('aria-label', Ext.String.formatEncode(me.submenuText, text)); } }, applyShowCheckbox: function (showCheckbox) { return !!showCheckbox; }, updateShowCheckbox: function (showCheckbox) { this.checkboxElement.setDisplayed(showCheckbox); }, updateIcon: function(icon, oldIcon) { this.callParent([icon, oldIcon]); if (!this.isConfiguring) { this.syncCheckboxCls(); } }, updateIconCls: function(iconCls, oldIconCls) { this.callParent([iconCls, oldIconCls]); if (!this.isConfiguring) { this.syncCheckboxCls(); } }, updateIconAlign: function (iconAlign, oldIconAlign) { this.callParent([iconAlign, oldIconAlign]); if (!this.isConfiguring) { this.syncCheckboxCls(); } }, privates: { onSpace: function (e) { // Disabled menuitems are still focusable, but must not react if (this.getDisabled()) { e.preventDefault(); } }, onClick: function (e) { var me = this, arrowElement = me.arrowElement, result, parentResult, region; // Disabled menuitems are still focusable, but must not react if (me.getDisabled()) { e.preventDefault(); } // Clicking on the checkboxElement is processed natively, and we react to the // change event. if (e.pointerType !== 'mouse') { region = me.bodyElement.getRegion(); if (me.getMenu()) { region.setWidth(region.getWidth() - arrowElement.getWidth() - arrowElement.getMargin('lr')); } // When interacting with a menucheckitem via a touch screen the submenu // is shown by tapping directly on the arrow. Tapping anywhere else on // the item will simply toggle the checked state. if (region.contains(e.getPoint())) { // clicked on the icon or text - veto menu show result = false; } else { // clicked on the arrow - allow the menu to be shown, but preventDefault // to stop the checkbox from being toggled e.preventDefault(); } } parentResult = me.callParent([e]); // Allow either to veto menu showing return (result === false) ? result : parentResult; }, onCheckboxMousedown: function(e) { // Prevent focus movement away from the checkboxElement on mousedown outside of the checkboxElement. // The mouseover will have focused it. // Also, checkboxes are not focusable by default on Apple Operating Systems. // See http://www.weba11y.com/blog/2014/07/07/keyboard-navigation-in-mac-browsers/ // So to prevent focus flying to body on mousedown, we prevent default. if ((Ext.isApple && !Ext.isChrome) || !this.checkboxElement.contains(e.target)) { e.preventDefault(); } }, onCheckboxChange: function() { var me = this, checkboxElement = me.checkboxElement.dom, meChecked = me.getChecked(), isChecked = checkboxElement.checked; if (me.getCheckChangeDisabled()) { checkboxElement.checked = meChecked; return false; } if (isChecked === meChecked || me.getDisabled()) { return; } // Allow an event to veto by flipping the DOM state back. // This will cause another DOM change event, but that will be // rejected above. if (me.fireEvent('beforecheckchange', me, isChecked) === false) { checkboxElement.checked = !isChecked; } else { // Sync widget state in response to the checkbox changing state. me.setChecked(isChecked); } }, onCheckChange: function () { var me = this, checked = me.checkboxElement.dom.checked, el = me.el, ariaDom = me.ariaEl.dom; el.toggleCls(me.checkedCls, !!checked); if (ariaDom) { ariaDom.setAttribute('aria-checked', me.getMenu() ? 'mixed' : checked); } me.publishState('checked', checked); // Do not fire events if set in configuration if (!me.isConfiguring) { Ext.callback(me.getCheckHandler(), me.scope, [me, checked], 0, me); me.fireEvent('checkchange', me, checked); } }, syncHasIconCls: function () { var me = this; me.toggleCls(me.hasRightIconCls, me.hasIcon()); }, syncCheckboxCls: function() { var me = this, leftIconElement = me.leftIconElement, rightIconElement = me.rightIconElement, checkboxIconElCls = me.checkboxIconElCls, checkboxIconElement, oldCheckboxIconElement; if (me.hasIcon() && (me.getIconAlign() === 'left')) { checkboxIconElement = rightIconElement; oldCheckboxIconElement = leftIconElement; } else { checkboxIconElement = leftIconElement; oldCheckboxIconElement = rightIconElement; } checkboxIconElement.addCls(checkboxIconElCls); oldCheckboxIconElement.removeCls(checkboxIconElCls); } }});