Guest User

Untitled

a guest
Nov 17th, 2018
87
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 3.04 KB | None | 0 0
  1. /**
  2. * @class Scrollspy
  3. */
  4.  
  5. import React from "react";
  6. import classnames from "classnames";
  7.  
  8. const SPY_INTERVAL = 100;
  9.  
  10. export interface SpyItem {
  11. inView: boolean;
  12. element: HTMLElement;
  13. }
  14.  
  15. /**
  16. * @interface ScrollspyProps
  17. * @property {string[]} ids - the dom IDs of the page elements to scroll to
  18. * @property {number} offset - a number of pixels to offset whether the page element is 'in view'
  19. */
  20. export interface ScrollspyProps {
  21. ids: string[];
  22. offset: number;
  23. itemContainerClassName?: string;
  24. activeItemClassName?: string;
  25. itemClassName?: string;
  26. containerElement?: JSX.Element;
  27. itemElement?: JSX.Element;
  28. }
  29.  
  30. export interface ScrollspyState {
  31. items: SpyItem[];
  32. }
  33.  
  34. export default class Scrollspy extends React.Component<
  35. ScrollspyProps,
  36. ScrollspyState
  37. > {
  38. constructor(props: any) {
  39. super(props);
  40. this.state = {
  41. items: []
  42. };
  43. }
  44.  
  45. public static defaultProps: Partial<ScrollspyProps> = {
  46. offset: 2,
  47. ids: [],
  48. containerElement: <ul />,
  49. itemElement: <li />
  50. };
  51.  
  52. private timer: number;
  53.  
  54. private spy() {
  55. const items = this.props.ids
  56. .map(id => {
  57. const element = document.getElementById(id);
  58. if (element) {
  59. return {
  60. inView: this.isInView(element),
  61. element
  62. } as SpyItem;
  63. } else {
  64. return;
  65. }
  66. })
  67. .filter(item => item);
  68.  
  69. const firstTrueItem = items.find(item => !!item && item.inView);
  70.  
  71. if (!firstTrueItem) {
  72. return; // dont update state
  73. } else {
  74. const update = items.map(item => {
  75. return { ...item, inView: item === firstTrueItem } as SpyItem;
  76. });
  77.  
  78. this.setState({ items: update });
  79. }
  80. }
  81.  
  82. public componentDidMount() {
  83. this.timer = window.setInterval(() => this.spy(), SPY_INTERVAL);
  84. }
  85.  
  86. public componentWillUnmount() {
  87. window.clearInterval(this.timer);
  88. }
  89.  
  90. private isInView = (element: HTMLElement) => {
  91. if (!element) {
  92. return false;
  93. }
  94. const { offset } = this.props;
  95. const rect = element.getBoundingClientRect();
  96.  
  97. return rect.top >= 0 - offset && rect.bottom <= window.innerHeight + offset;
  98. };
  99.  
  100. private scrollTo(element: HTMLElement) {
  101. element.scrollIntoView({
  102. behavior: "smooth",
  103. block: "start",
  104. inline: "nearest"
  105. });
  106. }
  107.  
  108. private renderItems() {
  109. const { itemElement, activeItemClassName, itemClassName } = this.props;
  110. return this.state.items.map((item, k) => {
  111. return itemElement
  112. ? React.cloneElement(itemElement, {
  113. key: k,
  114. className: classnames(
  115. itemClassName,
  116. item.inView ? activeItemClassName : null
  117. ),
  118. onClick: () => this.scrollTo(item.element),
  119. children: item.element.innerText
  120. })
  121. : null;
  122. });
  123. }
  124.  
  125. public render() {
  126. const { itemContainerClassName, containerElement } = this.props;
  127. return containerElement
  128. ? React.cloneElement(containerElement, {
  129. className: classnames(itemContainerClassName),
  130. children: this.renderItems()
  131. })
  132. : null;
  133. }
  134. }
Add Comment
Please, Sign In to add comment