mirror of
https://gitee.com/vant-contrib/vant.git
synced 2025-04-06 03:57:59 +08:00
249 lines
5.7 KiB
JavaScript
249 lines
5.7 KiB
JavaScript
import { ref, computed, watch, nextTick } from 'vue';
|
|
|
|
// Utils
|
|
import {
|
|
isDef,
|
|
isHidden,
|
|
getScrollTop,
|
|
getElementTop,
|
|
preventDefault,
|
|
createNamespace,
|
|
getRootScrollTop,
|
|
setRootScrollTop,
|
|
} from '../utils';
|
|
|
|
// Composition
|
|
import {
|
|
useRect,
|
|
useChildren,
|
|
useScrollParent,
|
|
useEventListener,
|
|
} from '@vant/use';
|
|
import { useTouch } from '../composition/use-touch';
|
|
|
|
export const INDEX_BAR_KEY = 'vanIndexBar';
|
|
|
|
function genAlphabet() {
|
|
const indexList = [];
|
|
const charCodeOfA = 'A'.charCodeAt(0);
|
|
|
|
for (let i = 0; i < 26; i++) {
|
|
indexList.push(String.fromCharCode(charCodeOfA + i));
|
|
}
|
|
|
|
return indexList;
|
|
}
|
|
|
|
const [createComponent, bem] = createNamespace('index-bar');
|
|
|
|
export default createComponent({
|
|
props: {
|
|
zIndex: [Number, String],
|
|
highlightColor: String,
|
|
sticky: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
stickyOffsetTop: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
indexList: {
|
|
type: Array,
|
|
default: genAlphabet,
|
|
},
|
|
},
|
|
|
|
emits: ['select'],
|
|
|
|
setup(props, { emit, slots }) {
|
|
const root = ref();
|
|
const activeAnchor = ref();
|
|
|
|
const touch = useTouch();
|
|
const scrollParent = useScrollParent(root);
|
|
const { children, linkChildren } = useChildren(INDEX_BAR_KEY);
|
|
|
|
linkChildren({ props });
|
|
|
|
const sidebarStyle = computed(() => {
|
|
if (isDef(props.zIndex)) {
|
|
return {
|
|
zIndex: 1 + props.zIndex,
|
|
};
|
|
}
|
|
});
|
|
|
|
const highlightStyle = computed(() => {
|
|
if (props.highlightColor) {
|
|
return {
|
|
color: props.highlightColor,
|
|
};
|
|
}
|
|
});
|
|
|
|
const getScrollerRect = () => {
|
|
if (scrollParent.value.getBoundingClientRect) {
|
|
return useRect(scrollParent);
|
|
}
|
|
return {
|
|
top: 0,
|
|
left: 0,
|
|
};
|
|
};
|
|
|
|
const getAnchorTop = (element, scrollParentRect) => {
|
|
if (
|
|
scrollParent.value === window ||
|
|
scrollParent.value === document.body
|
|
) {
|
|
return getElementTop(element);
|
|
}
|
|
|
|
const rect = useRect(element);
|
|
return rect.top - scrollParentRect.top + getScrollTop(scrollParent);
|
|
};
|
|
|
|
const getActiveAnchor = (scrollTop, rects) => {
|
|
for (let i = children.length - 1; i >= 0; i--) {
|
|
const prevHeight = i > 0 ? rects[i - 1].height : 0;
|
|
const reachTop = props.sticky ? prevHeight + props.stickyOffsetTop : 0;
|
|
|
|
if (scrollTop + reachTop >= rects[i].top) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
};
|
|
|
|
const onScroll = () => {
|
|
if (isHidden(root)) {
|
|
return;
|
|
}
|
|
|
|
const { sticky, indexList } = props;
|
|
const scrollTop = getScrollTop(scrollParent.value);
|
|
const scrollParentRect = getScrollerRect();
|
|
|
|
const rects = children.map((item) => ({
|
|
height: item.height.value,
|
|
top: getAnchorTop(item.$el, scrollParentRect),
|
|
}));
|
|
|
|
const active = getActiveAnchor(scrollTop, rects);
|
|
|
|
activeAnchor.value = indexList[active];
|
|
|
|
if (sticky) {
|
|
children.forEach((item, index) => {
|
|
const { state, height, $el } = item;
|
|
if (index === active || index === active - 1) {
|
|
const rect = $el.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) +
|
|
scrollParentRect.top;
|
|
} else if (index === active - 1) {
|
|
const activeItemTop = rects[active].top - scrollTop;
|
|
state.active = activeItemTop > 0;
|
|
state.top = activeItemTop + scrollParentRect.top - height.value;
|
|
} else {
|
|
state.active = false;
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
useEventListener('scroll', onScroll, { target: scrollParent });
|
|
|
|
watch(
|
|
() => props.indexList,
|
|
() => {
|
|
nextTick(onScroll);
|
|
}
|
|
);
|
|
|
|
const renderIndexes = () =>
|
|
props.indexList.map((index) => {
|
|
const active = index === activeAnchor.value;
|
|
return (
|
|
<span
|
|
class={bem('index', { active })}
|
|
style={active ? highlightStyle.value : null}
|
|
data-index={index}
|
|
>
|
|
{index}
|
|
</span>
|
|
);
|
|
});
|
|
|
|
const scrollToElement = (element) => {
|
|
const { index } = element.dataset;
|
|
if (!index) {
|
|
return;
|
|
}
|
|
|
|
const match = children.filter((item) => String(item.index) === index);
|
|
|
|
if (match[0]) {
|
|
match[0].$el.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.isVertical()) {
|
|
preventDefault(event);
|
|
|
|
const { clientX, clientY } = event.touches[0];
|
|
const target = document.elementFromPoint(clientX, clientY);
|
|
if (target) {
|
|
const { index } = target.dataset;
|
|
|
|
/* istanbul ignore else */
|
|
if (touchActiveIndex !== index) {
|
|
touchActiveIndex = index;
|
|
scrollToElement(target);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
return () => (
|
|
<div ref={root} class={bem()}>
|
|
<div
|
|
class={bem('sidebar')}
|
|
style={sidebarStyle.value}
|
|
onClick={onClick}
|
|
onTouchstart={touch.start}
|
|
onTouchmove={onTouchMove}
|
|
>
|
|
{renderIndexes()}
|
|
</div>
|
|
{slots.default?.()}
|
|
</div>
|
|
);
|
|
},
|
|
});
|