zul/src/main/resources/web/js/zul/tab/Tabs.ts

Summary

Maintainability
A
0 mins
Test Coverage
/* Tabs.ts

{{IS_NOTE
    Purpose:

    Description:

    History:
        Fri Jan 23 10:32:43 TST 2009, Created by Flyworld
}}IS_NOTE

Copyright (C) 2008 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
}}IS_RIGHT
*/
/**
 * A collection of tabs ({@link Tab}).
 *
 * @defaultValue {@link getZclass}: z-tabs.
 */
@zk.WrapClass('zul.tab.Tabs')
export class Tabs extends zul.Widget {
    override parent!: zul.tab.Tabbox | undefined;

    /** @internal */
    _tabsScrollLeft = 0;
    /** @internal */
    _tabsScrollTop = 0;
    /** @internal */
    _shallCheck?: boolean;
    /** @internal */
    _doingScroll?: Record<string, number>;

    /**
     * @returns the tabbox owns this component.
     */
    getTabbox(): zul.tab.Tabbox | undefined {
        return this.parent;
    }

    override getWidth(): string | undefined {
        var wd = this._width;
        if (!wd) {
            if (this.getTabbox()?.isVertical())
                return '50px';
        }
        return wd;
    }

    override onSize(): void {
        this._fixWidth(true); //ZK-2810: set height to tabbox when onSize (maybe setHeight or setWidth)

        // ZK-5429, ZK-5433: reset the cache when onSize
        let tabs = this.$n_();
        this._tabsScrollLeft = tabs.scrollLeft;
        this._tabsScrollTop = tabs.scrollTop;

        // Bug Z35-tabbox-004.zul, we need to check again.
        this._scrollcheck('init');
    }

    beforeSize(): void {
        var tabbox = this.getTabbox()!,
            width = tabbox.getWidth(),
            style = this.$n_().style;
        if ((!width || width.endsWith('%') || width == 'auto') && !tabbox.inAccordionMold() && !tabbox.isVertical()) {
            this.$n_('cave').style.width = '';
            if (style.width) {
                style.width = '';
                if (!tabbox.isTabscroll())
                    tabbox.$n_().style.width = '';
            }
        }
    }

    /** @internal */
    override insertChildHTML_(child: zk.Widget, before?: zk.Widget, desktop?: zk.Desktop): void {
        var last = child.previousSibling,
            outHTML;
        if (before)
            jq(before).before(outHTML = child.redrawHTML_());
        else if (last)
            jq(last).after(outHTML = child.redrawHTML_());
        else {
            var edge = this.$n('edge');
            if (edge)
                jq(edge).before(outHTML = child.redrawHTML_());
            else
                jq(this.getCaveNode()).append(outHTML = child.redrawHTML_());
        }
        // ZK-5009, if out is empty, ignore for bind()
        if (outHTML) {
            child.bind(desktop);
        }
    }

    //bug #3014664
    override setVflex(vflex: boolean | string | undefined): this { //vflex ignored for Tabs
        if (vflex != 'min') vflex = false;
        return super.setVflex(vflex);
    }

    //bug #3014664
    override setHflex(hflex: boolean | string | undefined): this { //hflex ignored for Tabs
        if (hflex != 'min') hflex = false;
        return super.setHflex(hflex);
    }

    /** @internal */
    override bind_(desktop?: zk.Desktop, skipper?: zk.Skipper, after?: CallableFunction[]): void {
        super.bind_(desktop, skipper, after);
        zWatch.listen({onSize: this, onResponse: this, beforeSize: this});
    }

    /** @internal */
    override unbind_(skipper?: zk.Skipper, after?: CallableFunction[], keepRod?: boolean): void {
        zWatch.unlisten({onSize: this, onResponse: this, beforeSize: this});
        super.unbind_(skipper, after, keepRod);
    }

