nvpt
5/16/2018 - 1:01 PM

ScrollableTabs

Прокручиваемые табы меню

import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';

const Pane = ({ className, children, key }) => (
    <div className={`${className}`}>
        {children}
    </div>
);

Pane.propTypes = {
    className: PropTypes.string.isRequired,
    children: PropTypes.element.isRequired,
};

Pane.dafaultTypes = {

};

const StyledLink = styled(Pane)`

`;

export default StyledLink;
import React from 'react';
import PropTypes from 'prop-types';
import { NavLink } from 'react-router-dom';
import styled from 'styled-components';

import { Tabs } from 'components/Tabs';

import { debounce } from 'helpers';
import { COLOR, FONT } from '../../global.variables';


class ScrollableTabs extends Tabs {
    static propTypes = {
        className: PropTypes.string.isRequired,
        active: PropTypes.number,
        history: PropTypes.bool,
        children: PropTypes.oneOfType([ PropTypes.array, PropTypes.element ]).isRequired,
    };

    static defaultProps = {
        active: 1,
        history: false,
    };

    static getTabsWidth(tabsWrap) {
        let tabsWidth = 0;
        const tabs = tabsWrap.children;

        for (let i = 0; i < tabs.length; i += 1) {
            const tab = tabs[i];
            tabsWidth += tab.getBoundingClientRect().width;
        }
        return tabsWidth;
    }

    static getFirstTabWidth(tabsWrap) {
        return tabsWrap.children[0].getBoundingClientRect().width;
    }

    static getLastTabWidth(tabsWrap) {
        return tabsWrap.children[tabsWrap.children.length - 1].getBoundingClientRect().width;
    }

    state = {
        active: this.props.active || 0,
        xStartMove: 0, // координата начала движения курсора
        xOffset: 0, // сдвиг курсора относительно кооринаты начала движения
        xOffsetLastMouseUp: 0, // промежуточное хранилище последнего сдвига
        offsetLimitPosition: 'start', // start, center, end
        leftOffsetLimit: 0, // граница прокрутки слева
        rightOffsetLimit: 0, // граница прокрутки справа
        allowTabScroll: false, // разрешение прокрутки (зависит от ширины экрана)

    };

    componentDidMount() {
        const { offsetLimitPosition } = this.state;

        this.ODDS = 40; // прибавка к краю сдвига
        this.clickableFlag = true; // разрешен ли клик по табу
        this.movedFlag = false; // было ли выполнено прокручивание табов (было ли выполнено событие onMouseMove)
        this.mouseMovingFlag = false; // отслеживание клика начала скроллинга
        // this.allowDoubleTouch = true; // флаг предотвращения двойного срабатывания касания
        this.getTabsData(this.navWrap);
        this.checkScrollAllow(this.navWrap);

        window.addEventListener('resize', debounce(() => {
            this.checkScrollAllow(this.navWrap);
            /* пересчитываем данные табов при ресайзе только для крайних выравниваний (start и end),
            где в рассчетах используется ширина окна */
            if (offsetLimitPosition === 'start' || offsetLimitPosition === 'end') {
                this.getTabsData(this.navWrap);
            }
        }, 500));
    }

    getTabsData(tabsWrap) {
        if (tabsWrap) {
            const tabsWidth = ScrollableTabs.getTabsWidth(tabsWrap);
            const firstTabWidth = ScrollableTabs.getFirstTabWidth(tabsWrap);
            const lastTabWidth = ScrollableTabs.getLastTabWidth(tabsWrap);
            const screenWidth = window.innerWidth;

            let leftOffsetLimit = 0;
            let rightOffsetLimit = 0;

            switch (this.state.offsetLimitPosition) {
                case 'start': // конец прокрутки при входе последнего таба в экран
                    leftOffsetLimit = ((tabsWidth - screenWidth) / 2) + this.ODDS;
                    rightOffsetLimit = ((tabsWidth - screenWidth) / 2) + this.ODDS;
                    break;
                case 'center': // конец прокрутки - последний таб посередине экрана
                    leftOffsetLimit = tabsWidth - ((tabsWidth + lastTabWidth) / 2);
                    rightOffsetLimit = tabsWidth - ((tabsWidth + firstTabWidth) / 2);
                    break;
                case 'end': // конец прокрутки перед выходом последнего таба за экран
                    leftOffsetLimit = tabsWidth + ((screenWidth - tabsWidth) / 2) - lastTabWidth;
                    rightOffsetLimit = tabsWidth + ((screenWidth - tabsWidth) / 2) - firstTabWidth;
                    break;
                default:
                    leftOffsetLimit = ((tabsWidth - screenWidth) / 2) + this.ODDS;
                    rightOffsetLimit = ((tabsWidth - screenWidth) / 2) + this.ODDS;
                    break;
            }

            this.setState({
                leftOffsetLimit,
                rightOffsetLimit,
            });
        }
    }

