UNPKG

devextreme

Version:

HTML5 JavaScript Component Suite for Responsive Web Development

701 lines (698 loc) • 28.5 kB
/** * DevExtreme (cjs/__internal/ui/tabs/m_tabs.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/ */ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _events_engine = _interopRequireDefault(require("../../../common/core/events/core/events_engine")); var _hold = _interopRequireDefault(require("../../../common/core/events/hold")); var _pointer = _interopRequireDefault(require("../../../common/core/events/pointer")); var _index = require("../../../common/core/events/utils/index"); var _component_registrator = _interopRequireDefault(require("../../../core/component_registrator")); var _devices = _interopRequireDefault(require("../../../core/devices")); var _renderer = _interopRequireDefault(require("../../../core/renderer")); var _bindable_template = require("../../../core/templates/bindable_template"); var _icon = require("../../../core/utils/icon"); var _iterator = require("../../../core/utils/iterator"); var _size = require("../../../core/utils/size"); var _type = require("../../../core/utils/type"); var _window = require("../../../core/utils/window"); var _button = _interopRequireDefault(require("../../../ui/button")); var _uiCollection_widget = _interopRequireDefault(require("../../../ui/collection/ui.collection_widget.live_update")); var _themes = require("../../../ui/themes"); var _utils = require("../../../ui/widget/utils.ink_ripple"); var _m_scrollable = _interopRequireDefault(require("../../ui/scroll_view/m_scrollable")); var _get_boundary_props = require("../../ui/scroll_view/utils/get_boundary_props"); var _get_scroll_left_max = require("../../ui/scroll_view/utils/get_scroll_left_max"); var _constants = require("./constants"); var _m_item = _interopRequireDefault(require("./m_item")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e } } function _extends() { return _extends = Object.assign ? Object.assign.bind() : function(n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) { ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]) } } return n }, _extends.apply(null, arguments) } const TABS_CLASS = "dx-tabs"; const TABS_WRAPPER_CLASS = "dx-tabs-wrapper"; const TABS_STRETCHED_CLASS = "dx-tabs-stretched"; const TABS_SCROLLABLE_CLASS = "dx-tabs-scrollable"; const TABS_NAV_BUTTONS_CLASS = "dx-tabs-nav-buttons"; const OVERFLOW_HIDDEN_CLASS = "dx-overflow-hidden"; const TABS_ITEM_CLASS = "dx-tab"; const TABS_ITEM_SELECTED_CLASS = "dx-tab-selected"; const TABS_SCROLLING_ENABLED_CLASS = "dx-tabs-scrolling-enabled"; const TABS_NAV_BUTTON_CLASS = "dx-tabs-nav-button"; const TABS_LEFT_NAV_BUTTON_CLASS = "dx-tabs-nav-button-left"; const TABS_RIGHT_NAV_BUTTON_CLASS = "dx-tabs-nav-button-right"; const TABS_ITEM_TEXT_CLASS = "dx-tab-text"; const TABS_ITEM_TEXT_SPAN_CLASS = "dx-tab-text-span"; const TABS_ITEM_TEXT_SPAN_PSEUDO_CLASS = "dx-tab-text-span-pseudo"; const STATE_DISABLED_CLASS = "dx-state-disabled"; const FOCUSED_DISABLED_NEXT_TAB_CLASS = "dx-focused-disabled-next-tab"; const FOCUSED_DISABLED_PREV_TAB_CLASS = "dx-focused-disabled-prev-tab"; const TABS_ORIENTATION_CLASS = { vertical: "dx-tabs-vertical", horizontal: "dx-tabs-horizontal" }; const 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" }; const 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" }; const TABS_STYLING_MODE_CLASS = { primary: "dx-tabs-styling-mode-primary", secondary: "dx-tabs-styling-mode-secondary" }; const TABS_ITEM_DATA_KEY = "dxTabData"; const BUTTON_NEXT_ICON = "chevronnext"; const BUTTON_PREV_ICON = "chevronprev"; const FEEDBACK_HIDE_TIMEOUT = 100; const FEEDBACK_DURATION_INTERVAL = 5; const FEEDBACK_SCROLL_TIMEOUT = 300; const TAB_OFFSET = 30; const ORIENTATION = { horizontal: "horizontal", vertical: "vertical" }; const INDICATOR_POSITION = { top: "top", right: "right", bottom: "bottom", left: "left" }; const SCROLLABLE_DIRECTION = { horizontal: "horizontal", vertical: "vertical" }; const ICON_POSITION = { top: "top", end: "end", bottom: "bottom", start: "start" }; const STYLING_MODE = { primary: "primary", secondary: "secondary" }; class Tabs extends _uiCollection_widget.default { _getDefaultOptions() { return _extends({}, super._getDefaultOptions(), { 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: data => data ? data.badge : void 0, _itemAttributes: { role: "tab" }, _indicatorPosition: null }) } _defaultOptionsRules() { const themeName = (0, _themes.current)(); return super._defaultOptionsRules().concat([{ device: () => "desktop" !== _devices.default.real().deviceType, options: { showNavButtons: false } }, { device: { deviceType: "desktop" }, options: { scrollByContent: false } }, { device: () => "desktop" === _devices.default.real().deviceType && !_devices.default.isSimulator(), options: { focusStateEnabled: true } }, { device: () => (0, _themes.isFluent)(themeName), options: { iconPosition: ICON_POSITION.top, stylingMode: STYLING_MODE.secondary } }, { device: () => (0, _themes.isMaterial)(themeName), options: { useInkRipple: true, selectOnFocus: false, iconPosition: ICON_POSITION.top } }]) } _init() { const { orientation: orientation, stylingMode: stylingMode, scrollingEnabled: scrollingEnabled } = this.option(); const indicatorPosition = this._getIndicatorPosition(); super._init(); this._activeStateUnit = ".dx-tab"; this.setAria("role", "tablist"); this.$element().addClass("dx-tabs"); this._toggleScrollingEnabledClass(scrollingEnabled); this._toggleOrientationClass(orientation); this._toggleIndicatorPositionClass(indicatorPosition); this._toggleIconPositionClass(); this._toggleStylingModeClass(stylingMode); this._renderWrapper(); this._renderMultiple(); this._feedbackHideTimeout = 100 } _prepareDefaultItemTemplate(data, $container) { const text = (0, _type.isPlainObject)(data) ? null === data || void 0 === data ? void 0 : data.text : data; if ((0, _type.isDefined)(text)) { const $tabTextSpan = (0, _renderer.default)("<span>").addClass("dx-tab-text-span"); $tabTextSpan.text(text); const $tabTextSpanPseudo = (0, _renderer.default)("<span>").addClass("dx-tab-text-span-pseudo"); $tabTextSpanPseudo.text(text); $tabTextSpanPseudo.appendTo($tabTextSpan); $tabTextSpan.appendTo($container) } if ((0, _type.isDefined)(data.html)) { $container.html(data.html) } } _initTemplates() { super._initTemplates(); this._templateManager.addDefaultTemplates({ item: new _bindable_template.BindableTemplate((($container, data) => { this._prepareDefaultItemTemplate(data, $container); const $iconElement = (0, _icon.getImageContainer)(data.icon); $iconElement && $iconElement.prependTo($container); const $tabItem = (0, _renderer.default)("<div>").addClass("dx-tab-text"); $container.wrapInner($tabItem) }), ["text", "html", "icon"], this.option("integrationOptions.watchMethod")) }) } _createItemByTemplate(itemTemplate, renderArgs) { const { itemData: itemData, container: container, index: index } = renderArgs; return itemTemplate.render({ model: itemData, container: container, index: index, onRendered: this._onItemTemplateRendered(itemTemplate, renderArgs) }) } _itemClass() { return "dx-tab" } _selectedItemClass() { return "dx-tab-selected" } _itemDataKey() { return "dxTabData" } _initMarkup() { super._initMarkup(); this.option("useInkRipple") && this._renderInkRipple(); this.$element().addClass("dx-overflow-hidden") } _postProcessRenderItems() { this._renderScrolling() } _renderScrolling() { const removeClasses = ["dx-tabs-stretched", _constants.TABS_EXPANDED_CLASS, "dx-overflow-hidden"]; this.$element().removeClass(removeClasses.join(" ")); if (this.option("scrollingEnabled") && this._isItemsSizeExceeded()) { if (!this._scrollable) { this._renderScrollable(); this._renderNavButtons() } const scrollable = this.getScrollable(); null === scrollable || void 0 === scrollable || scrollable.update(); if (this.option("rtlEnabled")) { const maxLeftOffset = (0, _get_scroll_left_max.getScrollLeftMax)((0, _renderer.default)(this.getScrollable().container()).get(0)); null === scrollable || void 0 === scrollable || 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("dx-tabs-stretched") } this.$element().removeClass("dx-tabs-nav-buttons").addClass(_constants.TABS_EXPANDED_CLASS) } } _isVertical() { const { orientation: orientation } = this.option(); return orientation === ORIENTATION.vertical } _isItemsSizeExceeded() { const isVertical = this._isVertical(); const isItemsSizeExceeded = isVertical ? this._isItemsHeightExceeded() : this._isItemsWidthExceeded(); return isItemsSizeExceeded } _isItemsWidthExceeded() { const $visibleItems = this._getVisibleItems(); const tabItemTotalWidth = this._getSummaryItemsSize("width", $visibleItems, true); const elementWidth = (0, _size.getWidth)(this.$element()); if ([tabItemTotalWidth, elementWidth].includes(0)) { return false } const isItemsWidthExceeded = tabItemTotalWidth > elementWidth - 1; return isItemsWidthExceeded } _isItemsHeightExceeded() { const $visibleItems = this._getVisibleItems(); const itemsHeight = this._getSummaryItemsSize("height", $visibleItems, true); const elementHeight = (0, _size.getHeight)(this.$element()); const isItemsHeightExceeded = itemsHeight - 1 > elementHeight; return isItemsHeightExceeded } _needStretchItems() { const $visibleItems = this._getVisibleItems(); const elementWidth = (0, _size.getWidth)(this.$element()); const itemsWidth = []; (0, _iterator.each)($visibleItems, ((_, item) => { itemsWidth.push((0, _size.getOuterWidth)(item, true)) })); const maxTabItemWidth = Math.max.apply(null, itemsWidth); const requireWidth = elementWidth / $visibleItems.length; const needStretchItems = maxTabItemWidth > requireWidth + 1; return needStretchItems } _cleanNavButtons() { if (!this._leftButton || !this._rightButton) { return } this._leftButton.$element().remove(); this._rightButton.$element().remove(); this._leftButton = null; this._rightButton = null } _cleanScrolling() { if (!this._scrollable) { return } this._$wrapper.appendTo(this.$element()); this._scrollable.$element().remove(); this._scrollable = null; this._cleanNavButtons() } _renderInkRipple() { this._inkRipple = (0, _utils.render)() } _getPointerEvent() { return _pointer.default.up } _toggleActiveState($element, value, e) { super._toggleActiveState.apply(this, arguments); if (!this._inkRipple) { return } const config = { element: $element, event: e }; if (value) { this._inkRipple.showWave(config) } else { this._inkRipple.hideWave(config) } } _renderMultiple() { const { selectionMode: selectionMode } = this.option(); if ("multiple" === selectionMode) { this.option("selectOnFocus", false) } } _renderWrapper() { this._$wrapper = (0, _renderer.default)("<div>").addClass("dx-tabs-wrapper"); this.$element().append(this._$wrapper) } _itemContainer() { return this._$wrapper } _getScrollableDirection() { const isVertical = this._isVertical(); const scrollableDirection = isVertical ? SCROLLABLE_DIRECTION.vertical : SCROLLABLE_DIRECTION.horizontal; return scrollableDirection } _updateScrollable() { if (this.getScrollable()) { this._cleanScrolling() } this._renderScrolling() } _renderScrollable() { const $itemContainer = this.$element().wrapInner((0, _renderer.default)("<div>").addClass("dx-tabs-scrollable")).children(); this._scrollable = this._createComponent($itemContainer, _m_scrollable.default, { direction: this._getScrollableDirection(), showScrollbar: "never", useKeyboard: false, useNative: false, scrollByContent: this.option("scrollByContent"), onScroll: () => { this._updateNavButtonsState() } }); this.$element().append(this._scrollable.$element()) } _scrollToItem(itemData) { if (!this._scrollable) { return } const $item = this._editStrategy.getItemElement(itemData); this._scrollable.scrollToElement($item) } _renderNavButtons() { const { showNavButtons: showNavButtons, rtlEnabled: rtlEnabled } = this.option(); this.$element().toggleClass("dx-tabs-nav-buttons", showNavButtons); if (!showNavButtons) { return } this._leftButton = this._createNavButton(-30, rtlEnabled ? "chevronnext" : "chevronprev"); const $leftButton = this._leftButton.$element(); $leftButton.addClass("dx-tabs-nav-button-left"); this.$element().prepend($leftButton); this._rightButton = this._createNavButton(30, rtlEnabled ? "chevronprev" : "chevronnext"); const $rightButton = this._rightButton.$element(); $rightButton.addClass("dx-tabs-nav-button-right"); this.$element().append($rightButton) } _updateNavButtonsAriaDisabled() { const buttons = [this._leftButton, this._rightButton]; buttons.forEach((button => { null === button || void 0 === button || button.$element().attr({ "aria-disabled": null }) })) } _updateNavButtonsState() { const isVertical = this._isVertical(); const scrollable = this.getScrollable(); if (isVertical) { var _this$_leftButton, _this$_rightButton; null === (_this$_leftButton = this._leftButton) || void 0 === _this$_leftButton || _this$_leftButton.option("disabled", (0, _get_boundary_props.isReachedTop)(scrollable.scrollTop(), 1)); null === (_this$_rightButton = this._rightButton) || void 0 === _this$_rightButton || _this$_rightButton.option("disabled", (0, _get_boundary_props.isReachedBottom)((0, _renderer.default)(scrollable.container()).get(0), scrollable.scrollTop(), 0, 1)) } else { var _this$_leftButton2, _this$_rightButton2; null === (_this$_leftButton2 = this._leftButton) || void 0 === _this$_leftButton2 || _this$_leftButton2.option("disabled", (0, _get_boundary_props.isReachedLeft)(scrollable.scrollLeft(), 1)); null === (_this$_rightButton2 = this._rightButton) || void 0 === _this$_rightButton2 || _this$_rightButton2.option("disabled", (0, _get_boundary_props.isReachedRight)((0, _renderer.default)(scrollable.container()).get(0), scrollable.scrollLeft(), 1)) } this._updateNavButtonsAriaDisabled() } _updateScrollPosition(offset, duration) { var _this$_scrollable, _this$_scrollable2; null === (_this$_scrollable = this._scrollable) || void 0 === _this$_scrollable || _this$_scrollable.update(); null === (_this$_scrollable2 = this._scrollable) || void 0 === _this$_scrollable2 || _this$_scrollable2.scrollBy(offset / duration) } _createNavButton(offset, icon) { const holdAction = this._createAction((() => { this._holdInterval = setInterval((() => { this._updateScrollPosition(offset, 5) }), 5) })); const holdEventName = (0, _index.addNamespace)(_hold.default.name, "dxNavButton"); const pointerUpEventName = (0, _index.addNamespace)(_pointer.default.up, "dxNavButton"); const pointerOutEventName = (0, _index.addNamespace)(_pointer.default.out, "dxNavButton"); const navButton = this._createComponent((0, _renderer.default)("<div>").addClass("dx-tabs-nav-button"), _button.default, { focusStateEnabled: false, icon: icon, integrationOptions: {}, elementAttr: { role: null, "aria-label": null, "aria-disabled": null }, onClick: () => { this._updateScrollPosition(offset, 1) } }); const $navButton = navButton.$element(); _events_engine.default.on($navButton, holdEventName, { timeout: 300 }, (e => { holdAction({ event: e }) })); _events_engine.default.on($navButton, pointerUpEventName, (() => { this._clearInterval() })); _events_engine.default.on($navButton, pointerOutEventName, (() => { this._clearInterval() })); return navButton } _clearInterval() { if (this._holdInterval) { clearInterval(this._holdInterval) } } _updateSelection(addedSelection) { this._scrollable && this._scrollable.scrollToElement(this.itemElements().eq(addedSelection[0])) } _visibilityChanged(visible) { if (visible) { this._dimensionChanged() } } _dimensionChanged() { this._renderScrolling() } _itemSelectHandler(e) { const { selectionMode: selectionMode } = this.option(); if ("single" === selectionMode && this.isItemSelected(e.currentTarget)) { return } super._itemSelectHandler(e) } _clean() { this._cleanScrolling(); super._clean() } _toggleTabsVerticalClass(value) { this.$element().toggleClass(TABS_ORIENTATION_CLASS.vertical, value) } _toggleTabsHorizontalClass(value) { this.$element().toggleClass(TABS_ORIENTATION_CLASS.horizontal, value) } _getIndicatorPositionClass(indicatorPosition) { return INDICATOR_POSITION_CLASS[indicatorPosition] } _getIndicatorPosition() { const { _indicatorPosition: _indicatorPosition, rtlEnabled: rtlEnabled } = this.option(); if (_indicatorPosition) { return _indicatorPosition } const isVertical = this._isVertical(); if (rtlEnabled) { return isVertical ? INDICATOR_POSITION.left : INDICATOR_POSITION.bottom } return isVertical ? INDICATOR_POSITION.right : INDICATOR_POSITION.bottom } _toggleIndicatorPositionClass(indicatorPosition) { const newClass = this._getIndicatorPositionClass(indicatorPosition); this._toggleElementClasses(INDICATOR_POSITION_CLASS, newClass) } _toggleScrollingEnabledClass(scrollingEnabled) { this.$element().toggleClass("dx-tabs-scrolling-enabled", Boolean(scrollingEnabled)) } _toggleOrientationClass(orientation) { const isVertical = orientation === ORIENTATION.vertical; this._toggleTabsVerticalClass(isVertical); this._toggleTabsHorizontalClass(!isVertical) } _getTabsIconPositionClass() { const 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; default: return TABS_ICON_POSITION_CLASS.start } } _toggleIconPositionClass() { const newClass = this._getTabsIconPositionClass(); this._toggleElementClasses(TABS_ICON_POSITION_CLASS, newClass) } _toggleStylingModeClass(value) { const newClass = TABS_STYLING_MODE_CLASS[value] ?? TABS_STYLING_MODE_CLASS.primary; this._toggleElementClasses(TABS_STYLING_MODE_CLASS, newClass) } _toggleElementClasses(classMap, newClass) { for (const key in classMap) { this.$element().removeClass(classMap[key]) } this.$element().addClass(newClass) } _toggleFocusedDisabledNextClass(currentIndex, isNextDisabled) { this._itemElements().eq(currentIndex).toggleClass("dx-focused-disabled-next-tab", isNextDisabled) } _toggleFocusedDisabledPrevClass(currentIndex, isPrevDisabled) { this._itemElements().eq(currentIndex).toggleClass("dx-focused-disabled-prev-tab", isPrevDisabled) } _toggleFocusedDisabledClasses(value) { const { selectedIndex: currentIndex } = this.option(); this._itemElements().removeClass("dx-focused-disabled-next-tab").removeClass("dx-focused-disabled-prev-tab"); const prevItemIndex = currentIndex - 1; const nextItemIndex = currentIndex + 1; const nextFocusedIndex = (0, _renderer.default)(value).index(); const isNextDisabled = this._itemElements().eq(nextItemIndex).hasClass("dx-state-disabled"); const isPrevDisabled = this._itemElements().eq(prevItemIndex).hasClass("dx-state-disabled"); const shouldNextClassBeSetted = isNextDisabled && nextFocusedIndex === nextItemIndex; const shouldPrevClassBeSetted = isPrevDisabled && nextFocusedIndex === prevItemIndex; this._toggleFocusedDisabledNextClass(currentIndex, shouldNextClassBeSetted); this._toggleFocusedDisabledPrevClass(currentIndex, shouldPrevClassBeSetted) } _updateFocusedElement() { const { focusStateEnabled: focusStateEnabled, selectedIndex: selectedIndex } = this.option(); const itemElements = this._itemElements(); if (focusStateEnabled && itemElements.length) { const selectedItem = itemElements.get(selectedIndex); this.option({ focusedElement: selectedItem }) } } _optionChanged(args) { var _this$_scrollable3; switch (args.name) { case "useInkRipple": case "scrollingEnabled": this._toggleScrollingEnabledClass(args.value); this._invalidate(); break; case "showNavButtons": case "badgeExpr": this._invalidate(); break; case "scrollByContent": null === (_this$_scrollable3 = this._scrollable) || void 0 === _this$_scrollable3 || _this$_scrollable3.option(args.name, args.value); break; case "width": case "height": super._optionChanged(args); this._dimensionChanged(); break; case "selectionMode": this._renderMultiple(); super._optionChanged(args); break; case "focusedElement": this._toggleFocusedDisabledClasses(args.value); super._optionChanged(args); this._scrollToItem(args.value); break; case "rtlEnabled": { super._optionChanged(args); const indicatorPosition = this._getIndicatorPosition(); this._toggleIndicatorPositionClass(indicatorPosition); break } case "orientation": { this._toggleOrientationClass(args.value); const indicatorPosition = this._getIndicatorPosition(); this._toggleIndicatorPositionClass(indicatorPosition); if ((0, _window.hasWindow)()) { this._updateScrollable() } break } case "iconPosition": this._toggleIconPositionClass(); if ((0, _window.hasWindow)()) { this._dimensionChanged() } break; case "stylingMode": this._toggleStylingModeClass(args.value); if ((0, _window.hasWindow)()) { this._dimensionChanged() } break; case "_indicatorPosition": this._toggleIndicatorPositionClass(args.value); break; case "selectedIndex": case "selectedItem": case "selectedItems": super._optionChanged(args); this._updateFocusedElement(); break; default: super._optionChanged(args) } } _afterItemElementInserted() { super._afterItemElementInserted(); this._planPostRenderActions() } _afterItemElementDeleted($item, deletedActionArgs) { super._afterItemElementDeleted($item, deletedActionArgs); this._renderScrolling() } getScrollable() { return this._scrollable } } Tabs.ItemClass = _m_item.default; (0, _component_registrator.default)("dxTabs", Tabs); var _default = exports.default = Tabs;