summaryrefslogtreecommitdiffstats
path: root/src/components/Widgets/TopicsList
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-04-07 22:54:27 +0200
committerArmand Philippot <git@armandphilippot.com>2022-04-07 22:54:27 +0200
commit4bd651b9e32c568d86b30463858c20ef290d8c07 (patch)
tree825e17e6b8d44d72fce01b014f4b79ae52f77d93 /src/components/Widgets/TopicsList
parent06396f8e942c58254ee4e87f610d3e33197e0d73 (diff)
chore: add a HeadingButton component
Diffstat (limited to 'src/components/Widgets/TopicsList')
0 files changed, 0 insertions, 0 deletions
7 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
import { slugify } from '@utils/helpers/strings';
import { useCallback, useEffect, useMemo, useState } from 'react';

export type Heading = {
  /**
   * The heading depth.
   */
  depth: number;
  /**
   * The heading id.
   */
  id: string;
  /**
   * The heading children.
   */
  children: Heading[];
  /**
   * The heading title.
   */
  title: string;
};

/**
 * Get the headings tree of the given HTML element.
 *
 * @param {HTMLElement} wrapper - An HTML element that contains the headings.
 * @returns {Heading[]} The headings tree.
 */
const useHeadingsTree = (wrapper: HTMLElement): Heading[] => {
  const depths = useMemo(() => ['h2', 'h3', 'h4', 'h5', 'h6'], []);
  const [allHeadings, setAllHeadings] =
    useState<NodeListOf<HTMLHeadingElement>>();
  const [headingsTree, setHeadingsTree] = useState<Heading[]>([]);

  useEffect(() => {
    const query = depths.join(', ');
    const result: NodeListOf<HTMLHeadingElement> =
      wrapper.querySelectorAll(query);
    setAllHeadings(result);
  }, [depths, wrapper]);

  const getDepth = useCallback(
    /**
     * Retrieve the heading element depth.
     *
     * @param {HTMLHeadingElement} el - An heading element.
     * @returns {number} The heading depth.
     */
    (el: HTMLHeadingElement): number => {
      return depths.findIndex((depth) => depth === el.localName);
    },
    [depths]
  );

  const formatHeadings = useCallback(
    /**
     * Convert a list of headings into an array of Heading objects.
     *
     * @param {NodeListOf<HTMLHeadingElement>} headings - A list of headings.
     * @returns {Heading[]} An array of Heading objects.
     */
    (headings: NodeListOf<HTMLHeadingElement>): Heading[] => {
      const formattedHeadings: Heading[] = [];

      Array.from(headings).forEach((heading) => {
        const title: string = heading.textContent!;
        const id = slugify(title);
        const depth = getDepth(heading);
        const children: Heading[] = [];

        heading.id = id;

        formattedHeadings.push({
          depth,
          id,
          children,
          title,
        });
      });

      return formattedHeadings;
    },
    [getDepth]
  );

  const buildSubTree = useCallback(
    /**
     * Build the heading subtree.
     *
     * @param {Heading} parent - The heading parent.
     * @param {Heading} currentHeading - The current heading element.
     */
    (parent: Heading, currentHeading: Heading): void => {
      if (parent.depth === currentHeading.depth - 1) {
        parent.children.push(currentHeading);
      } else {
        const lastItem = parent.children[parent.children.length - 1];
        buildSubTree(lastItem, currentHeading);
      }
    },
    []
  );

  const buildTree = useCallback(
    /**
     * Build a heading tree.
     *
     * @param {Heading[]} headings - An array of Heading objects.
     * @returns {Heading[]} The headings tree.
     */
    (headings: Heading[]): Heading[] => {
      const tree: Heading[] = [];

      headings.forEach((heading) => {
        if (heading.depth === 0) {
          tree.push(heading);
        } else {
          const lastItem = tree[tree.length - 1];
          buildSubTree(lastItem, heading);
        }
      });

      return tree;
    },
    [buildSubTree]
  );

  const getHeadingsTree = useCallback(
    /**
     * Retrieve a headings tree from a list of headings element.
     *
     * @param {NodeListOf<HTMLHeadingElement>} headings - A headings list.
     * @returns {Heading[]} The headings tree.
     */
    (headings: NodeListOf<HTMLHeadingElement>): Heading[] => {
      const formattedHeadings = formatHeadings(headings);

      return buildTree(formattedHeadings);
    },
    [formatHeadings, buildTree]
  );

  useEffect(() => {
    if (allHeadings) {
      const headingsList = getHeadingsTree(allHeadings);
      setHeadingsTree(headingsList);
    }
  }, [allHeadings, getHeadingsTree]);

  return headingsTree;
};

export default useHeadingsTree;