You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Global-sales/src/components/LexicalEditor/plugins/ToolbarPlugin.jsx

1055 lines
34 KiB
JavaScript

import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
CAN_REDO_COMMAND,
CAN_UNDO_COMMAND,
REDO_COMMAND,
UNDO_COMMAND,
SELECTION_CHANGE_COMMAND,
FORMAT_TEXT_COMMAND,
FORMAT_ELEMENT_COMMAND,
OUTDENT_CONTENT_COMMAND,
INDENT_CONTENT_COMMAND,
$getSelection,
$isElementNode,
$isRangeSelection,
$createParagraphNode,
$getNodeByKey,
$isTextNode,
} from 'lexical';
import { $isLinkNode, $toggleLink, TOGGLE_LINK_COMMAND } from '@lexical/link';
import {
$getSelectionStyleValueForProperty,
$isParentElementRTL,
$patchStyleText,
$setBlocksType,
// $wrapNodes,
$isAtNodeEnd,
} from '@lexical/selection';
import { $findMatchingParent, $getNearestNodeOfType, $getNearestBlockElementAncestorOrThrow, mergeRegister } from '@lexical/utils';
import { INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, REMOVE_LIST_COMMAND, $isListNode, ListNode } from '@lexical/list';
import { createPortal } from 'react-dom';
import { $createHeadingNode, $createQuoteNode, $isHeadingNode, $isQuoteNode } from '@lexical/rich-text';
import { $createCodeNode, $isCodeNode, getDefaultCodeLanguage, getCodeLanguages } from '@lexical/code';
import { INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontalRuleNode';
import {$isDecoratorBlockNode} from '@lexical/react/LexicalDecoratorBlockNode';
import {$isTableSelection} from '@lexical/table';
import DropDown, { DropDownItem } from './../ui/DropDown';
import DropdownColorPicker from '../ui/DropdownColorPicker';
import {
// INSERT_IMAGE_COMMAND,
InsertImageDialog,
// InsertImagePayload,
} from './ImagesPlugin';
import {InsertInlineImageDialog} from './InlineImagePlugin';
import FormatPainterToolbarButton from './FormatPaint/FormatPainterToolbarButton';
import useModal from './../hooks/useModal';
const LowPriority = 1;
const supportedBlockTypes = new Set(['paragraph', 'quote', 'code', 'h1', 'h2', 'h3', 'ul', 'ol']);
const blockTypeToBlockName = {
code: 'Code Block',
h1: 'Large Heading',
h2: 'Small Heading',
h3: 'Heading',
h4: 'Heading',
h5: 'Heading',
ol: 'Numbered List',
paragraph: 'Normal',
quote: 'Quote',
ul: 'Bulleted List',
};
const FONT_FAMILY_OPTIONS = [
['Arial', 'Arial'],
['Courier New', 'Courier New'],
['Georgia', 'Georgia'],
['Times New Roman', 'Times New Roman'],
['Trebuchet MS', 'Trebuchet MS'],
['Verdana', 'Verdana'],
];
const FONT_SIZE_OPTIONS = [
['10px', '10px'],
// ['11px', '11px'],
['12px', '12px'],
['13px', '13px'],
['14px', '14px'],
// ['15px', '15px'],
['16px', '16px'],
// ['17px', '17px'],
['18px', '18px'],
// ['19px', '19px'],
['20px', '20px'],
['24px', '24px'],
['32px', '32px'],
// ['48px', '48px'],
];
const LINE_SPACING_OPTIONS = [
['1', '1'],
['1.25', '1.25'],
['1.5', '1.5'],
['2', '2'],
['2.5', '2.5'],
['3', '3'],
// ['3.5', '3.5'],
// ['4', '4'],
// ['4.5', '4.5'],
// ['5', '5'],
];
const ELEMENT_FORMAT_OPTIONS = {
center: { icon: 'center-align', iconRTL: 'center-align', name: 'Center Align' },
end: { icon: 'right-align', iconRTL: 'left-align', name: 'End Align' },
justify: { icon: 'justify-align', iconRTL: 'justify-align', name: 'Justify Align' },
left: { icon: 'left-align', iconRTL: 'left-align', name: 'Left Align' },
right: { icon: 'right-align', iconRTL: 'right-align', name: 'Right Align' },
start: { icon: 'left-align', iconRTL: 'right-align', name: 'Start Align' },
};
// toolbar utils
const clearFormatting = (editor) => {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection) || $isTableSelection(selection)) {
const anchor = selection.anchor;
const focus = selection.focus;
const nodes = selection.getNodes();
const extractedNodes = selection.extract();
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)) {
// Use a separate variable to ensure TS does not lose the refinement
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 the selected text has one format applied
* selecting a portion of the text, could
* clear the format to the wrong portion of the text.
*
* The cleared text is based on the length of the selected text.
*/
// We need this in case the selected text only has one format
const extractedTextNode = extractedNodes[0];
if (nodes.length === 1 && $isTextNode(extractedTextNode)) {
textNode = extractedTextNode;
}
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('');
}
});
}
});
};
function dropDownActiveClass(active) {
if (active) {
return 'active dropdown-item-active';
} else {
return '';
}
}
function Divider() {
return <div className='divider' />;
}
function positionEditorElement(editor, rect) {
if (rect === null) {
editor.style.opacity = '0';
editor.style.top = '-1000px';
editor.style.left = '-1000px';
} else {
editor.style.opacity = '1';
editor.style.top = `${rect.top + rect.height + window.pageYOffset + 10}px`;
editor.style.left = `${rect.left + window.pageXOffset - editor.offsetWidth / 2 + rect.width / 2}px`;
}
}
function FloatingLinkEditor({ editor }) {
const editorRef = useRef(null);
const inputRef = useRef(null);
const mouseDownRef = useRef(false);
const [linkUrl, setLinkUrl] = useState('');
const [isEditMode, setEditMode] = useState(false);
const [lastSelection, setLastSelection] = useState(null);
const updateLinkEditor = useCallback(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
const node = getSelectedNode(selection);
const parent = node.getParent();
if ($isLinkNode(parent)) {
setLinkUrl(parent.getURL());
} else if ($isLinkNode(node)) {
setLinkUrl(node.getURL());
} else {
setLinkUrl('');
}
}
const editorElem = editorRef.current;
const nativeSelection = window.getSelection();
const activeElement = document.activeElement;
if (editorElem === null) {
return;
}
const rootElement = editor.getRootElement();
if (selection !== null && !nativeSelection.isCollapsed && rootElement !== null && rootElement.contains(nativeSelection.anchorNode)) {
const domRange = nativeSelection.getRangeAt(0);
let rect;
if (nativeSelection.anchorNode === rootElement) {
let inner = rootElement;
while (inner.firstElementChild != null) {
inner = inner.firstElementChild;
}
rect = inner.getBoundingClientRect();
} else {
rect = domRange.getBoundingClientRect();
}
if (!mouseDownRef.current) {
positionEditorElement(editorElem, rect);
}
setLastSelection(selection);
} else if (!activeElement || activeElement.className !== 'link-input') {
positionEditorElement(editorElem, null);
setLastSelection(null);
setEditMode(false);
setLinkUrl('');
}
return true;
}, [editor]);
useEffect(() => {
return mergeRegister(
editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
updateLinkEditor();
});
}),
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
() => {
updateLinkEditor();
return true;
},
LowPriority
)
);
}, [editor, updateLinkEditor]);
useEffect(() => {
editor.getEditorState().read(() => {
updateLinkEditor();
});
}, [editor, updateLinkEditor]);
useEffect(() => {
if (isEditMode && inputRef.current) {
inputRef.current.focus();
}
}, [isEditMode]);
return (
<div ref={editorRef} className='link-editor'>
{isEditMode ? (
<input
ref={inputRef}
className='link-input'
value={linkUrl}
onChange={(event) => {
setLinkUrl(event.target.value);
}}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
if (lastSelection !== null) {
if (linkUrl !== '') {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, linkUrl);
}
setEditMode(false);
}
} else if (event.key === 'Escape') {
event.preventDefault();
setEditMode(false);
}
}}
/>
) : (
<>
<div className='link-input'>
<a href={linkUrl} target='_blank' rel='noopener noreferrer'>
{linkUrl}
</a>
<div
className='link-edit'
role='button'
tabIndex={0}
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
setEditMode(true);
}}
/>
</div>
</>
)}
</div>
);
}
function Select({ onChange, className, options, value }) {
return (
<select className={className} onChange={onChange} value={value}>
<option hidden={true} value='' />
{options.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
);
}
function getSelectedNode(selection) {
const anchor = selection.anchor;
const focus = selection.focus;
const anchorNode = selection.anchor.getNode();
const focusNode = selection.focus.getNode();
if (anchorNode === focusNode) {
return anchorNode;
}
const isBackward = selection.isBackward();
if (isBackward) {
return $isAtNodeEnd(focus) ? anchorNode : focusNode;
} else {
return $isAtNodeEnd(anchor) ? focusNode : anchorNode;
}
}
function getDomRangeRect(nativeSelection, rootElement) {
const domRange = nativeSelection.getRangeAt(0);
let rect;
if (nativeSelection.anchorNode === rootElement) {
let inner = rootElement;
while (inner.firstElementChild != null) {
inner = inner.firstElementChild;
}
rect = inner.getBoundingClientRect();
} else {
rect = domRange.getBoundingClientRect();
}
return rect;
}
function BlockOptionsDropdownList({ editor, blockType, toolbarRef, setShowBlockOptionsDropDown }) {
const dropDownRef = useRef(null);
useEffect(() => {
const toolbar = toolbarRef.current;
const dropDown = dropDownRef.current;
if (toolbar !== null && dropDown !== null) {
const { top, left } = toolbar.getBoundingClientRect();
dropDown.style.top = `${top + 40}px`;
dropDown.style.left = `${left}px`;
}
}, [dropDownRef, toolbarRef]);
useEffect(() => {
const dropDown = dropDownRef.current;
const toolbar = toolbarRef.current;
if (dropDown !== null && toolbar !== null) {
const handle = (event) => {
const target = event.target;
if (!dropDown.contains(target) && !toolbar.contains(target)) {
setShowBlockOptionsDropDown(false);
}
};
document.addEventListener('click', handle);
return () => {
document.removeEventListener('click', handle);
};
}
}, [dropDownRef, setShowBlockOptionsDropDown, toolbarRef]);
const formatParagraph = () => {
if (blockType !== 'paragraph') {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$setBlocksType(selection, () => $createParagraphNode());
}
});
}
setShowBlockOptionsDropDown(false);
};
const formatLargeHeading = () => {
if (blockType !== 'h1') {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$setBlocksType(selection, () => $createHeadingNode('h1'));
}
});
}
setShowBlockOptionsDropDown(false);
};
const formatSmallHeading = () => {
if (blockType !== 'h2') {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$setBlocksType(selection, () => $createHeadingNode('h2'));
}
});
}
setShowBlockOptionsDropDown(false);
};
const formatSmallHeading3 = () => {
if (blockType !== 'h3') {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$setBlocksType(selection, () => $createHeadingNode('h3'));
}
});
}
setShowBlockOptionsDropDown(false);
};
const formatBulletList = () => {
if (blockType !== 'ul') {
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND);
} else {
editor.dispatchCommand(REMOVE_LIST_COMMAND);
}
setShowBlockOptionsDropDown(false);
};
const formatNumberedList = () => {
if (blockType !== 'ol') {
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND);
} else {
editor.dispatchCommand(REMOVE_LIST_COMMAND);
}
setShowBlockOptionsDropDown(false);
};
const formatQuote = () => {
if (blockType !== 'quote') {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$setBlocksType(selection, () => $createQuoteNode());
}
});
}
setShowBlockOptionsDropDown(false);
};
const formatCode = () => {
if (blockType !== 'code') {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$setBlocksType(selection, () => $createCodeNode());
}
});
}
setShowBlockOptionsDropDown(false);
};
return (
<div className='dropdown' ref={dropDownRef}>
<button type='button' className='item' onClick={formatParagraph}>
<span className='icon paragraph' />
<span className='text'>Normal</span>
{blockType === 'paragraph' && <span className='active' />}
</button>
<button type='button' className='item' onClick={formatLargeHeading}>
<span className='icon large-heading' />
<span className='text'>Heading 1</span>
{blockType === 'h1' && <span className='active' />}
</button>
<button type='button' className='item' onClick={formatSmallHeading}>
<span className='icon small-heading' />
<span className='text'>Heading 2</span>
{blockType === 'h2' && <span className='active' />}
</button>
<button type='button' className='item' onClick={formatSmallHeading3}>
<span className='icon h3' />
<span className='text'>Heading 3</span>
{blockType === 'h3' && <span className='active' />}
</button>
<button type='button' className='item' onClick={formatBulletList}>
<span className='icon bullet-list' />
<span className='text'>Bullet List</span>
{blockType === 'ul' && <span className='active' />}
</button>
<button className='item' onClick={formatNumberedList}>
<span className='icon numbered-list' />
<span className='text'>Numbered List</span>
{blockType === 'ol' && <span className='active' />}
</button>
<button className='item' onClick={formatQuote}>
<span className='icon quote' />
<span className='text'>Quote</span>
{blockType === 'quote' && <span className='active' />}
</button>
{/* <button className="item" onClick={formatCode}>
<span className="icon code" />
<span className="text">Code Block</span>
{blockType === "code" && <span className="active" />}
</button> */}
</div>
);
}
const FontDropDownMapped = {
'font-family': {
buttonAriaLabel: 'Formatting options for font family',
IconClassName: 'icon icon2 block-type font-family',
options: FONT_FAMILY_OPTIONS,
styleName: 'fontFamily',
},
'font-size': {
buttonAriaLabel: 'Formatting options for font size',
IconClassName: '',
options: FONT_SIZE_OPTIONS,
styleName: 'fontSize',
},
'line-height': {
buttonAriaLabel: 'Formatting options for line spacing',
IconClassName: 'icon icon2 line-height',
options: LINE_SPACING_OPTIONS,
styleName: 'lineHeight',
},
}
function FontDropDown({ editor, value, style, disabled = false }) {
const handleClick = useCallback(
(option) => {
editor.update(() => {
const selection = $getSelection();
if (selection !== null) {
$patchStyleText(selection, {
[style]: option,
});
}
});
},
[editor, style]
);
const buttonIconClassName = FontDropDownMapped[style].IconClassName;
const buttonAriaLabel = FontDropDownMapped[style].buttonAriaLabel;
const dropdownOptions = FontDropDownMapped[style].options;
return (
<DropDown disabled={disabled} buttonClassName={'toolbar-item ' + style} buttonLabel={value} buttonIconClassName={buttonIconClassName} buttonAriaLabel={buttonAriaLabel}>
{dropdownOptions.map(([option, text]) => (
<DropDownItem
className={`item font-m-${option.replace(/\s+/g, '_')} ${dropDownActiveClass(value === option)} ${style === 'font-size' ? 'fontsize-item' : ''}`}
style={{ fontFamily: style === 'font-family' ? option : undefined, fontSize: style === 'font-size' ? option : undefined, }}
onClick={() => handleClick(option)}
key={option}>
<span className='text'>{text}</span>
</DropDownItem>
))}
</DropDown>
)
}
function ElementFormatDropdown({ editor, value, isRTL, disabled = false }) {
const formatOption = ELEMENT_FORMAT_OPTIONS[value || 'left'];
return (
<DropDown
disabled={disabled}
// buttonLabel={formatOption.name}
buttonIconClassName={`icon ${isRTL ? formatOption.iconRTL : formatOption.icon}`}
buttonClassName='toolbar-item spaced alignment'
buttonAriaLabel='Formatting options for text alignment'>
<DropDownItem
onClick={() => {
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'left');
}}
className='item'>
<i className='icon left-align' />
<span className='text'>Left Align</span>
</DropDownItem>
<DropDownItem
onClick={() => {
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'center');
}}
className='item'>
<i className='icon center-align' />
<span className='text'>Center Align</span>
</DropDownItem>
<DropDownItem
onClick={() => {
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'right');
}}
className='item'>
<i className='icon right-align' />
<span className='text'>Right Align</span>
</DropDownItem>
<DropDownItem
onClick={() => {
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'justify');
}}
className='item'>
<i className='icon justify-align' />
<span className='text'>Justify Align</span>
</DropDownItem>
<DropDownItem
onClick={() => {
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'start');
}}
className='item'>
<i className={`icon ${isRTL ? ELEMENT_FORMAT_OPTIONS.start.iconRTL : ELEMENT_FORMAT_OPTIONS.start.icon}`} />
<span className='text'>Start Align</span>
</DropDownItem>
<DropDownItem
onClick={() => {
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'end');
}}
className='item'>
<i className={`icon ${isRTL ? ELEMENT_FORMAT_OPTIONS.end.iconRTL : ELEMENT_FORMAT_OPTIONS.end.icon}`} />
<span className='text'>End Align</span>
</DropDownItem>
<Divider />
<DropDownItem
onClick={() => {
editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined);
}}
className='item'>
<i className={'icon ' + (isRTL ? 'indent' : 'outdent')} />
<span className='text'>Outdent (Shift+Tab)</span>
</DropDownItem>
<DropDownItem
onClick={() => {
editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined);
}}
className='item'>
<i className={'icon ' + (isRTL ? 'outdent' : 'indent')} />
<span className='text'>Indent (Tab)</span>
</DropDownItem>
</DropDown>
);
}
export default function ToolbarPlugin() {
const [editor] = useLexicalComposerContext();
const [activeEditor, setActiveEditor] = useState(editor);
const toolbarRef = useRef(null);
const [isEditable, setIsEditable] = useState(() => editor.isEditable());
const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false);
const [blockType, setBlockType] = useState('paragraph');
const [selectedElementKey, setSelectedElementKey] = useState(null);
const [showBlockOptionsDropDown, setShowBlockOptionsDropDown] = useState(false);
const [codeLanguage, setCodeLanguage] = useState('');
const [isRTL, setIsRTL] = useState(false);
const [isLink, setIsLink] = useState(false);
const [isBold, setIsBold] = useState(false);
const [isItalic, setIsItalic] = useState(false);
const [isUnderline, setIsUnderline] = useState(false);
const [isStrikethrough, setIsStrikethrough] = useState(false);
const [isCode, setIsCode] = useState(false);
const [fontFamily, setFontFamily] = useState('Arial');
const [fontSize, setFontSize] = useState('16px');
const [fontColor, setFontColor] = useState('#000');
const [bgColor, setBgColor] = useState('#fff');
const [elementFormat, setElementFormat] = useState('left');
const [lineSpacing, setLineSpacing] = useState('1.5');
const [floatingAnchorElem, setFloatingAnchorElem] = useState(null);
const [isSmallWidthViewport, setIsSmallWidthViewport] = useState(false);
const [modal, showModal] = useModal();
const applyStyleText = useCallback(
(styles, skipHistoryStack = null) => {
editor.update(
() => {
const selection = $getSelection();
if (selection !== null) {
$patchStyleText(selection, styles);
}
},
skipHistoryStack ? { tag: 'historic' } : {}
);
},
[editor]
);
const onFontColorSelect = useCallback(
(value, skipHistoryStack) => {
applyStyleText({ color: value }, skipHistoryStack);
},
[applyStyleText]
);
const onBgColorSelect = useCallback(
(value, skipHistoryStack) => {
applyStyleText({ 'background-color': value }, skipHistoryStack);
},
[applyStyleText]
);
const updateToolbar = useCallback(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
const anchorNode = selection.anchor.getNode();
const element = anchorNode.getKey() === 'root' ? anchorNode : anchorNode.getTopLevelElementOrThrow();
const elementKey = element.getKey();
const elementDOM = editor.getElementByKey(elementKey);
if (elementDOM !== null) {
setSelectedElementKey(elementKey);
if ($isListNode(element)) {
const parentList = $getNearestNodeOfType(anchorNode, ListNode);
const type = parentList ? parentList.getTag() : element.getTag();
setBlockType(type);
} else {
const type = $isHeadingNode(element) ? element.getTag() : element.getType();
setBlockType(type);
if ($isCodeNode(element)) {
setCodeLanguage(element.getLanguage() || getDefaultCodeLanguage());
}
}
}
// Update text format
setIsBold(selection.hasFormat('bold'));
setIsItalic(selection.hasFormat('italic'));
setIsUnderline(selection.hasFormat('underline'));
setIsStrikethrough(selection.hasFormat('strikethrough'));
setIsCode(selection.hasFormat('code'));
setIsRTL($isParentElementRTL(selection));
// Update links
const node = getSelectedNode(selection);
const parent = node.getParent();
if ($isLinkNode(parent) || $isLinkNode(node)) {
setIsLink(true);
} else {
setIsLink(false);
}
// Handle buttons
setFontColor($getSelectionStyleValueForProperty(selection, 'color', '#000'));
setBgColor(
$getSelectionStyleValueForProperty(
selection,
'background-color',
'#fff',
),
);
setFontFamily(
$getSelectionStyleValueForProperty(selection, 'font-family', 'Arial'),
);
setFontSize(
$getSelectionStyleValueForProperty(selection, 'font-size', '16px'),
);
setLineSpacing(
$getSelectionStyleValueForProperty(selection, 'line-height', '1.5'),
);
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?.getFormatType() || 'left');
}
}, [editor]);
useEffect(() => {
return mergeRegister(
editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
updateToolbar();
});
}),
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
(_payload, newEditor) => {
setActiveEditor(newEditor);
updateToolbar();
return false;
},
LowPriority
// COMMAND_PRIORITY_CRITICAL,
),
editor.registerCommand(
CAN_UNDO_COMMAND,
(payload) => {
setCanUndo(payload);
return false;
},
LowPriority
),
editor.registerCommand(
CAN_REDO_COMMAND,
(payload) => {
setCanRedo(payload);
return false;
},
LowPriority
)
);
}, [editor, updateToolbar]);
const codeLanguges = useMemo(() => getCodeLanguages(), []);
const onCodeLanguageSelect = useCallback(
(e) => {
editor.update(() => {
if (selectedElementKey !== null) {
const node = $getNodeByKey(selectedElementKey);
if ($isCodeNode(node)) {
node.setLanguage(e.target.value);
}
}
});
},
[editor, selectedElementKey]
);
const insertLink = useCallback(() => {
if (!isLink) {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, 'https://');
} else {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
}
}, [editor, isLink]);
const insertHorizontalRule = useCallback(() => {
editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined);
}, [editor]);
return (
<div className='toolbar sticky top-[-10px] z-10' ref={toolbarRef}>
<button type='button'
disabled={!canUndo}
onClick={() => {
editor.dispatchCommand(UNDO_COMMAND);
}}
className='toolbar-item spaced'
aria-label='Undo'>
<i className='format undo' />
</button>
<button type='button'
disabled={!canRedo}
onClick={() => {
editor.dispatchCommand(REDO_COMMAND);
}}
className='toolbar-item'
aria-label='Redo'>
<i className='format redo' />
</button>
<FormatPainterToolbarButton />
<button type='button'
onClick={() => clearFormatting(activeEditor)}
className='toolbar-item'
title="清除格式"
aria-label='Clear'>
<i className='format clear' />
</button>
<Divider />
{supportedBlockTypes.has(blockType) && (
<>
<button type='button' className='toolbar-item block-controls' onClick={() => setShowBlockOptionsDropDown(!showBlockOptionsDropDown)} aria-label='Formatting Options'>
<span className={'icon block-type ' + blockType} />
<span className='text'>{blockTypeToBlockName[blockType]}</span>
<i className='chevron-down' />
</button>
{showBlockOptionsDropDown &&
createPortal(
<BlockOptionsDropdownList editor={editor} blockType={blockType} toolbarRef={toolbarRef} setShowBlockOptionsDropDown={setShowBlockOptionsDropDown} />,
document.body
)}
<Divider />
</>
)}
{blockType === 'code' ? (
<>
<Select className='toolbar-item code-language' onChange={onCodeLanguageSelect} options={codeLanguges} value={codeLanguage} />
<i className='chevron-down inside' />
</>
) : (
<>
<FontDropDown
disabled={!isEditable}
style={'font-family'}
value={fontFamily}
editor={editor}
/>
<FontDropDown
disabled={!isEditable}
style={'font-size'}
value={fontSize}
editor={editor}
/>
<FontDropDown
disabled={!isEditable}
style={'line-height'}
value={lineSpacing}
editor={editor}
/>
<Divider />
<button type='button'
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
}}
className={'toolbar-item spaced ' + (isBold ? 'active' : '')}
aria-label='Format Bold'>
<i className='format bold' />
</button>
<button type='button'
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
}}
className={'toolbar-item spaced ' + (isItalic ? 'active' : '')}
aria-label='Format Italics'>
<i className='format italic' />
</button>
<button type='button'
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');
}}
className={'toolbar-item spaced ' + (isUnderline ? 'active' : '')}
aria-label='Format Underline'>
<i className='format underline' />
</button>
<button type='button'
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');
}}
className={'toolbar-item spaced ' + (isStrikethrough ? 'active' : '')}
aria-label='Format Strikethrough'>
<i className='format strikethrough' />
</button>
{/* <button type='button'
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code");
}}
className={"toolbar-item spaced " + (isCode ? "active" : "")}
aria-label="Insert Code"
>
<i className="format code" />
</button> */}
<button type='button' onClick={insertLink} className={'toolbar-item spaced ' + (isLink ? 'active' : '')} aria-label='Insert Link'>
<i className='format link' />
</button>
{isLink && createPortal(<FloatingLinkEditor editor={editor} />, document.body)}
<button type='button' onClick={insertHorizontalRule}
// onClick={() => {
// editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined);
// }}
className={'toolbar-item spaced '}
aria-label='Insert Horizontal Rule'>
<i className='format icon horizontal-rule' />
{/* <span className="text">Horizontal Rule</span> */}
</button>
<DropdownColorPicker
disabled={!isEditable}
buttonClassName='toolbar-item color-picker'
buttonAriaLabel='Formatting text color'
buttonIconClassName='icon font-color'
color={fontColor}
onChange={onFontColorSelect}
title='text color'
/>
<DropdownColorPicker
disabled={!isEditable}
buttonClassName='toolbar-item color-picker'
buttonAriaLabel='Formatting background color'
buttonIconClassName='icon bg-color'
color={bgColor}
onChange={onBgColorSelect}
title='bg color'
/>
<Divider />
<ElementFormatDropdown disabled={!isEditable} value={elementFormat} editor={editor} isRTL={isRTL} />
<Divider />
<DropDown
disabled={!isEditable}
buttonClassName="toolbar-item spaced"
buttonLabel="Insert"
buttonAriaLabel="Insert specialized editor node"
buttonIconClassName="icon plus">
<DropDownItem
onClick={() => {
showModal('Insert Image', (onClose) => (
<InsertImageDialog
activeEditor={activeEditor}
onClose={onClose}
/>
));
}}
className="item">
<i className="icon image" />
<span className="text">Image</span>
</DropDownItem>
<DropDownItem
onClick={() => {
showModal('Insert Inline Image', (onClose) => (
<InsertInlineImageDialog
activeEditor={activeEditor}
onClose={onClose}
/>
));
}}
className="item">
<i className="icon image" />
<span className="text">Inline Image</span>
</DropDownItem>
</DropDown>
</>
)}
{modal}
</div>
);
}