Anchor.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
  2. import _extends from "@babel/runtime/helpers/esm/extends";
  3. import { createVNode as _createVNode, resolveDirective as _resolveDirective } from "vue";
  4. import { defineComponent, nextTick, onBeforeUnmount, onMounted, onUpdated, reactive, ref, computed } from 'vue';
  5. import scrollIntoView from 'scroll-into-view-if-needed';
  6. import classNames from '../_util/classNames';
  7. import addEventListener from '../vc-util/Dom/addEventListener';
  8. import Affix from '../affix';
  9. import scrollTo from '../_util/scrollTo';
  10. import getScroll from '../_util/getScroll';
  11. import useConfigInject from '../config-provider/hooks/useConfigInject';
  12. import useProvideAnchor from './context';
  13. import useStyle from './style';
  14. import AnchorLink from './AnchorLink';
  15. import PropTypes from '../_util/vue-types';
  16. import devWarning from '../vc-util/devWarning';
  17. import { arrayType } from '../_util/type';
  18. function getDefaultContainer() {
  19. return window;
  20. }
  21. function getOffsetTop(element, container) {
  22. if (!element.getClientRects().length) {
  23. return 0;
  24. }
  25. const rect = element.getBoundingClientRect();
  26. if (rect.width || rect.height) {
  27. if (container === window) {
  28. container = element.ownerDocument.documentElement;
  29. return rect.top - container.clientTop;
  30. }
  31. return rect.top - container.getBoundingClientRect().top;
  32. }
  33. return rect.top;
  34. }
  35. const sharpMatcherRegx = /#([\S ]+)$/;
  36. export const anchorProps = () => ({
  37. prefixCls: String,
  38. offsetTop: Number,
  39. bounds: Number,
  40. affix: {
  41. type: Boolean,
  42. default: true
  43. },
  44. showInkInFixed: {
  45. type: Boolean,
  46. default: false
  47. },
  48. getContainer: Function,
  49. wrapperClass: String,
  50. wrapperStyle: {
  51. type: Object,
  52. default: undefined
  53. },
  54. getCurrentAnchor: Function,
  55. targetOffset: Number,
  56. items: arrayType(),
  57. direction: PropTypes.oneOf(['vertical', 'horizontal']).def('vertical'),
  58. onChange: Function,
  59. onClick: Function
  60. });
  61. export default defineComponent({
  62. compatConfig: {
  63. MODE: 3
  64. },
  65. name: 'AAnchor',
  66. inheritAttrs: false,
  67. props: anchorProps(),
  68. setup(props, _ref) {
  69. let {
  70. emit,
  71. attrs,
  72. slots,
  73. expose
  74. } = _ref;
  75. var _a;
  76. const {
  77. prefixCls,
  78. getTargetContainer,
  79. direction
  80. } = useConfigInject('anchor', props);
  81. const anchorDirection = computed(() => {
  82. var _a;
  83. return (_a = props.direction) !== null && _a !== void 0 ? _a : 'vertical';
  84. });
  85. if (process.env.NODE_ENV !== 'production') {
  86. devWarning(props.items && typeof slots.default !== 'function', 'Anchor', '`Anchor children` is deprecated. Please use `items` instead.');
  87. }
  88. if (process.env.NODE_ENV !== 'production') {
  89. devWarning(!(anchorDirection.value === 'horizontal' && ((_a = props.items) === null || _a === void 0 ? void 0 : _a.some(n => 'children' in n))), 'Anchor', '`Anchor items#children` is not supported when `Anchor` direction is horizontal.');
  90. }
  91. const spanLinkNode = ref(null);
  92. const anchorRef = ref();
  93. const state = reactive({
  94. links: [],
  95. scrollContainer: null,
  96. scrollEvent: null,
  97. animating: false
  98. });
  99. const activeLink = ref(null);
  100. const getContainer = computed(() => {
  101. const {
  102. getContainer
  103. } = props;
  104. return getContainer || (getTargetContainer === null || getTargetContainer === void 0 ? void 0 : getTargetContainer.value) || getDefaultContainer;
  105. });
  106. // func...
  107. const getCurrentAnchor = function () {
  108. let offsetTop = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
  109. let bounds = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 5;
  110. const linkSections = [];
  111. const container = getContainer.value();
  112. state.links.forEach(link => {
  113. const sharpLinkMatch = sharpMatcherRegx.exec(link.toString());
  114. if (!sharpLinkMatch) {
  115. return;
  116. }
  117. const target = document.getElementById(sharpLinkMatch[1]);
  118. if (target) {
  119. const top = getOffsetTop(target, container);
  120. if (top < offsetTop + bounds) {
  121. linkSections.push({
  122. link,
  123. top
  124. });
  125. }
  126. }
  127. });
  128. if (linkSections.length) {
  129. const maxSection = linkSections.reduce((prev, curr) => curr.top > prev.top ? curr : prev);
  130. return maxSection.link;
  131. }
  132. return '';
  133. };
  134. const setCurrentActiveLink = link => {
  135. const {
  136. getCurrentAnchor
  137. } = props;
  138. if (activeLink.value === link) {
  139. return;
  140. }
  141. activeLink.value = typeof getCurrentAnchor === 'function' ? getCurrentAnchor(link) : link;
  142. emit('change', link);
  143. };
  144. const handleScrollTo = link => {
  145. const {
  146. offsetTop,
  147. targetOffset
  148. } = props;
  149. setCurrentActiveLink(link);
  150. const sharpLinkMatch = sharpMatcherRegx.exec(link);
  151. if (!sharpLinkMatch) {
  152. return;
  153. }
  154. const targetElement = document.getElementById(sharpLinkMatch[1]);
  155. if (!targetElement) {
  156. return;
  157. }
  158. const container = getContainer.value();
  159. const scrollTop = getScroll(container, true);
  160. const eleOffsetTop = getOffsetTop(targetElement, container);
  161. let y = scrollTop + eleOffsetTop;
  162. y -= targetOffset !== undefined ? targetOffset : offsetTop || 0;
  163. state.animating = true;
  164. scrollTo(y, {
  165. callback: () => {
  166. state.animating = false;
  167. },
  168. getContainer: getContainer.value
  169. });
  170. };
  171. expose({
  172. scrollTo: handleScrollTo
  173. });
  174. const handleScroll = () => {
  175. if (state.animating) {
  176. return;
  177. }
  178. const {
  179. offsetTop,
  180. bounds,
  181. targetOffset
  182. } = props;
  183. const currentActiveLink = getCurrentAnchor(targetOffset !== undefined ? targetOffset : offsetTop || 0, bounds);
  184. setCurrentActiveLink(currentActiveLink);
  185. };
  186. const updateInk = () => {
  187. const linkNode = anchorRef.value.querySelector(`.${prefixCls.value}-link-title-active`);
  188. if (linkNode && spanLinkNode.value) {
  189. const horizontalAnchor = anchorDirection.value === 'horizontal';
  190. spanLinkNode.value.style.top = horizontalAnchor ? '' : `${linkNode.offsetTop + linkNode.clientHeight / 2}px`;
  191. spanLinkNode.value.style.height = horizontalAnchor ? '' : `${linkNode.clientHeight}px`;
  192. spanLinkNode.value.style.left = horizontalAnchor ? `${linkNode.offsetLeft}px` : '';
  193. spanLinkNode.value.style.width = horizontalAnchor ? `${linkNode.clientWidth}px` : '';
  194. if (horizontalAnchor) {
  195. scrollIntoView(linkNode, {
  196. scrollMode: 'if-needed',
  197. block: 'nearest'
  198. });
  199. }
  200. }
  201. };
  202. useProvideAnchor({
  203. registerLink: link => {
  204. if (!state.links.includes(link)) {
  205. state.links.push(link);
  206. }
  207. },
  208. unregisterLink: link => {
  209. const index = state.links.indexOf(link);
  210. if (index !== -1) {
  211. state.links.splice(index, 1);
  212. }
  213. },
  214. activeLink,
  215. scrollTo: handleScrollTo,
  216. handleClick: (e, info) => {
  217. emit('click', e, info);
  218. },
  219. direction: anchorDirection
  220. });
  221. onMounted(() => {
  222. nextTick(() => {
  223. const container = getContainer.value();
  224. state.scrollContainer = container;
  225. state.scrollEvent = addEventListener(state.scrollContainer, 'scroll', handleScroll);
  226. handleScroll();
  227. });
  228. });
  229. onBeforeUnmount(() => {
  230. if (state.scrollEvent) {
  231. state.scrollEvent.remove();
  232. }
  233. });
  234. onUpdated(() => {
  235. if (state.scrollEvent) {
  236. const currentContainer = getContainer.value();
  237. if (state.scrollContainer !== currentContainer) {
  238. state.scrollContainer = currentContainer;
  239. state.scrollEvent.remove();
  240. state.scrollEvent = addEventListener(state.scrollContainer, 'scroll', handleScroll);
  241. handleScroll();
  242. }
  243. }
  244. updateInk();
  245. });
  246. const createNestedLink = options => Array.isArray(options) ? options.map(option => {
  247. const {
  248. children,
  249. key,
  250. href,
  251. target,
  252. class: cls,
  253. style,
  254. title
  255. } = option;
  256. return _createVNode(AnchorLink, {
  257. "key": key,
  258. "href": href,
  259. "target": target,
  260. "class": cls,
  261. "style": style,
  262. "title": title,
  263. "customTitleProps": option
  264. }, {
  265. default: () => [anchorDirection.value === 'vertical' ? createNestedLink(children) : null],
  266. customTitle: slots.customTitle
  267. });
  268. }) : null;
  269. const [wrapSSR, hashId] = useStyle(prefixCls);
  270. return () => {
  271. var _a;
  272. const {
  273. offsetTop,
  274. affix,
  275. showInkInFixed
  276. } = props;
  277. const pre = prefixCls.value;
  278. const inkClass = classNames(`${pre}-ink`, {
  279. [`${pre}-ink-visible`]: activeLink.value
  280. });
  281. const wrapperClass = classNames(hashId.value, props.wrapperClass, `${pre}-wrapper`, {
  282. [`${pre}-wrapper-horizontal`]: anchorDirection.value === 'horizontal',
  283. [`${pre}-rtl`]: direction.value === 'rtl'
  284. });
  285. const anchorClass = classNames(pre, {
  286. [`${pre}-fixed`]: !affix && !showInkInFixed
  287. });
  288. const wrapperStyle = _extends({
  289. maxHeight: offsetTop ? `calc(100vh - ${offsetTop}px)` : '100vh'
  290. }, props.wrapperStyle);
  291. const anchorContent = _createVNode("div", {
  292. "class": wrapperClass,
  293. "style": wrapperStyle,
  294. "ref": anchorRef
  295. }, [_createVNode("div", {
  296. "class": anchorClass
  297. }, [_createVNode("span", {
  298. "class": inkClass,
  299. "ref": spanLinkNode
  300. }, null), Array.isArray(props.items) ? createNestedLink(props.items) : (_a = slots.default) === null || _a === void 0 ? void 0 : _a.call(slots)])]);
  301. return wrapSSR(!affix ? anchorContent : _createVNode(Affix, _objectSpread(_objectSpread({}, attrs), {}, {
  302. "offsetTop": offsetTop,
  303. "target": getContainer.value
  304. }), {
  305. default: () => [anchorContent]
  306. }));
  307. };
  308. }
  309. });