13f2f3adebb5d9610176216850addf53912aabcff94f0bc0329e444d58f7f5e2ea11a5d09b6f61bbc6b22a40eb7ae95fca4313bac76d1c0a841dd72d7b64b1 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. import { defineComponent, useSlots, ref, computed, watch, onMounted, provide, openBlock, createElementBlock, normalizeClass, unref, normalizeStyle, createCommentVNode, createElementVNode, renderSlot, nextTick } from 'vue';
  2. import { useEventListener } from '@vueuse/core';
  3. import { anchorProps, anchorEmits } from './anchor.mjs';
  4. import { anchorKey } from './constants.mjs';
  5. import _export_sfc from '../../../_virtual/plugin-vue_export-helper.mjs';
  6. import { getElement } from '../../../utils/dom/element.mjs';
  7. import { throttleByRaf } from '../../../utils/throttleByRaf.mjs';
  8. import { getScrollElement, animateScrollTo, getScrollTop } from '../../../utils/dom/scroll.mjs';
  9. import { getOffsetTopDistance } from '../../../utils/dom/position.mjs';
  10. import { useNamespace } from '../../../hooks/use-namespace/index.mjs';
  11. import { isWindow, isUndefined } from '../../../utils/types.mjs';
  12. import { CHANGE_EVENT } from '../../../constants/event.mjs';
  13. const __default__ = defineComponent({
  14. name: "ElAnchor"
  15. });
  16. const _sfc_main = /* @__PURE__ */ defineComponent({
  17. ...__default__,
  18. props: anchorProps,
  19. emits: anchorEmits,
  20. setup(__props, { expose, emit }) {
  21. const props = __props;
  22. const slots = useSlots();
  23. const currentAnchor = ref("");
  24. const markerStyle = ref({});
  25. const anchorRef = ref(null);
  26. const markerRef = ref(null);
  27. const containerEl = ref();
  28. const links = {};
  29. let isScrolling = false;
  30. let currentScrollTop = 0;
  31. const ns = useNamespace("anchor");
  32. const cls = computed(() => [
  33. ns.b(),
  34. props.type === "underline" ? ns.m("underline") : "",
  35. ns.m(props.direction)
  36. ]);
  37. const addLink = (state) => {
  38. links[state.href] = state.el;
  39. };
  40. const removeLink = (href) => {
  41. delete links[href];
  42. };
  43. const setCurrentAnchor = (href) => {
  44. const activeHref = currentAnchor.value;
  45. if (activeHref !== href) {
  46. currentAnchor.value = href;
  47. emit(CHANGE_EVENT, href);
  48. }
  49. };
  50. let clearAnimate = null;
  51. const scrollToAnchor = (href) => {
  52. if (!containerEl.value)
  53. return;
  54. const target = getElement(href);
  55. if (!target)
  56. return;
  57. if (clearAnimate)
  58. clearAnimate();
  59. isScrolling = true;
  60. const scrollEle = getScrollElement(target, containerEl.value);
  61. const distance = getOffsetTopDistance(target, scrollEle);
  62. const max = scrollEle.scrollHeight - scrollEle.clientHeight;
  63. const to = Math.min(distance - props.offset, max);
  64. clearAnimate = animateScrollTo(containerEl.value, currentScrollTop, to, props.duration, () => {
  65. setTimeout(() => {
  66. isScrolling = false;
  67. }, 20);
  68. });
  69. };
  70. const scrollTo = (href) => {
  71. if (href) {
  72. setCurrentAnchor(href);
  73. scrollToAnchor(href);
  74. }
  75. };
  76. const handleClick = (e, href) => {
  77. emit("click", e, href);
  78. scrollTo(href);
  79. };
  80. const handleScroll = throttleByRaf(() => {
  81. if (containerEl.value) {
  82. currentScrollTop = getScrollTop(containerEl.value);
  83. }
  84. const currentHref = getCurrentHref();
  85. if (isScrolling || isUndefined(currentHref))
  86. return;
  87. setCurrentAnchor(currentHref);
  88. });
  89. const getCurrentHref = () => {
  90. if (!containerEl.value)
  91. return;
  92. const scrollTop = getScrollTop(containerEl.value);
  93. const anchorTopList = [];
  94. for (const href of Object.keys(links)) {
  95. const target = getElement(href);
  96. if (!target)
  97. continue;
  98. const scrollEle = getScrollElement(target, containerEl.value);
  99. const distance = getOffsetTopDistance(target, scrollEle);
  100. anchorTopList.push({
  101. top: distance - props.offset - props.bound,
  102. href
  103. });
  104. }
  105. anchorTopList.sort((prev, next) => prev.top - next.top);
  106. for (let i = 0; i < anchorTopList.length; i++) {
  107. const item = anchorTopList[i];
  108. const next = anchorTopList[i + 1];
  109. if (i === 0 && scrollTop === 0) {
  110. return props.selectScrollTop ? item.href : "";
  111. }
  112. if (item.top <= scrollTop && (!next || next.top > scrollTop)) {
  113. return item.href;
  114. }
  115. }
  116. };
  117. const getContainer = () => {
  118. const el = getElement(props.container);
  119. if (!el || isWindow(el)) {
  120. containerEl.value = window;
  121. } else {
  122. containerEl.value = el;
  123. }
  124. };
  125. useEventListener(containerEl, "scroll", handleScroll);
  126. const updateMarkerStyle = () => {
  127. nextTick(() => {
  128. if (!anchorRef.value || !markerRef.value || !currentAnchor.value) {
  129. markerStyle.value = {};
  130. return;
  131. }
  132. const currentLinkEl = links[currentAnchor.value];
  133. if (!currentLinkEl) {
  134. markerStyle.value = {};
  135. return;
  136. }
  137. const anchorRect = anchorRef.value.getBoundingClientRect();
  138. const markerRect = markerRef.value.getBoundingClientRect();
  139. const linkRect = currentLinkEl.getBoundingClientRect();
  140. if (props.direction === "horizontal") {
  141. const left = linkRect.left - anchorRect.left;
  142. markerStyle.value = {
  143. left: `${left}px`,
  144. width: `${linkRect.width}px`,
  145. opacity: 1
  146. };
  147. } else {
  148. const top = linkRect.top - anchorRect.top + (linkRect.height - markerRect.height) / 2;
  149. markerStyle.value = {
  150. top: `${top}px`,
  151. opacity: 1
  152. };
  153. }
  154. });
  155. };
  156. watch(currentAnchor, updateMarkerStyle);
  157. watch(() => {
  158. var _a;
  159. return (_a = slots.default) == null ? void 0 : _a.call(slots);
  160. }, updateMarkerStyle);
  161. onMounted(() => {
  162. getContainer();
  163. const hash = decodeURIComponent(window.location.hash);
  164. const target = getElement(hash);
  165. if (target) {
  166. scrollTo(hash);
  167. } else {
  168. handleScroll();
  169. }
  170. });
  171. watch(() => props.container, () => {
  172. getContainer();
  173. });
  174. provide(anchorKey, {
  175. ns,
  176. direction: props.direction,
  177. currentAnchor,
  178. addLink,
  179. removeLink,
  180. handleClick
  181. });
  182. expose({
  183. scrollTo
  184. });
  185. return (_ctx, _cache) => {
  186. return openBlock(), createElementBlock("div", {
  187. ref_key: "anchorRef",
  188. ref: anchorRef,
  189. class: normalizeClass(unref(cls))
  190. }, [
  191. _ctx.marker ? (openBlock(), createElementBlock("div", {
  192. key: 0,
  193. ref_key: "markerRef",
  194. ref: markerRef,
  195. class: normalizeClass(unref(ns).e("marker")),
  196. style: normalizeStyle(markerStyle.value)
  197. }, null, 6)) : createCommentVNode("v-if", true),
  198. createElementVNode("div", {
  199. class: normalizeClass(unref(ns).e("list"))
  200. }, [
  201. renderSlot(_ctx.$slots, "default")
  202. ], 2)
  203. ], 2);
  204. };
  205. }
  206. });
  207. var Anchor = /* @__PURE__ */ _export_sfc(_sfc_main, [["__file", "anchor.vue"]]);
  208. export { Anchor as default };
  209. //# sourceMappingURL=anchor2.mjs.map