devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
843 lines (696 loc) • 27 kB
JavaScript
"use strict";
var $ = require("../../core/renderer"),
commonUtils = require("../../core/utils/common"),
typeUtils = require("../../core/utils/type"),
each = require("../../core/utils/iterator").each,
extend = require("../../core/utils/extend").extend,
inkRipple = require("../widget/utils.ink_ripple"),
HierarchicalCollectionWidget = require("../hierarchical_collection/ui.hierarchical_collection_widget"),
MenuBaseEditStrategy = require("./ui.menu_base.edit.strategy"),
devices = require("../../core/devices"),
themes = require("../themes");
var DX_MENU_CLASS = "dx-menu",
DX_MENU_NO_ICONS_CLASS = DX_MENU_CLASS + "-no-icons",
DX_MENU_BASE_CLASS = "dx-menu-base",
ITEM_CLASS = DX_MENU_CLASS + "-item",
DX_ITEM_CONTENT_CLASS = ITEM_CLASS + "-content",
DX_MENU_SELECTED_ITEM_CLASS = ITEM_CLASS + "-selected",
DX_MENU_ITEM_WRAPPER_CLASS = ITEM_CLASS + "-wrapper",
DX_MENU_ITEMS_CONTAINER_CLASS = DX_MENU_CLASS + "-items-container",
DX_MENU_ITEM_EXPANDED_CLASS = ITEM_CLASS + "-expanded",
DX_MENU_SEPARATOR_CLASS = DX_MENU_CLASS + "-separator",
DX_MENU_ITEM_LAST_GROUP_ITEM = DX_MENU_CLASS + "-last-group-item",
DX_ITEM_HAS_TEXT = ITEM_CLASS + "-has-text",
DX_ITEM_HAS_ICON = ITEM_CLASS + "-has-icon",
DX_ITEM_HAS_SUBMENU = ITEM_CLASS + "-has-submenu",
DX_MENU_ITEM_POPOUT_CLASS = ITEM_CLASS + "-popout",
DX_MENU_ITEM_POPOUT_CONTAINER_CLASS = DX_MENU_ITEM_POPOUT_CLASS + "-container",
DX_MENU_ITEM_CAPTION_CLASS = ITEM_CLASS + "-text",
SINGLE_SELECTION_MODE = "single",
DEFAULT_DELAY = { "show": 50, "hide": 300 };
/**
* @name dxMenuBase
* @publicName dxMenuBase
* @type object
* @inherits HierarchicalCollectionWidget
* @hidden
*/
var MenuBase = HierarchicalCollectionWidget.inherit({
_getDefaultOptions: function _getDefaultOptions() {
return extend(this.callBase(), {
/**
* @name dxMenuBaseOptions.items
* @publicName items
* @type Array<dxMenuBaseItemTemplate>
*/
items: [],
/**
* @name dxMenuBaseOptions.cssClass
* @publicName cssClass
* @type string
* @default ""
*/
cssClass: "",
/**
* @name dxMenuBaseOptions.activeStateEnabled
* @publicName activeStateEnabled
* @type Boolean
* @default true
*/
activeStateEnabled: true,
/**
* @name dxMenuBaseOptions.showSubmenuMode
* @publicName showSubmenuMode
* @type Object|Enums.ShowSubmenuMode
* @default { name: "onHover", delay: { show: 0, hide: 0 } }
*/
showSubmenuMode: {
/**
* @name dxMenuBaseOptions.showSubmenuMode.name
* @publicName name
* @type Enums.ShowSubmenuMode
* @default "onHover"
*/
name: "onHover",
/**
* @name dxMenuBaseOptions.showSubmenuMode.delay
* @publicName delay
* @type Object|number
* @default { show: 50, hide: 300 }
*/
delay: {
/**
* @name dxMenuBaseOptions.showSubmenuMode.delay.show
* @publicName show
* @type number
* @default 50
*/
show: 50,
/**
* @name dxMenuBaseOptions.showSubmenuMode.delay.hide
* @publicName hide
* @type number
* @default 300
*/
hide: 300
}
},
/**
* @name dxMenuBaseOptions.animation
* @publicName animation
* @type object
* @default { show: { type: "fade", from: 0, to: 1, duration: 100 }, hide: { type: "fade", from: 1, to: 0, duration: 100 } }
* @ref
*/
animation: {
/**
* @name dxMenuBaseOptions.animation.show
* @publicName show
* @type animationConfig
* @default { type: "fade", from: 0, to: 1, duration: 100 }
*/
show: {
type: "fade",
from: 0,
to: 1,
duration: 100
},
/**
* @name dxMenuBaseOptions.animation.hide
* @publicName hide
* @type animationConfig
* @default { type: "fade", from: 1, to: 0, duration: 100 }
*/
hide: {
type: "fade",
from: 1,
to: 0,
duration: 100
}
},
/**
* @name dxMenuBaseOptions.selectByClick
* @publicName selectByClick
* @type boolean
* @default false
*/
selectByClick: false,
focusOnSelectedItem: false,
/**
* @name dxMenuBaseOptions.onItemHold
* @publicName onItemHold
* @hidden
* @action
* @inheritdoc
*/
/**
* @name dxMenuBaseOptions.itemHoldTimeout
* @publicName itemHoldTimeout
* @hidden
* @inheritdoc
*/
/**
* @name dxMenuBaseOptions.noDataText
* @publicName noDataText
* @hidden
* @inheritdoc
*/
/**
* @name dxMenuBaseOptions.selectedIndex
* @publicName selectedIndex
* @hidden
* @inheritdoc
*/
/**
* @name dxMenuBaseOptions.selectedItemKeys
* @publicName selectedItemKeys
* @hidden
* @inheritdoc
*/
/**
* @name dxMenuBaseOptions.keyExpr
* @publicName keyExpr
* @hidden
* @inheritdoc
*/
keyExpr: null,
/**
* @name dxMenuBaseOptions.parentIdExpr
* @publicName parentIdExpr
* @hidden
* @inheritdoc
*/
/**
* @name dxMenuBaseOptions.expandedExpr
* @publicName expandedExpr
* @hidden
* @inheritdoc
*/
/**
* @name dxMenuBaseItemTemplate
* @publicName dxMenuBaseItemTemplate
* @inherits CollectionWidgetItemTemplate
* @type object
*/
/**
* @name dxMenuBaseItemTemplate.beginGroup
* @publicName beginGroup
* @type Boolean
*/
/**
* @name dxMenuBaseOptions.selectionMode
* @publicName selectionMode
* @type Enums.MenuSelectionMode
* @default none
*/
_itemAttributes: { role: "menuitem" },
useInkRipple: false
/**
* @name dxMenuBaseItemTemplate.html
* @publicName html
* @type String
* @hidden
*/
/**
* @name dxMenuBaseItemTemplate.disabled
* @publicName disabled
* @type boolean
* @default false
*/
/**
* @name dxMenuBaseItemTemplate.visible
* @publicName visible
* @type boolean
* @default true
*/
/**
* @name dxMenuBaseItemTemplate.icon
* @publicName icon
* @type String
*/
/**
* @name dxMenuBaseItemTemplate.text
* @publicName text
* @type String
*/
/**
* @name dxMenuBaseItemTemplate.html
* @publicName html
* @type String
*/
/**
* @name dxMenuBaseItemTemplate.items
* @publicName items
* @type Array<dxMenuBaseItemTemplate>
*/
/**
* @name dxMenuBaseItemTemplate.selectable
* @publicName selectable
* @type boolean
* @default false
*/
/**
* @name dxMenuBaseItemTemplate.selected
* @publicName selected
* @type boolean
* @default false
*/
/**
* @name dxMenuBaseItemTemplate.closeMenuOnClick
* @publicName closeMenuOnClick
* @type boolean
* @default true
*/
});
},
_defaultOptionsRules: function _defaultOptionsRules() {
return this.callBase().concat([{
device: function device() {
return (/android5/.test(themes.current())
);
},
options: {
useInkRipple: true
}
}]);
},
_activeStateUnit: "." + ITEM_CLASS,
_itemDataKey: function _itemDataKey() {
return "dxMenuItemDataKey";
},
_itemClass: function _itemClass() {
return ITEM_CLASS;
},
_setAriaSelected: commonUtils.noop,
_selectedItemClass: function _selectedItemClass() {
return DX_MENU_SELECTED_ITEM_CLASS;
},
_widgetClass: function _widgetClass() {
return DX_MENU_BASE_CLASS;
},
_focusTarget: function _focusTarget() {
return this._itemContainer();
},
_clean: function _clean() {
this.option("focusedElement", null);
this.callBase();
},
_supportedKeys: function _supportedKeys() {
var selectItem = function selectItem() {
var $item = $(this.option("focusedElement"));
if (!$item.length || !this._isSelectionEnabled()) {
return;
}
this.selectItem($item[0]);
};
return extend(this.callBase(), {
space: selectItem,
pageUp: commonUtils.noop,
pageDown: commonUtils.noop
});
},
_isSelectionEnabled: function _isSelectionEnabled() {
return this.option("selectionMode") === SINGLE_SELECTION_MODE;
},
_init: function _init() {
this.callBase();
this._renderSelectedItem();
this._initActions();
},
_getTextContainer: function _getTextContainer(itemData) {
var itemText = itemData.text,
$itemContainer = $('<span>').addClass(DX_MENU_ITEM_CAPTION_CLASS),
itemContent = typeUtils.isPlainObject(itemData) ? itemText : String(itemData);
return itemText && $itemContainer.text(itemContent);
},
_getPopoutContainer: function _getPopoutContainer(itemData) {
var items = itemData.items,
$popOutContainer;
if (items && items.length) {
var $popOutImage = $('<div>').addClass(DX_MENU_ITEM_POPOUT_CLASS);
$popOutContainer = $('<span>').addClass(DX_MENU_ITEM_POPOUT_CONTAINER_CLASS).append($popOutImage);
}
return $popOutContainer;
},
_getDataAdapterOptions: function _getDataAdapterOptions() {
return {
rootValue: 0,
multipleSelection: false,
recursiveSelection: false,
recursiveExpansion: false,
searchValue: ""
};
},
_selectByItem: function _selectByItem(selectedItem) {
if (!selectedItem) return;
var nodeToSelect = this._dataAdapter.getNodeByItem(selectedItem);
this._dataAdapter.toggleSelection(nodeToSelect.internalFields.key, true);
},
_renderSelectedItem: function _renderSelectedItem() {
var selectedKeys = this._dataAdapter.getSelectedNodesKeys(),
selectedKey = selectedKeys.length && selectedKeys[0],
selectedItem = this.option("selectedItem");
if (!selectedKey) {
this._selectByItem(selectedItem);
return;
}
var node = this._dataAdapter.getNodeByKey(selectedKey);
if (node.selectable === false) return;
if (!selectedItem) {
this.option("selectedItem", node.internalFields.item);
return;
}
if (selectedItem !== node.internalFields.item) {
this._dataAdapter.toggleSelection(selectedKey, false);
this._selectByItem(selectedItem);
}
},
_initActions: commonUtils.noop,
_initMarkup: function _initMarkup() {
this.callBase();
this._addCustomCssClass(this.$element());
this.option("useInkRipple") && this._renderInkRipple();
},
_renderInkRipple: function _renderInkRipple() {
this._inkRipple = inkRipple.render();
},
_toggleActiveState: function _toggleActiveState($element, value, e) {
this.callBase.apply(this, arguments);
if (!this._inkRipple) {
return;
}
var config = {
element: $element,
event: e
};
if (value) {
this._inkRipple.showWave(config);
} else {
this._inkRipple.hideWave(config);
}
},
_getShowSubmenuMode: function _getShowSubmenuMode() {
var defaultValue = "onClick",
optionValue = this.option("showSubmenuMode");
optionValue = typeUtils.isObject(optionValue) ? optionValue.name : optionValue;
return this._isDesktopDevice() ? optionValue : defaultValue;
},
_initSelectedItems: commonUtils.noop,
_isDesktopDevice: function _isDesktopDevice() {
return devices.real().deviceType === "desktop";
},
_initEditStrategy: function _initEditStrategy() {
var Strategy = MenuBaseEditStrategy;
this._editStrategy = new Strategy(this);
},
_addCustomCssClass: function _addCustomCssClass($element) {
$element.addClass(this.option("cssClass"));
},
_itemWrapperSelector: function _itemWrapperSelector() {
return "." + DX_MENU_ITEM_WRAPPER_CLASS;
},
_hoverStartHandler: function _hoverStartHandler(e) {
var that = this,
$itemElement = that._getItemElementByEventArgs(e);
if (!$itemElement || that._isItemDisabled($itemElement)) return;
e.stopPropagation();
if (that._getShowSubmenuMode() === "onHover") {
clearTimeout(this._showSubmenusTimeout);
this._showSubmenusTimeout = setTimeout(that._showSubmenu.bind(that, $itemElement), that._getSubmenuDelay("show"));
}
},
_getAvailableItems: function _getAvailableItems($itemElements) {
return this.callBase($itemElements).filter(function () {
return $(this).css("visibility") !== "hidden";
});
},
_isItemDisabled: function _isItemDisabled($item) {
return this._disabledGetter($item.data(this._itemDataKey()));
},
_showSubmenu: function _showSubmenu($itemElement) {
this._addExpandedClass($itemElement);
},
_addExpandedClass: function _addExpandedClass(itemElement) {
$(itemElement).addClass(DX_MENU_ITEM_EXPANDED_CLASS);
},
_getSubmenuDelay: function _getSubmenuDelay(action) {
var delay = this.option("showSubmenuMode").delay;
if (!typeUtils.isDefined(delay)) {
return DEFAULT_DELAY[action];
}
return typeUtils.isObject(delay) ? delay[action] : delay;
},
// TODO: try to simplify
_getItemElementByEventArgs: function _getItemElementByEventArgs(eventArgs) {
var $target = $(eventArgs.target);
if ($target.hasClass(this._itemClass()) || $target.get(0) === eventArgs.currentTarget) {
return $target;
}
// TODO: move it to inheritors, menuBase don't know about dx-submenu
while (!$target.hasClass(this._itemClass())) {
$target = $target.parent();
if ($target.hasClass("dx-submenu")) {
return null;
}
}
return $target;
},
_hoverEndHandler: function _hoverEndHandler() {
clearTimeout(this._showSubmenusTimeout);
},
_hasSubmenu: function _hasSubmenu(node) {
return node.internalFields.childrenKeys.length;
},
_renderContentImpl: function _renderContentImpl() {
this._renderItems(this._dataAdapter.getRootNodes());
},
_renderItems: function _renderItems(nodes, submenuContainer) {
var that = this,
$nodeContainer;
if (nodes.length) {
this.hasIcons = false;
$nodeContainer = this._renderContainer(this.$element(), submenuContainer);
each(nodes, function (index, node) {
that._renderItem(index, node, $nodeContainer);
});
if (!this.hasIcons) $nodeContainer.addClass(DX_MENU_NO_ICONS_CLASS);
}
},
_renderContainer: function _renderContainer($wrapper) {
return $("<ul>").appendTo($wrapper).addClass(DX_MENU_ITEMS_CONTAINER_CLASS);
},
_createDOMElement: function _createDOMElement($nodeContainer) {
var $node = $("<li>").appendTo($nodeContainer).addClass(DX_MENU_ITEM_WRAPPER_CLASS);
return $node;
},
_renderItem: function _renderItem(index, node, $nodeContainer, $nodeElement) {
var items = this.option("items"),
$itemFrame;
this._renderSeparator(node, index, $nodeContainer);
if (node.internalFields.item.visible === false) return;
var $node = $nodeElement || this._createDOMElement($nodeContainer);
if (items[index + 1] && items[index + 1].beginGroup) {
$node.addClass(DX_MENU_ITEM_LAST_GROUP_ITEM);
}
$itemFrame = this.callBase(index, node.internalFields.item, $node);
if (node.internalFields.item === this.option("selectedItem")) {
$itemFrame.addClass(DX_MENU_SELECTED_ITEM_CLASS);
}
$itemFrame.attr("tabIndex", -1);
if (this._hasSubmenu(node)) this.setAria("haspopup", "true", $itemFrame);
},
_renderItemFrame: function _renderItemFrame(index, itemData, $itemContainer) {
var $itemFrame = $itemContainer.children("." + ITEM_CLASS);
return $itemFrame.length ? $itemFrame : this.callBase.apply(this, arguments);
},
_refreshItem: function _refreshItem($item, item) {
var node = this._dataAdapter.getNodeByItem(item),
index = $item.data(this._itemIndexKey()),
$nodeContainer = $item.closest("ul"),
$nodeElement = $item.closest("li");
this._renderItem(index, node, $nodeContainer, $nodeElement);
},
_addContentClasses: function _addContentClasses(itemData, $itemFrame) {
var hasText = itemData.text ? !!itemData.text.length : false,
hasIcon = !!itemData.icon,
hasSubmenu = itemData.items ? !!itemData.items.length : false;
$itemFrame.toggleClass(DX_ITEM_HAS_TEXT, hasText);
$itemFrame.toggleClass(DX_ITEM_HAS_ICON, hasIcon);
if (!this.hasIcons) {
this.hasIcons = hasIcon;
}
$itemFrame.toggleClass(DX_ITEM_HAS_SUBMENU, hasSubmenu);
},
_getItemContent: function _getItemContent($itemFrame) {
var $itemContent = this.callBase($itemFrame);
if (!$itemContent.length) {
$itemContent = $itemFrame.children("." + DX_ITEM_CONTENT_CLASS);
}
return $itemContent;
},
_postprocessRenderItem: function _postprocessRenderItem(args) {
var $itemElement = $(args.itemElement),
selectedIndex = this._dataAdapter.getSelectedNodesKeys(),
node;
if (!selectedIndex.length || !this._selectedGetter(args.itemData) || !this._isItemSelectable(args.itemData)) {
this._setAriaSelected($itemElement, "false");
return;
}
node = this._dataAdapter.getNodeByItem(args.itemData);
if (node.internalFields.key === selectedIndex[0]) {
$itemElement.addClass(this._selectedItemClass());
this._setAriaSelected($itemElement, "true");
} else {
this._setAriaSelected($itemElement, "false");
}
},
_isItemSelectable: function _isItemSelectable(item) {
return item.selectable !== false;
},
_renderSeparator: function _renderSeparator(node, index, $itemsContainer) {
if (node.beginGroup && index > 0) {
this._needSeparate = true;
}
if (node.visible !== false && this._needSeparate) {
if (index > 0) {
$("<li>").appendTo($itemsContainer).addClass(DX_MENU_SEPARATOR_CLASS);
}
this._needSeparate = false;
}
},
_itemClickHandler: function _itemClickHandler(e) {
if (e._skipHandling) return;
var itemClickActionHandler = this._createAction(this._updateSubmenuVisibilityOnClick.bind(this));
this._itemDXEventHandler(e, "onItemClick", {}, { afterExecute: itemClickActionHandler.bind(this) });
e._skipHandling = true;
},
_updateSubmenuVisibilityOnClick: function _updateSubmenuVisibilityOnClick(actionArgs) {
this._updateSelectedItemOnClick(actionArgs);
if (this._getShowSubmenuMode() === "onClick") {
this._addExpandedClass(actionArgs.args[0].itemElement);
}
},
_updateSelectedItemOnClick: function _updateSelectedItemOnClick(actionArgs) {
var args = actionArgs.args ? actionArgs.args[0] : actionArgs,
selectedItemKey;
if (!this._isItemSelectionAllowed(args.itemData)) {
return;
}
selectedItemKey = this._dataAdapter.getSelectedNodesKeys();
var selectedNode = selectedItemKey.length && this._dataAdapter.getNodeByKey(selectedItemKey[0]);
if (selectedNode) {
this._toggleItemSelection(selectedNode, false);
}
if (!selectedNode || selectedNode.internalFields.item !== args.itemData) {
this.selectItem(args.itemData);
} else {
this._fireSelectionChangeEvent(null, this.option("selectedItem"));
this._setOptionSilent("selectedItem", null);
}
},
_isItemSelectionAllowed: function _isItemSelectionAllowed(item) {
var isSelectionByClickEnabled = this._isSelectionEnabled() && this.option("selectByClick");
return !this._isContainerEmpty() && isSelectionByClickEnabled && this._isItemSelectable(item) && !this._itemsGetter(item);
},
_isContainerEmpty: function _isContainerEmpty() {
return this._itemContainer().is(':empty');
},
_syncSelectionOptions: commonUtils.noop,
_optionChanged: function _optionChanged(args) {
if (this._cancelOptionChange === args.name) {
return;
}
switch (args.name) {
case "showSubmenuMode":
break;
case "selectedItem":
var itemData = args.value,
node = this._dataAdapter.getNodeByItem(itemData),
selectedKey = this._dataAdapter.getSelectedNodesKeys()[0];
if (node && node.internalFields.key !== selectedKey) {
if (node.selectable === false) break;
if (selectedKey) {
this._toggleItemSelection(this._dataAdapter.getNodeByKey(selectedKey), false);
}
this._toggleItemSelection(node, true);
this._updateSelectedItems();
}
break;
case "cssClass":
case "position":
case "selectByClick":
case "animation":
case "useInkRipple":
this._invalidate();
break;
default:
this.callBase(args);
}
},
_toggleItemSelection: function _toggleItemSelection(node, value) {
var itemElement = this._getElementByItem(node.internalFields.item);
itemElement && $(itemElement).toggleClass(DX_MENU_SELECTED_ITEM_CLASS);
this._dataAdapter.toggleSelection(node.internalFields.key, value);
},
_getElementByItem: function _getElementByItem(itemData) {
var that = this,
result;
each(this._itemElements(), function (_, itemElement) {
if ($(itemElement).data(that._itemDataKey()) !== itemData) {
return true;
}
result = itemElement;
return false;
});
return result;
},
_updateSelectedItems: function _updateSelectedItems(oldSelection, newSelection) {
if (oldSelection || newSelection) {
this._updateSelection(newSelection, oldSelection);
this._fireSelectionChangeEvent(newSelection, oldSelection);
}
},
_fireSelectionChangeEvent: function _fireSelectionChangeEvent(addedSelection, removedSelection) {
this._createActionByOption("onSelectionChanged", {
excludeValidators: ["disabled", "readOnly"]
})({
addedItems: [addedSelection],
removedItems: [removedSelection]
});
},
/**
* @name dxMenuBaseMethods.selectItem
* @publicName selectItem(itemElement)
* @param1 itemElement:Node
*/
selectItem: function selectItem(itemElement) {
var itemData = itemElement.nodeType ? this._getItemData(itemElement) : itemElement,
node = this._dataAdapter.getNodeByItem(itemData),
selectedKey = this._dataAdapter.getSelectedNodesKeys()[0],
selectedItem = this.option("selectedItem");
if (node.internalFields.key !== selectedKey) {
if (selectedKey) {
this._toggleItemSelection(this._dataAdapter.getNodeByKey(selectedKey), false);
}
this._toggleItemSelection(node, true);
this._updateSelectedItems(selectedItem, itemData);
this._setOptionSilent("selectedItem", itemData);
}
},
/**
* @name dxMenuBaseMethods.unselectItem
* @publicName unselectItem(itemElement)
* @param1 itemElement:Node
*/
unselectItem: function unselectItem(itemElement) {
var itemData = itemElement.nodeType ? this._getItemData(itemElement) : itemElement,
node = this._dataAdapter.getNodeByItem(itemData),
selectedItem = this.option("selectedItem");
if (node.internalFields.selected) {
this._toggleItemSelection(node, false);
this._updateSelectedItems(selectedItem, null);
this._setOptionSilent("selectedItem", null);
}
}
});
module.exports = MenuBase;