feat(lexical): add declarative rich text editor
Introduce @workspace/lexical with composable actions, toolbars, embeds, rich-text nodes, media uploads, selection utilities, HTML serialization, and theme styling.\n\nLocalize built-in controls through MessageDescriptor catalogs for English and Simplified Chinese, while providing an English I18nProvider fallback for standalone editor use. Include focused unit coverage for actions, controls, serialization, plugins, public API, and catalogs.
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo, useState } from "react"
|
||||
import type { ComponentProps, ComponentType, ReactNode } from "react"
|
||||
import { LexicalComposer } from "@lexical/react/LexicalComposer"
|
||||
import { TextNode } from "lexical"
|
||||
import { I18nProvider, useI18nProvider } from "@workspace/i18n"
|
||||
import { cn } from "@workspace/ui/lib/utils"
|
||||
|
||||
import { LexicalTextNode } from "./nodes/text-node"
|
||||
import { lexicalTheme } from "./theme"
|
||||
import { LexicalEmbedProvider } from "./embed"
|
||||
import { LexicalHtmlPlugin } from "./plugins/html-value-plugin"
|
||||
import {
|
||||
compileLexicalActions,
|
||||
findLexicalActions,
|
||||
} from "./action-declarations"
|
||||
import { LexicalActionsProvider } from "./actions-context"
|
||||
import { messages as englishMessages } from "./locales/en"
|
||||
|
||||
const pluginKeys = new WeakMap<ComponentType, string>()
|
||||
let nextPluginKey = 0
|
||||
|
||||
function getPluginKey(plugin: ComponentType) {
|
||||
const existingKey = pluginKeys.get(plugin)
|
||||
if (existingKey) return existingKey
|
||||
|
||||
const key = `${plugin.displayName || plugin.name || "plugin"}:${nextPluginKey}`
|
||||
nextPluginKey += 1
|
||||
pluginKeys.set(plugin, key)
|
||||
return key
|
||||
}
|
||||
|
||||
export interface LexicalRootProps extends Omit<
|
||||
ComponentProps<"div">,
|
||||
"onChange"
|
||||
> {
|
||||
namespace?: string
|
||||
onChange: (html: string) => void
|
||||
value?: string
|
||||
}
|
||||
|
||||
function LexicalI18nBoundary({ children }: { children: ReactNode }) {
|
||||
const hasI18nProvider = useI18nProvider()
|
||||
|
||||
if (hasI18nProvider) return children
|
||||
|
||||
return (
|
||||
<I18nProvider catalogs={{ en: englishMessages }} locale="en">
|
||||
{children}
|
||||
</I18nProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export function LexicalRoot({
|
||||
className,
|
||||
children,
|
||||
namespace = "lexical-editor",
|
||||
onChange,
|
||||
value,
|
||||
...props
|
||||
}: LexicalRootProps) {
|
||||
const [compiledActions] = useState(() =>
|
||||
compileLexicalActions(findLexicalActions(children))
|
||||
)
|
||||
const initialConfig = useMemo(
|
||||
() => ({
|
||||
namespace,
|
||||
nodes: [
|
||||
LexicalTextNode,
|
||||
{
|
||||
replace: TextNode,
|
||||
with: (node: TextNode) => new LexicalTextNode(node.getTextContent()),
|
||||
withKlass: LexicalTextNode,
|
||||
},
|
||||
...compiledActions.nodes,
|
||||
],
|
||||
onError: (error: Error) => {
|
||||
throw error
|
||||
},
|
||||
theme: lexicalTheme,
|
||||
}),
|
||||
[compiledActions.nodes, namespace]
|
||||
)
|
||||
|
||||
return (
|
||||
<LexicalI18nBoundary>
|
||||
<LexicalComposer initialConfig={initialConfig}>
|
||||
<LexicalActionsProvider actions={compiledActions.actions}>
|
||||
<LexicalEmbedProvider definitions={compiledActions.embeds}>
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-hidden rounded-lg border bg-background",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{compiledActions.plugins.map((Plugin) => (
|
||||
<Plugin key={getPluginKey(Plugin)} />
|
||||
))}
|
||||
<LexicalHtmlPlugin value={value} onChange={onChange} />
|
||||
</LexicalEmbedProvider>
|
||||
</LexicalActionsProvider>
|
||||
</LexicalComposer>
|
||||
</LexicalI18nBoundary>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user