@tohuhono/puck-rich-text
Version:
A puck component for rich text editing made for OberonCMS
238 lines (237 loc) • 8.63 kB
JavaScript
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
};