Skip to content

Gists

Using react-markdown with table of content

type Props = {
  category: string
  filename: string
}

const fetchDocs = (token: string | undefined, category: string, filename: string): Promise<string> => {
  if (!token) throw "NO TOKEN"
  return fetch(`${process.env.REACT_APP_SERVER_BASE_URL}/texts/docs/${category}/${filename}`, {
    method: 'GET',
    headers: {'Authorization': 'Bearer ' + token, 'Content-Type': 'text/markdown'}
  }).then(res => {
    if (!res.ok) return Promise.reject(res)
    return res.text()
  }).catch(err => {
    console.log(err)
    throw new Error(err)
  })
}

interface Heading {
  level: number;
  id: string;
  title: string;
}

export default function PubText({category, filename}: Props) {
  const location = useLocation();
  const {keycloak} = useKeycloak();
  const token = keycloak && keycloak.token;
  const [tableOfContent, setTableOfContent] = useState<Heading[]>([]);
  const [activeId, setActiveId] = useState<string>("");

  const {data, isLoading, isRefetching} = useQuery(["FETCH_DOCS", category, filename], () =>
    fetchDocs(token, category, filename));

  const handleScroll = () => {
    const scrollPosition = window.scrollY;
    let newActiveId = "";

    tableOfContent.forEach(({id}) => {
      const element = document.getElementById(id);
      const elementTopOffset = element?.offsetTop ?? 0;
      const elementIsVisible = elementTopOffset - 100 <= scrollPosition;

      if (element && elementIsVisible) {
        newActiveId = id;
      }
    });

    if (newActiveId !== activeId) {
      setActiveId(newActiveId);
    }
  };

  useEffect(() => {
    setTableOfContent([])
  }, [location.pathname])

  useEffect(() => {
    window.addEventListener("scroll", handleScroll);
    return () => {
      window.removeEventListener("scroll", handleScroll);
    };
  }, [activeId, tableOfContent]);

  if (!data) return <><CircularProgress/></>
  if (isLoading || isRefetching) return <><CircularProgress/></>

  const addToTOC = ({children, ...props}: React.PropsWithChildren<HeadingProps>) => {
    const level = Number(props.node.tagName.match(/h(\d)/)?.slice(1));
    if (level && children && typeof children[0] === "string") {
      const id = children[0].toLowerCase().replace(/[^a-z0-9]+/g, "-");
      if (!tableOfContent.some(entry => entry.id === id)) {
        tableOfContent.push({level, id, title: children[0]});
      }
      return createElement(props.node.tagName, {id}, children)
    } else {
      return createElement(props.node.tagName, props, children);
    }
  };

  const onClickScrollToContentView = (e: React.MouseEvent<HTMLAnchorElement>, id: string) => {
    e.preventDefault();
    const element = document.getElementById(id);
    const offset = 80;
    const bodyRect = document.body.getBoundingClientRect().top;
    const elementRect = element?.getBoundingClientRect().top;
    const elementPosition = elementRect! - bodyRect;
    const offsetPosition = elementPosition - offset;

    window.scrollTo({
      top: offsetPosition,
      behavior: "smooth",
    });
  };

  const TableOfContent = () => {
    return (
      <div>
        <Typography variant={"h6"} fontWeight={"bold"} sx={{color: "#4b4b4b"}}>Table of Content</Typography>
        {tableOfContent.map(({level, id, title}) => (
          <div key={id} className={`toc-entry-level-${level}`} style={{listStyleType: "none", padding: 3}}>
            <a href={`#${id}`} onClick={e => onClickScrollToContentView(e, id)}
               style={{
                 textDecoration: "none",
                 color: activeId === id ? "#A040A0" : "#4b4b4b",
                 fontWeight: activeId === id ? "bold" : "normal"
               }}>{title}</a>
          </div>
        ))}
      </div>
    )
  }

  return (
    <Grid container border={"border"} direction={"row"}>
      <Grid item xs={9}>
        <ReactMarkdown
          components={{
            h1: addToTOC,
            h2: addToTOC,
            code: ({node, inline, className, children, ...props}) => {
              const newProps = {
                className: `highlight ${className}`,
                ...props,
              };
              return (
                <div>
                  <SyntaxHighlighter
                    children={String(children).replace(/\n$/, '')}
                    // @ts-ignore
                    style={routeros}
                    PreTag="div"
                    {...newProps}
                  />
                </div>
              );
            },
          }}
          children={data}
          remarkPlugins={[remarkGfm]}
          rehypePlugins={[rehypeRaw]}
          skipHtml={false}
          includeElementIndex={false}
        />
      </Grid>
      <Grid item xs={3} position={"fixed"} right={40} sx={{width: 300}}>
        <TableOfContent/>
      </Grid>
    </Grid>
  )
}

Prevent copy paste

const handleMouseUp = () => {
  if (window.getSelection) {
    const selection = window.getSelection();
    if (selection) selection.removeAllRanges();
  } else if (document.getSelection) {
    const selection = document.getSelection();
    if (selection) selection.removeAllRanges();
  }
}
<Grid item
      onCopy={e => e.preventDefault()}
      onMouseDown={e => e.preventDefault()}
      onMouseUp={handleMouseUp}
>
  <Typography variant={"body1"}>Text cant be copied</Typography>
</Grid>

Drag and drop

import {DragDropContext, Draggable, Droppable, DropResult} from 'react-beautiful-dnd';
const [queries, updateQueries] = useState<Query[]>();

const handleOnDragEnd = (result: DropResult) => {
  if (!result.destination) return;
  if (result.source.index === result.destination.index) return;
  const items = Array.from(queries);
  const [reorderedItem] = items.splice(result.source.index, 1);
  items.splice(result.destination.index, 0, reorderedItem);
  updateQueries(items);
  const updateQueriesPrioritiesRQ: RequestBodyUpdateQueriesPriorities = {
    assignmentId: assignmentId,
    queries: items.map(item => ({queryId: item.id, priority: item.priority}))
  }
  fetchUpdateQueriesPriorities(token, updateQueriesPrioritiesRQ).then(value => {
    setAssignment(value)
    updateQueries(value.queries)
  })
}

<DragDropContext onDragEnd={handleOnDragEnd}>
  <Droppable droppableId="queries">
    {provided => (
      <ul {...provided.droppableProps} ref={provided.innerRef}>
        <Grid container direction={"column"}>
          <Grid container direction={"column"} gap={1} mt={1}>
            {queries.map((query, index) => {
              return (
                <Draggable key={query.id} draggableId={query.id} index={index}>
                  {provided => (
                    <div ref={provided.innerRef}{...provided.draggableProps}{...provided.dragHandleProps}>
                        <div key={query.id}/>
                    </div>
                  )}
                </Draggable>
              )
            })}
          </Grid>)
        </Grid>
      </ul>
    )}
  </Droppable>
</DragDropContext>