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
This seems like a legitimate approach
Note that
children.filterwon't find headings that aren't direct children of root, for example if you have them inside a
quoteor a
block.
Since this is for a table of contents that's probably ok,
Not sure if this is helpful but lexical have a table of contents plugin you could use for inspiration
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>
)
}Nice! Thanks!
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} />
},
})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..)
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?
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()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.
Glad to help 😇
Star
Discord
online
Get dedicated engineering support directly from the Payload team.