Simplify your stack and build anything. Or everything.
Build tomorrow’s web with a modern solution you truly own.
Code-based nature means you can build on top of it to power anything.
It’s time to take back your content infrastructure.

Convert lexical header node to list node

default discord avatar
szymciodriftlast year
10

Hello, anybody has an idea how to convert header lexical node into list node?


What am i trying to do? In short iam building automatic table of content



1. I'm passing content as SerializedEditorState<SerializedLexicalNode> into my Table of Content component.


2. Filtering content root children by node.type to keep only not empty headings


export const Toc: React.FC<TocProps> = ({ content, className }) => {
  const headingsData = content?.root?.children.filter(
    (node: any) => node?.type === 'heading' && node?.children?.length > 0,
  )
...

3. I want to convert headings into nested list node.


4. Create jsx converter to render listitems as link's. I'll use here helper function to slugify content and create anchor href, the same function will be used to render headings with id in blog post



My idea is to create function that will manually convert headings into list node, but its neck breaking, cuz i have to manualy recreate list item object. Maybe there's a simplier way? Help please

  • default discord avatar
    german.jablolast year

    This seems like a legitimate approach



    Note that

    children.filter

    won't find headings that aren't direct children of root, for example if you have them inside a

    quote

    or a

    block

    .



    Since this is for a table of contents that's probably ok,

  • default discord avatar
    rilromlast year

    Not sure if this is helpful but lexical have a table of contents plugin you could use for inspiration



    https://github.com/facebook/lexical/tree/main/packages/lexical-playground/src/plugins/TableOfContentsPlugin
  • default discord avatar
    szymciodriftlast year

    If anybody else wondering how to implement Table Of Content. Here is my approach:


    1. Create Component for TableOfContent, accept prop including content from Post, filter direct headings and then pass it to the <RichText /> component.


    As you can see i also choose the converter in 'converter' prop in <RichText />



    TableOfContent.tsx


    import { Post } from '@/payload-types'
    import RichText from '../RichText'
    import { TocClient } from './index.client'
    
    export const Toc = ({ content }: { content: Post['content'] }) => {
      const headingsData = content?.root?.children.filter(
        (node: any) => node?.type === 'heading' && node?.children?.length > 0,
      )
    
      if (!headingsData || headingsData.length < 1) return
    
      const tocList = {
        type: 'tocList',
        children: headingsData,
        version: 1,
      }
    
      const tocListData = {
        ...content,
        root: {
          ...content?.root,
          children: [tocList],
        },
      }
    
      return (
        <TocClient>
          <RichText
            className="max-w-[48rem] mx-auto"
            data={tocListData}
            enableGutter={false}
            enableProse={false}
            disableContainer
            converter="toc"
          />
        </TocClient>
      )
    }
  • default discord avatar
    bettershifting.terrylast year

    Nice! Thanks!

  • default discord avatar
    szymciodriftlast year

    2. I created new jsxConverter for <RichText /> to convert data from my toc.


    jsxTocConverters.tsx


    import { slugifyHeading } from '@/utilities/slugifyHeading'
    import { DefaultNodeTypes, SerializedHeadingNode } from '@payloadcms/richtext-lexical'
    import { JSXConvertersFunction } from '@payloadcms/richtext-lexical/react'
    import React, { Fragment } from 'react'
    
    type HeaderNode = {
      level: number
      tag: string
      content: React.ReactNode[]
      href: string
      children: HeaderNode[]
    }
    
    export const jsxTocConverters: JSXConvertersFunction<DefaultNodeTypes> = ({
      defaultConverters,
    }) => ({
      ...defaultConverters,
      tocList: ({ node, nodesToJSX, converters }) => {
        const headers: SerializedHeadingNode[] = node.children
        const nestedHeaders = nestHeaders(headers)
    
        function nestHeaders(headers) {
          let stack: HeaderNode[] = []
          let result: HeaderNode[] = []
    
          headers.forEach((header) => {
            const headerContent = nodesToJSX({
              converters,
              disableIndent: false,
              disableTextAlign: false,
              nodes: header.children,
            })
            const headerHref = slugifyHeading(header)
            const headerLevel = Number(header.tag.replace('h', ''))
    
            let node = {
              level: headerLevel,
              tag: header.tag,
              content: headerContent,
              href: headerHref,
              children: [],
            }
    
            while (stack.length > 0 && stack[stack.length - 1].level >= node.level) {
              stack.pop()
            }
    
            if (stack.length > 0) {
              stack[stack.length - 1].children.push(node)
            } else {
              result.push(node)
            }
    
            stack.push(node)
          })
    
          return result
        }
    
        return <NestedList headers={nestedHeaders} />
      },
    })
  • default discord avatar
    bettershifting.terrylast year

    I needed to check for empty RichText content on my own site, but hadn't gotten around to doing that yet.. have some inspiration now 🙂



    (...my list of things to build is virtually endless..)

  • default discord avatar
    skaddictedlast year

    Hi

    @1059585844605882378

    , thanks for sharing! Can I use this for implementing anchor links in my website? So I want my editor to give the possibility to link to one section in my page. He/She takes the anchor link and links to it via the LinkFeature from RichText-Lexical. Is this possible?

  • default discord avatar
    szymciodriftlast year
    @240925952551550976

    Yes,


    Just add to jsxConverter the one for 'heading' node.


      heading: ({ node, nodesToJSX, converters }) => {
        const childrenText = nodesToJSX({
          converters,
          disableIndent: Boolean(node?.indent),
          disableTextAlign: Boolean(node?.format),
          nodes: node?.children,
        })
        const HeadingTag = node?.tag
        const HTMLId = slugifyHeading(node)
    
        return <HeadingTag id={HTMLId}>{childrenText </HeadingTag>
      },
    })


    Im using here slugifyHeading utility function to create id to my headings:


    export const slugifyHeading = (node: SerializedHeadingNode) => {
      const editorContent = editorStateToString(node?.children)
      const firstFourWords = editorContent.split(/\s+/).slice(0, 4).join(' ')
      return toKebabCase(firstFourWords)
    }

    The one above uses editorStateToString and toKebabCase:


    import { SerializedLexicalNode } from 'node_modules/lexical/LexicalNode'
    
    type LexicalTextObject = {
      props?: {
        children?: string
      }
    }
    
    type ExtendedSerializedLexicalNode = {
      text?: string | LexicalTextObject
      children?: SerializedLexicalNode[]
      type?: string
    }
    
    export default function editorStateToString(
      editorState: ExtendedSerializedLexicalNode[],
      i: number = 0,
    ): string {
      let textContent = ''
      for (const node of editorState) {
        if ('text' in node && node?.text) {
          if (typeof node.text === 'string') textContent += node.text + ' '
          if (typeof node.text === 'object') textContent += node.text?.props?.children + ' '
        }
    
        if ('children' in node && node?.children) {
          textContent += editorStateToString(node.children as SerializedLexicalNode[], i + 1) + ' '
        }
      }
    
      return textContent.trim()
    }


    I'm replacing all or most of polish special characters thanks to normalize NFD and replace. I manually had to add replacing 'Ł' and 'ł' which are coded another way


    export const toKebabCase = (string: string): string =>
      string
        ?.replace(/\s+/g, '-')
        .replace(/ł/g, 'l')
        .replace(/Ł/g, 'l')
        .normalize('NFD')
        .replace(/[^\w-]+/g, '')
        .toLowerCase()
  • default discord avatar
    skaddictedlast year
    @1059585844605882378

    awesome, thnaks for the help! And then you just set the Link to the Anchor Link via the LinkFeature from Lexical?



    Okay, I got it, thanks.

  • default discord avatar
    szymciodriftlast year

    Glad to help 😇

Star on GitHub

Star

Chat on Discord

Discord

online

Can't find what you're looking for?

Get dedicated engineering support directly from the Payload team.