devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
703 lines (702 loc) • 26.9 kB
JavaScript
/**
* DevExtreme (esm/ui/tabs.js)
* Version: 23.2.6
* Build date: Wed May 01 2024
*
* Copyright (c) 2012 - 2024 Developer Express Inc. ALL RIGHTS RESERVED
* Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
*/
import {
getWidth,
getHeight,
getOuterWidth
} from "../core/utils/size";
import $ from "../core/renderer";
import eventsEngine from "../events/core/events_engine";
import devices from "../core/devices";
import registerComponent from "../core/component_registrator";
import Button from "./button";
import {
render
} from "./widget/utils.ink_ripple";
import {
addNamespace
} from "../events/utils/index";
import {
extend
} from "../core/utils/extend";
import {
isPlainObject,
isDefined
} from "../core/utils/type";
import pointerEvents from "../events/pointer";
import {
each
} from "../core/utils/iterator";
import TabsItem from "./tabs/item";
import {
TABS_EXPANDED_CLASS
} from "./tabs/constants";
import {
isMaterial,
isFluent,
current as currentTheme
} from "./themes";
import holdEvent from "../events/hold";
import Scrollable from "./scroll_view/ui.scrollable";
import {
default as CollectionWidget
} from "./collection/ui.collection_widget.live_update";
import {
getImageContainer
} from "../core/utils/icon";
import {
BindableTemplate
} from "../core/templates/bindable_template";
import {
Deferred,
when
} from "../core/utils/deferred";
import {
isReachedLeft,
isReachedRight,
isReachedTop,
isReachedBottom
} from "../renovation/ui/scroll_view/utils/get_boundary_props";
import {
getScrollLeftMax
} from "../renovation/ui/scroll_view/utils/get_scroll_left_max";
import {
hasWindow
} from "../core/utils/window";
var TABS_CLASS = "dx-tabs";
var TABS_WRAPPER_CLASS = "dx-tabs-wrapper";
var TABS_STRETCHED_CLASS = "dx-tabs-stretched";
var TABS_SCROLLABLE_CLASS = "dx-tabs-scrollable";
var TABS_NAV_BUTTONS_CLASS = "dx-tabs-nav-buttons";
var OVERFLOW_HIDDEN_CLASS = "dx-overflow-hidden";
var TABS_ITEM_CLASS = "dx-tab";
var TABS_ITEM_SELECTED_CLASS = "dx-tab-selected";
var TABS_SCROLLING_ENABLED_CLASS = "dx-tabs-scrolling-enabled";
var TABS_NAV_BUTTON_CLASS = "dx-tabs-nav-button";
var TABS_LEFT_NAV_BUTTON_CLASS = "dx-tabs-nav-button-left";
var TABS_RIGHT_NAV_BUTTON_CLASS = "dx-tabs-nav-button-right";
var TABS_ITEM_TEXT_CLASS = "dx-tab-text";
var TABS_ITEM_TEXT_SPAN_CLASS = "dx-tab-text-span";
var TABS_ITEM_TEXT_SPAN_PSEUDO_CLASS = "dx-tab-text-span-pseudo";
var STATE_DISABLED_CLASS = "dx-state-disabled";
var FOCUSED_DISABLED_NEXT_TAB_CLASS = "dx-focused-disabled-next-tab";
var FOCUSED_DISABLED_PREV_TAB_CLASS = "dx-focused-disabled-prev-tab";
var TABS_ORIENTATION_CLASS = {
vertical: "dx-tabs-vertical",
horizontal: "dx-tabs-horizontal"
};
var INDICATOR_POSITION_CLASS = {
top: "dx-tab-indicator-position-top",
right: "dx-tab-indicator-position-right",
bottom: "dx-tab-indicator-position-bottom",
left: "dx-tab-indicator-position-left"
};
var TABS_ICON_POSITION_CLASS = {
top: "dx-tabs-icon-position-top",
end: "dx-tabs-icon-position-end",
bottom: "dx-tabs-icon-position-bottom",
start: "dx-tabs-icon-position-start"
};
var TABS_STYLING_MODE_CLASS = {
primary: "dx-tabs-styling-mode-primary",
secondary: "dx-tabs-styling-mode-secondary"
};
var TABS_ITEM_DATA_KEY = "dxTabData";
var BUTTON_NEXT_ICON = "chevronnext";
var BUTTON_PREV_ICON = "chevronprev";
var FEEDBACK_HIDE_TIMEOUT = 100;
var FEEDBACK_DURATION_INTERVAL = 5;
var FEEDBACK_SCROLL_TIMEOUT = 300;
var TAB_OFFSET = 30;
var ORIENTATION = {
horizontal: "horizontal",
vertical: "vertical"
};
var INDICATOR_POSITION = {
top: "top",
right: "right",
bottom: "bottom",
left: "left"
};
var SCROLLABLE_DIRECTION = {
horizontal: "horizontal",
vertical: "vertical"
};
var ICON_POSITION = {
top: "top",
end: "end",
bottom: "bottom",
start: "start"
};
var STYLING_MODE = {
primary: "primary",
secondary: "secondary"
};
var Tabs = CollectionWidget.inherit({
_activeStateUnit: "." + TABS_ITEM_CLASS,
_getDefaultOptions: function() {
return extend(this.callBase(), {
hoverStateEnabled: true,
showNavButtons: true,
scrollByContent: true,
scrollingEnabled: true,
selectionMode: "single",
orientation: ORIENTATION.horizontal,
iconPosition: ICON_POSITION.start,
stylingMode: STYLING_MODE.primary,
activeStateEnabled: true,
selectionRequired: false,
selectOnFocus: true,
loopItemFocus: false,
useInkRipple: false,
badgeExpr: function(data) {
return data ? data.badge : void 0
},
_itemAttributes: {
role: "tab"
},
_indicatorPosition: null
})
},
_defaultOptionsRules: function() {
var themeName = currentTheme();
return this.callBase().concat([{
device: () => "desktop" !== devices.real().deviceType,
options: {
showNavButtons: false
}
}, {
device: {
deviceType: "desktop"
},
options: {
scrollByContent: false
}
}, {
device: () => "desktop" === devices.real().deviceType && !devices.isSimulator(),
options: {
focusStateEnabled: true
}
}, {
device: () => isFluent(themeName),
options: {
iconPosition: ICON_POSITION.top,
stylingMode: STYLING_MODE.secondary
}
}, {
device: () => isMaterial(themeName),
options: {
useInkRipple: true,
selectOnFocus: false,
iconPosition: ICON_POSITION.top
}
}])
},
_init() {
var {
orientation: orientation,
stylingMode: stylingMode,
scrollingEnabled: scrollingEnabled
} = this.option();
var indicatorPosition = this._getIndicatorPosition();
this.callBase();
this.$element().addClass(TABS_CLASS);
this._toggleScrollingEnabledClass(scrollingEnabled);
this._toggleOrientationClass(orientation);
this._toggleIndicatorPositionClass(indicatorPosition);
this._toggleIconPositionClass();
this._toggleStylingModeClass(stylingMode);
this._renderWrapper();
this._renderMultiple();
this._feedbackHideTimeout = FEEDBACK_HIDE_TIMEOUT
},
_prepareDefaultItemTemplate(data, $container) {
var text = isPlainObject(data) ? null === data || void 0 === data ? void 0 : data.text : data;
if (isDefined(text)) {
var $tabTextSpan = $("<span>").addClass(TABS_ITEM_TEXT_SPAN_CLASS);
$tabTextSpan.text(text);
var $tabTextSpanPseudo = $("<span>").addClass(TABS_ITEM_TEXT_SPAN_PSEUDO_CLASS);
$tabTextSpanPseudo.text(text);
$tabTextSpanPseudo.appendTo($tabTextSpan);
$tabTextSpan.appendTo($container)
}
if (isDefined(data.html)) {
$container.html(data.html)
}
},
_initTemplates() {
this.callBase();
this._templateManager.addDefaultTemplates({
item: new BindableTemplate((($container, data) => {
this._prepareDefaultItemTemplate(data, $container);
var $iconElement = getImageContainer(data.icon);
$iconElement && $iconElement.prependTo($container);
var $tabItem = $("<div>").addClass(TABS_ITEM_TEXT_CLASS);
$container.wrapInner($tabItem)
}).bind(this), ["text", "html", "icon"], this.option("integrationOptions.watchMethod"))
})
},
_createItemByTemplate: function(itemTemplate, renderArgs) {
var {
itemData: itemData,
container: container,
index: index
} = renderArgs;
this._deferredTemplates[index] = new Deferred;
return itemTemplate.render({
model: itemData,
container: container,
index: index,
onRendered: () => this._deferredTemplates[index].resolve()
})
},
_itemClass: function() {
return TABS_ITEM_CLASS
},
_selectedItemClass: function() {
return TABS_ITEM_SELECTED_CLASS
},
_itemDataKey: function() {
return TABS_ITEM_DATA_KEY
},
_initMarkup: function() {
this._deferredTemplates = [];
this.callBase();
this.option("useInkRipple") && this._renderInkRipple();
this.$element().addClass(OVERFLOW_HIDDEN_CLASS)
},
_render: function() {
this.callBase();
this._deferRenderScrolling()
},
_deferRenderScrolling() {
when.apply(this, this._deferredTemplates).done(() => this._renderScrolling())
},
_renderScrolling() {
var removeClasses = [TABS_STRETCHED_CLASS, TABS_EXPANDED_CLASS, OVERFLOW_HIDDEN_CLASS];
this.$element().removeClass(removeClasses.join(" "));
if (this.option("scrollingEnabled") && this._isItemsSizeExceeded()) {
if (!this._scrollable) {
this._renderScrollable();
this._renderNavButtons()
}
var scrollable = this.getScrollable();
scrollable.update();
if (this.option("rtlEnabled")) {
var maxLeftOffset = getScrollLeftMax($(this.getScrollable().container()).get(0));
scrollable.scrollTo({
left: maxLeftOffset
})
}
this._updateNavButtonsState();
this._scrollToItem(this.option("selectedItem"))
}
if (!(this.option("scrollingEnabled") && this._isItemsSizeExceeded())) {
this._cleanScrolling();
if (this._needStretchItems()) {
this.$element().addClass(TABS_STRETCHED_CLASS)
}
this.$element().removeClass(TABS_NAV_BUTTONS_CLASS).addClass(TABS_EXPANDED_CLASS)
}
},
_isVertical() {
return this.option("orientation") === ORIENTATION.vertical
},
_isItemsSizeExceeded() {
var isVertical = this._isVertical();
var isItemsSizeExceeded = isVertical ? this._isItemsHeightExceeded() : this._isItemsWidthExceeded();
return isItemsSizeExceeded
},
_isItemsWidthExceeded() {
var $visibleItems = this._getVisibleItems();
var tabItemTotalWidth = this._getSummaryItemsSize("width", $visibleItems, true);
var elementWidth = getWidth(this.$element());
if ([tabItemTotalWidth, elementWidth].includes(0)) {
return false
}
var isItemsWidthExceeded = tabItemTotalWidth > elementWidth - 1;
return isItemsWidthExceeded
},
_isItemsHeightExceeded() {
var $visibleItems = this._getVisibleItems();
var itemsHeight = this._getSummaryItemsSize("height", $visibleItems, true);
var elementHeight = getHeight(this.$element());
var isItemsHeightExceeded = itemsHeight - 1 > elementHeight;
return isItemsHeightExceeded
},
_needStretchItems() {
var $visibleItems = this._getVisibleItems();
var elementWidth = getWidth(this.$element());
var itemsWidth = [];
each($visibleItems, (_, item) => {
itemsWidth.push(getOuterWidth(item, true))
});
var maxTabItemWidth = Math.max.apply(null, itemsWidth);
var requireWidth = elementWidth / $visibleItems.length;
var needStretchItems = maxTabItemWidth > requireWidth + 1;
return needStretchItems
},
_cleanNavButtons: function() {
if (!this._leftButton || !this._rightButton) {
return
}
this._leftButton.$element().remove();
this._rightButton.$element().remove();
this._leftButton = null;
this._rightButton = null
},
_cleanScrolling: function() {
if (!this._scrollable) {
return
}
this._$wrapper.appendTo(this.$element());
this._scrollable.$element().remove();
this._scrollable = null;
this._cleanNavButtons()
},
_renderInkRipple: function() {
this._inkRipple = render()
},
_getPointerEvent: () => pointerEvents.up,
_toggleActiveState: function($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)
}
},
_renderMultiple: function() {
if ("multiple" === this.option("selectionMode")) {
this.option("selectOnFocus", false)
}
},
_renderWrapper: function() {
this._$wrapper = $("<div>").addClass(TABS_WRAPPER_CLASS);
this.setAria("role", "tablist", this._$wrapper);
this.$element().append(this._$wrapper)
},
_itemContainer: function() {
return this._$wrapper
},
_getScrollableDirection() {
var isVertical = this._isVertical();
var scrollableDirection = isVertical ? SCROLLABLE_DIRECTION.vertical : SCROLLABLE_DIRECTION.horizontal;
return scrollableDirection
},
_updateScrollable() {
if (this.getScrollable()) {
this._cleanScrolling()
}
this._renderScrolling()
},
_renderScrollable() {
var $itemContainer = this.$element().wrapInner($("<div>").addClass(TABS_SCROLLABLE_CLASS)).children();
this._scrollable = this._createComponent($itemContainer, Scrollable, {
direction: this._getScrollableDirection(),
showScrollbar: "never",
useKeyboard: false,
useNative: false,
scrollByContent: this.option("scrollByContent"),
onScroll: () => {
this._updateNavButtonsState()
}
});
this.$element().append(this._scrollable.$element())
},
_scrollToItem: function(itemData) {
if (!this._scrollable) {
return
}
var $item = this._editStrategy.getItemElement(itemData);
this._scrollable.scrollToElement($item)
},
_renderNavButtons: function() {
this.$element().toggleClass(TABS_NAV_BUTTONS_CLASS, this.option("showNavButtons"));
if (!this.option("showNavButtons")) {
return
}
var rtlEnabled = this.option("rtlEnabled");
this._leftButton = this._createNavButton(-TAB_OFFSET, rtlEnabled ? BUTTON_NEXT_ICON : BUTTON_PREV_ICON);
var $leftButton = this._leftButton.$element();
$leftButton.addClass(TABS_LEFT_NAV_BUTTON_CLASS);
this.$element().prepend($leftButton);
this._rightButton = this._createNavButton(TAB_OFFSET, rtlEnabled ? BUTTON_PREV_ICON : BUTTON_NEXT_ICON);
var $rightButton = this._rightButton.$element();
$rightButton.addClass(TABS_RIGHT_NAV_BUTTON_CLASS);
this.$element().append($rightButton)
},
_updateNavButtonsState() {
var isVertical = this._isVertical();
var scrollable = this.getScrollable();
if (isVertical) {
var _this$_leftButton, _this$_rightButton;
null === (_this$_leftButton = this._leftButton) || void 0 === _this$_leftButton ? void 0 : _this$_leftButton.option("disabled", isReachedTop(scrollable.scrollTop(), 1));
null === (_this$_rightButton = this._rightButton) || void 0 === _this$_rightButton ? void 0 : _this$_rightButton.option("disabled", isReachedBottom($(scrollable.container()).get(0), scrollable.scrollTop(), 0, 1))
} else {
var _this$_leftButton2, _this$_rightButton2;
null === (_this$_leftButton2 = this._leftButton) || void 0 === _this$_leftButton2 ? void 0 : _this$_leftButton2.option("disabled", isReachedLeft(scrollable.scrollLeft(), 1));
null === (_this$_rightButton2 = this._rightButton) || void 0 === _this$_rightButton2 ? void 0 : _this$_rightButton2.option("disabled", isReachedRight($(scrollable.container()).get(0), scrollable.scrollLeft(), 1))
}
},
_updateScrollPosition: function(offset, duration) {
this._scrollable.update();
this._scrollable.scrollBy(offset / duration)
},
_createNavButton: function(offset, icon) {
var that = this;
var holdAction = that._createAction((function() {
that._holdInterval = setInterval((function() {
that._updateScrollPosition(offset, FEEDBACK_DURATION_INTERVAL)
}), FEEDBACK_DURATION_INTERVAL)
}));
var holdEventName = addNamespace(holdEvent.name, "dxNavButton");
var pointerUpEventName = addNamespace(pointerEvents.up, "dxNavButton");
var pointerOutEventName = addNamespace(pointerEvents.out, "dxNavButton");
var navButton = this._createComponent($("<div>").addClass(TABS_NAV_BUTTON_CLASS), Button, {
focusStateEnabled: false,
icon: icon,
onClick: function() {
that._updateScrollPosition(offset, 1)
},
integrationOptions: {}
});
var $navButton = navButton.$element();
eventsEngine.on($navButton, holdEventName, {
timeout: FEEDBACK_SCROLL_TIMEOUT
}, function(e) {
holdAction({
event: e
})
}.bind(this));
eventsEngine.on($navButton, pointerUpEventName, (function() {
that._clearInterval()
}));
eventsEngine.on($navButton, pointerOutEventName, (function() {
that._clearInterval()
}));
return navButton
},
_clearInterval: function() {
if (this._holdInterval) {
clearInterval(this._holdInterval)
}
},
_updateSelection: function(addedSelection) {
this._scrollable && this._scrollable.scrollToElement(this.itemElements().eq(addedSelection[0]))
},
_visibilityChanged: function(visible) {
if (visible) {
this._dimensionChanged()
}
},
_dimensionChanged: function() {
this._renderScrolling()
},
_itemSelectHandler: function(e) {
if ("single" === this.option("selectionMode") && this.isItemSelected(e.currentTarget)) {
return
}
this.callBase(e)
},
_refreshActiveDescendant: function() {
this.callBase(this._$wrapper)
},
_clean: function() {
this._deferredTemplates = [];
this._cleanScrolling();
this.callBase()
},
_toggleTabsVerticalClass(value) {
this.$element().toggleClass(TABS_ORIENTATION_CLASS.vertical, value)
},
_toggleTabsHorizontalClass(value) {
this.$element().toggleClass(TABS_ORIENTATION_CLASS.horizontal, value)
},
_getIndicatorPositionClass: indicatorPosition => INDICATOR_POSITION_CLASS[indicatorPosition],
_getIndicatorPosition() {
var {
_indicatorPosition: _indicatorPosition,
rtlEnabled: rtlEnabled
} = this.option();
if (_indicatorPosition) {
return _indicatorPosition
}
var isVertical = this._isVertical();
if (rtlEnabled) {
return isVertical ? INDICATOR_POSITION.left : INDICATOR_POSITION.bottom
} else {
return isVertical ? INDICATOR_POSITION.right : INDICATOR_POSITION.bottom
}
},
_toggleIndicatorPositionClass(indicatorPosition) {
var newClass = this._getIndicatorPositionClass(indicatorPosition);
this._toggleElementClasses(INDICATOR_POSITION_CLASS, newClass)
},
_toggleScrollingEnabledClass(scrollingEnabled) {
this.$element().toggleClass(TABS_SCROLLING_ENABLED_CLASS, Boolean(scrollingEnabled))
},
_toggleOrientationClass(orientation) {
var isVertical = orientation === ORIENTATION.vertical;
this._toggleTabsVerticalClass(isVertical);
this._toggleTabsHorizontalClass(!isVertical)
},
_getTabsIconPositionClass() {
var position = this.option("iconPosition");
switch (position) {
case ICON_POSITION.top:
return TABS_ICON_POSITION_CLASS.top;
case ICON_POSITION.end:
return TABS_ICON_POSITION_CLASS.end;
case ICON_POSITION.bottom:
return TABS_ICON_POSITION_CLASS.bottom;
case ICON_POSITION.start:
default:
return TABS_ICON_POSITION_CLASS.start
}
},
_toggleIconPositionClass() {
var newClass = this._getTabsIconPositionClass();
this._toggleElementClasses(TABS_ICON_POSITION_CLASS, newClass)
},
_toggleStylingModeClass(value) {
var _TABS_STYLING_MODE_CL;
var newClass = null !== (_TABS_STYLING_MODE_CL = TABS_STYLING_MODE_CLASS[value]) && void 0 !== _TABS_STYLING_MODE_CL ? _TABS_STYLING_MODE_CL : TABS_STYLING_MODE_CLASS.primary;
this._toggleElementClasses(TABS_STYLING_MODE_CLASS, newClass)
},
_toggleElementClasses(classMap, newClass) {
for (var key in classMap) {
this.$element().removeClass(classMap[key])
}
this.$element().addClass(newClass)
},
_toggleFocusedDisabledNextClass(currentIndex, isNextDisabled) {
this._itemElements().eq(currentIndex).toggleClass(FOCUSED_DISABLED_NEXT_TAB_CLASS, isNextDisabled)
},
_toggleFocusedDisabledPrevClass(currentIndex, isPrevDisabled) {
this._itemElements().eq(currentIndex).toggleClass(FOCUSED_DISABLED_PREV_TAB_CLASS, isPrevDisabled)
},
_toggleFocusedDisabledClasses(value) {
var {
selectedIndex: currentIndex
} = this.option();
this._itemElements().removeClass(FOCUSED_DISABLED_NEXT_TAB_CLASS).removeClass(FOCUSED_DISABLED_PREV_TAB_CLASS);
var prevItemIndex = currentIndex - 1;
var nextItemIndex = currentIndex + 1;
var nextFocusedIndex = $(value).index();
var isNextDisabled = this._itemElements().eq(nextItemIndex).hasClass(STATE_DISABLED_CLASS);
var isPrevDisabled = this._itemElements().eq(prevItemIndex).hasClass(STATE_DISABLED_CLASS);
var shouldNextClassBeSetted = isNextDisabled && nextFocusedIndex === nextItemIndex;
var shouldPrevClassBeSetted = isPrevDisabled && nextFocusedIndex === prevItemIndex;
this._toggleFocusedDisabledNextClass(currentIndex, shouldNextClassBeSetted);
this._toggleFocusedDisabledPrevClass(currentIndex, shouldPrevClassBeSetted)
},
_updateFocusedElement() {
var {
focusStateEnabled: focusStateEnabled,
selectedIndex: selectedIndex
} = this.option();
var itemElements = this._itemElements();
if (focusStateEnabled && itemElements.length) {
var selectedItem = itemElements.get(selectedIndex);
this.option({
focusedElement: selectedItem
})
}
},
_optionChanged: function(args) {
switch (args.name) {
case "useInkRipple":
case "scrollingEnabled":
this._toggleScrollingEnabledClass(args.value);
this._invalidate();
break;
case "showNavButtons":
this._invalidate();
break;
case "scrollByContent":
this._scrollable && this._scrollable.option(args.name, args.value);
break;
case "width":
case "height":
this.callBase(args);
this._dimensionChanged();
break;
case "selectionMode":
this._renderMultiple();
this.callBase(args);
break;
case "badgeExpr":
this._invalidate();
break;
case "focusedElement":
this._toggleFocusedDisabledClasses(args.value);
this.callBase(args);
this._scrollToItem(args.value);
break;
case "rtlEnabled":
this.callBase(args);
var indicatorPosition = this._getIndicatorPosition();
this._toggleIndicatorPositionClass(indicatorPosition);
break;
case "orientation":
this._toggleOrientationClass(args.value);
var _indicatorPosition2 = this._getIndicatorPosition();
this._toggleIndicatorPositionClass(_indicatorPosition2);
if (hasWindow()) {
this._updateScrollable()
}
break;
case "iconPosition":
this._toggleIconPositionClass();
if (hasWindow()) {
this._dimensionChanged()
}
break;
case "stylingMode":
this._toggleStylingModeClass(args.value);
if (hasWindow()) {
this._dimensionChanged()
}
break;
case "_indicatorPosition":
this._toggleIndicatorPositionClass(args.value);
break;
case "selectedIndex":
case "selectedItem":
case "selectedItems":
this.callBase(args);
this._updateFocusedElement();
break;
default:
this.callBase(args)
}
},
_afterItemElementInserted() {
this.callBase();
this._deferRenderScrolling()
},
_afterItemElementDeleted($item, deletedActionArgs) {
this.callBase($item, deletedActionArgs);
this._renderScrolling()
},
getScrollable() {
return this._scrollable
}
});
Tabs.ItemClass = TabsItem;
registerComponent("dxTabs", Tabs);
export default Tabs;