devextreme
Version: 
HTML5 JavaScript Component Suite for Responsive Web Development
395 lines (394 loc) • 14.8 kB
JavaScript
/**
 * DevExtreme (esm/__internal/ui/m_accordion.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 {
    fx
} from "../../common/core/animation";
import {
    name as clickEventName
} from "../../common/core/events/click";
import eventsEngine from "../../common/core/events/core/events_engine";
import {
    addNamespace
} from "../../common/core/events/utils/index";
import registerComponent from "../../core/component_registrator";
import devices from "../../core/devices";
import domAdapter from "../../core/dom_adapter";
import {
    getPublicElement
} from "../../core/element";
import $ from "../../core/renderer";
import {
    BindableTemplate
} from "../../core/templates/bindable_template";
import {
    deferRender
} from "../../core/utils/common";
import {
    Deferred,
    when
} from "../../core/utils/deferred";
import {
    extend
} from "../../core/utils/extend";
import {
    getImageContainer
} from "../../core/utils/icon";
import * as iteratorUtils from "../../core/utils/iterator";
import {
    getHeight,
    getOuterHeight,
    setHeight
} from "../../core/utils/size";
import {
    isDefined,
    isPlainObject
} from "../../core/utils/type";
import {
    isMaterialBased
} from "../../ui/themes";
import CollectionWidget from "../ui/collection/m_collection_widget.live_update";
const ACCORDION_CLASS = "dx-accordion";
const ACCORDION_WRAPPER_CLASS = "dx-accordion-wrapper";
const ACCORDION_ITEM_CLASS = "dx-accordion-item";
const ACCORDION_ITEM_OPENED_CLASS = "dx-accordion-item-opened";
const ACCORDION_ITEM_CLOSED_CLASS = "dx-accordion-item-closed";
const ACCORDION_ITEM_TITLE_CLASS = "dx-accordion-item-title";
const ACCORDION_ITEM_BODY_CLASS = "dx-accordion-item-body";
const ACCORDION_ITEM_TITLE_CAPTION_CLASS = "dx-accordion-item-title-caption";
const ACCORDION_ITEM_DATA_KEY = "dxAccordionItemData";
class Accordion extends CollectionWidget {
    _getDefaultOptions() {
        return _extends({}, super._getDefaultOptions(), {
            hoverStateEnabled: true,
            itemTitleTemplate: "title",
            onItemTitleClick: null,
            selectedIndex: 0,
            collapsible: false,
            multiple: false,
            animationDuration: 300,
            deferRendering: true,
            selectByClick: true,
            activeStateEnabled: true,
            _itemAttributes: {
                role: "tab"
            },
            _animationEasing: "ease"
        })
    }
    _defaultOptionsRules() {
        return super._defaultOptionsRules().concat([{
            device: () => "desktop" === devices.real().deviceType && !devices.isSimulator(),
            options: {
                focusStateEnabled: true
            }
        }, {
            device: () => isMaterialBased(),
            options: {
                animationDuration: 200,
                _animationEasing: "cubic-bezier(0.4, 0, 0.2, 1)"
            }
        }])
    }
    _itemElements() {
        return this._itemContainer().children(this._itemSelector())
    }
    _init() {
        super._init();
        this._activeStateUnit = ".dx-accordion-item";
        this.option("selectionRequired", !this.option("collapsible"));
        this.option("selectionMode", this.option("multiple") ? "multiple" : "single");
        const $element = this.$element();
        $element.addClass("dx-accordion");
        this._$container = $("<div>").addClass("dx-accordion-wrapper");
        $element.append(this._$container)
    }
    _initTemplates() {
        super._initTemplates();
        this._templateManager.addDefaultTemplates({
            title: new BindableTemplate((($container, data) => {
                if (isPlainObject(data)) {
                    const $iconElement = getImageContainer(data.icon);
                    if ($iconElement) {
                        $container.append($iconElement)
                    }
                    if (isDefined(data.title) && !isPlainObject(data.title)) {
                        $container.append(domAdapter.createTextNode(data.title))
                    }
                } else if (isDefined(data)) {
                    $container.text(String(data))
                }
                $container.wrapInner($("<div>").addClass("dx-accordion-item-title-caption"))
            }), ["title", "icon"], this.option("integrationOptions.watchMethod"))
        })
    }
    _initMarkup() {
        this._deferredItems = [];
        super._initMarkup();
        this.setAria({
            role: "tablist",
            multiselectable: this.option("multiple")
        });
        deferRender((() => {
            const selectedItemIndices = this._getSelectedItemIndices();
            this._renderSelection(selectedItemIndices, [])
        }))
    }
    _postProcessRenderItems() {
        this._updateItemHeights(true)
    }
    _itemDataKey() {
        return "dxAccordionItemData"
    }
    _itemClass() {
        return "dx-accordion-item"
    }
    _itemContainer() {
        return this._$container
    }
    _itemTitles() {
        return this._itemElements().find(".dx-accordion-item-title")
    }
    _itemContents() {
        return this._itemElements().find(".dx-accordion-item-body")
    }
    _getItemData(target) {
        return $(target).parent().data(this._itemDataKey()) || super._getItemData.apply(this, arguments)
    }
    _executeItemRenderAction(itemData) {
        if (itemData.type) {
            return
        }
        super._executeItemRenderAction.apply(this, arguments)
    }
    _itemSelectHandler(e) {
        if ($(e.target).closest(this._itemContents()).length) {
            return
        }
        super._itemSelectHandler.apply(this, arguments)
    }
    _afterItemElementDeleted($item, deletedActionArgs) {
        this._deferredItems.splice(deletedActionArgs.itemIndex, 1);
        super._afterItemElementDeleted.apply(this, arguments)
    }
    _renderItemContent(args) {
        const itemTitleDeferred = super._renderItemContent(extend({}, args, {
            contentClass: "dx-accordion-item-title",
            templateProperty: "titleTemplate",
            defaultTemplateName: this.option("itemTitleTemplate")
        }));
        const callBase = super._renderItemContent.bind(this);
        itemTitleDeferred.done((itemTitle => {
            this._attachItemTitleClickAction(itemTitle);
            const deferred = Deferred();
            if (isDefined(this._deferredItems[args.index])) {
                this._deferredItems[args.index] = deferred
            } else {
                this._deferredItems.push(deferred)
            }
            if (!this.option("deferRendering") || this._getSelectedItemIndices().includes(args.index)) {
                deferred.resolve()
            }
            deferred.done((() => {
                callBase(extend({}, args, {
                    contentClass: "dx-accordion-item-body",
                    container: getPublicElement($("<div>").appendTo($(itemTitle).parent()))
                }))
            }))
        }))
    }
    _attachItemTitleClickAction(itemTitle) {
        const eventName = addNamespace(clickEventName, this.NAME);
        eventsEngine.off(itemTitle, eventName);
        eventsEngine.on(itemTitle, eventName, this._itemTitleClickHandler.bind(this))
    }
    _itemTitleClickHandler(e) {
        this._itemDXEventHandler(e, "onItemTitleClick")
    }
    _renderSelection(addedSelection, removedSelection) {
        this._itemElements().addClass("dx-accordion-item-closed");
        this.setAria("hidden", true, this._itemContents());
        this._updateItems(addedSelection, removedSelection)
    }
    _updateSelection(addedSelection, removedSelection) {
        this._updateItems(addedSelection, removedSelection);
        this._updateItemHeightsWrapper(false)
    }
    _updateItems(addedSelection, removedSelection) {
        const $items = this._itemElements();
        iteratorUtils.each(addedSelection, ((_, index) => {
            var _this$_deferredItems$;
            null === (_this$_deferredItems$ = this._deferredItems[index]) || void 0 === _this$_deferredItems$ || _this$_deferredItems$.resolve();
            const $item = $items.eq(index).addClass("dx-accordion-item-opened").removeClass("dx-accordion-item-closed");
            this.setAria("hidden", false, $item.find(".dx-accordion-item-body"))
        }));
        iteratorUtils.each(removedSelection, ((_, index) => {
            const $item = $items.eq(index).removeClass("dx-accordion-item-opened");
            this.setAria("hidden", true, $item.find(".dx-accordion-item-body"))
        }))
    }
    _updateItemHeightsWrapper(skipAnimation) {
        if (this.option("templatesRenderAsynchronously")) {
            this._animationTimer = setTimeout((() => {
                this._updateItemHeights(skipAnimation)
            }))
        } else {
            this._updateItemHeights(skipAnimation)
        }
    }
    _updateItemHeights(skipAnimation) {
        const that = this;
        const deferredAnimate = that._deferredAnimate;
        const itemHeight = this._splitFreeSpace(this._calculateFreeSpace());
        clearTimeout(this._animationTimer);
        return when.apply($, [].slice.call(this._itemElements()).map((item => that._updateItemHeight($(item), itemHeight, skipAnimation)))).done((() => {
            if (deferredAnimate) {
                deferredAnimate.resolveWith(that)
            }
        }))
    }
    _updateItemHeight($item, itemHeight, skipAnimation) {
        const $title = $item.children(".dx-accordion-item-title");
        if (fx.isAnimating($item)) {
            fx.stop($item)
        }
        const startItemHeight = getOuterHeight($item);
        let finalItemHeight;
        if ($item.hasClass("dx-accordion-item-opened")) {
            finalItemHeight = itemHeight + getOuterHeight($title);
            if (!finalItemHeight) {
                setHeight($item, "auto");
                finalItemHeight = getOuterHeight($item)
            }
        } else {
            finalItemHeight = getOuterHeight($title)
        }
        return this._animateItem($item, startItemHeight, finalItemHeight, skipAnimation, !!itemHeight)
    }
    _animateItem($element, startHeight, endHeight, skipAnimation, fixedHeight) {
        let d;
        if (skipAnimation || startHeight === endHeight) {
            $element.css("height", endHeight);
            d = Deferred().resolve()
        } else {
            const {
                animationDuration: animationDuration,
                _animationEasing: easing
            } = this.option();
            d = fx.animate($element, {
                type: "custom",
                from: {
                    height: startHeight
                },
                to: {
                    height: endHeight
                },
                duration: animationDuration,
                easing: easing
            })
        }
        return d.done((() => {
            if ($element.hasClass("dx-accordion-item-opened") && !fixedHeight) {
                $element.css("height", "")
            }
            $element.not(".dx-accordion-item-opened").addClass("dx-accordion-item-closed")
        }))
    }
    _splitFreeSpace(freeSpace) {
        if (!freeSpace) {
            return freeSpace
        }
        return freeSpace / this.option("selectedItems").length
    }
    _calculateFreeSpace() {
        const {
            height: height
        } = this.option();
        if (void 0 === height || "auto" === height) {
            return
        }
        const $titles = this._itemTitles();
        let itemsHeight = 0;
        iteratorUtils.each($titles, ((_, title) => {
            itemsHeight += getOuterHeight(title)
        }));
        return getHeight(this.$element()) - itemsHeight
    }
    _visibilityChanged(visible) {
        if (visible) {
            this._dimensionChanged()
        }
    }
    _dimensionChanged() {
        this._updateItemHeights(true)
    }
    _clean() {
        clearTimeout(this._animationTimer);
        super._clean()
    }
    _tryParseItemPropertyName(fullName) {
        const matches = fullName.match(/.*\.(.*)/);
        if (isDefined(matches) && matches.length >= 1) {
            return matches[1]
        }
    }
    _optionChanged(args) {
        switch (args.name) {
            case "items": {
                super._optionChanged(args);
                if ("title" === this._tryParseItemPropertyName(args.fullName)) {
                    this._renderSelection(this._getSelectedItemIndices(), [])
                }
                if ("visible" === this._tryParseItemPropertyName(args.fullName)) {
                    this._updateItemHeightsWrapper(true)
                }
                const {
                    repaintChangesOnly: repaintChangesOnly
                } = this.option();
                if (true === repaintChangesOnly && "items" === args.fullName) {
                    this._updateItemHeightsWrapper(true);
                    this._renderSelection(this._getSelectedItemIndices(), [])
                }
                break
            }
            case "animationDuration":
            case "onItemTitleClick":
            case "_animationEasing":
                break;
            case "collapsible":
                this.option("selectionRequired", !this.option("collapsible"));
                break;
            case "itemTitleTemplate":
            case "height":
            case "deferRendering":
                this._invalidate();
                break;
            case "multiple":
                this.option("selectionMode", args.value ? "multiple" : "single");
                break;
            default:
                super._optionChanged(args)
        }
    }
    expandItem(index) {
        this._deferredAnimate = Deferred();
        this.selectItem(index);
        return this._deferredAnimate.promise()
    }
    collapseItem(index) {
        this._deferredAnimate = Deferred();
        this.unselectItem(index);
        return this._deferredAnimate.promise()
    }
    updateDimensions() {
        return this._updateItemHeights(false)
    }
}
registerComponent("dxAccordion", Accordion);
export default Accordion;