UNPKG

@tohuhono/puck-rich-text

Version:

A puck component for rich text editing made for OberonCMS

238 lines (237 loc) 8.63 kB
import { jsxs, jsx } from "react/jsx-runtime"; import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link"; import { $isListNode, ListNode } from "@lexical/list"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; import { $isDecoratorBlockNode } from "@lexical/react/LexicalDecoratorBlockNode"; import { $isHeadingNode, $isQuoteNode } from "@lexical/rich-text"; import { $isParentElementRTL } from "@lexical/selection"; import { $findMatchingParent, $getNearestNodeOfType, mergeRegister, $getNearestBlockElementAncestorOrThrow } from "@lexical/utils"; import { $getSelection, $isRangeSelection, $isRootOrShadowRoot, $isElementNode, SELECTION_CHANGE_COMMAND, COMMAND_PRIORITY_CRITICAL, KEY_MODIFIER_COMMAND, COMMAND_PRIORITY_NORMAL, $isTextNode, $createParagraphNode, FORMAT_TEXT_COMMAND } from "lexical"; import { useState, useCallback, useEffect } from "react"; import { createPortal } from "react-dom"; import { Bold, Italic, RemoveFormatting } from "lucide-react"; import { getSelectedNode } from "../../utils/get-selected-node.js"; import { sanitizeUrl } from "../../utils/url.js"; import { Button } from "../../ui/button/index.js"; import { getToolbarPortal } from "../../utils/get-toolbar-portal.js"; import { isApple } from "../../utils/is-apple.js"; import { blockFormats, BlockFormatDropDown } from "./block-format-dropdown.js"; import { ElementFormatDropdown } from "./element-format-dropdown.js"; const IS_APPLE = isApple(); function ToolbarPlugin({ id, showToolbar, setIsLinkEditMode }) { const [editor] = useLexicalComposerContext(); const [activeEditor, setActiveEditor] = useState(editor); const [blockType, setBlockType] = useState("paragraph"); const [elementFormat, setElementFormat] = useState("left"); const [isLink, setIsLink] = useState(false); const [isBold, setIsBold] = useState(false); const [isItalic, setIsItalic] = useState(false); const [_isUnderline, setIsUnderline] = useState(false); const [isRTL, setIsRTL] = useState(false); const $updateToolbar = useCallback(() => { const selection = $getSelection(); if ($isRangeSelection(selection)) { const anchorNode = selection.anchor.getNode(); let element = anchorNode.getKey() === "root" ? anchorNode : $findMatchingParent(anchorNode, (e) => { const parent2 = e.getParent(); return parent2 !== null && $isRootOrShadowRoot(parent2); }); if (element === null) { element = anchorNode.getTopLevelElementOrThrow(); } const elementKey = element.getKey(); const elementDOM = activeEditor.getElementByKey(elementKey); setIsBold(selection.hasFormat("bold")); setIsItalic(selection.hasFormat("italic")); setIsUnderline(selection.hasFormat("underline")); setIsRTL($isParentElementRTL(selection)); const node = getSelectedNode(selection); const parent = node.getParent(); if ($isLinkNode(parent) || $isLinkNode(node)) { setIsLink(true); } else { setIsLink(false); } if (elementDOM !== null) { if ($isListNode(element)) { const parentList = $getNearestNodeOfType( anchorNode, ListNode ); const type = parentList ? parentList.getListType() : element.getListType(); if (type in blockFormats) { setBlockType(type); } } else { const type = $isHeadingNode(element) ? element.getTag() : element.getType(); if (type in blockFormats) { setBlockType(type); } } } let matchingParent; if ($isLinkNode(parent)) { matchingParent = $findMatchingParent( node, (parentNode) => $isElementNode(parentNode) && !parentNode.isInline() ); } setElementFormat( $isElementNode(matchingParent) ? matchingParent.getFormatType() : $isElementNode(node) ? node.getFormatType() : (parent == null ? void 0 : parent.getFormatType()) || "left" ); } }, [activeEditor]); useEffect(() => { return editor.registerCommand( SELECTION_CHANGE_COMMAND, (_payload, newEditor) => { $updateToolbar(); setActiveEditor(newEditor); return false; }, COMMAND_PRIORITY_CRITICAL ); }, [editor, $updateToolbar]); useEffect(() => { return mergeRegister( activeEditor.registerUpdateListener(({ editorState }) => { editorState.read(() => { $updateToolbar(); }); }) ); }, [$updateToolbar, editor, activeEditor]); useEffect(() => { return activeEditor.registerCommand( KEY_MODIFIER_COMMAND, (payload) => { const event = payload; const { code, ctrlKey, metaKey } = event; if (code === "KeyK" && (ctrlKey || metaKey)) { event.preventDefault(); let url; if (!isLink) { setIsLinkEditMode(true); url = sanitizeUrl("https://"); } else { setIsLinkEditMode(false); url = null; } return activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, url); } return false; }, COMMAND_PRIORITY_NORMAL ); }, [activeEditor, isLink, setIsLinkEditMode]); const clearFormatting = useCallback(() => { activeEditor.update(() => { const selection = $getSelection(); if ($isRangeSelection(selection)) { const anchor = selection.anchor; const focus = selection.focus; const nodes = selection.getNodes(); if (anchor.key === focus.key && anchor.offset === focus.offset) { return; } nodes.forEach((node, idx) => { if ($isTextNode(node)) { let textNode = node; if (idx === 0 && anchor.offset !== 0) { textNode = textNode.splitText(anchor.offset)[1] || textNode; } if (idx === nodes.length - 1) { textNode = textNode.splitText(focus.offset)[0] || textNode; } if (textNode.__style !== "") { textNode.setStyle(""); } if (textNode.__format !== 0) { textNode.setFormat(0); $getNearestBlockElementAncestorOrThrow(textNode).setFormat(""); } node = textNode; } else if ($isHeadingNode(node) || $isQuoteNode(node)) { node.replace($createParagraphNode(), true); } else if ($isDecoratorBlockNode(node)) { node.setFormat(""); } }); } }); }, [activeEditor]); if (!showToolbar) { return null; } const portalTarget = getToolbarPortal(); if (!portalTarget) { return null; } return createPortal( /* @__PURE__ */ jsxs( "div", { style: { display: "flex", gap: "4px", paddingLeft: "12px", borderLeft: "1px solid var(--puck-color-grey-04)", marginLeft: "8px" }, children: [ /* @__PURE__ */ jsx(BlockFormatDropDown, { blockType, editor }), /* @__PURE__ */ jsx( ElementFormatDropdown, { value: elementFormat, editor, isRTL } ), /* @__PURE__ */ jsx( Button, { onClick: () => { activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold"); }, active: isBold, title: IS_APPLE ? "Bold (⌘B)" : "Bold (Ctrl+B)", "aria-label": `Format text as bold. Shortcut: ${IS_APPLE ? "⌘B" : "Ctrl+B"}`, children: /* @__PURE__ */ jsx(Bold, { size: 16 }) } ), /* @__PURE__ */ jsx( Button, { onClick: () => { activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic"); }, active: isItalic, title: IS_APPLE ? "Italic (⌘I)" : "Italic (Ctrl+I)", "aria-label": `Format text as italics. Shortcut: ${IS_APPLE ? "⌘I" : "Ctrl+I"}`, children: /* @__PURE__ */ jsx(Italic, { size: 16 }) } ), /* @__PURE__ */ jsx( Button, { onClick: clearFormatting, "aria-label": "Clear all text formatting", title: "Clear text formatting", children: /* @__PURE__ */ jsx(RemoveFormatting, { size: 16 }) } ) ] } ), portalTarget, id ); } export { ToolbarPlugin };