diff --git a/src/index-anchor/index.js b/src/index-anchor/index.js index 65ed1c9a1..f2896263e 100644 --- a/src/index-anchor/index.js +++ b/src/index-anchor/index.js @@ -1,67 +1,71 @@ +import { reactive, ref, computed } from 'vue'; + +// Utils import { createNamespace } from '../utils'; -import { ChildrenMixin } from '../mixins/relation'; import { BORDER_BOTTOM } from '../utils/constant'; +import { INDEX_BAR_KEY } from '../index-bar'; + +// Composition +import { useHeight } from '../composition/use-rect'; +import { useParent } from '../composition/use-relation'; const [createComponent, bem] = createNamespace('index-anchor'); export default createComponent({ - mixins: [ChildrenMixin('vanIndexBar', { indexKey: 'childrenIndex' })], - props: { index: [Number, String], }, - data() { - return { + setup(props, { slots }) { + const state = reactive({ top: 0, left: null, width: null, active: false, - }; - }, + }); - computed: { - sticky() { - return this.active && this.parent.sticky; - }, + const rootRef = ref(); + const height = useHeight(rootRef); - anchorStyle() { - if (this.sticky) { + const { parent } = useParent(INDEX_BAR_KEY, { + props, + state, + height, + rootRef, + }); + + const isSticky = () => state.active && parent.props.sticky; + + const anchorStyle = computed(() => { + const { zIndex, highlightColor } = parent.props; + + if (isSticky()) { return { - zIndex: `${this.parent.zIndex}`, - left: this.left ? `${this.left}px` : null, - width: this.width ? `${this.width}px` : null, - transform: `translate3d(0, ${this.top}px, 0)`, - color: this.parent.highlightColor, + zIndex: `${zIndex}`, + left: state.left ? `${state.left}px` : null, + width: state.width ? `${state.width}px` : null, + transform: state.top ? `translate3d(0, ${state.top}px, 0)` : null, + color: highlightColor, }; } - }, - }, - - mounted() { - this.$nextTick(() => { - this.height = this.$el.offsetHeight; }); - }, - methods: { - scrollIntoView() { - this.$el.scrollIntoView(); - }, - }, + return () => { + const sticky = isSticky(); - render() { - const { sticky } = this; - - return ( -
+ return (
- {this.$slots.default ? this.$slots.default() : this.index} +
+ {slots.default ? slots.default() : props.index} +
-
- ); + ); + }; }, }); diff --git a/src/index-bar/index.js b/src/index-bar/index.js index 2e5d8ffe8..b26de97af 100644 --- a/src/index-bar/index.js +++ b/src/index-bar/index.js @@ -1,19 +1,23 @@ +import { ref, computed, watch, nextTick, reactive, provide } from 'vue'; + // Utils import { createNamespace, isDef } from '../utils'; import { isHidden } from '../utils/dom/style'; import { preventDefault } from '../utils/dom/event'; import { - getScroller, getScrollTop, getElementTop, getRootScrollTop, setRootScrollTop, } from '../utils/dom/scroll'; -// Mixins -import { TouchMixin } from '../mixins/touch'; -import { ParentMixin } from '../mixins/relation'; -import { BindEventMixin } from '../mixins/bind-event'; +// Composition +import { useRect } from '../composition/use-rect'; +import { useTouch } from '../composition/use-touch'; +import { useScroller } from '../composition/use-scroller'; +import { useGlobalEvent } from '../composition/use-global-event'; + +export const INDEX_BAR_KEY = 'vanIndexBar'; function genAlphabet() { const indexList = []; @@ -29,18 +33,6 @@ function genAlphabet() { const [createComponent, bem] = createNamespace('index-bar'); export default createComponent({ - mixins: [ - TouchMixin, - ParentMixin('vanIndexBar'), - BindEventMixin(function (bind) { - if (!this.scroller) { - this.scroller = getScroller(this.$el); - } - - bind(this.scroller, 'scroll', this.onScroll); - }), - ], - props: { zIndex: [Number, String], highlightColor: String, @@ -60,124 +52,163 @@ export default createComponent({ emits: ['select'], - data() { - return { - activeAnchorIndex: null, - }; - }, + setup(props, { emit, slots }) { + const rootRef = ref(); + const activeAnchor = ref(); + const children = reactive([]); - computed: { - sidebarStyle() { - if (isDef(this.zIndex)) { + const touch = useTouch(); + const scroller = useScroller(rootRef); + + provide(INDEX_BAR_KEY, { props, children }); + + const sidebarStyle = computed(() => { + if (isDef(props.zIndex)) { return { - zIndex: this.zIndex + 1, + zIndex: 1 + props.zIndex, }; } - }, + }); - highlightStyle() { - const { highlightColor } = this; - if (highlightColor) { + const highlightStyle = computed(() => { + if (props.highlightColor) { return { - color: highlightColor, + color: props.highlightColor, }; } - }, - }, + }); - watch: { - indexList() { - this.$nextTick(this.onScroll); - }, - }, - - methods: { - onScroll() { - if (isHidden(this.$el)) { - return; + const getScrollerRect = () => { + if (scroller.value.getBoundingClientRect) { + return useRect(scroller); } - - const scrollTop = getScrollTop(this.scroller); - const scrollerRect = this.getScrollerRect(); - const rects = this.children.map((item) => ({ - height: item.height, - top: this.getElementTop(item.$el, scrollerRect), - })); - - const active = this.getActiveAnchorIndex(scrollTop, rects); - - this.activeAnchorIndex = this.indexList[active]; - - if (this.sticky) { - this.children.forEach((item, index) => { - if (index === active || index === active - 1) { - const rect = item.$el.getBoundingClientRect(); - item.left = rect.left; - item.width = rect.width; - } else { - item.left = null; - item.width = null; - } - - if (index === active) { - item.active = true; - item.top = - Math.max(this.stickyOffsetTop, rects[index].top - scrollTop) + - scrollerRect.top; - } else if (index === active - 1) { - const activeItemTop = rects[active].top - scrollTop; - item.active = activeItemTop > 0; - item.top = activeItemTop + scrollerRect.top - item.height; - } else { - item.active = false; - } - }); - } - }, - - getScrollerRect() { - if (this.scroller.getBoundingClientRect) { - return this.scroller.getBoundingClientRect(); - } - return { top: 0, left: 0, }; - }, + }; - getElementTop(ele, scrollerRect) { - const { scroller } = this; - - if (scroller === window || scroller === document.body) { - return getElementTop(ele); + const getAnchorTop = (element, scrollerRect) => { + if (scroller.value === window || scroller.value === document.body) { + return getElementTop(element); } - const eleRect = ele.getBoundingClientRect(); + const rect = useRect(element); + return rect.top - scrollerRect.top + getScrollTop(scroller); + }; - return eleRect.top - scrollerRect.top + getScrollTop(scroller); - }, - - getActiveAnchorIndex(scrollTop, rects) { - for (let i = this.children.length - 1; i >= 0; i--) { + const getActiveAnchor = (scrollTop, rects) => { + for (let i = children.length - 1; i >= 0; i--) { const prevHeight = i > 0 ? rects[i - 1].height : 0; - const reachTop = this.sticky ? prevHeight + this.stickyOffsetTop : 0; + const reachTop = props.sticky ? prevHeight + props.stickyOffsetTop : 0; if (scrollTop + reachTop >= rects[i].top) { return i; } } + return -1; - }, + }; - onClick(event) { - this.scrollToElement(event.target); - }, + const onScroll = () => { + if (isHidden(rootRef.value)) { + return; + } - onTouchMove(event) { - this.touchMove(event); + const { sticky, indexList } = props; + const scrollTop = getScrollTop(scroller.value); + const scrollerRect = getScrollerRect(); - if (this.direction === 'vertical') { + const rects = children.map((item) => ({ + height: item.height, + top: getAnchorTop(item.rootRef, scrollerRect), + })); + + const active = getActiveAnchor(scrollTop, rects); + + activeAnchor.value = indexList[active]; + + if (sticky) { + children.forEach((item, index) => { + const { state, height, rootRef } = item; + if (index === active || index === active - 1) { + const rect = rootRef.getBoundingClientRect(); + state.left = rect.left; + state.width = rect.width; + } else { + state.left = null; + state.width = null; + } + + if (index === active) { + state.active = true; + state.top = + Math.max(props.stickyOffsetTop, rects[index].top - scrollTop) + + scrollerRect.top; + } else if (index === active - 1) { + const activeItemTop = rects[active].top - scrollTop; + state.active = activeItemTop > 0; + state.top = activeItemTop + scrollerRect.top - height; + } else { + state.active = false; + } + }); + } + }; + + useGlobalEvent(scroller, 'scroll', onScroll); + + watch( + () => props.indexList, + () => { + nextTick(onScroll); + } + ); + + const renderIndexes = () => + props.indexList.map((index) => { + const active = index === activeAnchor.value; + return ( + + {index} + + ); + }); + + const scrollToElement = (element) => { + const { index } = element.dataset; + if (!index) { + return; + } + + const match = children.filter( + (item) => String(item.props.index) === index + ); + + if (match[0]) { + match[0].rootRef.scrollIntoView(); + + if (props.sticky && props.stickyOffsetTop) { + setRootScrollTop(getRootScrollTop() - props.stickyOffsetTop); + } + + emit('select', match[0].index); + } + }; + + const onClick = (event) => { + scrollToElement(event.target); + }; + + let touchActiveIndex; + const onTouchMove = (event) => { + touch.move(event); + + if (touch.direction.value === 'vertical') { preventDefault(event); const { clientX, clientY } = event.touches[0]; @@ -186,68 +217,26 @@ export default createComponent({ const { index } = target.dataset; /* istanbul ignore else */ - if (this.touchActiveIndex !== index) { - this.touchActiveIndex = index; - this.scrollToElement(target); + if (touchActiveIndex !== index) { + touchActiveIndex = index; + scrollToElement(target); } } } - }, + }; - scrollToElement(element) { - const { index } = element.dataset; - if (!index) { - return; - } - - const match = this.children.filter( - (item) => String(item.index) === index - ); - if (match[0]) { - match[0].scrollIntoView(); - - if (this.sticky && this.stickyOffsetTop) { - setRootScrollTop(getRootScrollTop() - this.stickyOffsetTop); - } - - this.$emit('select', match[0].index); - } - }, - - onTouchEnd() { - this.active = null; - }, - }, - - render() { - const Indexes = this.indexList.map((index) => { - const active = index === this.activeAnchorIndex; - - return ( - - {index} - - ); - }); - - return ( -
+ return () => ( +
- {Indexes} + {renderIndexes()}
- {this.$slots.default?.()} + {slots.default?.()}
); },