    checkScrollAllow(tabsWrap) {
        if (tabsWrap) {
            const screenWidth = window.innerWidth;
            const tabsWidth = ScrollableTabs.getTabsWidth(tabsWrap);
            this.setState({ allowTabScroll: screenWidth < tabsWidth + this.ODDS });

            if (screenWidth > tabsWidth + this.ODDS) {
                this.setState({
                    xOffset: 0,
                    xOffsetLastMouseUp: 0,
                });
            }
        }
    }

    mouseDownHandler(e) {
        if (this.state.allowTabScroll) {
            this.mouseMovingFlag = true;
            let touchPosition;
            if (e.touches) {
                touchPosition = e.touches[0].screenX;
            }
            this.setState({
                xStartMove: e.pageX || touchPosition,
            });
        }
        this.clickableFlag = true;
    }

    mouseMoveHandler(e) {
        if (this.mouseMovingFlag && this.state.allowTabScroll) {
            let touchPosition;
            if (e.touches) {
                touchPosition = e.touches[0].screenX;
            }
            const clickTouchPosition = e.clientX || touchPosition;
            const offsetFinal = (this.state.xOffsetLastMouseUp + clickTouchPosition) - this.state.xStartMove;
            if (offsetFinal > -(this.state.leftOffsetLimit) &&
                offsetFinal < this.state.rightOffsetLimit) {
                this.setState({
                    xOffset: offsetFinal,
                });
                this.movedFlag = true;
            }
        }
    }

    mouseUpHandler(e) {
        if (this.mouseMovingFlag) {
            this.mouseMovingFlag = false;
            this.setState({
                xOffsetLastMouseUp: this.state.xOffset,
            });
        }

        if (this.movedFlag) {
            this.clickableFlag = false;
            this.movedFlag = false;
        } else {
            const item = e.target;

            if (item.getBoundingClientRect().x < 0) {
                const actualOffset = (this.state.xOffset - item.getBoundingClientRect().x) + this.ODDS;
                this.setState({
                    xOffset: actualOffset,
                    xOffsetLastMouseUp: actualOffset,
                });
            } else if ((item.getBoundingClientRect().x + item.getBoundingClientRect().width) > window.innerWidth) {
                const actualOffset = this.state.xOffset - (item.getBoundingClientRect().x +
                    (item.getBoundingClientRect().width - window.innerWidth) + this.ODDS);
                this.setState({
                    xOffset: actualOffset,
                    xOffsetLastMouseUp: actualOffset,
                });
            }
        }
    }

    touchEndHandler(e) {
        if (this.mouseMovingFlag) {
            this.mouseMovingFlag = false;
            this.setState({
                xOffsetLastMouseUp: this.state.xOffset,
            });
        }

        if (this.movedFlag) {
            this.movedFlag = false;
        } else {
            const item = e.target;

            if (item.getBoundingClientRect().x < 0) {
                const actualOffset = (this.state.xOffset - item.getBoundingClientRect().x) + this.ODDS;
                this.setState({
                    xOffset: actualOffset,
                    xOffsetLastMouseUp: actualOffset,
                });
            } else if ((item.getBoundingClientRect().x + item.getBoundingClientRect().width) > window.innerWidth) {
                const actualOffset = this.state.xOffset - (item.getBoundingClientRect().x +
                    (item.getBoundingClientRect().width - window.innerWidth) + this.ODDS);
                this.setState({
                    xOffset: actualOffset,
                    xOffsetLastMouseUp: actualOffset,
                });
            }
        }
    }

    clickHandler() {
        this.clickableFlag = true;
    }

    mouseLeaveHandler() {
        if (this.mouseMovingFlag) {
            this.mouseMovingFlag = false;
        }
    }

