@selfcommunity/react-ui
Version:
React UI Components to integrate a Community created with SelfCommunity Platform.
273 lines (272 loc) • 14.5 kB
JavaScript
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, {})] })));
}