UNPKG

phx-react

Version:

PHX REACT

226 lines • 12.2 kB
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'; import { $isListItemNode, $isListNode, ListNode } from '@lexical/list'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { $isHeadingNode } from '@lexical/rich-text'; import { $getSelectionStyleValueForProperty, getStyleObjectFromCSS } from '@lexical/selection'; import { $findMatchingParent, $getNearestNodeOfType, mergeRegister } from '@lexical/utils'; import { $getSelection, $isElementNode, $isNodeSelection, $isRangeSelection, $isRootOrShadowRoot, COMMAND_PRIORITY_CRITICAL, COMMAND_PRIORITY_NORMAL, KEY_MODIFIER_COMMAND, SELECTION_CHANGE_COMMAND, } from 'lexical'; import { useCallback, useEffect, useState } from 'react'; import * as React from 'react'; import { getSelectedNode } from '../../utils/getSelectedNode'; import { sanitizeUrl } from '../../utils/url'; import HeaderDropdown, { DEFAULT_HEADING_FONT_SIZES, getListItemBlockType } from './components/heading'; import FontSize from './components/font-size'; import BoldText from './components/bold-text'; import ItalicText from './components/italic-text'; import UnderlineText from './components/underline-text'; import TextColor from './components/text-color'; import TextAlign from './components/text-align'; import Bullet from './components/bullet'; import NumberBullet from './components/number-bullet'; import UploadPdf from './components/upload-pdf'; import UploadImage from './components/upload-image'; import Link from './components/link'; import { $isImageNode } from '../../nodes/ImageNode'; import { $isInlineImageNode } from '../../nodes/InlineImageNode'; const blockTypeToBlockName = { bullet: 'Bulleted List', check: 'Check List', code: 'Code Block', h1: 'Heading 1', h2: 'Heading 2', h3: 'Heading 3', h4: 'Heading 4', h5: 'Heading 5', h6: 'Heading 6', number: 'Numbered List', paragraph: 'Normal', quote: 'Quote', }; /** * Checks whether a block type is a heading type. * @param value Block type to test. * @returns True when the value is a heading block type. */ function isHeadingBlockType(value) { return value in DEFAULT_HEADING_FONT_SIZES; } /** * Gets the fallback block type represented by a list item font size. * @param fontSize Font size from the selected list item. * @returns Matching heading type, or paragraph when there is no match. */ function getBlockTypeFromFontSize(fontSize) { var _a; const headingType = (_a = Object.entries(DEFAULT_HEADING_FONT_SIZES).find(([, size]) => size === fontSize)) === null || _a === void 0 ? void 0 : _a[0]; return headingType !== undefined ? headingType : 'paragraph'; } /** * Reads font size from a serialized style string. * @param style Serialized CSS style string. * @returns Font size value when present. */ function getFontSizeFromStyle(style) { return getStyleObjectFromCSS(style)['font-size']; } function DisabledToolbarGroup({ children, disabled, }) { return (React.createElement("div", { className: disabled ? 'relative cursor-not-allowed opacity-40' : undefined }, children, disabled && React.createElement("div", { className: 'absolute inset-0 z-10 cursor-not-allowed' }))); } export default function ToolbarPlugin({ apiCdnUpload, disabled, }) { const [editor] = useLexicalComposerContext(); const [activeEditor, setActiveEditor] = useState(editor); const [blockType, setBlockType] = useState('paragraph'); const [listBlockType, setListBlockType] = useState(null); const [fontSize, setFontSize] = useState('14px'); const [fontColor, setFontColor] = useState('#000'); const [elementFormat, setElementFormat] = useState('left'); const [isImageSelected, setIsImageSelected] = useState(false); const [isLink, setIsLink] = useState(false); const [isBold, setIsBold] = useState(false); const [isItalic, setIsItalic] = useState(false); const [isUnderline, setIsUnderline] = useState(false); const $updateToolbar = useCallback(() => { const selection = $getSelection(); if ($isNodeSelection(selection)) { const node = selection.getNodes()[0]; if ($isImageNode(node) || $isInlineImageNode(node)) { setIsImageSelected(true); setElementFormat((node.getPosition() || 'center')); } else { setIsImageSelected(false); } return; } if ($isRangeSelection(selection)) { setIsImageSelected(false); const anchorNode = selection.anchor.getNode(); let element = anchorNode.getKey() === 'root' ? anchorNode : $findMatchingParent(anchorNode, (e) => { const parent = e.getParent(); return parent !== null && $isRootOrShadowRoot(parent); }); if (element === null) { element = anchorNode.getTopLevelElementOrThrow(); } const elementKey = element.getKey(); const elementDOM = activeEditor.getElementByKey(elementKey); // Update text format setIsBold(selection.hasFormat('bold')); setIsItalic(selection.hasFormat('italic')); setIsUnderline(selection.hasFormat('underline')); // Update links const node = getSelectedNode(selection); const parent = node.getParent(); if ($isLinkNode(parent) || $isLinkNode(node)) { setIsLink(true); } else { setIsLink(false); } const defaultFontSize = $isHeadingNode(element) ? DEFAULT_HEADING_FONT_SIZES[element.getTag()] : '14px'; let selectionFontSize = $getSelectionStyleValueForProperty(selection, 'font-size', defaultFontSize); if (elementDOM !== null) { const parentList = $getNearestNodeOfType(anchorNode, ListNode); if (parentList !== null || $isListNode(element)) { const listItemNode = $isListItemNode(anchorNode) ? anchorNode : $findMatchingParent(anchorNode, (node) => $isListItemNode(node)); const listItemFontSize = $isListItemNode(listItemNode) ? getFontSizeFromStyle(listItemNode.getTextStyle()) : undefined; const listItemBlockType = $isListItemNode(listItemNode) ? getListItemBlockType(listItemNode) : null; // @ts-ignore const type = parentList ? parentList.getListType() : element.getListType(); if (listItemFontSize !== undefined && !selection.isCollapsed()) { selectionFontSize = listItemFontSize; } setListBlockType(type); setBlockType(listItemBlockType !== null && listItemBlockType !== void 0 ? listItemBlockType : getBlockTypeFromFontSize(selectionFontSize)); } else { setListBlockType(null); const type = $isHeadingNode(element) ? element.getTag() : element.getType(); if (type in blockTypeToBlockName) { setBlockType(type); } } } // Handle buttons setFontSize(selectionFontSize); setFontColor($getSelectionStyleValueForProperty(selection, 'color', '#000')); let matchingParent; if ($isLinkNode(parent)) { // If node is a link, we need to fetch the parent paragraph node to set format matchingParent = $findMatchingParent(node, (parentNode) => $isElementNode(parentNode) && !parentNode.isInline()); } // If matchingParent is a valid node, pass it's format type setElementFormat($isElementNode(matchingParent) ? matchingParent.getFormatType() : $isElementNode(node) ? node.getFormatType() : (parent === null || parent === void 0 ? void 0 : parent.getFormatType()) || 'left'); } }, [activeEditor]); useEffect(() => activeEditor.registerCommand(SELECTION_CHANGE_COMMAND, (_payload, newEditor) => { $updateToolbar(); setActiveEditor(newEditor); return false; }, COMMAND_PRIORITY_CRITICAL), [activeEditor, $updateToolbar]); useEffect(() => mergeRegister(activeEditor.registerUpdateListener(({ editorState }) => { editorState.read(() => { $updateToolbar(); }); })), [$updateToolbar, activeEditor]); useEffect(() => activeEditor.registerCommand(KEY_MODIFIER_COMMAND, (payload) => { const event = payload; const { code, ctrlKey, metaKey } = event; if (code === 'KeyK' && (ctrlKey || metaKey)) { event.preventDefault(); return activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl('https://')); } return false; }, COMMAND_PRIORITY_NORMAL), [activeEditor, isLink]); useEffect(() => { if (!window.pdfjsLib) { const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js'; document.body.appendChild(script); } }, []); useEffect(() => { if (activeEditor) { activeEditor.setEditable(!disabled); } }, [activeEditor, disabled]); const parsedFontSize = parseInt(fontSize, 10); const activeListHeadingTag = listBlockType !== null && isHeadingBlockType(blockType) ? blockType : null; return (React.createElement("div", { className: 'flex relative flex-wrap gap-1 p-2 bg-gray-100 border-b border-gray-300' }, disabled && React.createElement("div", { className: 'cursor-not-allowed z-20 absolute inset-0 bg-gray-200 opacity-20' }), React.createElement(DisabledToolbarGroup, { disabled: isImageSelected }, blockType in blockTypeToBlockName && activeEditor === editor && (React.createElement(HeaderDropdown, { value: blockType, editor: activeEditor }))), React.createElement(DisabledToolbarGroup, { disabled: isImageSelected }, React.createElement(FontSize, { editor: activeEditor, fontSize: Number.isNaN(parsedFontSize) ? null : parsedFontSize, listHeadingTag: activeListHeadingTag })), React.createElement(DisabledToolbarGroup, { disabled: isImageSelected }, React.createElement(BoldText, { isActive: isBold, editor: activeEditor })), React.createElement(DisabledToolbarGroup, { disabled: isImageSelected }, React.createElement(ItalicText, { isActive: isItalic, editor: activeEditor })), React.createElement(DisabledToolbarGroup, { disabled: isImageSelected }, React.createElement(UnderlineText, { isActive: isUnderline, editor: activeEditor })), React.createElement(DisabledToolbarGroup, { disabled: isImageSelected }, React.createElement(TextColor, { editor: activeEditor, color: fontColor })), React.createElement(TextAlign, { value: elementFormat, editor: activeEditor, isImageSelected: isImageSelected }), React.createElement(DisabledToolbarGroup, { disabled: isImageSelected }, React.createElement(Bullet, { isActive: listBlockType === 'bullet', editor: activeEditor })), React.createElement(DisabledToolbarGroup, { disabled: isImageSelected }, React.createElement(NumberBullet, { isActive: listBlockType === 'number', editor: activeEditor })), React.createElement(DisabledToolbarGroup, { disabled: isImageSelected }, React.createElement(UploadPdf, { editor: activeEditor, apiCdnUpload: apiCdnUpload })), React.createElement(DisabledToolbarGroup, { disabled: isImageSelected }, React.createElement(UploadImage, { editor: activeEditor, apiCdnUpload: apiCdnUpload })), React.createElement(DisabledToolbarGroup, { disabled: isImageSelected }, React.createElement(Link, { editor: activeEditor })))); } //# sourceMappingURL=index.js.map