|
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
}
|