foundation.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import BaseFoundation, { DefaultAdapter } from '../base/foundation';
  2. import { isArray, get } from 'lodash-es';
  3. import scrollIntoView, { CustomBehaviorOptions } from 'scroll-into-view-if-needed';
  4. import { cssClasses } from './constants';
  5. import React from 'react';
  6. const prefixCls = cssClasses.PREFIX;
  7. export interface AnchorAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
  8. addLink: (link: string) => void;
  9. removeLink: (link: string) => void;
  10. setChildMap: (value: Record<string, Set<string>>) => void;
  11. setScrollHeight: (heigh: string) => void;
  12. setSlideBarTop: (height: number) => void;
  13. setClickLink: (value: boolean) => void;
  14. setActiveLink: (link: string, cb: () => void) => void;
  15. setClickLinkWithCallBack: (value: boolean, link: string, cb: (link: string) => void) => void;
  16. getContainer: () => HTMLElement | Window;
  17. getContainerBoundingTop: () => number;
  18. getLinksBoundingTop: () => number[];
  19. getAnchorNode: (selector: string) => HTMLElement;
  20. getContentNode: (selector: string) => HTMLElement;
  21. notifyChange: (currentLink: string, previousLink: string) => void;
  22. notifyClick: (e: React.MouseEvent<HTMLElement>, link: string) => void;
  23. canSmoothScroll: () => boolean;
  24. }
  25. export default class AnchorFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<AnchorAdapter<P, S>, P, S> {
  26. constructor(adapter: AnchorAdapter<P, S>) {
  27. super({ ...AnchorFoundation.defaultAdapter, ...adapter });
  28. }
  29. // eslint-disable-next-line @typescript-eslint/no-empty-function
  30. init = () => {};
  31. // eslint-disable-next-line @typescript-eslint/no-empty-function
  32. destroy = () => {};
  33. addLink = (link: string) => {
  34. this._adapter.addLink(link);
  35. };
  36. removeLink = (link: string) => {
  37. this._adapter.removeLink(link);
  38. };
  39. setActiveLink = (link: string, prevLink: string, shouldNotify = true) => {
  40. const activeLink = this._adapter.getState('activeLink');
  41. const onChange = this._adapter.getProp('onChange');
  42. if (activeLink !== link) {
  43. this._adapter.setActiveLink(link, this._setActiveSlide);
  44. if (onChange && shouldNotify) {
  45. this._adapter.notifyChange(link, prevLink);
  46. }
  47. }
  48. };
  49. // Adjust rail height according to text link content height
  50. setScrollHeight = () => {
  51. const anchorWrapper = `.${prefixCls}-link-wrapper`;
  52. const anchorNode = this._adapter.getAnchorNode(anchorWrapper);
  53. if (anchorNode) {
  54. const scrollHeight = `${anchorNode.scrollHeight}px`;
  55. this._adapter.setScrollHeight(scrollHeight);
  56. }
  57. };
  58. updateScrollHeight = (prevState: any, state: any) => {
  59. const prevLinks = prevState.links.join('');
  60. const links = state.links.join('');
  61. if (prevLinks !== links) {
  62. this.setScrollHeight();
  63. }
  64. };
  65. setChildMap = () => {
  66. const children = this._adapter.getProp('children');
  67. const childMap = {};
  68. if (isArray(children)) {
  69. for (const link of children) {
  70. this._getLinkToMap(link, [], childMap);
  71. }
  72. } else {
  73. this._getLinkToMap(children, [], childMap);
  74. }
  75. this._adapter.setChildMap(childMap);
  76. };
  77. updateChildMap = (prevState: any, state: any) => {
  78. const prevLinks = prevState.links.join('');
  79. const links = state.links.join('');
  80. if (prevLinks !== links) {
  81. this.setChildMap();
  82. }
  83. };
  84. getLinksTop = () => this._adapter.getLinksBoundingTop();
  85. handleScroll = () => {
  86. const { clickLink, links, activeLink: prevActiveLink } = this.getStates(); // TODO check this._adapter -> this.
  87. // ActiveLink Determined by the clicked link
  88. if (clickLink) {
  89. return;
  90. }
  91. const elTop = this.getLinksTop();
  92. let lastNegative = -Infinity;
  93. let lastNegativeIndex = -1;
  94. for (let i = 0; i < elTop.length; i++) {
  95. if (elTop[i] < 0 && elTop[i] > lastNegative) {
  96. lastNegative = elTop[i];
  97. lastNegativeIndex = i;
  98. }
  99. }
  100. const activeLink = links[lastNegativeIndex];
  101. this.setActiveLink(activeLink, prevActiveLink);
  102. };
  103. handleClick = (e: any, link: string, shouldNotify = true) => {
  104. const destNode = this._adapter.getContentNode(link);
  105. const prevLink = this._adapter.getState('activeLink');
  106. this.setActiveLink(link, prevLink, shouldNotify);
  107. if (destNode) {
  108. try {
  109. this._adapter.setClickLinkWithCallBack(true, link, this._scrollIntoView);
  110. } catch (error) {}
  111. }
  112. shouldNotify && this._adapter.notifyClick(e, link);
  113. };
  114. handleClickLink = () => {
  115. this._adapter.setClickLink(false);
  116. };
  117. // Get the child nodes of each link
  118. _getLinkToMap = (link: any, parents: string[], linkMap: { [key: string]: Set<string> }) => {
  119. const node = link && link.props;
  120. if (!node || !node.href) {
  121. return;
  122. }
  123. if (!(node.href in linkMap)) {
  124. linkMap[node.href] = new Set();
  125. }
  126. // Every ancestor kept a map
  127. for (const parent of parents) {
  128. linkMap[parent].add(node.href);
  129. }
  130. if (node.children && node.children.length) {
  131. parents.push(node.href);
  132. // Maintain child node map
  133. for (const child of node.children) {
  134. this._getLinkToMap(child, parents, linkMap);
  135. }
  136. parents.pop();
  137. }
  138. };
  139. _scrollIntoView = (link: string) => {
  140. const { scrollMotion, targetOffset } = this.getProps(); // TODO check this._adapter -> this.
  141. const behavior = scrollMotion ? 'smooth' : 'auto';
  142. const canSmoothScroll = this._adapter.canSmoothScroll();
  143. if (link) {
  144. const destNode = this._adapter.getContentNode(link);
  145. const scrollOpts: CustomBehaviorOptions<void> = {
  146. /**
  147. * Behavior defines scrolling behavior
  148. * - Optional'auto '|' smooth '| Function
  149. * - Function Custom scrolling behavior
  150. * - Enter parameters as actions, each action contains an element that should be scrolled
  151. * - Actions include scrolling containers to the outermost scrollable container (document.body), the scrollable capacity needs to meet
  152. * 1. The parent of the scroll container (directly or indirectly)
  153. * 2. There is a scroll axis (clientHeight < scrollHeight | | clientWidth < scrollWidth)
  154. * 3.overflowX or overflowY has a value and is not visible or clip
  155. * For details, please see https://github.com/stipsan/compute-scroll-into-view
  156. *
  157. * behavior定义滚动行为
  158. * - 可选 'auto' | 'smooth' | Function
  159. * - Function 自定义滚动行为
  160. * - 入参为 actions,每个action包含一个应该滚动的元素
  161. * - actions包括滚动容器到最外层的可滚动容器(document.body),可滚动容需满足
  162. * 1. 滚动容器的父级(直接或间接)
  163. * 2. 有滚动轴(clientHeight < scrollHeight || clientWidth < scrollWidth)
  164. * 3. overflowX 或 overflowY 有值且不为 visible 或 clip
  165. * 详情请看https://github.com/stipsan/compute-scroll-into-view
  166. */
  167. behavior: actions => {
  168. // We just need to scroll the innermost target container
  169. const innermostAction = get(actions, '0');
  170. const el = get(innermostAction, 'el');
  171. const top = get(innermostAction, 'top');
  172. if (el) {
  173. const offsetTop = top - targetOffset;
  174. if (el.scroll && canSmoothScroll) {
  175. el.scroll({ top: offsetTop, behavior });
  176. } else {
  177. el.scrollTop = offsetTop;
  178. }
  179. }
  180. },
  181. block: 'start',
  182. };
  183. if (destNode) {
  184. scrollIntoView(destNode, scrollOpts);
  185. }
  186. }
  187. };
  188. _setActiveSlide = () => {
  189. const activeClass = `.${cssClasses.PREFIX}-link-title-active`;
  190. const linkNode = this._adapter.getAnchorNode(activeClass);
  191. if (linkNode) {
  192. const height = linkNode.offsetTop;
  193. this._adapter.setSlideBarTop(height);
  194. }
  195. };
  196. }