This commit is contained in:
51
src/components/LexicalContent/RichTextNodeFormat.ts
Normal file
51
src/components/LexicalContent/RichTextNodeFormat.ts
Normal file
@ -0,0 +1,51 @@
|
||||
//This copy-and-pasted from somewhere in lexical here: https://github.com/facebook/lexical/blob/c2ceee223f46543d12c574e62155e619f9a18a5d/packages/lexical/src/LexicalConstants.ts
|
||||
|
||||
// DOM
|
||||
export const DOM_ELEMENT_TYPE = 1
|
||||
export const DOM_TEXT_TYPE = 3
|
||||
|
||||
// Reconciling
|
||||
export const NO_DIRTY_NODES = 0
|
||||
export const HAS_DIRTY_NODES = 1
|
||||
export const FULL_RECONCILE = 2
|
||||
|
||||
// Text node modes
|
||||
export const IS_NORMAL = 0
|
||||
export const IS_TOKEN = 1
|
||||
export const IS_SEGMENTED = 2
|
||||
// IS_INERT = 3
|
||||
|
||||
// Text node formatting
|
||||
export const IS_BOLD = 1
|
||||
export const IS_ITALIC = 1 << 1
|
||||
export const IS_STRIKETHROUGH = 1 << 2
|
||||
export const IS_UNDERLINE = 1 << 3
|
||||
export const IS_CODE = 1 << 4
|
||||
export const IS_SUBSCRIPT = 1 << 5
|
||||
export const IS_SUPERSCRIPT = 1 << 6
|
||||
export const IS_HIGHLIGHT = 1 << 7
|
||||
|
||||
export const IS_ALL_FORMATTING = IS_BOLD | IS_ITALIC | IS_STRIKETHROUGH | IS_UNDERLINE | IS_CODE | IS_SUBSCRIPT | IS_SUPERSCRIPT | IS_HIGHLIGHT
|
||||
|
||||
export const IS_DIRECTIONLESS = 1
|
||||
export const IS_UNMERGEABLE = 1 << 1
|
||||
|
||||
// Element node formatting
|
||||
export const IS_ALIGN_LEFT = 1
|
||||
export const IS_ALIGN_CENTER = 2
|
||||
export const IS_ALIGN_RIGHT = 3
|
||||
export const IS_ALIGN_JUSTIFY = 4
|
||||
export const IS_ALIGN_START = 5
|
||||
export const IS_ALIGN_END = 6
|
||||
|
||||
export const TEXT_TYPE_TO_FORMAT: Record<TextFormatType | string, number> = {
|
||||
bold: IS_BOLD,
|
||||
code: IS_CODE,
|
||||
italic: IS_ITALIC,
|
||||
strikethrough: IS_STRIKETHROUGH,
|
||||
subscript: IS_SUBSCRIPT,
|
||||
superscript: IS_SUPERSCRIPT,
|
||||
underline: IS_UNDERLINE
|
||||
}
|
||||
|
||||
export type TextFormatType = 'bold' | 'underline' | 'strikethrough' | 'italic' | 'code' | 'subscript' | 'superscript'
|
||||
213
src/components/LexicalContent/index.tsx
Normal file
213
src/components/LexicalContent/index.tsx
Normal file
@ -0,0 +1,213 @@
|
||||
/* eslint-disable react/no-children-prop */
|
||||
import normalizePath from '@/utils/normalizePath'
|
||||
import clsx from 'clsx'
|
||||
import Link from 'next/link'
|
||||
import React, { CSSProperties, type FC, type ReactElement } from 'react'
|
||||
import Image from 'next/image'
|
||||
import {
|
||||
IS_BOLD,
|
||||
IS_CODE,
|
||||
IS_ITALIC,
|
||||
IS_STRIKETHROUGH,
|
||||
IS_SUBSCRIPT,
|
||||
IS_SUPERSCRIPT,
|
||||
IS_UNDERLINE,
|
||||
} from './RichTextNodeFormat'
|
||||
|
||||
type SerializedLexicalNode = {
|
||||
children?: SerializedLexicalNode[]
|
||||
direction: string
|
||||
format: number
|
||||
indent?: string | number
|
||||
type: string
|
||||
version: number
|
||||
style?: string
|
||||
mode?: string
|
||||
text?: string
|
||||
[other: string]: any
|
||||
}
|
||||
|
||||
type TextComponentProps = {
|
||||
children: ReactElement | string
|
||||
format: number
|
||||
}
|
||||
|
||||
const getLinkForDocument = (doc: any, locale?: string): string => {
|
||||
let path = doc?.path
|
||||
if (!path || path.startsWith('/home') || path === '/' || path === '') path = '/'
|
||||
return normalizePath(`/${locale}${path}`)
|
||||
}
|
||||
|
||||
function gcd(a: number, b: number): number {
|
||||
return b === 0 ? a : gcd(b, a % b)
|
||||
}
|
||||
|
||||
function calculateAspectRatio(width: number, height: number): string {
|
||||
const divisor = gcd(width, height)
|
||||
const simplifiedWidth = width / divisor
|
||||
const simplifiedHeight = height / divisor
|
||||
|
||||
return `${simplifiedWidth}x${simplifiedHeight}`
|
||||
}
|
||||
|
||||
const TextComponent: FC<TextComponentProps> = ({ children, format }) => {
|
||||
const formatFunctions: { [key: number]: (child: ReactElement | string) => ReactElement } = {
|
||||
[IS_BOLD]: (child) => <strong>{child}</strong>,
|
||||
[IS_ITALIC]: (child) => <em>{child}</em>,
|
||||
[IS_STRIKETHROUGH]: (child) => <del>{child}</del>,
|
||||
[IS_UNDERLINE]: (child) => <u>{child}</u>,
|
||||
[IS_CODE]: (child) => <code>{child}</code>,
|
||||
[IS_SUBSCRIPT]: (child) => <sub>{child}</sub>,
|
||||
[IS_SUPERSCRIPT]: (child) => <sup>{child}</sup>,
|
||||
}
|
||||
|
||||
const formattedText = Object.entries(formatFunctions).reduce(
|
||||
(formattedText, [key, formatter]) => {
|
||||
return format & Number(key) ? formatter(formattedText) : formattedText
|
||||
},
|
||||
children,
|
||||
)
|
||||
|
||||
return <>{formattedText}</>
|
||||
}
|
||||
|
||||
const SerializedLink: React.FC<{
|
||||
node: SerializedLexicalNode
|
||||
locale: string
|
||||
children: JSX.Element | null
|
||||
}> = ({ node, locale, children }) => {
|
||||
const { doc, url, newTab, linkType } = node.fields as any
|
||||
const document = doc?.value
|
||||
const href = linkType === 'custom' ? url : getLinkForDocument(document, locale)
|
||||
const target = newTab ? '_blank' : undefined
|
||||
|
||||
return (
|
||||
<Link href={href} target={target}>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const getNodeClassNames = (node: SerializedLexicalNode) => {
|
||||
const attributes: Record<string, any> = {}
|
||||
if (!node) return attributes
|
||||
|
||||
let classNames = ''
|
||||
if (String(node?.format).toString()?.includes('left') && node.direction !== 'ltr')
|
||||
classNames += 'text-left '
|
||||
if (String(node?.format).toString()?.includes('center')) classNames += 'text-center '
|
||||
if (String(node?.format).toString()?.includes('right') && node.direction !== 'rtl')
|
||||
classNames += 'text-right '
|
||||
|
||||
if (classNames.length > 0) attributes.className = classNames.trim()
|
||||
|
||||
const indent = parseInt(`${node?.indent || 0}`)
|
||||
if (!isNaN(indent) && indent !== 0) {
|
||||
attributes.style = { '--indent': `${indent * 10}px` } as CSSProperties
|
||||
attributes.className = `${attributes.className ?? ''} ml-[--indent]`.trim()
|
||||
}
|
||||
|
||||
return attributes
|
||||
}
|
||||
|
||||
const LexicalContent: React.FC<{
|
||||
childrenNodes: SerializedLexicalNode[]
|
||||
locale: string
|
||||
className?: string
|
||||
lazyLoadImages: boolean
|
||||
}> = ({ childrenNodes, locale, lazyLoadImages = false }) => {
|
||||
if (!Array.isArray(childrenNodes)) return null
|
||||
|
||||
const renderedChildren = childrenNodes.map((node, ix) => {
|
||||
if (!node) return null
|
||||
const attributes = getNodeClassNames(node || '')
|
||||
if (node.type === 'text') {
|
||||
return (
|
||||
<TextComponent key={ix} format={node.format}>
|
||||
<>
|
||||
{Object.keys(attributes).length > 0 && <span {...attributes}>{node?.text || ''}</span>}
|
||||
{(Object.keys(attributes).length === 0 && node?.text) || ''}
|
||||
</>
|
||||
</TextComponent>
|
||||
)
|
||||
}
|
||||
|
||||
const serializedChildren = node.children ? (
|
||||
<LexicalContent
|
||||
key={ix}
|
||||
childrenNodes={node.children}
|
||||
locale={locale}
|
||||
lazyLoadImages={lazyLoadImages}
|
||||
/>
|
||||
) : null
|
||||
switch (node.type) {
|
||||
case 'linebreak':
|
||||
return <br key={ix} />
|
||||
case 'link':
|
||||
return <SerializedLink key={ix} node={node} locale={locale} children={serializedChildren} />
|
||||
case 'list':
|
||||
const ListTag = node.listType === 'bullet' ? 'ul' : 'ol'
|
||||
attributes.className = clsx(
|
||||
attributes.className,
|
||||
'mb-4 pl-8',
|
||||
ListTag === 'ol' ? 'list-decimal' : 'list-disc',
|
||||
)
|
||||
return (
|
||||
<ListTag key={ix} {...attributes}>
|
||||
{serializedChildren}
|
||||
</ListTag>
|
||||
)
|
||||
case 'listitem':
|
||||
return (
|
||||
<li key={ix} {...attributes}>
|
||||
{serializedChildren}
|
||||
</li>
|
||||
)
|
||||
case 'heading':
|
||||
const HeadingTag = node.tag as keyof JSX.IntrinsicElements
|
||||
return (
|
||||
<HeadingTag key={ix} {...attributes}>
|
||||
{serializedChildren}
|
||||
</HeadingTag>
|
||||
)
|
||||
case 'quote':
|
||||
return (
|
||||
<blockquote key={ix} {...attributes}>
|
||||
{serializedChildren}
|
||||
</blockquote>
|
||||
)
|
||||
case 'upload':
|
||||
const upload = node?.value
|
||||
if (!upload) return null
|
||||
const imageAspectRatio = calculateAspectRatio(upload.width, upload.height)
|
||||
return (
|
||||
<Image
|
||||
key={ix}
|
||||
width={upload.width}
|
||||
height={upload.height}
|
||||
src={upload?.url}
|
||||
loading={lazyLoadImages ? 'lazy' : 'eager'}
|
||||
fetchPriority={lazyLoadImages ? 'low' : 'high'}
|
||||
sizes="(max-width: 768px) 65ch, 100vw"
|
||||
className="max-w-[calc(100%+40px)] translate-x-[-20px]"
|
||||
alt={upload?.alt || upload.filename}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
if (
|
||||
Array.isArray(serializedChildren?.props?.childrenNodes) &&
|
||||
serializedChildren?.props?.childrenNodes.length === 0
|
||||
)
|
||||
return <br key={ix} />
|
||||
return (
|
||||
<p key={ix} {...attributes}>
|
||||
{serializedChildren}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return <>{renderedChildren.filter((node) => node !== null)}</>
|
||||
}
|
||||
|
||||
export default LexicalContent
|
||||
Reference in New Issue
Block a user