v3/frontend/src/editor/plugins/formatted_text_plugin.tsx

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);
}