IndexBar.mjs 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import { ref, watch, computed, nextTick, Teleport, onMounted, defineComponent, createVNode as _createVNode } from "vue";
  2. import { isDef, isHidden, truthProp, numericProp, getScrollTop, preventDefault, makeNumberProp, createNamespace, getRootScrollTop, setRootScrollTop } from "../utils/index.mjs";
  3. import { useRect, useChildren, useScrollParent, useEventListener } from "@vant/use";
  4. import { useTouch } from "../composables/use-touch.mjs";
  5. import { useExpose } from "../composables/use-expose.mjs";
  6. function genAlphabet() {
  7. const charCodeOfA = "A".charCodeAt(0);
  8. const indexList = Array(26).fill("").map((_, i) => String.fromCharCode(charCodeOfA + i));
  9. return indexList;
  10. }
  11. const [name, bem] = createNamespace("index-bar");
  12. const indexBarProps = {
  13. sticky: truthProp,
  14. zIndex: numericProp,
  15. teleport: [String, Object],
  16. highlightColor: String,
  17. stickyOffsetTop: makeNumberProp(0),
  18. indexList: {
  19. type: Array,
  20. default: genAlphabet
  21. }
  22. };
  23. const INDEX_BAR_KEY = Symbol(name);
  24. var stdin_default = defineComponent({
  25. name,
  26. props: indexBarProps,
  27. emits: ["select", "change"],
  28. setup(props, {
  29. emit,
  30. slots
  31. }) {
  32. const root = ref();
  33. const sidebar = ref();
  34. const activeAnchor = ref("");
  35. const touch = useTouch();
  36. const scrollParent = useScrollParent(root);
  37. const {
  38. children,
  39. linkChildren
  40. } = useChildren(INDEX_BAR_KEY);
  41. let selectActiveIndex;
  42. linkChildren({
  43. props
  44. });
  45. const sidebarStyle = computed(() => {
  46. if (isDef(props.zIndex)) {
  47. return {
  48. zIndex: +props.zIndex + 1
  49. };
  50. }
  51. });
  52. const highlightStyle = computed(() => {
  53. if (props.highlightColor) {
  54. return {
  55. color: props.highlightColor
  56. };
  57. }
  58. });
  59. const getActiveAnchor = (scrollTop, rects) => {
  60. for (let i = children.length - 1; i >= 0; i--) {
  61. const prevHeight = i > 0 ? rects[i - 1].height : 0;
  62. const reachTop = props.sticky ? prevHeight + props.stickyOffsetTop : 0;
  63. if (scrollTop + reachTop >= rects[i].top) {
  64. return i;
  65. }
  66. }
  67. return -1;
  68. };
  69. const getMatchAnchor = (index) => children.find((item) => String(item.index) === index);
  70. const onScroll = () => {
  71. if (isHidden(root)) {
  72. return;
  73. }
  74. const {
  75. sticky,
  76. indexList
  77. } = props;
  78. const scrollTop = getScrollTop(scrollParent.value);
  79. const scrollParentRect = useRect(scrollParent);
  80. const rects = children.map((item) => item.getRect(scrollParent.value, scrollParentRect));
  81. let active = -1;
  82. if (selectActiveIndex) {
  83. const match = getMatchAnchor(selectActiveIndex);
  84. if (match) {
  85. const rect = match.getRect(scrollParent.value, scrollParentRect);
  86. if (props.sticky && props.stickyOffsetTop) {
  87. active = getActiveAnchor(rect.top - props.stickyOffsetTop, rects);
  88. } else {
  89. active = getActiveAnchor(rect.top, rects);
  90. }
  91. }
  92. } else {
  93. active = getActiveAnchor(scrollTop, rects);
  94. }
  95. activeAnchor.value = indexList[active];
  96. if (sticky) {
  97. children.forEach((item, index) => {
  98. const {
  99. state,
  100. $el
  101. } = item;
  102. if (index === active || index === active - 1) {
  103. const rect = $el.getBoundingClientRect();
  104. state.left = rect.left;
  105. state.width = rect.width;
  106. } else {
  107. state.left = null;
  108. state.width = null;
  109. }
  110. if (index === active) {
  111. state.active = true;
  112. state.top = Math.max(props.stickyOffsetTop, rects[index].top - scrollTop) + scrollParentRect.top;
  113. } else if (index === active - 1 && selectActiveIndex === "") {
  114. const activeItemTop = rects[active].top - scrollTop;
  115. state.active = activeItemTop > 0;
  116. state.top = activeItemTop + scrollParentRect.top - rects[index].height;
  117. } else {
  118. state.active = false;
  119. }
  120. });
  121. }
  122. selectActiveIndex = "";
  123. };
  124. const init = () => {
  125. nextTick(onScroll);
  126. };
  127. useEventListener("scroll", onScroll, {
  128. target: scrollParent,
  129. passive: true
  130. });
  131. onMounted(init);
  132. watch(() => props.indexList, init);
  133. watch(activeAnchor, (value) => {
  134. if (value) {
  135. emit("change", value);
  136. }
  137. });
  138. const renderIndexes = () => props.indexList.map((index) => {
  139. const active = index === activeAnchor.value;
  140. return _createVNode("span", {
  141. "class": bem("index", {
  142. active
  143. }),
  144. "style": active ? highlightStyle.value : void 0,
  145. "data-index": index
  146. }, [index]);
  147. });
  148. const scrollTo = (index) => {
  149. selectActiveIndex = String(index);
  150. const match = getMatchAnchor(selectActiveIndex);
  151. if (match) {
  152. const scrollTop = getScrollTop(scrollParent.value);
  153. const scrollParentRect = useRect(scrollParent);
  154. const {
  155. offsetHeight
  156. } = document.documentElement;
  157. match.$el.scrollIntoView();
  158. if (scrollTop === offsetHeight - scrollParentRect.height) {
  159. onScroll();
  160. return;
  161. }
  162. if (props.sticky && props.stickyOffsetTop) {
  163. if (getRootScrollTop() === offsetHeight - scrollParentRect.height) {
  164. setRootScrollTop(getRootScrollTop());
  165. } else {
  166. setRootScrollTop(getRootScrollTop() - props.stickyOffsetTop);
  167. }
  168. }
  169. emit("select", match.index);
  170. }
  171. };
  172. const scrollToElement = (element) => {
  173. const {
  174. index
  175. } = element.dataset;
  176. if (index) {
  177. scrollTo(index);
  178. }
  179. };
  180. const onClickSidebar = (event) => {
  181. scrollToElement(event.target);
  182. };
  183. let touchActiveIndex;
  184. const onTouchMove = (event) => {
  185. touch.move(event);
  186. if (touch.isVertical()) {
  187. preventDefault(event);
  188. const {
  189. clientX,
  190. clientY
  191. } = event.touches[0];
  192. const target = document.elementFromPoint(clientX, clientY);
  193. if (target) {
  194. const {
  195. index
  196. } = target.dataset;
  197. if (index && touchActiveIndex !== index) {
  198. touchActiveIndex = index;
  199. scrollToElement(target);
  200. }
  201. }
  202. }
  203. };
  204. const renderSidebar = () => _createVNode("div", {
  205. "ref": sidebar,
  206. "class": bem("sidebar"),
  207. "style": sidebarStyle.value,
  208. "onClick": onClickSidebar,
  209. "onTouchstartPassive": touch.start
  210. }, [renderIndexes()]);
  211. useExpose({
  212. scrollTo
  213. });
  214. useEventListener("touchmove", onTouchMove, {
  215. target: sidebar
  216. });
  217. return () => {
  218. var _a;
  219. return _createVNode("div", {
  220. "ref": root,
  221. "class": bem()
  222. }, [props.teleport ? _createVNode(Teleport, {
  223. "to": props.teleport
  224. }, {
  225. default: () => [renderSidebar()]
  226. }) : renderSidebar(), (_a = slots.default) == null ? void 0 : _a.call(slots)]);
  227. };
  228. }
  229. });
  230. export {
  231. INDEX_BAR_KEY,
  232. stdin_default as default,
  233. indexBarProps
  234. };