UNPKG

vui-design

Version:

A high quality UI Toolkit based on Vue.js

425 lines (376 loc) 12.3 kB
import VuiResizeObserver from "../resize-observer"; import VuiIcon from "../icon"; import VuiTabsTab from "./tabs-tab"; import VuiTabsPanel from "./tabs-panel"; import PropTypes from "../../utils/prop-types"; import getClassNamePrefix from "../../utils/getClassNamePrefix"; export const createProps = () => { return { classNamePrefix: PropTypes.string, type: PropTypes.oneOf(["line", "card"]).def("line"), size: PropTypes.oneOf(["small", "medium", "large"]), activeKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), tabs: PropTypes.array.def([]), extra: PropTypes.any, addable: PropTypes.bool.def(false), closable: PropTypes.bool.def(false), destroyOnHide: PropTypes.bool.def(false), headerStyle: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), bodyStyle: PropTypes.oneOfType([PropTypes.string, PropTypes.object]) }; }; export default { name: "vui-tabs", provide() { return { vuiTabs: this }; }, components: { VuiResizeObserver, VuiIcon, VuiTabsTab, VuiTabsPanel }, props: createProps(), data() { const { $props: props } = this; const state = { activeKey: undefined, scrollable: false, scrollWidth: 0, ping: "left" }; return { state }; }, computed: { maybeScroll() { return { activeKey: this.state.activeKey, scrollable: this.state.scrollable }; } }, watch: { activeKey: { immediate: true, deep: true, handler(value) { this.state.activeKey = value; } }, maybeScroll: { immediate: true, deep: true, handler(value) { if (!value.scrollable) { return; } this.$nextTick(() => this.scrollToActiveTab()); } } }, methods: { getNavigationTab(tabs, activeKey, direction) { let index = tabs.findIndex(tab => tab.key === activeKey); index = (index + direction + tabs.length) % tabs.length; const tab = tabs[index]; if (tab.disabled) { return this.getNavigationTab(tabs, tab.key, direction); } else { return tab; } }, getHiddenParent() { let parentNode = this.$el.parentNode; while(parentNode && parentNode !== document.body) { if (parentNode.style && parentNode.style.display === "none") { return parentNode; } parentNode = parentNode.parentNode; } return null; }, getScrollWidth() { return this.state.scrollWidth; }, setScrollWidth(value) { const scrollerWidth = this.$refs.scroller.offsetWidth; const scrollerContentWidth = this.$refs.scrollerContent.offsetWidth; let ping = ""; if (value === 0) { ping = "left"; } else if (scrollerContentWidth - value <= scrollerWidth) { ping = "right"; } this.state.scrollWidth = value; this.state.ping = ping; }, scrollToActiveTab() { if (!this.state.scrollable) { return; } const { $props: props } = this; const scroller = this.$refs.scroller; const scrollerContent = this.$refs.scrollerContent; const classNamePrefix = getClassNamePrefix(props.classNamePrefix, "tabs"); const activeTab = scrollerContent.querySelector(`.${classNamePrefix}-tab-active`); if (!activeTab) { return; } const scrollerBounding = scroller.getBoundingClientRect(); const scrollerContentBounding = scrollerContent.getBoundingClientRect(); const activeTabBounding = activeTab.getBoundingClientRect(); const scrollWidth = this.getScrollWidth(); let value = scrollWidth; if (scrollerContentBounding.right < scrollerBounding.right) { value = scrollerContent.offsetWidth - scrollerBounding.width; } if (activeTabBounding.left < scrollerBounding.left) { value = scrollWidth - (scrollerBounding.left - activeTabBounding.left); } else if (activeTabBounding.right > scrollerBounding.right) { value = scrollWidth + activeTabBounding.right - scrollerBounding.right; } if (scrollWidth !== value) { this.setScrollWidth(Math.max(value, 0)); } }, handleResize() { const scrollerWidth = this.$refs.scroller.offsetWidth; const scrollerContentWidth = this.$refs.scrollerContent.offsetWidth; const scrollWidth = this.getScrollWidth(); if (scrollerWidth < scrollerContentWidth) { this.state.scrollable = true; if (scrollerContentWidth - scrollWidth < scrollerWidth) { this.setScrollWidth(scrollerContentWidth - scrollerWidth); } } else { this.state.scrollable = false; if (scrollWidth > 0) { this.setScrollWidth(0); } } }, handleScroll(e) { if (!this.state.scrollable) { return; } e.preventDefault(); e.stopPropagation(); const type = e.type; let delta = 0; if (type === "DOMMouseScroll" || type === "mousewheel") { delta = (e.wheelDelta) ? e.wheelDelta : -(e.detail || 0) * 40; } if (delta > 0) { this.handleScrollPrev(); } else { this.handleScrollNext(); } }, handleScrollPrev() { const scrollerWidth = this.$refs.scroller.offsetWidth; const scrollWidth = this.getScrollWidth(); if (!scrollWidth) { return; } const value = scrollWidth > scrollerWidth ? (scrollWidth - scrollerWidth) : 0; this.setScrollWidth(value); }, handleScrollNext() { const scrollerWidth = this.$refs.scroller.offsetWidth; const scrollerContentWidth = this.$refs.scrollerContent.offsetWidth; const scrollWidth = this.getScrollWidth(); if (scrollerContentWidth - scrollWidth <= scrollerWidth) { return; } const value = (scrollerContentWidth - scrollWidth) > scrollerWidth * 2 ? (scrollWidth + scrollerWidth) : (scrollerContentWidth - scrollerWidth); this.setScrollWidth(value); }, handleAdd() { this.$emit("add"); }, handleClose(key) { const { $props: props, state } = this; if (state.activeKey === key) { let tabIndex = props.tabs.findIndex(tab => tab.key === state.activeKey); if (tabIndex > 0) { tabIndex = tabIndex - 1; } else { tabIndex = tabIndex + 1; } const tab = props.tabs[tabIndex]; this.handleChange(tab ? tab.key : undefined); } this.$emit("close", key); }, handleNavigation(e) { if (e.keyCode !== 37 && e.keyCode !== 39) { return; } const { $props: props, state } = this; if (props.tabs.length < 2) { return; } const tab = this.getNavigationTab(props.tabs, state.activeKey, e.keyCode === 39 ? 1 : -1); this.handleChange(tab.key); }, handleChange(key) { this.state.activeKey = key; this.$emit("input", key); this.$emit("change", key); } }, mounted() { const hiddenParent = this.getHiddenParent(); if (hiddenParent) { const callback = () => { if (hiddenParent.style.display !== "none") { this.$nextTick(() => setTimeout(() => this.scrollToActiveTab(), 0)); this.parentObserver.disconnect(); } }; const config = { attributes: true, childList: true, characterData: true, attributeFilter: ["style"] } this.parentObserver = new MutationObserver(callback); this.parentObserver.observe(hiddenParent, config); } }, beforeDestroy() { if (this.parentObserver) { this.parentObserver.disconnect(); } }, render() { const { $props: props, state } = this; const { handleResize, handleScroll, handleScrollPrev, handleScrollNext, handleAdd, handleClose, handleNavigation, handleChange } = this; // size let size; if (props.size) { size = props.size; } else { size = "medium"; } // class const classNamePrefix = getClassNamePrefix(props.classNamePrefix, "tabs"); let classes = {}; classes.el = { [`${classNamePrefix}`]: true, [`${classNamePrefix}-${props.type}`]: true, [`${classNamePrefix}-${size}`]: size }; classes.elHeader = `${classNamePrefix}-header`; classes.elHeaderContent = `${classNamePrefix}-header-content`; classes.elHeaderExtra = `${classNamePrefix}-header-extra`; classes.elBody = `${classNamePrefix}-body` ; classes.elBodyContent = `${classNamePrefix}-body-content`; classes.elScroller = { [`${classNamePrefix}-scroller`]: true, [`${classNamePrefix}-scroller-scrollable`]: state.scrollable, [`${classNamePrefix}-scroller-ping-${state.ping}`]: state.scrollable && state.ping }; classes.elScrollerContent = `${classNamePrefix}-scroller-content`; classes.elBtnPrev = { [`${classNamePrefix}-btn`]: true, [`${classNamePrefix}-btn-prev`]: true }; classes.elBtnNext = { [`${classNamePrefix}-btn`]: true, [`${classNamePrefix}-btn-next`]: true }; classes.elBtnAdd = { [`${classNamePrefix}-btn`]: true, [`${classNamePrefix}-btn-add`]: true }; // style let styles = {}; styles.elScrollerContent = { transform: `translateX(-${state.scrollWidth}px)` }; // render return ( <div class={classes.el}> <VuiResizeObserver onResize={handleResize}> <div key="header" class={classes.elHeader} style={props.headerStyle}> <div key="headerContent" class={classes.elHeaderContent}> { state.scrollable ? ( <div key="btn-prev" class={classes.elBtnPrev} onClick={handleScrollPrev}> <VuiIcon type="chevron-left" /> </div> ) : null } <div key="scroller" ref="scroller" class={classes.elScroller} onDOMMouseScroll={handleScroll} onMousewheel={handleScroll}> <VuiResizeObserver onResize={handleResize}> <div key="scrollerContent" ref="scrollerContent" tabIndex="0" class={classes.elScrollerContent} style={styles.elScrollerContent} onKeydown={handleNavigation}> { props.tabs.map(tab => { return ( <VuiTabsTab classNamePrefix={classNamePrefix} key={tab.key} data={tab} onClick={handleChange} onClose={handleClose} /> ); }) } </div> </VuiResizeObserver> </div> { state.scrollable ? ( <div key="btn-next" class={classes.elBtnNext} onClick={handleScrollNext}> <VuiIcon type="chevron-right" /> </div> ) : null } { props.addable ? ( <div key="btn-add" class={classes.elBtnAdd} onClick={handleAdd}> <VuiIcon type="plus" /> </div> ) : null } </div> { props.extra ? ( <div class={classes.elHeaderExtra}>{props.extra}</div> ) : null } </div> </VuiResizeObserver> <div key="body" class={classes.elBody} style={props.bodyStyle}> <div key="bodyContent" class={classes.elBodyContent}> { props.tabs.map(tab => { return ( <VuiTabsPanel classNamePrefix={classNamePrefix} key={tab.key} data={tab} destroyOnHide={props.destroyOnHide} /> ); }) } </div> </div> </div> ); } };