    /** @internal */
    _scrollcheck(way: string, tb?: zul.tab.Tab): void {
        this._shallCheck = false;
        var tabbox = this.getTabbox()!;
        if (!this.desktop
            || (tabbox && (!tabbox.isRealVisible() || !tabbox.isTabscroll())))
            return;

        var tabs = this.$n(),
            tbx = tabbox.$n();

        if (!tabs || !tbx)
            return;    // tabbox is delete , no need to check scroll

        if (tabbox.isVertical()) { //vertical
            var tabsOffsetHeight = tabs.offsetHeight,
                tabsScrollTop = tabs.scrollTop,
                childHeight = 0;

            jq(this.$n_('cave')).children().each(function () {
                childHeight += this.offsetHeight;
            });

            if (tabbox._scrolling) { //already in scrolling status
                var btnsize = this._getArrowSize();
                if (tabs.offsetHeight <= btnsize) return;

                var sel = tabbox.getSelectedTab(),
                    node = tb ? tb.$n() : (sel ? sel.$n() : undefined),
                    nodeOffsetTop = node ? node.offsetTop : 0,
                    nodeOffsetHeight = node ? node.offsetHeight : 0;

                if (childHeight <= tabsOffsetHeight + btnsize) {
                    tabbox._scrolling = false;
                    this._showbutton(false);
                    tabs.style.height = jq.px0(tbx.offsetHeight - 2);
                    tabs.scrollTop = 0;
                }
                switch (way) {
                case 'end':
                    var d = childHeight - tabsOffsetHeight - tabsScrollTop;
                    this._doScroll(d >= 0 ? 'down' : 'up', d >= 0 ? d : Math.abs(d));
                    break;
                case 'init':
                case 'vsel':
                    if (nodeOffsetTop < tabsScrollTop) {
                        this._doScroll('up', tabsScrollTop - nodeOffsetTop);
                    } else if (nodeOffsetTop + nodeOffsetHeight > tabsScrollTop + tabsOffsetHeight) {
                        this._doScroll('down', nodeOffsetTop + nodeOffsetHeight - tabsScrollTop - tabsOffsetHeight);
                    }
                    break;
                }
            } else { // not enough tab to scroll
                if (childHeight - tabsOffsetHeight > 0) {
                    tabbox._scrolling = true;
                    this._showbutton(true);
                    var btnsize = this._getArrowSize(),
                        temp = tbx.offsetHeight - btnsize;
                    tabs.style.height = temp > 0 ? temp + 'px' : '';
                    if (way == 'end') {
                        var d = childHeight - tabsOffsetHeight - tabsScrollTop + 2;
                        if (d >= 0)
                            this._doScroll(this.uuid, d);
                    }
                } else {
                    this._showbutton(false);
                }
            }
        } else if (!tabbox.inAccordionMold()) {
            var cave = this.$n_('cave'),
                sel = tabbox.getSelectedTab(),
                node = tb ? tb.$n() : (sel ? sel.$n() : undefined),
                nodeOffsetLeft = node ? node.offsetLeft : 0,
                nodeOffsetWidth = node ? node.offsetWidth : 0,
                tabsOffsetWidth = tabs.offsetWidth,
                tabsScrollLeft = this._tabsScrollLeft,
                childWidth = 0,
                toolbar = tabbox.toolbar,
                toolbarWidth = 0;

            jq(cave).children().each(function () {
                childWidth += this.offsetWidth;
            });

            if (toolbar && toolbar.desktop)
                toolbarWidth = toolbar.$n_().offsetWidth;

            if (tabbox._scrolling) { //already in scrolling status
                var btnsize = this._getArrowSize();
                tabbox.$n_('right').style.right = toolbarWidth + 'px';

                if (childWidth <= tabsOffsetWidth + btnsize) {
                    tabbox._scrolling = false;
                    this._showbutton(false);
                    tabs.style.width = jq.px0(tbx.offsetWidth - toolbarWidth);
                    this._fixTabsScrollLeft(0);
                }
                // scroll to specific position
                switch (way) {
                case 'end':
                    var d = childWidth - tabsOffsetWidth - tabsScrollLeft;
                    this._doScroll(d >= 0 ? 'right' : 'left', d >= 0 ? d : Math.abs(d));
                    break;
                case 'init':
                case 'sel':
                    if (nodeOffsetLeft == tabsScrollLeft) // nothing to do
                        break;

                    if (nodeOffsetLeft < tabsScrollLeft) {
                        this._doScroll('left', tabsScrollLeft - nodeOffsetLeft);
                    } else if (nodeOffsetLeft + nodeOffsetWidth > tabsScrollLeft + tabsOffsetWidth) {
                        this._doScroll('right', nodeOffsetLeft + nodeOffsetWidth - tabsScrollLeft - tabsOffsetWidth);
                    }
                    break;
                }
            } else { // not enough tab to scroll
                if (childWidth - tabsOffsetWidth > 0) {
                    tabbox._scrolling = true;
                    this._showbutton(true);
                    var btnsize = this._getArrowSize(),
                        temp = tbx.offsetWidth - toolbarWidth - btnsize;//coz show button then getsize again
                    tabs.style.width = temp > 0 ? temp + 'px' : '';
                    tabbox.$n_('right').style.right = toolbarWidth + 'px';

                    if (way == 'sel') {
                        if (nodeOffsetLeft < tabsScrollLeft) {
                            this._doScroll('left', tabsScrollLeft - nodeOffsetLeft);
                        } else if (nodeOffsetLeft + nodeOffsetWidth > tabsScrollLeft + tabsOffsetWidth) {
                            this._doScroll('right', nodeOffsetLeft + nodeOffsetWidth - tabsScrollLeft - tabsOffsetWidth);
                        }
                    }
                } else {
                    this._showbutton(false);
                }
            }
        }
    }

