Render MarkDown in antd and customize an anchor directory TOC (focusing on solving the problem that the navigation directory does not follow the scrolling of the document)

1. Overall idea

Since there are many long documents that need to be rendered, I think the MarkDown method is more suitable for management, so I have tested integrating the MarkDown rendering module in antd in the past two days.
General idea reference:
https://blog.csdn.net/Sakuraaaa_/article/details/128400497
Thank you for your generous efforts.

    1. Parse MarkDown using react-markdown
    1. Use antd’s Anchor component to create a TOC-like jump directory

2. Don’t talk nonsense

1. Complete code

import React, {<!-- -->useState, useEffect, useRef} from 'react';
import Markdown from 'react-markdown';
import {<!-- -->getMarkDown} from "@/services/ant-design-pro/api";
import 'github-markdown-css';
import {<!-- -->Collapse, Anchor, BackTop, Col, Row} from "antd";
const {<!-- --> Link } = Anchor;
const {<!-- --> Panel } = Collapse;
import styles from './style.less';

const App = ({<!-- -->markdownFileName}) => {<!-- -->

  const [mdContent, setMdContent] = useState('')
  const [at, setAt] = useState('')

  useEffect(() => {<!-- -->
    let ret = getMarkDown(markdownFileName)
    ret.then((res)=>{<!-- -->
      setMdContent(res)
      addAnchor()
    })
  }, []);


  const addAnchor = () => {<!-- -->
    const ele = document.getElementsByClassName('markdown-body')[0];
    let eid = 0;
    let ttitles = [];
    for (const e of ele.childNodes) {<!-- -->
      // if (e.nodeName === 'H1' || e.nodeName === 'H2' || e.nodeName === 'H3' || e.nodeName === 'H4' || e. nodeName === 'H5' || e.nodeName === 'H6') {<!-- -->
      if (e.nodeName === 'H2') {<!-- -->
        let a = document.createElement('a');
        a.setAttribute('id', 'h2_' + eid);
        a.setAttribute('class', 'anchor-title');
        a.setAttribute('href', '#' + eid);
        a.innerText = ' '
        let title = {<!-- -->
          type: e.nodeName,
          id: eid,
          name: e.innerText
        };
        ttitles.push(title);
        e.appendChild(a);
        eid + + ;
      }
    }
    setAt((
      <Anchor
        onClick={<!-- -->handleClickFun}
        getContainer={<!-- -->()=>ele}
      >
        {<!-- -->
          ttitles.map(t => (
            <Link href={<!-- -->'#h2_' + t.id} title={<!-- -->t.name}/>
          ))
        }
      </Anchor>
    ))

  }


  const handleClickFun = (e, link) => {<!-- -->
    console.log('click')
    e.preventDefault();
    if (link.href) {<!-- -->
      // Find the node corresponding to the anchor point
      let element = document.getElementById(link.href);
      // If the anchor point corresponding to the id exists, scroll to the top of the anchor point
      element & amp; & amp; element.scrollIntoView({<!-- --> block: 'start', behavior: 'auto' });
    }
  }


  return (
      <div className="scrollable-container">
        <div
            className={<!-- -->styles.markdownnav}
        >
          <Collapse defaultActiveKey={<!-- -->['1']} ghost>
            <Panel header="Chapter Table of Contents" key="1">
            {<!-- -->at}
          </Panel>
        </Collapse>
        </div>

          <Markdown
            className='markdown-body'
          >{<!-- -->mdContent}
          </Markdown>
        <BackTop/>
      </div>
  )
}

export default App

The effect, since I don’t have a long md on hand, I will give a simple demonstration.

The code is very simple. I mainly talk to you about a few problems I encountered.

2. Difficulty? Or say the main points

1.MarkDown custom style

import 'github-markdown-css';

After introducing this style, the style of the document is indeed more beautiful.
But I still need to modify a few styles according to my own requirements. Just use the global parameter in the introduced less to overwrite the above styles. For the principle, you can read my custom dot article.