    renderNav(children) {
        if (this.props.history) {
            return (
                <div
                    className="nav"
                    ref={navWrap => {
                        this.navWrap = navWrap;
                    }}
                    style={{
                        transform: `translateX(${this.state.xOffset}px)`,
                        transition: `${this.movedFlag ? 'none' : 'transform 0.2s'}`,
                    }}
                    onMouseDown={e => {
                        // console.log('MOUSE DOWN')
                        e.stopPropagation();
                        e.preventDefault();
                        this.mouseDownHandler(e);
                    }}
                    onMouseUp={e => {
                        // console.log('MOUSE UP')
                        e.stopPropagation();
                        e.preventDefault();
                        this.mouseUpHandler(e);
                    }}
                    onMouseMove={e => {
                        // console.log('MOUSE MOVE')
                        e.stopPropagation();
                        e.preventDefault();
                        this.mouseMoveHandler(e);
                    }}
                    onClick={e => {
                        // console.log('MOUSE CLICK')
                        e.stopPropagation();
                        e.preventDefault();
                        this.clickHandler(e);
                    }}
                    onMouseLeave={e => {
                        // console.log('MOUSE LEAVE')
                        e.stopPropagation();
                        e.preventDefault();
                        this.mouseLeaveHandler(e);
                    }}
                    onTouchStart={e => {
                        // console.log('TOUCH START')
                        e.stopPropagation();
                        this.mouseDownHandler(e);
                    }}
                    onTouchEnd={e => {
                        // console.log('TOUCH END')
                        e.stopPropagation();
                        this.touchEndHandler(e);
                    }}
                    onTouchMove={e => {
                        // console.log('TOUCH MOVE')
                        e.stopPropagation();
                        this.mouseMoveHandler(e);
                    }}
                >
                    {children.map((child, index) => (
                        <NavLink
                            className="nav-item"
                            key={child.props.name}
                            to={this.clickableFlag ? child.props.href : '#'}
                            activeClassName="active"
                        >
                            {child.props.label ? child.props.label : `Tab ${index + 1}`}
                        </NavLink>
                    ))}
                </div>
            );
        }

        return (
            <ul
                className="nav"
                ref={navWrap => {
                    this.navWrap = navWrap;
                }}
                style={{
                    transform: `translateX(${this.state.xOffset}px)`,
                    transition: `${this.movedFlag ? 'none' : 'transform 0.2s'}`,
                }}
                onMouseDown={e => {
                    // console.log('MOUSE DOWN')
                    e.stopPropagation();
                    e.preventDefault();
                    this.mouseDownHandler(e);
                }}
                onMouseUp={e => {
                    // console.log('MOUSE UP')
                    e.stopPropagation();
                    e.preventDefault();
                    this.mouseUpHandler(e);
                }}
                onMouseMove={e => {
                    // console.log('MOUSE MOVE')
                    e.stopPropagation();
                    e.preventDefault();
                    this.mouseMoveHandler(e);
                }}
                onClick={e => {
                    // console.log('MOUSE CLICK')
                    e.stopPropagation();
                    e.preventDefault();
                    this.clickHandler(e);
                }}
                onMouseLeave={e => {
                    // console.log('MOUSE LEAVE')
                    e.stopPropagation();
                    e.preventDefault();
                    this.mouseLeaveHandler(e);
                }}
                onTouchStart={e => {
                    // console.log('TOUCH START')
                    e.stopPropagation();
                    this.mouseDownHandler(e);
                }}
                onTouchEnd={e => {
                    // console.log('TOUCH END')
                    e.stopPropagation();
                    this.touchEndHandler(e);
                }}
                onTouchMove={e => {
                    // console.log('TOUCH MOVE')
                    e.stopPropagation();
                    this.mouseMoveHandler(e);
                }}
            >
                {children.map((child, index) => (
                    <li
                        className={`nav-item ${this.state.active === index && 'active'}`}
                        key={child.props.name}
                        onClick={() => this.setState({ active: index })}
                    >
                        {child.props.label ? child.props.label : `Tab${index + 1}`}
                    </li>
                ))}
            </ul>
        );
    }
}

const StyledTabs = styled(ScrollableTabs)`
    background: #ffffff;
    width: 100%;

    .tabs-content {
        padding: 16px;
    }

    .nav-wrapper {
        width: 100%;
        overflow: hidden;
    }

     .nav-inner { 
        position: relative;
        width: 100%;
        background:#fff; 
        border-bottom: 1px solid ${COLOR.bg_dark};
    }

    .nav {
        display: flex;
        flex-direction: row;
        justify-content: center;
        list-style: none;
        padding: 0 16px;
    }

    .nav-item {
        padding: 26px 20px 20px;
        margin: 0;
        transition: border 0.3s ease-in-out;
        border-bottom: 3px solid transparent;
        line-height: 1;
        font-size: 13px;
        text-align: center;
        white-space: nowrap;
        color: ${COLOR.aux};
        text-decoration: none;
        cursor: pointer;
        
        &.active {
            border-bottom: 2px solid ${COLOR.crimson};
            color: ${COLOR.main};
        }
    }
`;

export default StyledTabs;