Replies: 1 comment
-
I found a way to solve this (though I'm not sure if it's the best approach, it works well for me). For anyone struggling with custom Lexical features, there is a helpful guide. It would be great if Payload could include my example in the documentation, as selecting and modifying nodes is a common use case I think so. If anyone has a better solution, I'd appreciate it if you could share your code strong rich text serializing and custom featureimport React from 'react'
import {
JSXConverters,
type JSXConvertersFunction,
RichText as PayloadRichText,
} from '@payloadcms/richtext-lexical/react'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { cn } from '@/utilities/cn'
const states = {
IS_BOLD: 1,
IS_ITALIC: 2,
IS_STRIKETHROUGH: 4,
IS_UNDERLINE: 8,
IS_CODE: 16,
IS_SUBSCRIPT: 32,
IS_SUPERSCRIPT: 64,
IS_HIGHLIGHT: 128,
}
const getFormattingStates = (decimalNumber: number) =>
Object.fromEntries(Object.entries(states).map(([k, v]) => [k, !!(v & decimalNumber)]))
const getActiveFormattingStates = (decimalNumber: number) =>
Object.entries(states).reduce(
(acc, [k, v]) => (v & decimalNumber ? { ...acc, [k]: true } : acc),
{},
)
type Props = {
className?: string
converters?: JSXConverters | JSXConvertersFunction
data: SerializedEditorState
disableIndent?: boolean | string[]
disableTextAlign?: boolean | string[]
}
const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) => {
return {
...defaultConverters,
text: (args) => {
const { text } = args.node
const formatStates = getFormattingStates(args.node.format)
if (formatStates.IS_BOLD) {
return <strong className="font-medium">{text}</strong>
} else {
if (defaultConverters.text) return defaultConverters.text(args)
}
},
font: (args) => {
const { mode, text, type, detail, format, version } = args.node
const formatStates = getFormattingStates(args.node.format)
return (
<span className={cn(`font-alt font-light`, formatStates.IS_ITALIC && 'italic')}>
{text}
</span>
)
},
blocks: {},
}
}
export const RichText: React.FC<Props> = ({ data, className, ...rest }) => {
return (
<PayloadRichText
converters={jsxConverters}
data={data}
className={cn('', className)}
{...rest}
/>
)
} client.tsx'use client'
import {
createClientFeature,
toolbarAddDropdownGroupWithItems,
} from '@payloadcms/richtext-lexical/client'
import { APPLY_FONT_COMMAND } from './nodes/fontNode'
import { FontNode } from './nodes/fontNode'
import { FontPlugin } from './plugin'
import { Icon } from './icon'
const FontFeatureClient = createClientFeature({
plugins: [
{
Component: FontPlugin,
position: 'normal',
},
],
nodes: [FontNode],
toolbarInline: {
groups: [
toolbarAddDropdownGroupWithItems([
{
key: 'font',
ChildComponent: Icon,
label: 'Font',
onSelect: ({ editor }) => {
editor.dispatchCommand(APPLY_FONT_COMMAND, undefined)
},
},
]),
],
},
toolbarFixed: {
groups: [
toolbarAddDropdownGroupWithItems([
{
key: 'font',
ChildComponent: Icon,
label: 'Font',
onSelect: ({ editor }) => {
editor.dispatchCommand(APPLY_FONT_COMMAND, undefined)
},
},
]),
],
},
})
export default FontFeatureClient server.tsx// src/features/customFont/feature.server.ts
import { createNode, createServerFeature } from '@payloadcms/richtext-lexical'
import { FontNode } from './nodes/fontNode'
export const FontFeature = createServerFeature({
feature: {
nodes: [
createNode({
converters: {},
node: FontNode,
}),
],
ClientFeature: {
path: '@/payload/lexical/FontFeature/client',
},
},
key: 'fontFeature',
}) plugin.tsximport { $createFontNode, APPLY_FONT_COMMAND } from './nodes/fontNode'
import {
$createTextNode,
$getSelection,
$isRangeSelection,
$isTextNode,
TextNode,
} from '@payloadcms/richtext-lexical/lexical'
import { $isFontNode } from './nodes/fontNode'
import { useEffect } from 'react'
import { useLexicalComposerContext } from '@payloadcms/richtext-lexical/lexical/react/LexicalComposerContext'
export const FontPlugin = () => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
editor.registerCommand(
APPLY_FONT_COMMAND,
() => {
editor.update(() => {
const selection = $getSelection()
if ($isRangeSelection(selection)) {
// Extract the selected nodes, splitting at selection boundaries
const nodes = selection.extract()
nodes.forEach((node) => {
if ($isFontNode(node)) {
// Remove the font by replacing with a TextNode
const textNode = $createTextNode(node.getTextContent())
node.replace(textNode)
} else if ($isTextNode(node)) {
// Apply the font by replacing with a FontNode
const fontNode = $createFontNode(node.getTextContent())
node.replace(fontNode)
}
})
}
})
return true
},
0,
)
}, [editor])
return null
} node.tsximport {
EditorConfig,
LexicalCommand,
LexicalNode,
SerializedTextNode,
TextNode,
createCommand,
} from '@payloadcms/richtext-lexical/lexical'
import { Spread } from '@payloadcms/richtext-lexical/lexical'
export const APPLY_FONT_COMMAND: LexicalCommand<void> = createCommand('APPLY_FONT')
export type SerializedFontNode = Spread<
{
type: 'font'
version: 1
},
SerializedTextNode
>
export class FontNode extends TextNode {
static getType(): string {
return 'font'
}
static clone(node: FontNode): FontNode {
return new FontNode(node.__text, node.__key)
}
canHaveFormat(): boolean {
return true
}
createDOM(config: EditorConfig): HTMLElement {
const element = super.createDOM(config)
element.style.fontFamily = 'kazimir'
element.className = 'font-light'
return element
}
updateDOM(prevNode: FontNode, dom: HTMLElement, config: EditorConfig): boolean {
const update = super.updateDOM(prevNode, dom, config)
return update
}
static importJSON(serializedNode: SerializedFontNode): FontNode {
const node = $createFontNode(serializedNode.text)
node.setFormat(serializedNode.format)
return node
}
exportJSON(): SerializedFontNode {
return {
...super.exportJSON(),
type: 'font',
version: 1,
}
}
setFormat(format: number): this {
return super.setFormat(format)
}
}
export function $createFontNode(text: string): FontNode {
return new FontNode(text)
}
export function $isFontNode(node: LexicalNode | null | undefined): node is FontNode {
return node instanceof FontNode
} icon.tsximport React from 'react'
interface Props {}
const Icon: React.FC<Props> = () => {
return <div>Icon</div>
}
export { Icon } |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
Can I change
<strong />
element with custom classnames? Or change it to span or something like thisBeta Was this translation helpful? Give feedback.
All reactions