devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
730 lines (729 loc) • 27.7 kB
JavaScript
/**
* DevExtreme (esm/__internal/ui/m_drop_down_button.js)
* Version: 24.2.6
* Build date: Mon Mar 17 2025
*
* Copyright (c) 2012 - 2025 Developer Express Inc. ALL RIGHTS RESERVED
* Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
*/
import _extends from "@babel/runtime/helpers/esm/extends";
import messageLocalization from "../../common/core/localization/message";
import registerComponent from "../../core/component_registrator";
import {
getPublicElement
} from "../../core/element";
import Guid from "../../core/guid";
import $ from "../../core/renderer";
import {
FunctionTemplate
} from "../../core/templates/function_template";
import {
ensureDefined
} from "../../core/utils/common";
import {
compileGetter
} from "../../core/utils/data";
import {
Deferred
} from "../../core/utils/deferred";
import {
extend
} from "../../core/utils/extend";
import {
getImageContainer
} from "../../core/utils/icon";
import {
isDefined,
isObject,
isPlainObject
} from "../../core/utils/type";
import DataController from "../../data_controller";
import ButtonGroup from "../../ui/button_group";
import List from "../../ui/list_light";
import Widget from "../core/widget/widget";
import {
getElementWidth,
getSizeValue
} from "../ui/drop_down_editor/m_utils";
import Popup from "../ui/popup/m_popup";
const DROP_DOWN_BUTTON_CLASS = "dx-dropdownbutton";
const DROP_DOWN_BUTTON_CONTENT = "dx-dropdownbutton-content";
const DROP_DOWN_BUTTON_ACTION_CLASS = "dx-dropdownbutton-action";
const DROP_DOWN_BUTTON_TOGGLE_CLASS = "dx-dropdownbutton-toggle";
const DROP_DOWN_BUTTON_HAS_ARROW_CLASS = "dx-dropdownbutton-has-arrow";
const DROP_DOWN_BUTTON_POPUP_WRAPPER_CLASS = "dx-dropdownbutton-popup-wrapper";
const DROP_DOWN_EDITOR_OVERLAY_CLASS = "dx-dropdowneditor-overlay";
const DX_BUTTON_CLASS = "dx-button";
const DX_BUTTON_TEXT_CLASS = "dx-button-text";
const DX_ICON_RIGHT_CLASS = "dx-icon-right";
const OVERLAY_CONTENT_LABEL = "Dropdown";
class DropDownButton extends Widget {
_getDefaultOptions() {
return _extends({}, super._getDefaultOptions(), {
itemTemplate: "item",
keyExpr: "this",
selectedItem: null,
selectedItemKey: null,
stylingMode: "outlined",
deferRendering: true,
noDataText: messageLocalization.format("dxCollectionWidget-noDataText"),
useSelectMode: false,
splitButton: false,
showArrowIcon: true,
template: null,
text: "",
type: "normal",
onButtonClick: null,
onSelectionChanged: null,
onItemClick: null,
opened: false,
items: null,
dataSource: null,
focusStateEnabled: true,
hoverStateEnabled: true,
dropDownOptions: {},
dropDownContentTemplate: "content",
wrapItemText: false,
useItemTextAsTitle: true,
grouped: false,
groupTemplate: "group",
buttonGroupOptions: {}
})
}
_setOptionsByReference() {
super._setOptionsByReference();
extend(this._optionsByReference, {
selectedItem: true
})
}
_init() {
super._init();
this._createItemClickAction();
this._createActionClickAction();
this._createSelectionChangedAction();
this._initDataController();
this._compileKeyGetter();
this._compileDisplayGetter();
this._options.cache("buttonGroupOptions", this.option("buttonGroupOptions"));
this._options.cache("dropDownOptions", this.option("dropDownOptions"))
}
_initDataController() {
const dataSource = this.option("dataSource");
this._dataController = new DataController(dataSource ?? this.option("items"), {
key: this.option("keyExpr")
})
}
_initTemplates() {
this._templateManager.addDefaultTemplates({
content: new FunctionTemplate((options => {
const $popupContent = $(options.container);
const $listContainer = $("<div>").appendTo($popupContent);
this._list = this._createComponent($listContainer, List, this._listOptions());
this._list.registerKeyHandler("escape", this._escHandler.bind(this));
this._list.registerKeyHandler("tab", this._escHandler.bind(this));
this._list.registerKeyHandler("leftArrow", this._escHandler.bind(this));
this._list.registerKeyHandler("rightArrow", this._escHandler.bind(this))
}))
});
super._initTemplates()
}
_compileKeyGetter() {
this._keyGetter = compileGetter(this._dataController.key())
}
_compileDisplayGetter() {
const {
displayExpr: displayExpr
} = this.option();
this._displayGetter = compileGetter(displayExpr)
}
_initMarkup() {
super._initMarkup();
this.$element().addClass("dx-dropdownbutton");
this._renderButtonGroup();
this._updateArrowClass();
if (isDefined(this.option("selectedItemKey"))) {
this._loadSelectedItem().done(this._updateActionButton.bind(this))
}
}
_renderFocusTarget() {}
_render() {
if (!this.option("deferRendering") || this.option("opened")) {
this._renderPopup()
}
super._render()
}
_renderContentImpl() {
if (this._popup) {
this._renderPopupContent()
}
return super._renderContentImpl()
}
_loadSelectedItem() {
var _this$_loadSingleDefe;
null === (_this$_loadSingleDefe = this._loadSingleDeferred) || void 0 === _this$_loadSingleDefe || _this$_loadSingleDefe.reject();
const d = Deferred();
if (this._list && void 0 !== this._lastSelectedItemData) {
const cachedResult = this.option("useSelectMode") ? this._list.option("selectedItem") : this._lastSelectedItemData;
return d.resolve(cachedResult)
}
this._lastSelectedItemData = void 0;
const selectedItemKey = this.option("selectedItemKey");
this._dataController.loadSingle(selectedItemKey).done(d.resolve).fail((() => {
d.reject(null)
}));
this._loadSingleDeferred = d;
return d.promise()
}
_createActionClickAction() {
this._actionClickAction = this._createActionByOption("onButtonClick")
}
_createSelectionChangedAction() {
this._selectionChangedAction = this._createActionByOption("onSelectionChanged")
}
_createItemClickAction() {
this._itemClickAction = this._createActionByOption("onItemClick")
}
_fireSelectionChangedAction(_ref) {
let {
previousValue: previousValue,
value: value
} = _ref;
this._selectionChangedAction({
item: value,
previousItem: previousValue
})
}
_fireItemClickAction(_ref2) {
let {
event: event,
itemElement: itemElement,
itemData: itemData
} = _ref2;
return this._itemClickAction({
event: event,
itemElement: itemElement,
itemData: this._actionItem || itemData
})
}
_getButtonTemplate() {
const {
template: template,
splitButton: splitButton,
showArrowIcon: showArrowIcon
} = this.option();
if (template) {
return template
}
return splitButton || !showArrowIcon ? "content" : (_ref3, buttonContent) => {
let {
text: text,
icon: icon
} = _ref3;
const $firstIcon = getImageContainer(icon);
const $textContainer = text ? $("<span>").text(text).addClass("dx-button-text") : void 0;
const $secondIcon = getImageContainer("spindown").addClass("dx-icon-right");
$(buttonContent).append($firstIcon, $textContainer, $secondIcon)
}
}
_getActionButtonConfig() {
const {
icon: icon,
text: text,
type: type,
splitButton: splitButton
} = this.option();
const actionButtonConfig = {
text: text,
icon: icon,
type: type,
template: this._getButtonTemplate(),
elementAttr: {
class: "dx-dropdownbutton-action"
}
};
if (splitButton) {
actionButtonConfig.elementAttr.role = "menuitem"
}
return actionButtonConfig
}
_getSpinButtonConfig() {
const {
type: type
} = this.option();
const config = {
type: type,
icon: "spindown",
elementAttr: {
class: "dx-dropdownbutton-toggle",
role: "menuitem"
}
};
return config
}
_getButtonGroupItems() {
const {
splitButton: splitButton
} = this.option();
const items = [this._getActionButtonConfig()];
if (splitButton) {
items.push(this._getSpinButtonConfig())
}
return items
}
_buttonGroupItemClick(_ref4) {
let {
event: event,
itemData: itemData
} = _ref4;
const isActionButton = "dx-dropdownbutton-action" === itemData.elementAttr.class;
const isToggleButton = "dx-dropdownbutton-toggle" === itemData.elementAttr.class;
if (isToggleButton) {
this.toggle()
} else if (isActionButton) {
this._actionClickAction({
event: event,
selectedItem: this.option("selectedItem")
});
if (!this.option("splitButton")) {
this.toggle()
}
}
}
_getButtonGroupOptions() {
const {
accessKey: accessKey,
focusStateEnabled: focusStateEnabled,
hoverStateEnabled: hoverStateEnabled,
splitButton: splitButton,
stylingMode: stylingMode,
tabIndex: tabIndex
} = this.option();
const buttonGroupOptions = _extends({
items: this._getButtonGroupItems(),
width: "100%",
height: "100%",
selectionMode: "none",
focusStateEnabled: focusStateEnabled,
hoverStateEnabled: hoverStateEnabled,
stylingMode: stylingMode,
accessKey: accessKey,
tabIndex: tabIndex,
elementAttr: {
role: splitButton ? "menu" : "group"
},
onItemClick: this._buttonGroupItemClick.bind(this),
onKeyboardHandled: e => this._keyboardHandler(e)
}, this._options.cache("buttonGroupOptions"));
return buttonGroupOptions
}
_renderPopupContent() {
const $content = this._popup.$content();
const template = this._getTemplateByOption("dropDownContentTemplate");
$content.empty();
this._popupContentId = `dx-${new Guid}`;
this.setAria("id", this._popupContentId, $content);
return template.render({
container: getPublicElement($content),
model: this.option("items") || this._dataController.getDataSource()
})
}
_popupOptions() {
const horizontalAlignment = this.option("rtlEnabled") ? "right" : "left";
return extend({
dragEnabled: false,
focusStateEnabled: false,
deferRendering: this.option("deferRendering"),
hideOnOutsideClick: e => {
const $element = this.$element();
const $buttonClicked = $(e.target).closest(".dx-dropdownbutton");
return !$buttonClicked.is($element)
},
showTitle: false,
animation: {
show: {
type: "fade",
duration: 0,
from: 0,
to: 1
},
hide: {
type: "fade",
duration: 400,
from: 1,
to: 0
}
},
_ignoreFunctionValueDeprecation: true,
width: () => getElementWidth(this.$element()),
height: "auto",
shading: false,
position: {
of: this.$element(),
collision: "flipfit",
my: `${horizontalAlignment} top`,
at: `${horizontalAlignment} bottom`
},
_wrapperClassExternal: "dx-dropdowneditor-overlay"
}, this._options.cache("dropDownOptions"), {
visible: this.option("opened")
})
}
_listOptions() {
const selectedItemKey = this.option("selectedItemKey");
const useSelectMode = this.option("useSelectMode");
return {
selectionMode: useSelectMode ? "single" : "none",
wrapItemText: this.option("wrapItemText"),
focusStateEnabled: this.option("focusStateEnabled"),
hoverStateEnabled: this.option("hoverStateEnabled"),
useItemTextAsTitle: this.option("useItemTextAsTitle"),
onContentReady: () => this._fireContentReadyAction(),
selectedItemKeys: isDefined(selectedItemKey) && useSelectMode ? [selectedItemKey] : [],
grouped: this.option("grouped"),
groupTemplate: this.option("groupTemplate"),
keyExpr: this._dataController.key(),
noDataText: this.option("noDataText"),
displayExpr: this.option("displayExpr"),
itemTemplate: this.option("itemTemplate"),
items: this.option("items"),
dataSource: this._dataController.getDataSource(),
onItemClick: e => {
if (!this.option("useSelectMode")) {
this._lastSelectedItemData = e.itemData
}
this.option("selectedItemKey", this._keyGetter(e.itemData));
const actionResult = this._fireItemClickAction(e);
if (false !== actionResult) {
this.toggle(false);
this._buttonGroup.focus()
}
}
}
}
_upDownKeyHandler() {
var _this$_popup;
if (null !== (_this$_popup = this._popup) && void 0 !== _this$_popup && _this$_popup.option("visible") && this._list) {
this._list.focus()
} else {
this.open()
}
return true
}
_escHandler() {
this.close();
this._buttonGroup.focus();
return true
}
_tabHandler() {
this.close();
return true
}
_renderPopup() {
const $popup = $("<div>");
this.$element().append($popup);
this._popup = this._createComponent($popup, Popup, this._popupOptions());
this._popup.$content().addClass(DROP_DOWN_BUTTON_CONTENT);
this._popup.$wrapper().addClass("dx-dropdownbutton-popup-wrapper");
this._popup.$overlayContent().attr("aria-label", "Dropdown");
this._popup.on("hiding", this._popupHidingHandler.bind(this));
this._popup.on("showing", this._popupShowingHandler.bind(this));
this._bindInnerWidgetOptions(this._popup, "dropDownOptions")
}
_popupHidingHandler() {
this.option("opened", false);
this._updateAriaAttributes(false)
}
_popupOptionChanged(args) {
const options = Widget.getOptionsFromContainer(args);
this._setPopupOption(options);
const optionsKeys = Object.keys(options);
if (optionsKeys.includes("width") || optionsKeys.includes("height")) {
this._dimensionChanged()
}
}
_dimensionChanged() {
const popupWidth = getSizeValue(this.option("dropDownOptions.width"));
if (void 0 === popupWidth) {
this._setPopupOption("width", (() => getElementWidth(this.$element())))
}
}
_setPopupOption(optionName, value) {
this._setWidgetOption("_popup", arguments)
}
_popupShowingHandler() {
this.option("opened", true);
this._updateAriaAttributes(true)
}
_setElementAria(value) {
const elementAria = {
owns: value ? this._popupContentId : void 0
};
this.setAria(elementAria, this.$element())
}
_setButtonsAria(value) {
const commonButtonAria = {
expanded: value,
haspopup: "listbox"
};
const firstButtonAria = {};
if (!this.option("text")) {
firstButtonAria.label = "dropdownbutton"
}
this._getButtons().each(((index, $button) => {
if (0 === index) {
this.setAria(_extends({}, firstButtonAria, commonButtonAria), $($button))
} else {
this.setAria(commonButtonAria, $($button))
}
}))
}
_updateAriaAttributes(value) {
this._setElementAria(value);
this._setButtonsAria(value)
}
_getButtons() {
return this._buttonGroup.$element().find(".dx-button")
}
_renderButtonGroup() {
var _this$_buttonGroup;
const $buttonGroup = (null === (_this$_buttonGroup = this._buttonGroup) || void 0 === _this$_buttonGroup ? void 0 : _this$_buttonGroup.$element()) || $("<div>");
if (!this._buttonGroup) {
this.$element().append($buttonGroup)
}
this._buttonGroup = this._createComponent($buttonGroup, ButtonGroup, this._getButtonGroupOptions());
this._buttonGroup.registerKeyHandler("downArrow", this._upDownKeyHandler.bind(this));
this._buttonGroup.registerKeyHandler("tab", this._tabHandler.bind(this));
this._buttonGroup.registerKeyHandler("upArrow", this._upDownKeyHandler.bind(this));
this._buttonGroup.registerKeyHandler("escape", this._escHandler.bind(this));
this._bindInnerWidgetOptions(this._buttonGroup, "buttonGroupOptions");
this._updateAriaAttributes(this.option("opened"))
}
_updateArrowClass() {
const hasArrow = this.option("splitButton") || this.option("showArrowIcon");
this.$element().toggleClass("dx-dropdownbutton-has-arrow", hasArrow)
}
toggle(visible) {
var _this$_popup2;
if (!this._popup) {
this._renderPopup();
this._renderContent()
}
return null === (_this$_popup2 = this._popup) || void 0 === _this$_popup2 ? void 0 : _this$_popup2.toggle(visible)
}
open() {
return this.toggle(true)
}
close() {
return this.toggle(false)
}
_setListOption(name, value) {
var _this$_list;
null === (_this$_list = this._list) || void 0 === _this$_list || _this$_list.option(name, value)
}
_getDisplayValue(item) {
const isPrimitiveItem = !isObject(item);
const displayValue = isPrimitiveItem ? item : this._displayGetter(item);
return !isObject(displayValue) ? String(ensureDefined(displayValue, "")) : ""
}
_updateActionButton(selectedItem) {
if (this.option("useSelectMode")) {
this.option({
text: this._getDisplayValue(selectedItem),
icon: isPlainObject(selectedItem) ? selectedItem.icon : void 0
})
}
this._setOptionWithoutOptionChange("selectedItem", selectedItem);
this._setOptionWithoutOptionChange("selectedItemKey", this._keyGetter(selectedItem))
}
_clean() {
var _this$_list2, _this$_popup3;
null === (_this$_list2 = this._list) || void 0 === _this$_list2 || _this$_list2.$element().remove();
null === (_this$_popup3 = this._popup) || void 0 === _this$_popup3 || _this$_popup3.$element().remove()
}
_selectedItemKeyChanged(value) {
this._setListOption("selectedItemKeys", this.option("useSelectMode") && isDefined(value) ? [value] : []);
const previousItem = this.option("selectedItem");
this._loadSelectedItem().always((selectedItem => {
this._updateActionButton(selectedItem);
if (this._displayGetter(previousItem) !== this._displayGetter(selectedItem)) {
this._fireSelectionChangedAction({
previousValue: previousItem,
value: selectedItem
})
}
}))
}
_updateButtonGroup(name, value) {
this._buttonGroup.option(name, value);
this._updateAriaAttributes(this.option("opened"))
}
_actionButtonOptionChanged(_ref5) {
let {
name: name,
value: value
} = _ref5;
const newConfig = {};
newConfig[name] = value;
this._updateButtonGroup("items[0]", extend({}, this._getActionButtonConfig(), newConfig));
this._popup && this._popup.repaint()
}
_selectModeChanged(value) {
if (value) {
this._setListOption("selectionMode", "single");
const selectedItemKey = this.option("selectedItemKey");
this._setListOption("selectedItemKeys", isDefined(selectedItemKey) ? [selectedItemKey] : []);
this._selectedItemKeyChanged(this.option("selectedItemKey"))
} else {
this._setListOption("selectionMode", "none");
this.option({
selectedItemKey: void 0,
selectedItem: void 0
});
this._actionButtonOptionChanged({
text: this.option("text")
})
}
}
_updateItemCollection(optionName) {
const selectedItemKey = this.option("selectedItemKey");
this._setListOption("selectedItem", null);
this._setWidgetOption("_list", [optionName]);
if (isDefined(selectedItemKey)) {
this._loadSelectedItem().done((selectedItem => {
this._setListOption("selectedItemKeys", [selectedItemKey]);
this._setListOption("selectedItem", selectedItem)
})).fail((error => {
this._setListOption("selectedItemKeys", [])
})).always(this._updateActionButton.bind(this))
}
}
_updateDataController(items) {
this._dataController.updateDataSource(items, this.option("keyExpr"));
this._updateKeyExpr()
}
_updateKeyExpr() {
this._compileKeyGetter();
this._setListOption("keyExpr", this._dataController.key())
}
focus() {
this._buttonGroup.focus()
}
_optionChanged(args) {
var _this$_popup4;
const {
name: name,
value: value
} = args;
switch (name) {
case "useSelectMode":
this._selectModeChanged(value);
break;
case "splitButton":
this._updateArrowClass();
this._renderButtonGroup();
break;
case "displayExpr":
this._compileDisplayGetter();
this._setListOption(name, value);
this._updateActionButton(this.option("selectedItem"));
break;
case "keyExpr":
this._updateDataController();
break;
case "buttonGroupOptions":
this._innerWidgetOptionChanged(this._buttonGroup, args);
break;
case "dropDownOptions":
if ("dropDownOptions.visible" === args.fullName) {
break
}
if (void 0 !== args.value.visible) {
delete args.value.visible
}
this._popupOptionChanged(args);
this._innerWidgetOptionChanged(this._popup, args);
break;
case "opened":
this.toggle(value);
break;
case "focusStateEnabled":
case "hoverStateEnabled":
this._setListOption(name, value);
this._updateButtonGroup(name, value);
super._optionChanged(args);
break;
case "items":
this._updateDataController(this.option("items"));
this._updateItemCollection(name);
break;
case "dataSource":
this._dataController.updateDataSource(value);
this._updateKeyExpr();
this._updateItemCollection(name);
break;
case "icon":
case "text":
this._actionButtonOptionChanged(args);
break;
case "showArrowIcon":
this._updateArrowClass();
this._renderButtonGroup();
this._popup && this._popup.repaint();
break;
case "width":
case "height":
super._optionChanged(args);
null === (_this$_popup4 = this._popup) || void 0 === _this$_popup4 || _this$_popup4.repaint();
break;
case "stylingMode":
case "tabIndex":
this._updateButtonGroup(name, value);
break;
case "type":
this._updateButtonGroup("items", this._getButtonGroupItems());
break;
case "itemTemplate":
case "grouped":
case "noDataText":
case "groupTemplate":
case "wrapItemText":
case "useItemTextAsTitle":
this._setListOption(name, value);
break;
case "dropDownContentTemplate":
this._renderContent();
break;
case "selectedItemKey":
this._selectedItemKeyChanged(value);
break;
case "selectedItem":
break;
case "onItemClick":
this._createItemClickAction();
break;
case "onButtonClick":
this._createActionClickAction();
break;
case "onSelectionChanged":
this._createSelectionChangedAction();
break;
case "deferRendering": {
const {
opened: opened
} = this.option();
this.toggle(opened);
break
}
case "template":
this._renderButtonGroup();
break;
default:
super._optionChanged(args)
}
}
getDataSource() {
return this._dataController.getDataSource()
}
}
registerComponent("dxDropDownButton", DropDownButton);
export default DropDownButton;