IndexBar.mjs 6.7 KB

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