:global {<!-- -->
  .markdown-body h1 {<!-- -->
    //margin: .67em 0;
    font-weight: var(--base-text-weight-semibold, 600);
    padding-bottom: .3em;
    font-size: 2em;
    border-bottom: 1px solid var(--color-border-muted);
    margin: 0 auto;
    text-align: center;
  }

  .markdown-body p{<!-- -->
    text-indent: 2em; /*First line indent*/
  }
}

2. Navigation directory generation

Although I have been exposed to react for a long time, I have not studied the order of several callbacks in the rendering process of react in detail. In particular, information on functional components is harder to find.
You see in my jsx here, the Anchor component is not written directly here, but in the callback after the MarkDown rendering is completed.

setAt((
      <Anchor
        onClick={<!-- -->handleClickFun}
        getContainer={<!-- -->()=>ele}
      >
        {<!-- -->
          ttitles.map(t => (
            <Link href={<!-- -->'#h2_' + t.id} title={<!-- -->t.name}/>
          ))
        }
      </Anchor>
    ))

The reason is that I need to set the value of getContainer here. The default is to get the window. The window will definitely not scroll. We also need to hang the MarkDown element. If you write directly, you need to use ref to get it, but the MarkDown component cannot get its ref. I don’t know how to solve this problem, so I just useEffect to insert the directory component, because the rendering of the Dom has been completed here, we MarkDown elements can be obtained through document.getElementsByClassName.

3. The navigation directory scrolls with the document

According to the code written by Mr. S above, no matter how I adjust the getContainer container, it cannot follow the scrolling of the document.
To be honest, this is not the first time I have complained about antd. This document is really crude and not enough for me to learn from it this time.
According to the idea of https://juejin.cn/post/7158430758070140942, I have implemented a method of using webstrom breakpoints to debug the source code. Finally figured out why I couldn’t scroll along with the document.

We need to understand one point first: How does scrolling monitoring be implemented in Anchor’s code?
The implementation is quite rough. It monitors the scrolling of the container you passed in to getContainer. When a scrolling event occurs, check the distance between all jump links in the container and the top to set the activated directory.

The problem lies in the ID of a link inserted in Boss S’s code.

You can see that Mr. S has added a # in front of the ID. This # will not cause problems when clicking to jump, but there will be problems when scrolling. The reason is that this place has a regular filter

The main job of this regular expression is to filter out the # sign in front of it

However, the ID configured according to Boss S starts with #. Therefore, the id here cannot find the corresponding a tag, so it cannot monitor its scroll position.
The solution is simple too.

  • 1. Set the jump label, which is where the a label is inserted. The ID of a tag cannot be preceded by #
  • 2. It is required to include # in Anchor’s Link

After setting this up, you can see the effect of the table of contents scrolling with the articles.

I said above that I want to complain about antd’s documentation, because he didn’t even give a sample code for such an important setting. Everything needs to be analyzed and guessed by yourself. I’m curious whether they use this thing themselves? Just like Yuque, do one thing on the outside and another on the inside. Let’s just do the outside part for fun.

PS

1. Webstrom encountered the problem of not being able to connect to chrome when debugging with Chrome

Error message:

Please ensure that the browser was started successfully with remote debugging port opened.

The reason is: Due to laziness, I am still using the 2021.3 version of webstrom, so I cannot link to the new version of chrome. Solved after updating webstorm.

The specific configuration method can be viewed at:
https://juejin.cn/post/7126550003585122334

2. How to get element elegantly in antd

// value
const child = useRef()
<div className="scrollable-container" ref={<!-- -->child}>
</div>

// transfer
child.current

However, MarkDown for react-markdown does not work, and an error will be reported:

But I don’t have much code aesthetics, so I just use document.getElementsByClassName. I hope someone with research can teach me.