    /** @internal */
    _doScroll(to: string, move: number): void {
        if (!this._doingScroll)
            this._doingScroll = {};
        if (move <= 0 || this._doingScroll[to])
            return;
        var step: number,
            self = this,
            tabs = this.$n();

        this._doingScroll[to] = move;
        //the tab bigger , the scroll speed faster
        step = move <= 60 ? 5 : (5 * (zk.parseInt(move / 60) + 1));
        //Use to scroll
        var goscroll = function (tabs, to: string, step: number): void {
            switch (to) {
            case 'right':
                self._fixTabsScrollLeft(self._tabsScrollLeft + step);
                break;
            case 'left':
                self._fixTabsScrollLeft(self._tabsScrollLeft - step);
                break;
            case 'up':
                self._fixTabsScrollTop(self._tabsScrollTop - step);
                break;
            default:
                self._fixTabsScrollTop(self._tabsScrollTop + step);
            }
            var tabsScrollLeft = self._tabsScrollLeft,
                tabsScrollTop = self._tabsScrollTop;
            self._fixTabsScrollLeft(tabsScrollLeft <= 0 ? 0 : tabsScrollLeft);
            self._fixTabsScrollTop(tabsScrollTop <= 0 ? 0 : tabsScrollTop);
        },
        run = setInterval(function () {
            if (!move || !self.desktop) {
                delete self._doingScroll![to];
                clearInterval(run);
                return;
            } else {
                //high speed scroll, need break
                move < step ? goscroll(tabs, to, move) : goscroll(tabs, to, step);
                move -= step;
                move = move < 0 ? 0 : move;
            }
        }, 10);
    }

    /** @internal */
    _getArrowSize(): number {
        var tabbox = this.getTabbox()!,
            isVer = tabbox.isVertical(),
            btnA = isVer ? tabbox.$n('up') : tabbox.$n('left'),
            btnB = isVer ? tabbox.$n('down') : tabbox.$n('right'),
            size = 0;
        if (btnA && btnB) {
            size = isVer ? btnA.offsetHeight + btnB.offsetHeight : btnA.offsetWidth + btnB.offsetWidth;
        }
        return size;
    }

    /** @internal */
    _showbutton(show: boolean): void {
        var tabbox = this.getTabbox()!;
        if (tabbox.isTabscroll()) {
            var cls = tabbox.$s('scroll');
            jq(tabbox).removeClass(cls);
            if (show) {
                // ZK-1959: the height of arrow should not change when the tabbox add tab
                if (!tabbox.isVertical() && !tabbox.inAccordionMold()) {
                    var tb = tabbox.toolbar;
                    tabbox.$n_('left').style.height = tabbox.$n_('right').style.height = '';
                    if (tb)
                        tb.$n_().style.height = '';
                }

                jq(tabbox).addClass(cls);
            }
        }
    }

    /** @internal */
    _fixWidth(toSel: boolean): void {
        var tabs = this.$n_(),
            tabbox = this.getTabbox()!,
            tbx = tabbox.$n_(),
            btnsize = tabbox._scrolling ? this._getArrowSize() : 0;
        this._fixHgh(toSel); //ZK-2810: don't set height to tabbox when deselect
        if (tabbox.isVertical()) {
            //LI in IE doesn't have width...
            if (tabs.style.width) {
                tabs._width = tabs.style.width;
            } else {
                //vertical tabs have default width 50px
                tabs.style.width = tabs._width ?? '50px';
            }
        } else if (!tabbox.inAccordionMold()) {
            if (tbx.offsetWidth < btnsize)
                return;
            if (tabbox.isTabscroll()) {
                var toolbar: zul.wgt.Toolbar | HTMLElement | undefined = tabbox.toolbar;
                if (toolbar)
                    toolbar = toolbar.$n();
                if (!tbx.style.width) {
                    tbx.style.width = '100%';
                    if (tabbox._scrolling)
                        tabs.style.width = jq.px0(zk(tbx).contentWidth() - (toolbar ? toolbar.offsetWidth : 0) - btnsize);
                    else
                        tabs.style.width = jq.px0(zk(tbx).contentWidth() - (toolbar ? toolbar.offsetWidth : 0));
                } else {
                    if (tabbox._scrolling)
                        tabs.style.width = jq.px0(zk(tbx).contentWidth() - (toolbar ? toolbar.offsetWidth : 0) - btnsize);
                    else
                        tabs.style.width = jq.px0(zk(tbx).contentWidth() - (toolbar ? toolbar.offsetWidth : 0));
                }
                if (toolbar && tabbox._scrolling)
                    tabbox.$n_('right').style.right = toolbar.offsetWidth + 'px';

            } else {
                if (!tbx.style.width) {
                    if (tbx.offsetWidth) {
                        tbx.style.width = jq.px0(tbx.offsetWidth);
                        tabs.style.width = jq.px0(zk(tbx).contentWidth() - zk(tabs).marginWidth());
                    }
                } else {
                    tabs.style.width = jq.px0(zk(tbx).contentWidth() - zk(tabs).marginWidth());
                }
            }
        }
    }

