102 lines
4.1 KiB
TypeScript
102 lines
4.1 KiB
TypeScript
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
import { mergeRegister } from "@lexical/utils";
|
|
import { type LexicalEditor, SELECTION_CHANGE_COMMAND, TextNode } from "lexical";
|
|
import { useEffect } from "react";
|
|
import { $createFormattedTextMarkerNode, $createFormattedTextNode, $isFormattedTextMarkerNode, $isFormattedTextNode, FormattedTextMarkerNode, FormattedTextNode } from "../nodes/formatted_text";
|
|
import { type TextFormat, textFormats } from "../serialized_editor_content";
|
|
|
|
export function FormattedTextPlugin() {
|
|
const [editor] = useLexicalComposerContext();
|
|
|
|
useEffect(() => mergeRegister(
|
|
// Create formatted text
|
|
...Object.keys(textFormats).map((format) =>
|
|
editor.registerNodeTransform(TextNode, createFormattedTextNode(format as TextFormat, editor))
|
|
),
|
|
// Remove formatted text marker nodes when not inside formatted text node
|
|
editor.registerNodeTransform(FormattedTextMarkerNode, (node) => {
|
|
const parent = node.getParent();
|
|
if (!parent) return;
|
|
|
|
if ($isFormattedTextNode(parent)) {
|
|
const format = parent.getStyle();
|
|
const markerChars = textFormats[format];
|
|
if (node.getTextContent() === markerChars) return;
|
|
}
|
|
|
|
node.getChildren().reverse().forEach((child) => node.insertAfter(child));
|
|
node.remove();
|
|
}),
|
|
// Remove formatted text nodes without matching markers
|
|
editor.registerNodeTransform(FormattedTextNode, (node) => {
|
|
const format = node.getStyle();
|
|
const markerChars = textFormats[format];
|
|
|
|
const firstMarker = node.getFirstChild();
|
|
const lastMarker = node.getLastChild();
|
|
if (
|
|
!$isFormattedTextMarkerNode(firstMarker) ||
|
|
!$isFormattedTextMarkerNode(lastMarker)
|
|
) {
|
|
node.getChildren().reverse().forEach((child) => node.insertAfter(child));
|
|
node.remove();
|
|
return;
|
|
}
|
|
const firstMarkerContent = firstMarker.getTextContent();
|
|
const lastMarkerContent = lastMarker.getTextContent();
|
|
|
|
if (firstMarkerContent !== markerChars || lastMarkerContent !== markerChars) {
|
|
node.getChildren().reverse().forEach((child) => node.insertAfter(child));
|
|
node.remove();
|
|
return;
|
|
}
|
|
}),
|
|
// Remove formatted text nodes without content
|
|
editor.registerNodeTransform(FormattedTextNode, (node) => {
|
|
const format = node.getStyle();
|
|
const formattedChars = textFormats[format];
|
|
|
|
const content = node.getTextContent();
|
|
if (content !== formattedChars + formattedChars) return;
|
|
|
|
node.getChildren().reverse().forEach((child) => node.insertAfter(child));
|
|
node.remove();
|
|
}),
|
|
));
|
|
|
|
return null;
|
|
}
|
|
|
|
const createFormattedTextNode = (format: TextFormat, editor: LexicalEditor) => (node: TextNode) => {
|
|
const markerChars = textFormats[format];
|
|
const markerCharsLen = markerChars.length;
|
|
|
|
const content = node.getTextContent();
|
|
|
|
// TODO: search in all text nodes within the parent block node
|
|
const start = content.indexOf(markerChars);
|
|
if (start === -1) return;
|
|
const end = content.indexOf(markerChars, start + markerCharsLen);
|
|
if (end === -1) return;
|
|
if (start === end - markerCharsLen) return;
|
|
|
|
const startIndex = start > 0 ? 1 : 0;
|
|
const contentIndex = startIndex + 1;
|
|
const endIndex = contentIndex + 1;
|
|
|
|
const textNodes = node.splitText(start, start + markerCharsLen, end, end + markerCharsLen);
|
|
|
|
const formattedTextNode = $createFormattedTextNode(format);
|
|
textNodes[startIndex]!.insertBefore(formattedTextNode);
|
|
|
|
|
|
const startMarker = $createFormattedTextMarkerNode();
|
|
startMarker.append(textNodes[startIndex]!);
|
|
|
|
const endMarker = $createFormattedTextMarkerNode();
|
|
endMarker.append(textNodes[endIndex]!);
|
|
|
|
formattedTextNode.append(startMarker, textNodes[contentIndex]!, endMarker);
|
|
|
|
editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
|
|
}
|