Skip to content
Snippets Groups Projects
Toc.tsx 3.73 KiB
Newer Older
import React from "react";
import { useEffect, useState } from "react";
import { useHeadsObserver } from "./utils/hook";

Toavina's avatar
Toavina committed
const rnd = (() => {
  const gen = (min, max) => max++ && [...Array(max-min)].map((s, i) => String.fromCharCode(min+i));

  const sets = {
      alphaLower: gen(97,122),
      alphaUpper: gen(65,90)
  };

  function* iter(len, set) {
      if (set.length < 1) set = Object.values(sets).flat(); 
      for (let i = 0; i < len; i++) yield set[Math.random() * set.length|0]
  }

  return Object.assign(((len, ...set) => [...iter(len, set.flat())].join('')), sets);
})();

const navStyle = {
  position: "sticky -webkit-sticky",
  top: "24px",
  maxHeight: "calc(100vh - 40px)",
  overflow: "auto",
} as unknown as React.CSSProperties;

let navulliStyle = {
  marginBottom: "15px",
} as React.CSSProperties;

const getClassName = (level) => {
  switch (level) {
    case 1:
      navulliStyle = {
        ...navulliStyle,
        marginLeft: "0px",
      };
      return "head1";
    case 2:
      navulliStyle = {
        ...navulliStyle,
        marginLeft: "10px",
      };
      return "head2";
    case 3:
      navulliStyle = {
        ...navulliStyle,
        marginLeft: "20px",
      };
      return "head3";
    case 4:
      navulliStyle = {
        ...navulliStyle,
        marginLeft: "30px",
      };
      return "head4";
    default:
      return undefined;
  }
};

interface Props {
  className?: string;
}

const Toc = (props: Props) => {
  const { className = "" } = props;
  const [headings, setHeadings] = useState<any>([]);
  const { activeId } = useHeadsObserver();  

  useEffect(() => {
    const elements = Array.from(
      document.querySelectorAll("h1, h2, h3, h4")
    ).map((elem) => ({
Toavina's avatar
Toavina committed
      id: elem.id ? elem.id : elem.id=rnd(4),
      text: elem.textContent,
      level: Number(elem.nodeName.charAt(1)),
Toavina's avatar
Toavina committed
    }));    
    setHeadings(elements);
  }, []);

  return (
    <aside
      className={`w-[20%] fixed right-0 top-auto h-screen bg-slate-50 p-5 ${className}`}
    >
      <div className="text-2xl pb-5 flex flex-row">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          fill="none"
          viewBox="0 0 24 24"
          strokeWidth={1.5}
          stroke="currentColor"
          className="w-6 h-6 mt-2"
        >
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 12h.007v.008H3.75V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm-.375 5.25h.007v.008H3.75v-.008zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
          />
        </svg>
        <p className="px-2 mt-0.5"> Menu interne </p>
      </div>
      <nav style={navStyle}>
        <ul style={navulliStyle}>
          {headings.map((heading) => (
            <li
              key={heading.id}
              className={getClassName(heading.level)}
              style={navulliStyle}
            >
              <a
                href={`#${heading.id}`}
                onClick={(e) => {
                  e.preventDefault();
                  const element = document.querySelector(`#${heading.id}`);
                  if(element) {
                    element?.scrollIntoView({
                      behavior: "smooth",
                    });
                  }
                  else {
Toavina's avatar
Toavina committed
                    console.log("No element");
                  }
                }}
                style={{
                  fontWeight: activeId == heading.id ? "bold" : "normal",
                  fontFamily: "Georgia, serif",
                }}
              >
                {heading.text}
              </a>
            </li>
          ))}
        </ul>
      </nav>
    </aside>
  );
};

export default Toc;