    /** @internal */
    _fixHgh(toSel: boolean): void {
        if (this.getTabbox()!._scrolling) return;
        var tabbox = this.getTabbox()!;
        //fix tabpanels's height if tabbox's height is specified
        //Ignore accordion since its height is controlled by each tabpanel
        if (tabbox.isVertical()) {
            const tabs = this.$n_(),
                tbx = tabbox.$n_(),
                u = tabbox.$n('up'),
                d = tabbox.$n('down'),
                cave = this.$n_('cave'),
                allTab = jq(cave).children();

            if (!tabbox.getHeight() && (!tabbox._vflex || tabbox._vflex == 'min')) { // B50-ZK-473: vflex 1
                if (!toSel) { //ZK-2810: clear height of tabbox when deselect
                    jq(tbx).css('height', '');
                } else {
                    var tabsHgh = allTab.length * allTab[0].offsetHeight, // default height
                        seldPanel = tabbox.getSelectedPanel(),
                        panelsHgh = seldPanel && seldPanel.getPanelContentHeight_() || 0,  //B60-ZK-965
                    // NOTE: seldPanel.getPanelContentHeight_() could return NaN; the return value must
                    // be validated before use.
                    realHgh = Math.max(tabsHgh, panelsHgh);
                    tbx.style.height = jq.px0(realHgh + zk(tbx).padBorderHeight());
                }
            }
            tabs.style.height = jq.px0(zk(tbx).contentHeight() - zk(tabs).marginHeight());

            if (u && d) {
                u.style.width = d.style.width = tabs.style.width;
            }
        } else {
            const r = tabbox.$n('right'),
                l = tabbox.$n('left'),
                tb = tabbox.toolbar?.$n(),
                tabs = this.$n(),
                hgh = jq.px0(tabs ? tabs.offsetHeight : 0);
            if (r && l) {
                r.style.height = l.style.height = hgh;
            }
            if (tb) {
                tb.style.height = hgh;
            }
            if (tabs)
                tabs.style.height = '';
        }
    }

    onResponse(): void {
        if (this._shallCheck) {
            this._scrollcheck('init');
        }
    }

    /** @internal */
    override beforeChildReplaced_(oldTab: zul.tab.Tab, newTab: zul.tab.Tab): void {
        newTab.setSelected(oldTab.isSelected());
    }

    /** @internal */
    override onChildRemoved_(child: zk.Widget): void {
        var p = this.parent;
        if (p && child == p._selTab) {
            p._selTab = undefined;
        }
        if (this.desktop)
            this._shallCheck = true;
        super.onChildRemoved_(child);
    }

    /** @internal */
    override onChildAdded_(child: zk.Widget): void {
        if (this.desktop)
            this._shallCheck = true;
        super.onChildAdded_(child);
    }

    /** @internal */
    override onChildVisible_(child: zk.Widget): void {
        if (this.desktop) {
            var tabbox = this.getTabbox()!;
            if (tabbox.inAccordionMold() && tabbox.getHeight()) {
                tabbox.syncSize();
            }
        }
        super.onChildVisible_(child);
    }

    /** @internal */
    override ignoreFlexSize_(attr: zk.FlexOrient): boolean {
        var p = this.getTabbox()!;
        return (p.isVertical() && 'h' == attr)
            || (p.isHorizontal() && 'w' == attr);
    }

    /** @internal */
    _fixTabsScrollLeft(scrollLeft: number): void {
        this.$n_().scrollLeft = this._tabsScrollLeft = scrollLeft;
    }

    /** @internal */
    _fixTabsScrollTop(scrollTop: number): void {
        this.$n_().scrollTop = this._tabsScrollTop = scrollTop;
    }
}