/**
* A base class for all menu items that require menu-related functionality such as click handling,
* sub-menus, icons, etc.
*
* @example
* Ext.create('Ext.menu.Menu', {
* width: 100,
* height: 100,
* floating: false, // usually you want this set to True (default)
* renderTo: Ext.getBody(), // usually rendered by it's containing component
* items: [{
* text: 'icon item',
* iconCls: 'add16'
* }, {
* text: 'text item'
* }, {
* text: 'plain item',
* plain: true
* }]
* });
*/
Ext.define('Ext.menu.Item', {
extend: 'Ext.Component',
alias: 'widget.menuitem',
alternateClassName: 'Ext.menu.TextItem',
/**
* @property {Boolean} isMenuItem
* `true` in this class to identify an object as an instantiated Menu Item, or subclass thereof.
*/
isMenuItem: true,
mixins: [
'Ext.mixin.Queryable'
],
requires: [
'Ext.Glyph'
],
config: {
/**
* @cfg glyph
* @inheritdoc Ext.panel.Header#cfg-glyph
*/
glyph: null
},
/**
* @property {Boolean} activated
* Whether or not this item is currently activated
*/
activated: false,
/**
* @property {Ext.menu.Menu} parentMenu
* The parent Menu of this item.
*/
/**
* @cfg {String} activeCls
* The CSS class added to the menu item when the item is focused.
*/
activeCls: Ext.baseCSSPrefix + 'menu-item-active',
/**
* @cfg {Boolean} canActivate
* Whether or not this menu item can be focused.
* @deprecated 5.1.0 Use the {@link #focusable} config.
*/
/**
* @cfg {Number} clickHideDelay
* The delay in milliseconds to wait before hiding the menu after clicking the menu item.
* This only has an effect when `hideOnClick: true`.
*/
clickHideDelay: 0,
/**
* @cfg {Boolean} destroyMenu
* Whether or not to destroy any associated sub-menu when this item is destroyed.
*/
destroyMenu: true,
/**
* @cfg {String} disabledCls
* The CSS class added to the menu item when the item is disabled.
*/
disabledCls: Ext.baseCSSPrefix + 'menu-item-disabled',
/**
* @cfg {String} emptyText
* The text to display when the the {@link #text} is empty.
* @since 7.2.0
*/
emptyText: '\u00a0',
/**
* @cfg {String} [href='#']
* The href attribute to use for the underlying anchor link.
*/
/**
* @cfg {String} hrefTarget
* The target attribute to use for the underlying anchor link.
*/
/**
* @cfg {Boolean} hideOnClick
* Whether to not to hide the owning menu when this item is clicked.
*/
hideOnClick: true,
/**
* @cfg [icon=Ext#BLANK_IMAGE_URL]
* @inheritdoc Ext.panel.Header#cfg-icon
*/
/**
* @cfg iconCls
* @inheritdoc Ext.panel.Header#cfg-iconCls
*/
/**
* @cfg {Ext.menu.Menu/Object} menu
* Either an instance of {@link Ext.menu.Menu} or a config object for an {@link Ext.menu.Menu}
* which will act as a sub-menu to this item.
*/
/**
* @property {Ext.menu.Menu} menu The sub-menu associated with this item, if one was configured.
*/
/**
* @cfg {String} menuAlign
* The default {@link Ext.util.Positionable#getAlignToXY Ext.util.Positionable.getAlignToXY}
* anchor position value for this item's sub-menu relative to this item's position.
*/
menuAlign: 'tl-tr?',
/**
* @cfg {Number} menuExpandDelay
* The delay in milliseconds before this item's sub-menu expands after this item is moused over.
*/
menuExpandDelay: 200,
/**
* @cfg {Number} menuHideDelay
* The delay in milliseconds before this item's sub-menu hides after this item is moused out.
*/
menuHideDelay: 200,
/**
* @cfg {Boolean} plain
* Whether or not this item is plain text/html with no icon or visual submenu indication.
*/
/**
* @cfg {String/Object} tooltip
* The tooltip for the button - can be a string to be used as innerHTML (html tags are accepted)
* or QuickTips config object.
*/
/**
* @cfg {String} tooltipType
* The type of tooltip to use. Either 'qtip' for QuickTips or 'title' for title attribute.
*/
tooltipType: 'qtip',
/**
* @property focusable
* @inheritdoc
*/
focusable: true,
/**
* @property ariaRole
* @inheritdoc
*/
ariaRole: 'menuitem',
/**
* @property ariaEl
* @inheritdoc
*/
ariaEl: 'itemEl',
/**
* @cfg baseCls
* @inheritdoc
*/
baseCls: Ext.baseCSSPrefix + 'menu-item',
arrowCls: Ext.baseCSSPrefix + 'menu-item-arrow',
baseIconCls: Ext.baseCSSPrefix + 'menu-item-icon',
textCls: Ext.baseCSSPrefix + 'menu-item-text',
indentCls: Ext.baseCSSPrefix + 'menu-item-indent',
indentNoSeparatorCls: Ext.baseCSSPrefix + 'menu-item-indent-no-separator',
indentRightIconCls: Ext.baseCSSPrefix + 'menu-item-indent-right-icon',
indentRightArrowCls: Ext.baseCSSPrefix + 'menu-item-indent-right-arrow',
linkCls: Ext.baseCSSPrefix + 'menu-item-link',
linkHrefCls: Ext.baseCSSPrefix + 'menu-item-link-href',
/**
* @cfg childEls
* @inheritdoc
*/
childEls: [
'itemEl', 'iconEl', 'textEl', 'arrowEl'
],
/* eslint-disable indent, max-len */
/**
* @cfg renderTpl
* @inheritdoc
*/
renderTpl:
'<tpl if="plain">' +
'{text}' +
'<tpl else>' +
'<a id="{id}-itemEl" data-ref="itemEl"' +
' class="{linkCls}<tpl if="hasHref"> {linkHrefCls}</tpl>{childElCls}"' +
' href="{href}" ' +
'<tpl if="hrefTarget"> target="{hrefTarget}"</tpl>' +
' hidefocus="true"' +
// For most browsers the text is already unselectable but Opera needs an explicit unselectable="on".
' unselectable="on"' +
'<tpl if="tabIndex != null">' +
' tabindex="{tabIndex}"' +
'</tpl>' +
'<tpl foreach="ariaAttributes"> {$}="{.}"</tpl>' +
'>' +
'<span id="{id}-textEl" data-ref="textEl" class="{textCls} {textCls}-{ui} {indentCls}{childElCls}" unselectable="on" role="presentation">{text}</span>' +
'<tpl if="hasIcon">' +
'<div role="presentation" id="{id}-iconEl" data-ref="iconEl" class="{baseIconCls}-{ui} {baseIconCls}' +
'{[values.rightIcon ? "-right" : ""]} {iconCls}' +
'{childElCls} {glyphCls}" style="<tpl if="icon">background-image:url({icon});</tpl>' +
'<tpl if="glyph">' +
'<tpl if="glyphFontFamily">' +
'font-family:{glyphFontFamily};' +
'</tpl>' +
'">' +
'{glyph}' +
'<tpl else>' +
'">' +
'</tpl>' +
'</div>' +
'</tpl>' +
'<tpl if="showCheckbox">' +
'<div role="presentation" id="{id}-checkEl" data-ref="checkEl" class="{baseIconCls}-{ui} {baseIconCls}' +
'{[(values.hasIcon && !values.rightIcon) ? "-right" : ""]} ' +
'{groupCls} {checkboxCls}{childElCls}">' +
'</div>' +
'</tpl>' +
'<tpl if="hasMenu">' +
'<div role="presentation" id="{id}-arrowEl" data-ref="arrowEl" class="{arrowCls} {arrowCls}-{ui}{childElCls}"></div>' +
'</tpl>' +
'</a>' +
'</tpl>',
/* eslint-enable indent, max-len */
/**
* @cfg autoEl
* @inheritdoc
*/
autoEl: {
role: 'presentation'
},
/**
* @property maskOnDisable
* @inheritdoc
*/
maskOnDisable: false,
iconAlign: 'left',
/**
* @cfg {String} text
* The text/html to display in this item.
*/
/**
* @cfg {Function/String} handler
* A function called when the menu item is clicked (can be used instead of {@link #click}
* event).
* @cfg {Ext.menu.Item} handler.item The item that was clicked
* @cfg {Ext.event.Event} handler.e The underlying {@link Ext.event.Event}.
* @controllable
*/
/**
* @event activate
* Fires when this item is activated
* @param {Ext.menu.Item} item The activated item
*/
/**
* @event click
* Fires when this item is clicked
* @param {Ext.menu.Item} item The item that was clicked
* @param {Ext.event.Event} e The underlying {@link Ext.event.Event}.
*/
/**
* @event deactivate
* Fires when this item is deactivated
* @param {Ext.menu.Item} item The deactivated item
*/
/**
* @event textchange
* Fired when the item's text is changed by the {@link #setText} method.
* @param {Ext.menu.Item} this
* @param {String} oldText
* @param {String} newText
*/
/**
* @event iconchange
* Fired when the item's icon is changed by the {@link #setIcon} or {@link #setIconCls} methods.
* @param {Ext.menu.Item} this
* @param {String} oldIcon
* @param {String} newIcon
*/
initComponent: function() {
var me = this,
cls = me.cls ? [me.cls] : [],
menu;
// During deprecation period of canActivate config, copy it into focusable config.
if (me.hasOwnProperty('canActivate')) {
me.focusable = me.canActivate;
}
if (me.plain) {
cls.push(Ext.baseCSSPrefix + 'menu-item-plain');
}
if (cls.length) {
me.cls = cls.join(' ');
}
if (me.menu) {
menu = me.menu;
me.menu = null;
me.setMenu(menu);
}
me.callParent(arguments);
},
canFocus: function() {
var me = this;
// This is an override of the implementation in Focusable.
// We do not refuse focus if the Item is disabled.
// http://www.w3.org/TR/2013/WD-wai-aria-practices-20130307/#menu
// "Disabled menu items receive focus but have no action when Enter or
// Left Arrow/Right Arrow is pressed."
// Test that deprecated canActivate config has not been set to false.
return me.focusable && me.rendered && me.canActivate !== false &&
!me.destroying && !me.destroyed &&
me.isVisible(true);
},
onFocus: function(e) {
var me = this;
me.callParent([e]);
// We do not refuse activation if the Item is disabled.
// http://www.w3.org/TR/2013/WD-wai-aria-practices-20130307/#menu
// "Disabled menu items receive focus but have no action when Enter or
// Left Arrow/Right Arrow is pressed."
if (!me.plain) {
me.addCls(me.activeCls);
}
me.activated = true;
if (me.hasListeners.activate) {
me.fireEvent('activate', me);
}
},
onFocusLeave: function(e) {
var me = this;
me.callParent([e]);
if (!me.plain) {
me.removeCls(me.activeCls);
}
me.doHideMenu();
me.activated = false;
if (me.hasListeners.deactivate) {
me.fireEvent('deactivate', me);
}
},
doHideMenu: function() {
var menu = this.menu;
this.cancelDeferExpand();
if (menu && menu.isVisible()) {
menu.hide();
}
},
/**
* @private
* Hides the entire floating menu tree that we are within.
* Walks up the refOwner axis hiding each Menu instance it find until it hits
* a non-floating ancestor.
*/
deferHideParentMenus: function() {
var menu;
// eslint-disable-next-line max-len
for (menu = this.getRefOwner(); menu && ((menu.isMenu && menu.floating) || menu.isMenuItem); menu = menu.getRefOwner()) {
if (menu.isMenu) {
menu.hide();
}
}
},
expandMenu: function(event, delay) {
var me = this;
// An item can be focused (active), but disabled.
// Disabled items must not action on click (or up/down arrow)
// http://www.w3.org/TR/2013/WD-wai-aria-practices-20130307/#menu
// "Disabled menu items receive focus but have no action when Enter or
// Left Arrow/Right Arrow is pressed."
if (!me.disabled && me.activated && me.menu) {
// hideOnClick makes no sense when there's a child menu
me.hideOnClick = false;
me.cancelDeferHide();
// Allow configuration of zero to perform immediate expansion.
delay = delay == null ? me.menuExpandDelay : delay;
if (delay === 0) {
me.doExpandMenu(event);
}
else {
me.cancelDeferExpand();
// Delay can't be 0 by this point
me.expandMenuTimer = Ext.defer(me.doExpandMenu, delay, me, [event]);
}
}
},
doExpandMenu: function(clickEvent) {
var me = this,
menu = me.menu,
ariaDom;
if (!menu.isVisible()) {
me.parentMenu.activeChild = menu;
menu.ownerCmp = me;
menu.parentMenu = me.parentMenu;
menu.constrainTo = document.body;
// Pointer-invoked menus do not auto focus, key invoked ones do.
menu.autoFocus = !clickEvent || !clickEvent.pointerType;
menu.showBy(me, me.menuAlign);
ariaDom = me.ariaEl.dom;
if (ariaDom) {
ariaDom.setAttribute('aria-owns', menu.id);
}
}
// Keyboard events should focus the first menu item even if it was already expanded
else if (clickEvent && clickEvent.type === 'keydown') {
menu.focus();
}
},
getRefItems: function(deep) {
var menu = this.menu,
items;
if (menu) {
items = menu.getRefItems(deep);
items.unshift(menu);
}
return items || [];
},
getValue: function() {
return this.value;
},
hideMenu: function(delay) {
var me = this;
if (me.menu) {
me.cancelDeferExpand();
me.hideMenuTimer = Ext.defer(
me.doHideMenu, Ext.isNumber(delay) ? delay : me.menuHideDelay, me
);
}
},
onClick: function(e) {
var me = this,
clickHideDelay = me.clickHideDelay,
browserEvent = e.browserEvent,
clickResult, preventDefault;
if (!me.href || me.disabled) {
e.stopEvent();
if (me.disabled) {
return false;
}
}
if (me.disabled || me.handlingClick) {
return;
}
if (me.hideOnClick && !me.menu) {
// on mobile webkit, when the menu item has an href, a longpress will
// trigger the touch call-out menu to show. If this is the case, the tap
// event object's browser event type will be 'touchcancel', and we do not
// want to hide the menu.
// items with submenus are activated by touchstart on mobile browsers, so
// we cannot hide the menu on "tap"
if (!clickHideDelay) {
me.deferHideParentMenus();
}
else {
me.deferHideParentMenusTimer =
Ext.defer(me.deferHideParentMenus, clickHideDelay, me);
}
}
// Click event may have destroyed the menu, don't do anything further
clickResult = me.fireEvent('click', me, e);
// Click listener could have destroyed the menu and/or item.
if (me.destroyed) {
return;
}
if (clickResult !== false && me.handler) {
Ext.callback(me.handler, me.scope, [me, e], 0, me);
}
// And the handler could have done the same. We check this twice
// because if the menu was destroyed in the click listener, the handler
// should not have been called.
if (me.destroyed) {
return;
}
// If there's an href, invoke dom.click() after we've fired the click event in case a click
// listener wants to handle it.
//
// Note that we're having to do this because the key navigation code will blindly call
// stopEvent() on all key events that it handles!
//
// But, we need to check the browser event object that was passed to the listeners
// to determine if the default action has been prevented.
// If so, we don't want to honor the .href config.
if (Ext.isIE9m) {
// Here we need to invert the value since it's meaning is the opposite
// of defaultPrevented.
preventDefault = browserEvent.returnValue === false ? true : false;
}
else {
preventDefault = !!browserEvent.defaultPrevented;
}
// We only manually need to trigger the click event if it's come from a key event.
if (me.href && e.type !== 'click' && !preventDefault) {
me.handlingClick = true;
me.itemEl.dom.click();
me.handlingClick = false;
}
if (!me.hideOnClick && !me.hasFocus) {
me.focus();
}
return clickResult;
},
onRemoved: function() {
var me = this;
// Removing the active item, must deactivate it.
if (me.activated && me.parentMenu.activeItem === me) {
me.parentMenu.deactivateActiveItem();
}
me.callParent(arguments);
me.parentMenu = me.ownerCmp = null;
},
doDestroy: function() {
var me = this;
if (me.rendered) {
me.clearTip();
}
me.cancelDeferExpand();
me.cancelDeferHide();
Ext.undefer(me.deferHideParentMenusTimer);
me.setMenu(null);
me.callParent();
},
beforeRender: function() {
var me = this,
glyph = me.glyph,
glyphFontFamily,
hasIcon = !!(me.icon || me.iconCls || glyph),
hasMenu = !!me.menu,
rightIcon = ((me.iconAlign === 'right') && !hasMenu),
isCheckItem = me.isMenuCheckItem,
indentCls = [],
ownerCt = me.ownerCt,
isOwnerPlain = ownerCt.plain;
if (me.plain) {
me.ariaEl = 'el';
}
me.callParent();
if (hasIcon) {
if (hasMenu && me.showCheckbox) {
// nowhere to put the icon, menu arrow on one side, checkbox on the other.
// TODO: maybe put the icon or checkbox next to the arrow?
hasIcon = false;
}
}
// Transform Glyph to the useful parts
if (glyph) {
glyphFontFamily = glyph.fontFamily;
glyph = glyph.character;
}
if (!isOwnerPlain || (hasIcon && !rightIcon) || isCheckItem) {
if (ownerCt.showSeparator && !isOwnerPlain) {
indentCls.push(me.indentCls);
}
else {
indentCls.push(me.indentNoSeparatorCls);
}
}
if (hasMenu) {
indentCls.push(me.indentRightArrowCls);
}
else if (hasIcon && (rightIcon || isCheckItem)) {
indentCls.push(me.indentRightIconCls);
}
Ext.applyIf(me.renderData, {
hasHref: !!me.href,
href: me.href || '#',
hrefTarget: me.hrefTarget,
icon: me.icon,
iconCls: me.iconCls,
glyph: glyph,
glyphCls: glyph ? Ext.baseCSSPrefix + 'menu-item-glyph' : undefined,
glyphFontFamily: glyphFontFamily,
hasIcon: hasIcon,
hasMenu: hasMenu,
indent: !isOwnerPlain || hasIcon || isCheckItem,
isCheckItem: isCheckItem,
rightIcon: rightIcon,
plain: me.plain,
text: me.getDisplayText(),
arrowCls: me.arrowCls,
baseIconCls: me.baseIconCls,
textCls: me.textCls,
indentCls: indentCls.join(' '),
linkCls: me.linkCls,
linkHrefCls: me.linkHrefCls,
groupCls: me.group ? me.groupCls : '',
tabIndex: me.tabIndex
});
},
onRender: function() {
var me = this;
me.callParent(arguments);
if (me.tooltip) {
me.setTooltip(me.tooltip, true);
}
},
/**
* Get the attached sub-menu for this item.
* @return {Ext.menu.Menu} The sub-menu. `null` if it doesn't exist.
*/
getMenu: function() {
return this.menu || null;
},
/**
* Set a child menu for this item. See the {@link #cfg-menu} configuration.
* @param {Ext.menu.Menu/Object} menu A menu, or menu configuration. null may be
* passed to remove the menu.
* @param {Boolean} [destroyMenu] True to destroy any existing menu. False to
* prevent destruction. If not specified, the {@link #destroyMenu} configuration
* will be used.
*/
setMenu: function(menu, destroyMenu) {
var me = this,
oldMenu = me.menu,
arrowEl = me.arrowEl,
ariaDom = me.ariaEl.dom,
ariaAttr, instanced;
if (oldMenu) {
oldMenu.ownerCmp = oldMenu.parentMenu = null;
if (destroyMenu === true || (destroyMenu !== false && me.destroyMenu)) {
Ext.destroy(oldMenu);
}
if (ariaDom) {
ariaDom.removeAttribute('aria-haspopup');
ariaDom.removeAttribute('aria-owns');
}
else {
ariaAttr = (me.ariaRenderAttributes || (me.ariaRenderAttributes = {}));
delete ariaAttr['aria-haspopup'];
delete ariaAttr['aria-owns'];
}
}
if (menu) {
instanced = menu.isMenu;
menu = me.menu = Ext.menu.Manager.get(menu, {
ownerCmp: me,
focusOnToFront: false
});
// We need to forcibly set this here because we could be passed
// an existing menu, which means the config above won't get applied
// during creation.
menu.setOwnerCmp(me, instanced);
if (ariaDom) {
ariaDom.setAttribute('aria-haspopup', true);
ariaDom.setAttribute('aria-owns', menu.id);
}
else {
ariaAttr = (me.ariaRenderAttributes || (me.ariaRenderAttributes = {}));
ariaAttr['aria-haspopup'] = true;
if (!menu.hidden) {
ariaAttr['aria-owns'] = menu.id;
}
}
}
else {
menu = me.menu = null;
}
if (menu && me.rendered && !me.destroying && arrowEl) {
arrowEl[menu ? 'addCls' : 'removeCls'](me.arrowCls);
}
},
/**
* Sets the {@link #click} handler of this item
* @param {Function} fn The handler function
* @param {Object} [scope] The scope of the handler function
*/
setHandler: function(fn, scope) {
this.handler = fn || null;
this.scope = scope;
},
/**
* Sets the {@link #icon} on this item.
* @param {String} icon The new icon URL. If this `MenuItem` was configured with a
* {@link #cfg-glyph}, this may be a glyph configuration. See {@link #cfg-glyph}.
*/
setIcon: function(icon) {
var me = this,
iconEl = me.iconEl,
oldIcon = me.icon;
// If setIcon is called when we are configured with a glyph, clear the glyph
if (me.glyph) {
me.setGlyph(null);
}
if (iconEl) {
iconEl.setStyle('background-image', icon ? 'url(' + icon + ')' : '');
}
me.icon = icon;
me.fireEvent('iconchange', me, oldIcon, icon);
},
/**
* Sets the {@link #iconCls} of this item
* @param {String} iconCls The CSS class to set to {@link #iconCls}
*/
setIconCls: function(iconCls) {
var me = this,
iconEl = me.iconEl,
oldCls = me.iconCls;
// If setIcon is called when we are configured with a glyph, clear the glyph
if (me.glyph) {
me.setGlyph(null);
}
if (iconEl) {
// In case it had been set to 'none' by a glyph setting.
iconEl.setStyle('background-image', '');
if (me.iconCls) {
iconEl.removeCls(me.iconCls);
}
if (iconCls) {
iconEl.addCls(iconCls);
}
}
me.iconCls = iconCls;
me.fireEvent('iconchange', me, oldCls, iconCls);
},
/**
* Sets the {@link #text} of this item
* @param {String} text The {@link #text}
*/
setText: function(text) {
var me = this,
el = me.textEl || me.el,
oldText = me.text;
me.text = text;
if (me.rendered) {
el.setHtml(me.getDisplayText());
me.updateLayout();
}
me.fireEvent('textchange', me, oldText, text);
},
getTipAttr: function() {
return this.tooltipType === 'qtip' ? 'data-qtip' : 'title';
},
/**
* @private
*/
clearTip: function() {
if (Ext.quickTipsActive && Ext.isObject(this.tooltip)) {
Ext.tip.QuickTipManager.unregister(this.itemEl);
}
},
/**
* Sets the tooltip for this menu item.
*
* @param {String/Object} tooltip This may be:
*
* - **String** : A string to be used as innerHTML (html tags are accepted) to show
* in a tooltip
* - **Object** : A configuration object for {@link Ext.tip.QuickTipManager#register}.
*
* @param {Boolean} [initial] (private)
*
* @return {Ext.menu.Item} this
*/
setTooltip: function(tooltip, initial) {
var me = this;
if (me.rendered) {
if (!initial) {
me.clearTip();
}
if (Ext.quickTipsActive && Ext.isObject(tooltip)) {
Ext.tip.QuickTipManager.register(
Ext.apply({
target: me.itemEl.id
}, tooltip)
);
me.tooltip = tooltip;
}
else {
me.itemEl.dom.setAttribute(me.getTipAttr(), tooltip);
}
}
else {
me.tooltip = tooltip;
}
return me;
},
getFocusEl: function() {
return this.plain ? this.el : this.itemEl;
},
getFocusClsEl: function() {
return this.el;
},
getDisplayText: function() {
return this.text || this.emptyText;
},
privates: {
cancelDeferExpand: function() {
window.clearTimeout(this.expandMenuTimer);
},
cancelDeferHide: function() {
window.clearTimeout(this.hideMenuTimer);
}
},
applyGlyph: function(glyph, oldGlyph) {
if (glyph) {
if (!glyph.isGlyph) {
glyph = new Ext.Glyph(glyph);
}
if (glyph.isEqual(oldGlyph)) {
glyph = undefined;
}
}
return glyph;
},
updateGlyph: function(glyph, oldGlyph) {
var iconEl = this.iconEl;
if (iconEl) {
iconEl.setStyle('background-image', 'none');
this.icon = null;
if (glyph) {
iconEl.dom.innerHTML = glyph.character;
iconEl.setStyle(glyph.getStyle());
}
else {
iconEl.dom.innerHTML = '';
}
}
}
});