UNPKG

devextreme

Version:

HTML5 JavaScript Component Suite for Responsive Web Development

565 lines (564 loc) • 20.6 kB
/** * DevExtreme (esm/__internal/ui/multi_view/multi_view.js) * Version: 25.2.3 * Build date: Fri Dec 12 2025 * * Copyright (c) 2012 - 2025 Developer Express Inc. ALL RIGHTS RESERVED * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/ */ import { locate } from "../../../common/core/animation/translator"; import Swipeable from "../../../common/core/events/gesture/swipeable"; import { triggerResizeEvent } from "../../../common/core/events/visibility_change"; import messageLocalization from "../../../common/core/localization/message"; import registerComponent from "../../../core/component_registrator"; import devices from "../../../core/devices"; import { getPublicElement } from "../../../core/element"; import $ from "../../../core/renderer"; import { deferRender, noop } from "../../../core/utils/common"; import { Deferred } from "../../../core/utils/deferred"; import { sign } from "../../../core/utils/math"; import { getWidth } from "../../../core/utils/size"; import { isDefined } from "../../../core/utils/type"; import CollectionWidgetLiveUpdate from "../../../ui/collection/ui.collection_widget.live_update"; import { _translator, animation } from "./multi_view.animation"; const MULTIVIEW_CLASS = "dx-multiview"; const MULTIVIEW_WRAPPER_CLASS = "dx-multiview-wrapper"; const MULTIVIEW_ITEM_CONTAINER_CLASS = "dx-multiview-item-container"; const MULTIVIEW_ITEM_CLASS = "dx-multiview-item"; const MULTIVIEW_ITEM_HIDDEN_CLASS = "dx-multiview-item-hidden"; const MULTIVIEW_ITEM_DATA_KEY = "dxMultiViewItemData"; const MULTIVIEW_ANIMATION_DURATION = 200; const toNumber = value => +value; const getPosition = $element => locate($element).left; class MultiView extends CollectionWidgetLiveUpdate { _activeStateUnit() { return ".dx-multiview-item" } _supportedKeys() { return Object.assign({}, super._supportedKeys(), { pageUp: noop, pageDown: noop }) } _getDefaultOptions() { return Object.assign({}, super._getDefaultOptions(), { selectedIndex: 0, swipeEnabled: true, animationEnabled: true, loop: false, deferRendering: true, loopItemFocus: false, selectOnFocus: true, selectionMode: "single", selectionRequired: true, selectByClick: false }) } _defaultOptionsRules() { return super._defaultOptionsRules().concat([{ device: () => "desktop" === devices.real().deviceType && !devices.isSimulator(), options: { focusStateEnabled: true } }]) } _itemClass() { return "dx-multiview-item" } _itemDataKey() { return "dxMultiViewItemData" } _itemContainer() { return this._$itemContainer } _itemElements() { return this._itemContainer().children(this._itemSelector()) } _itemWidth() { if (this._itemWidthValue) { return this._itemWidthValue } return getWidth(this._$wrapper) } _clearItemWidthCache() { delete this._itemWidthValue } _itemsCount() { const { items: items = [] } = this.option(); return items.length } _isAllItemsHidden() { const { items: items = [] } = this.option(); return items.every(((_item, index) => !this._isItemVisible(index))) } _normalizeIndex(index, direction) { let loop = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : true; const count = this._itemsCount(); let normalizedIndex = index; if (this._isAllItemsHidden()) { return } if (index < 0) { normalizedIndex += count } if (index >= count) { normalizedIndex -= count } const step = direction > 0 ? -1 : 1; const lastNotLoopedIndex = -1 === step ? 0 : count - 1; while (!this._isItemVisible(normalizedIndex) && (loop || normalizedIndex !== lastNotLoopedIndex)) { normalizedIndex = (normalizedIndex + step + count) % count } return normalizedIndex } _getRTLSignCorrection() { const { rtlEnabled: rtlEnabled } = this.option(); return rtlEnabled ? -1 : 1 } _init() { super._init(); const $element = this.$element(); $element.addClass("dx-multiview"); this._$wrapper = $("<div>").addClass("dx-multiview-wrapper"); this._$wrapper.appendTo($element); this._$itemContainer = $("<div>").addClass("dx-multiview-item-container"); this._$itemContainer.appendTo(this._$wrapper); const { loop: loop } = this.option(); this.option("loopItemFocus", loop); this._findBoundaryIndices(); this._initSwipeable() } _ensureSelectedItemIsVisible() { const { loop: loop, selectedIndex: currentSelectedIndex } = this.option(); if (!isDefined(currentSelectedIndex)) { return } if (this._isItemVisible(currentSelectedIndex)) { return } if (this._isAllItemsHidden()) { this.option("selectedIndex", 0); return } const direction = -1 * this._getRTLSignCorrection(); let newSelectedIndex = this._normalizeIndex(currentSelectedIndex, direction, loop); if (newSelectedIndex === currentSelectedIndex) { newSelectedIndex = this._normalizeIndex(currentSelectedIndex, -direction, loop) } this.option("selectedIndex", newSelectedIndex) } _initMarkup() { this._deferredItems = []; super._initMarkup(); this._ensureSelectedItemIsVisible(); const selectedItemIndices = this._getSelectedItemIndices(); this._updateItemsVisibility(selectedItemIndices[0]); this._setElementAria(); this._setItemsAria() } _afterItemElementDeleted($item, deletedActionArgs) { super._afterItemElementDeleted($item, deletedActionArgs); if (this._deferredItems) { this._deferredItems.splice(deletedActionArgs.itemIndex, 1) } } _beforeItemElementInserted(change) { super._beforeItemElementInserted(change); if (this._deferredItems) { this._deferredItems.splice(change.index, 0, null) } } _executeItemRenderAction(_index, itemData, itemElement) { const { items: items = [] } = this.option(); const index = items.indexOf(itemData); super._executeItemRenderAction(index, itemData, itemElement) } _renderItemContent(args) { const renderContentDeferred = Deferred(); const deferred = Deferred(); deferred.done((() => { const $itemContent = super._renderItemContent(args); renderContentDeferred.resolve($itemContent) })); this._deferredItems[args.index] = deferred; const { deferRendering: deferRendering } = this.option(); if (!deferRendering) { deferred.resolve() } return renderContentDeferred.promise() } _render() { super._render(); deferRender((() => { const selectedItemIndices = this._getSelectedItemIndices(); this._updateItems(selectedItemIndices[0]) })) } _getElementAria() { return { role: "group", roledescription: messageLocalization.format("dxMultiView-elementAriaRoleDescription"), label: messageLocalization.format("dxMultiView-elementAriaLabel") } } _setElementAria() { const aria = this._getElementAria(); this.setAria(aria, this.$element()) } _setItemsAria() { const $itemElements = this._itemElements(); const itemsCount = this._itemsCount(); $itemElements.each(((itemIndex, item) => { const aria = this._getItemAria({ itemIndex: itemIndex, itemsCount: itemsCount }); this.setAria(aria, $(item)) })) } _getItemAria(args) { const { itemIndex: itemIndex, itemsCount: itemsCount } = args; return { role: "group", roledescription: messageLocalization.format("dxMultiView-itemAriaRoleDescription"), label: messageLocalization.format("dxMultiView-itemAriaLabel", itemIndex + 1, itemsCount) } } _updateItems(selectedIndex, newIndex) { this._updateItemsPosition(selectedIndex, newIndex); this._updateItemsVisibility(selectedIndex, newIndex) } _modifyByChanges(changes, isPartialRefresh) { super._modifyByChanges(changes, isPartialRefresh); const selectedItemIndices = this._getSelectedItemIndices(); this._updateItemsVisibility(selectedItemIndices[0]) } _updateItemsPosition(selectedIndex, newIndex) { const $itemElements = this._itemElements(); const positionSign = isDefined(newIndex) ? -this._animationDirection(newIndex, selectedIndex) : void 0; const $selectedItem = $itemElements.eq(selectedIndex); _translator.move($selectedItem, 0); if (isDefined(newIndex) && isDefined(positionSign)) { _translator.move($itemElements.eq(newIndex), 100 * positionSign + "%") } } _isItemVisible(index) { var _items$index; const { items: items = [] } = this.option(); return (isDefined(index) && (null === (_items$index = items[index]) || void 0 === _items$index ? void 0 : _items$index.visible)) ?? true } _updateItemsVisibility(selectedIndex, newIndex) { const $itemElements = this._itemElements(); $itemElements.each(((itemIndex, item) => { const $item = $(item); const isHidden = itemIndex !== selectedIndex && itemIndex !== newIndex; if (!isHidden) { this._renderSpecificItem(itemIndex) } $item.toggleClass("dx-multiview-item-hidden", isHidden); this.setAria("hidden", isHidden || void 0, $item) })) } _renderSpecificItem(index) { const $item = this._itemElements().eq(index); const hasItemContent = $item.find(this._itemContentClass()).length > 0; if (isDefined(index) && !hasItemContent) { var _this$_deferredItems$; null === (_this$_deferredItems$ = this._deferredItems[index]) || void 0 === _this$_deferredItems$ || _this$_deferredItems$.resolve(); triggerResizeEvent($item) } } _refreshItem($item, item) { super._refreshItem($item, item); const { selectedIndex: selectedIndex = 0 } = this.option(); this._updateItemsVisibility(selectedIndex) } _setAriaSelectionAttribute() {} _updateSelection(addedSelection, removedSelection) { const newIndex = addedSelection[0]; const prevIndex = removedSelection[0]; animation.complete(this._$itemContainer); this._updateItems(prevIndex, newIndex); const animationDirection = this._animationDirection(newIndex, prevIndex); this._animateItemContainer(animationDirection * this._itemWidth(), (() => { _translator.move(this._$itemContainer, 0); this._updateItems(newIndex); getWidth(this._$itemContainer) })) } _animateItemContainer(position, completeCallback) { const { animationEnabled: animationEnabled } = this.option(); const duration = animationEnabled ? 200 : 0; animation.moveTo(this._$itemContainer, position, duration, completeCallback) } _animationDirection(newIndex, prevIndex) { const containerPosition = getPosition(this._$itemContainer); const signCorrection = this._getRTLSignCorrection() * this._getItemFocusLoopSignCorrection(); const indexDifference = (prevIndex - newIndex) * signCorrection; const isSwipePresent = 0 !== containerPosition; const directionSignVariable = isSwipePresent ? containerPosition : indexDifference; return sign(directionSignVariable) } _getSwipeDisabledState() { const { swipeEnabled: swipeEnabled } = this.option(); return !swipeEnabled || this._itemsCount() <= 1 } _initSwipeable() { this._createComponent(this.$element(), Swipeable, { disabled: this._getSwipeDisabledState(), elastic: false, itemSizeFunc: this._itemWidth.bind(this), onStart: args => { this._swipeStartHandler(args.event) }, onUpdated: args => { this._swipeUpdateHandler(args.event) }, onEnd: args => { this._swipeEndHandler(args.event) } }) } _findBoundaryIndices() { const { items: items = [] } = this.option(); let firstIndex; let lastIndex; items.forEach(((item, index) => { const isDisabled = Boolean(null === item || void 0 === item ? void 0 : item.disabled); const isVisible = this._isItemVisible(index); if (!isDisabled && isVisible) { firstIndex ?? (firstIndex = index); lastIndex = index } })); this._boundaryIndices = { firstAvailableIndex: firstIndex ?? 0, lastAvailableIndex: lastIndex ?? items.length - 1, firstTrueIndex: 0, lastTrueIndex: items.length - 1 } } _swipeStartHandler(e) { animation.complete(this._$itemContainer); const { selectedIndex: selectedIndex, loop: loop, rtlEnabled: rtlEnabled } = this.option(); if (!isDefined(selectedIndex) || !isDefined(this._boundaryIndices)) { return } const { firstAvailableIndex: firstAvailableIndex, lastAvailableIndex: lastAvailableIndex } = this._boundaryIndices; const canSwipeLeft = rtlEnabled ? selectedIndex > firstAvailableIndex : selectedIndex < lastAvailableIndex; const canSwipeRight = rtlEnabled ? selectedIndex < lastAvailableIndex : selectedIndex > firstAvailableIndex; e.maxLeftOffset = toNumber(!!loop || canSwipeLeft); e.maxRightOffset = toNumber(!!loop || canSwipeRight) } _swipeUpdateHandler(e) { const { offset: offset } = e; const swipeDirection = sign(offset) * this._getRTLSignCorrection(); const { selectedIndex: selectedIndex } = this.option(); if (!isDefined(selectedIndex)) { return } const newIndex = this._normalizeIndex(selectedIndex - swipeDirection, swipeDirection); if (selectedIndex === newIndex) { return } _translator.move(this._$itemContainer, offset * this._itemWidth()); this._updateItems(selectedIndex, newIndex) } _findNextAvailableIndex(index, offset) { if (!isDefined(this._boundaryIndices)) { return index } const { items: items = [], loop: loop } = this.option(); const { firstAvailableIndex: firstAvailableIndex, lastAvailableIndex: lastAvailableIndex, firstTrueIndex: firstTrueIndex, lastTrueIndex: lastTrueIndex } = this._boundaryIndices; const isFirstActive = [firstTrueIndex, firstAvailableIndex].includes(index); const isLastActive = [lastTrueIndex, lastAvailableIndex].includes(index); if (loop) { if (isFirstActive && offset < 0) { return lastAvailableIndex } if (isLastActive && offset > 0) { return firstAvailableIndex } } for (let i = index + offset; i >= firstAvailableIndex && i <= lastAvailableIndex; i += offset) { const isDisabled = Boolean(items[i].disabled); const isVisible = this._isItemVisible(i); if (!isDisabled && isVisible) { return i } } return index } _postprocessSwipe(args) {} _swipeEndHandler(e) { const targetOffset = e.targetOffset * this._getRTLSignCorrection(); if (targetOffset) { const { selectedIndex: selectedIndex } = this.option(); if (!isDefined(selectedIndex)) { return } const newSelectedIndex = this._findNextAvailableIndex(selectedIndex, -targetOffset); this.selectItem(newSelectedIndex).fail((() => { this._animateItemContainer(0, noop) })).done((() => { this._postprocessSwipe({ swipedTabsIndex: newSelectedIndex }) })); const $selectedElement = this.itemElements().filter(".dx-item-selected"); const { focusStateEnabled: focusStateEnabled } = this.option(); if (focusStateEnabled) { this.option("focusedElement", getPublicElement($selectedElement)) } } else { this._animateItemContainer(0, noop) } } _getItemFocusLoopSignCorrection() { return this._itemFocusLooped ? -1 : 1 } _moveFocus(location, e) { super._moveFocus(location, e); this._itemFocusLooped = false } _prevItem($items) { const $result = super._prevItem($items); this._itemFocusLooped = $result.is($items.last()); return $result } _nextItem($items) { const $result = super._nextItem($items); this._itemFocusLooped = $result.is($items.first()); return $result } _dimensionChanged() { this._clearItemWidthCache() } _visibilityChanged(visible) { if (visible) { this._dimensionChanged() } } _updateSwipeDisabledState() { const disabled = this._getSwipeDisabledState(); Swipeable.getInstance(this.$element()).option("disabled", disabled) } _dispose() { delete this._boundaryIndices; super._dispose() } _itemOptionChanged(item, property, value, prevValue) { super._itemOptionChanged(item, property, value, prevValue); const { selectedItem: selectedItem } = this.option(); if ("visible" === property && item === selectedItem) { this._ensureSelectedItemIsVisible() } } _optionChanged(args) { const { value: value } = args; switch (args.name) { case "loop": this.option("loopItemFocus", value); break; case "animationEnabled": break; case "swipeEnabled": this._updateSwipeDisabledState(); break; case "deferRendering": this._invalidate(); break; case "items": this._updateSwipeDisabledState(); this._findBoundaryIndices(); super._optionChanged(args); break; case "selectedIndex": if (this._isItemVisible(value)) { super._optionChanged(args) } else { this._ensureSelectedItemIsVisible() } break; default: super._optionChanged(args) } } } registerComponent("dxMultiView", MultiView); export default MultiView;