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,82 @@
|
||||
/* eslint-disable no-underscore-dangle -- Lexical nodes use protected __ fields by convention. */
|
||||
|
||||
import { $isTextNode, TextNode } from "lexical"
|
||||
import type {
|
||||
DOMConversionMap,
|
||||
DOMConversionOutput,
|
||||
DOMConversionProp,
|
||||
SerializedTextNode,
|
||||
} from "lexical"
|
||||
|
||||
function patchTextStyleConversion(
|
||||
originalDOMConverter?: DOMConversionProp<HTMLElement>
|
||||
) {
|
||||
return (node: HTMLElement): DOMConversionOutput | null => {
|
||||
const original = originalDOMConverter?.(node)
|
||||
const output = original?.conversion(node)
|
||||
if (!output) return null
|
||||
|
||||
const color = node.style.color
|
||||
const backgroundColor = node.style.backgroundColor
|
||||
const textDecoration = node.style.textDecoration
|
||||
const style = [
|
||||
color ? `color: ${color}` : null,
|
||||
backgroundColor ? `background-color: ${backgroundColor}` : null,
|
||||
textDecoration ? `text-decoration: ${textDecoration}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("; ")
|
||||
|
||||
return {
|
||||
...output,
|
||||
forChild: (lexicalNode, parent) => {
|
||||
const converted = output.forChild
|
||||
? output.forChild(lexicalNode, parent)
|
||||
: lexicalNode
|
||||
if ($isTextNode(converted) && style) converted.setStyle(style)
|
||||
return converted
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class LexicalTextNode extends TextNode {
|
||||
static getType(): string {
|
||||
return "lexical-text"
|
||||
}
|
||||
|
||||
static clone(node: LexicalTextNode): LexicalTextNode {
|
||||
return new LexicalTextNode(node.__text, node.__key)
|
||||
}
|
||||
|
||||
static importJSON(node: SerializedTextNode): LexicalTextNode {
|
||||
return new LexicalTextNode().updateFromJSON(node)
|
||||
}
|
||||
|
||||
static importDOM(): DOMConversionMap | null {
|
||||
const importers = TextNode.importDOM()
|
||||
return {
|
||||
...importers,
|
||||
code: () => ({
|
||||
conversion: patchTextStyleConversion(importers?.code),
|
||||
priority: 1,
|
||||
}),
|
||||
em: () => ({
|
||||
conversion: patchTextStyleConversion(importers?.em),
|
||||
priority: 1,
|
||||
}),
|
||||
span: () => ({
|
||||
conversion: patchTextStyleConversion(importers?.span),
|
||||
priority: 1,
|
||||
}),
|
||||
strong: () => ({
|
||||
conversion: patchTextStyleConversion(importers?.strong),
|
||||
priority: 1,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
isSimpleText(): boolean {
|
||||
return this.__type === "lexical-text" && this.__mode === 0
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user