UNPKG

@selfcommunity/react-ui

Version:

React UI Components to integrate a Community created with SelfCommunity Platform.

273 lines (272 loc) • 14.5 kB
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; import { $createParagraphNode, $getSelection, $isRangeSelection, $isRootOrShadowRoot, $isTextNode, COMMAND_PRIORITY_CRITICAL, FORMAT_ELEMENT_COMMAND, FORMAT_TEXT_COMMAND, SELECTION_CHANGE_COMMAND } from 'lexical'; import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'; import { $isListNode, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListNode, REMOVE_LIST_COMMAND } from '@lexical/list'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { $isDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'; import { $createHeadingNode, $createQuoteNode, $isHeadingNode, $isQuoteNode } from '@lexical/rich-text'; import { $setBlocksType } from '@lexical/selection'; import { $isTableNode, $isTableSelection } from '@lexical/table'; import { $findMatchingParent, $getNearestBlockElementAncestorOrThrow, $getNearestNodeOfType, mergeRegister } from '@lexical/utils'; import * as React from 'react'; import { useCallback, useEffect, useState } from 'react'; import { getSelectedNode } from '../../../utils/editor'; import { Box, Icon, IconButton, Menu, MenuItem, ToggleButton, ToggleButtonGroup, Tooltip } from '@mui/material'; import { FormattedMessage } from 'react-intl'; import { styled } from '@mui/material/styles'; import { useThemeProps } from '@mui/system'; import ImagePlugin from './ImagePlugin'; import EmojiPlugin from './EmojiPlugin'; import { INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontalRuleNode'; import { PREFIX } from '../constants'; const blockTypeToBlockIcon = { h1: 'format_heading_1', h2: 'format_heading_2', h3: 'format_heading_3', bullet: 'format_list_bulleted', number: 'format_list_numbered', quote: 'format_quote', paragraph: 'format_paragraph' }; const rootTypeToRootName = { root: 'Root', table: 'Table' }; const FORMATS = ['bold', 'underline', 'italic', 'strikethrough', 'subscript', 'superscript']; const ALIGNMENTS = ['left', 'right', 'center', 'justify']; function BlockFormatIconButton({ className = '', editor, blockType, disabled = false }) { // STATE const [anchorEl, setAnchorEl] = React.useState(null); // FORMAT METHODS const formatParagraph = () => { editor.update(() => { const selection = $getSelection(); if ($isRangeSelection(selection) || $isTableSelection(selection)) { $setBlocksType(selection, () => $createParagraphNode()); } }); }; const formatHeading = (headingSize) => { if (blockType !== headingSize) { editor.update(() => { const selection = $getSelection(); if ($isRangeSelection(selection) || $isTableSelection(selection)) { $setBlocksType(selection, () => $createHeadingNode(headingSize)); } }); } }; const formatBulletList = () => { if (blockType !== 'bullet') { editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined); } else { editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined); } }; const formatNumberedList = () => { if (blockType !== 'number') { editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined); } else { editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined); } }; const formatQuote = () => { if (blockType !== 'quote') { editor.update(() => { const selection = $getSelection(); if ($isRangeSelection(selection) || $isTableSelection(selection)) { $setBlocksType(selection, () => $createQuoteNode()); } }); } }; // HANDLERS const handleSelect = (block) => (event) => { switch (block) { case 'bullet': formatBulletList(); break; case 'number': formatNumberedList(); break; case 'paragraph': formatParagraph(); break; case 'quote': formatQuote(); break; default: formatHeading(block); break; } setAnchorEl(null); }; const handleOpen = (event) => { setAnchorEl(event.currentTarget); }; const handleClose = () => { setAnchorEl(null); }; return (_jsxs(_Fragment, { children: [_jsx(IconButton, Object.assign({ className: className, disabled: disabled, onClick: handleOpen }, { children: _jsx(Tooltip, Object.assign({ title: _jsx(FormattedMessage, { id: "ui.editor.toolbarPlugin.blockType", defaultMessage: "ui.editor.toolbarPlugin.blockType" }) }, { children: _jsxs(_Fragment, { children: [_jsx(Icon, { children: blockTypeToBlockIcon[blockType] }), _jsx(Icon, { children: anchorEl ? 'expand_less' : 'expand_more' })] }) })) })), _jsx(Menu, Object.assign({ anchorEl: anchorEl, open: Boolean(anchorEl), onClose: handleClose }, { children: Object.keys(blockTypeToBlockIcon).map((block) => (_jsx(MenuItem, Object.assign({ onClick: handleSelect(block) }, { children: _jsx(Icon, { children: blockTypeToBlockIcon[block] }) }), block))) }))] })); } const classes = { root: `${PREFIX}-toolbar-plugin-root`, blockFormat: `${PREFIX}-block-format` }; const Root = styled(Box, { name: PREFIX, slot: 'ToolbarPluginRoot' })(() => ({})); export default function ToolbarPlugin(inProps) { // PROPS const props = useThemeProps({ props: inProps, name: PREFIX }); const { uploadImage = false } = props; // STATE const [editor] = useLexicalComposerContext(); const [activeEditor, setActiveEditor] = useState(editor); const [blockType, setBlockType] = useState('paragraph'); const [rootType, setRootType] = useState('root'); const [selectedElementKey, setSelectedElementKey] = useState(null); const [isLink, setIsLink] = useState(false); const [isEditable, setIsEditable] = useState(() => editor.isEditable()); const [formats, setFormats] = useState([]); const [alignment, setAlignment] = useState(ALIGNMENTS[0]); const $updateToolbar = useCallback(() => { const selection = $getSelection(); if ($isRangeSelection(selection)) { 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 setFormats(FORMATS.filter((f) => selection.hasFormat(f))); // eslint-disable-next-line @typescript-eslint/ban-ts-ignore // @ts-ignore setAlignment(ALIGNMENTS.find((a) => { var _a; return ((_a = element.getFormatType) === null || _a === void 0 ? void 0 : _a.call(element)) === a; }) || ALIGNMENTS[0]); // Update links const node = getSelectedNode(selection); const parent = node.getParent(); if ($isLinkNode(parent) || $isLinkNode(node)) { setIsLink(true); } else { setIsLink(false); } const tableNode = $findMatchingParent(node, $isTableNode); if ($isTableNode(tableNode)) { setRootType('table'); } else { setRootType('root'); } if (elementDOM !== null) { setSelectedElementKey(elementKey); if ($isListNode(element)) { const parentList = $getNearestNodeOfType(anchorNode, ListNode); const type = parentList ? parentList.getListType() : element.getListType(); setBlockType(type); } else { // eslint-disable-next-line @typescript-eslint/ban-ts-ignore // @ts-ignore const type = $isHeadingNode(element) ? element.getTag() : element.getType(); if (type in blockTypeToBlockIcon) { setBlockType(type); } } } } }, [activeEditor]); useEffect(() => { return editor.registerCommand(SELECTION_CHANGE_COMMAND, (_payload, newEditor) => { $updateToolbar(); setActiveEditor(newEditor); return false; }, COMMAND_PRIORITY_CRITICAL); }, [editor, $updateToolbar]); useEffect(() => { return mergeRegister(editor.registerEditableListener((editable) => { setIsEditable(editable); }), activeEditor.registerUpdateListener(({ editorState }) => { editorState.read(() => { $updateToolbar(); }); })); }, [$updateToolbar, activeEditor, editor]); 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) => { // We split the first and last node by the selection // So that we don't format unselected text inside those nodes if ($isTextNode(node)) { if (idx === 0 && anchor.offset !== 0) { node = node.splitText(anchor.offset)[1] || node; } if (idx === nodes.length - 1) { // eslint-disable-next-line @typescript-eslint/ban-ts-ignore // @ts-ignore node = node.splitText(focus.offset)[0] || node; } // eslint-disable-next-line @typescript-eslint/ban-ts-ignore // @ts-ignore if (node.__style !== '') { // eslint-disable-next-line @typescript-eslint/ban-ts-ignore // @ts-ignore node.setStyle(''); } // eslint-disable-next-line @typescript-eslint/ban-ts-ignore // @ts-ignore if (node.__format !== 0) { // eslint-disable-next-line @typescript-eslint/ban-ts-ignore // @ts-ignore node.setFormat(0); $getNearestBlockElementAncestorOrThrow(node).setFormat(''); } } else if ($isHeadingNode(node) || $isQuoteNode(node)) { node.replace($createParagraphNode(), true); } else if ($isDecoratorBlockNode(node)) { node.setFormat(''); } }); } }); }, [activeEditor]); const insertLink = useCallback(() => { if (!isLink) { editor.dispatchCommand(TOGGLE_LINK_COMMAND, 'https://'); } else { editor.dispatchCommand(TOGGLE_LINK_COMMAND, null); } }, [editor, isLink]); return (_jsxs(Root, Object.assign({ className: classes.root }, { children: [blockType in blockTypeToBlockIcon && activeEditor === editor && (_jsx(BlockFormatIconButton, { className: classes.blockFormat, disabled: !isEditable, blockType: blockType, editor: editor })), _jsx(IconButton, Object.assign({ disabled: !isEditable, onClick: () => { activeEditor.dispatchCommand(FORMAT_ELEMENT_COMMAND, ALIGNMENTS[(ALIGNMENTS.findIndex((a) => alignment === a) + 1) % ALIGNMENTS.length]); } }, { children: _jsx(Tooltip, Object.assign({ title: _jsx(FormattedMessage, { id: `ui.editor.toolbarPlugin.${alignment}`, defaultMessage: `ui.editor.toolbarPlugin.${alignment}` }) }, { children: _jsx(Icon, { children: `format_align_${alignment}` }) })) })), _jsx(ToggleButtonGroup, Object.assign({ value: formats }, { children: FORMATS.map((format) => (_jsx(ToggleButton, Object.assign({ value: format, disabled: !isEditable, onClick: () => { activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, format); } }, { children: _jsx(Tooltip, Object.assign({ title: _jsx(FormattedMessage, { id: `ui.editor.toolbarPlugin.${format}`, defaultMessage: `ui.editor.toolbarPlugin.${format}` }) }, { children: _jsx(Icon, { children: `format_${format}` }) })) }), format))) })), _jsx(IconButton, Object.assign({ disabled: !isEditable, onClick: clearFormatting }, { children: _jsx(Tooltip, Object.assign({ title: _jsx(FormattedMessage, { id: "ui.editor.toolbarPlugin.clear", defaultMessage: "ui.editor.toolbarPlugin.clear" }) }, { children: _jsx(Icon, { children: "format_clear" }) })) })), _jsx(IconButton, Object.assign({ disabled: !isEditable, onClick: () => { activeEditor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined); } }, { children: _jsx(Tooltip, Object.assign({ title: _jsx(FormattedMessage, { id: "ui.editor.toolbarPlugin.horizontalRule", defaultMessage: "ui.editor.toolbarPlugin.horizontalRule" }) }, { children: _jsx(Icon, { children: "format_horizontal_rule" }) })) })), uploadImage && _jsx(ImagePlugin, {}), _jsx(IconButton, Object.assign({ disabled: !isEditable, onClick: insertLink }, { children: _jsx(Tooltip, Object.assign({ title: _jsx(FormattedMessage, { id: "ui.editor.toolbarPlugin.link", defaultMessage: "ui.editor.toolbarPlugin.link" }) }, { children: _jsx(Icon, { children: "format_link" }) })) })), _jsx(EmojiPlugin, {})] }))); }