|
|
|
@ -1,5 +1,5 @@
|
|
|
|
|
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
|
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
|
|
import {
|
|
|
|
|
CAN_REDO_COMMAND,
|
|
|
|
|
CAN_UNDO_COMMAND,
|
|
|
|
@ -8,80 +8,71 @@ import {
|
|
|
|
|
SELECTION_CHANGE_COMMAND,
|
|
|
|
|
FORMAT_TEXT_COMMAND,
|
|
|
|
|
FORMAT_ELEMENT_COMMAND,
|
|
|
|
|
OUTDENT_CONTENT_COMMAND,
|
|
|
|
|
INDENT_CONTENT_COMMAND,
|
|
|
|
|
$getSelection,
|
|
|
|
|
$isElementNode,
|
|
|
|
|
$isRangeSelection,
|
|
|
|
|
$createParagraphNode,
|
|
|
|
|
$getNodeByKey
|
|
|
|
|
} from "lexical";
|
|
|
|
|
import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
|
|
|
|
|
$getNodeByKey,
|
|
|
|
|
} from 'lexical';
|
|
|
|
|
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
|
|
|
|
|
import {
|
|
|
|
|
$getSelectionStyleValueForProperty,
|
|
|
|
|
$isParentElementRTL,
|
|
|
|
|
$wrapNodes,
|
|
|
|
|
$isAtNodeEnd
|
|
|
|
|
} from "@lexical/selection";
|
|
|
|
|
import { $getNearestNodeOfType, 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
|
|
|
|
|
} from "@lexical/rich-text";
|
|
|
|
|
import {
|
|
|
|
|
$createCodeNode,
|
|
|
|
|
$isCodeNode,
|
|
|
|
|
getDefaultCodeLanguage,
|
|
|
|
|
getCodeLanguages
|
|
|
|
|
} from "@lexical/code";
|
|
|
|
|
import { INSERT_HORIZONTAL_RULE_COMMAND, } from '@lexical/react/LexicalHorizontalRuleNode';
|
|
|
|
|
$patchStyleText,
|
|
|
|
|
$setBlocksType,
|
|
|
|
|
// $wrapNodes,
|
|
|
|
|
$isAtNodeEnd,
|
|
|
|
|
} from '@lexical/selection';
|
|
|
|
|
import { $findMatchingParent, $getNearestNodeOfType, 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 } from '@lexical/rich-text';
|
|
|
|
|
import { $createCodeNode, $isCodeNode, getDefaultCodeLanguage, getCodeLanguages } from '@lexical/code';
|
|
|
|
|
import { INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontalRuleNode';
|
|
|
|
|
import DropDown, { DropDownItem } from './../ui/DropDown';
|
|
|
|
|
import DropdownColorPicker from '../ui/DropdownColorPicker';
|
|
|
|
|
|
|
|
|
|
const LowPriority = 1;
|
|
|
|
|
|
|
|
|
|
const supportedBlockTypes = new Set([
|
|
|
|
|
"paragraph",
|
|
|
|
|
"quote",
|
|
|
|
|
"code",
|
|
|
|
|
"h1",
|
|
|
|
|
"h2",
|
|
|
|
|
"h3",
|
|
|
|
|
"ul",
|
|
|
|
|
"ol"
|
|
|
|
|
]);
|
|
|
|
|
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"
|
|
|
|
|
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 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' },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function Divider() {
|
|
|
|
|
return <div className="divider" />;
|
|
|
|
|
return <div className='divider' />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function positionEditorElement(editor, rect) {
|
|
|
|
|
if (rect === null) {
|
|
|
|
|
editor.style.opacity = "0";
|
|
|
|
|
editor.style.top = "-1000px";
|
|
|
|
|
editor.style.left = "-1000px";
|
|
|
|
|
editor.style.opacity = '0';
|
|
|
|
|
editor.style.top = '-1000px';
|
|
|
|
|
editor.style.left = '-1000px';
|
|
|
|
|
} else {
|
|
|
|
|
editor.style.opacity = "1";
|
|
|
|
|
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`;
|
|
|
|
|
editor.style.left = `${rect.left + window.pageXOffset - editor.offsetWidth / 2 + rect.width / 2}px`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -89,7 +80,7 @@ function FloatingLinkEditor({ editor }) {
|
|
|
|
|
const editorRef = useRef(null);
|
|
|
|
|
const inputRef = useRef(null);
|
|
|
|
|
const mouseDownRef = useRef(false);
|
|
|
|
|
const [linkUrl, setLinkUrl] = useState("");
|
|
|
|
|
const [linkUrl, setLinkUrl] = useState('');
|
|
|
|
|
const [isEditMode, setEditMode] = useState(false);
|
|
|
|
|
const [lastSelection, setLastSelection] = useState(null);
|
|
|
|
|
|
|
|
|
@ -103,7 +94,7 @@ function FloatingLinkEditor({ editor }) {
|
|
|
|
|
} else if ($isLinkNode(node)) {
|
|
|
|
|
setLinkUrl(node.getURL());
|
|
|
|
|
} else {
|
|
|
|
|
setLinkUrl("");
|
|
|
|
|
setLinkUrl('');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const editorElem = editorRef.current;
|
|
|
|
@ -115,12 +106,7 @@ function FloatingLinkEditor({ editor }) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rootElement = editor.getRootElement();
|
|
|
|
|
if (
|
|
|
|
|
selection !== null &&
|
|
|
|
|
!nativeSelection.isCollapsed &&
|
|
|
|
|
rootElement !== null &&
|
|
|
|
|
rootElement.contains(nativeSelection.anchorNode)
|
|
|
|
|
) {
|
|
|
|
|
if (selection !== null && !nativeSelection.isCollapsed && rootElement !== null && rootElement.contains(nativeSelection.anchorNode)) {
|
|
|
|
|
const domRange = nativeSelection.getRangeAt(0);
|
|
|
|
|
let rect;
|
|
|
|
|
if (nativeSelection.anchorNode === rootElement) {
|
|
|
|
@ -137,11 +123,11 @@ function FloatingLinkEditor({ editor }) {
|
|
|
|
|
positionEditorElement(editorElem, rect);
|
|
|
|
|
}
|
|
|
|
|
setLastSelection(selection);
|
|
|
|
|
} else if (!activeElement || activeElement.className !== "link-input") {
|
|
|
|
|
} else if (!activeElement || activeElement.className !== 'link-input') {
|
|
|
|
|
positionEditorElement(editorElem, null);
|
|
|
|
|
setLastSelection(null);
|
|
|
|
|
setEditMode(false);
|
|
|
|
|
setLinkUrl("");
|
|
|
|
|
setLinkUrl('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
@ -179,25 +165,25 @@ function FloatingLinkEditor({ editor }) {
|
|
|
|
|
}, [isEditMode]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div ref={editorRef} className="link-editor">
|
|
|
|
|
<div ref={editorRef} className='link-editor'>
|
|
|
|
|
{isEditMode ? (
|
|
|
|
|
<input
|
|
|
|
|
ref={inputRef}
|
|
|
|
|
className="link-input"
|
|
|
|
|
className='link-input'
|
|
|
|
|
value={linkUrl}
|
|
|
|
|
onChange={(event) => {
|
|
|
|
|
setLinkUrl(event.target.value);
|
|
|
|
|
}}
|
|
|
|
|
onKeyDown={(event) => {
|
|
|
|
|
if (event.key === "Enter") {
|
|
|
|
|
if (event.key === 'Enter') {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
if (lastSelection !== null) {
|
|
|
|
|
if (linkUrl !== "") {
|
|
|
|
|
if (linkUrl !== '') {
|
|
|
|
|
editor.dispatchCommand(TOGGLE_LINK_COMMAND, linkUrl);
|
|
|
|
|
}
|
|
|
|
|
setEditMode(false);
|
|
|
|
|
}
|
|
|
|
|
} else if (event.key === "Escape") {
|
|
|
|
|
} else if (event.key === 'Escape') {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
setEditMode(false);
|
|
|
|
|
}
|
|
|
|
@ -205,13 +191,13 @@ function FloatingLinkEditor({ editor }) {
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<div className="link-input">
|
|
|
|
|
<a href={linkUrl} target="_blank" rel="noopener noreferrer">
|
|
|
|
|
<div className='link-input'>
|
|
|
|
|
<a href={linkUrl} target='_blank' rel='noopener noreferrer'>
|
|
|
|
|
{linkUrl}
|
|
|
|
|
</a>
|
|
|
|
|
<div
|
|
|
|
|
className="link-edit"
|
|
|
|
|
role="button"
|
|
|
|
|
className='link-edit'
|
|
|
|
|
role='button'
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
onMouseDown={(event) => event.preventDefault()}
|
|
|
|
|
onClick={() => {
|
|
|
|
@ -228,7 +214,7 @@ function FloatingLinkEditor({ editor }) {
|
|
|
|
|
function Select({ onChange, className, options, value }) {
|
|
|
|
|
return (
|
|
|
|
|
<select className={className} onChange={onChange} value={value}>
|
|
|
|
|
<option hidden={true} value="" />
|
|
|
|
|
<option hidden={true} value='' />
|
|
|
|
|
{options.map((option) => (
|
|
|
|
|
<option key={option} value={option}>
|
|
|
|
|
{option}
|
|
|
|
@ -271,12 +257,7 @@ function getDomRangeRect(nativeSelection, rootElement) {
|
|
|
|
|
return rect;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function BlockOptionsDropdownList({
|
|
|
|
|
editor,
|
|
|
|
|
blockType,
|
|
|
|
|
toolbarRef,
|
|
|
|
|
setShowBlockOptionsDropDown
|
|
|
|
|
}) {
|
|
|
|
|
function BlockOptionsDropdownList({ editor, blockType, toolbarRef, setShowBlockOptionsDropDown }) {
|
|
|
|
|
const dropDownRef = useRef(null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
@ -302,21 +283,21 @@ function BlockOptionsDropdownList({
|
|
|
|
|
setShowBlockOptionsDropDown(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
document.addEventListener("click", handle);
|
|
|
|
|
document.addEventListener('click', handle);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
document.removeEventListener("click", handle);
|
|
|
|
|
document.removeEventListener('click', handle);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}, [dropDownRef, setShowBlockOptionsDropDown, toolbarRef]);
|
|
|
|
|
|
|
|
|
|
const formatParagraph = () => {
|
|
|
|
|
if (blockType !== "paragraph") {
|
|
|
|
|
if (blockType !== 'paragraph') {
|
|
|
|
|
editor.update(() => {
|
|
|
|
|
const selection = $getSelection();
|
|
|
|
|
|
|
|
|
|
if ($isRangeSelection(selection)) {
|
|
|
|
|
$wrapNodes(selection, () => $createParagraphNode());
|
|
|
|
|
$setBlocksType(selection, () => $createParagraphNode());
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
@ -324,12 +305,12 @@ function BlockOptionsDropdownList({
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatLargeHeading = () => {
|
|
|
|
|
if (blockType !== "h1") {
|
|
|
|
|
if (blockType !== 'h1') {
|
|
|
|
|
editor.update(() => {
|
|
|
|
|
const selection = $getSelection();
|
|
|
|
|
|
|
|
|
|
if ($isRangeSelection(selection)) {
|
|
|
|
|
$wrapNodes(selection, () => $createHeadingNode("h1"));
|
|
|
|
|
$setBlocksType(selection, () => $createHeadingNode('h1'));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
@ -337,24 +318,24 @@ function BlockOptionsDropdownList({
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatSmallHeading = () => {
|
|
|
|
|
if (blockType !== "h2") {
|
|
|
|
|
if (blockType !== 'h2') {
|
|
|
|
|
editor.update(() => {
|
|
|
|
|
const selection = $getSelection();
|
|
|
|
|
|
|
|
|
|
if ($isRangeSelection(selection)) {
|
|
|
|
|
$wrapNodes(selection, () => $createHeadingNode("h2"));
|
|
|
|
|
$setBlocksType(selection, () => $createHeadingNode('h2'));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
setShowBlockOptionsDropDown(false);
|
|
|
|
|
};
|
|
|
|
|
const formatSmallHeading3 = () => {
|
|
|
|
|
if (blockType !== "h3") {
|
|
|
|
|
if (blockType !== 'h3') {
|
|
|
|
|
editor.update(() => {
|
|
|
|
|
const selection = $getSelection();
|
|
|
|
|
|
|
|
|
|
if ($isRangeSelection(selection)) {
|
|
|
|
|
$wrapNodes(selection, () => $createHeadingNode("h3"));
|
|
|
|
|
$setBlocksType(selection, () => $createHeadingNode('h3'));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
@ -362,7 +343,7 @@ function BlockOptionsDropdownList({
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatBulletList = () => {
|
|
|
|
|
if (blockType !== "ul") {
|
|
|
|
|
if (blockType !== 'ul') {
|
|
|
|
|
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND);
|
|
|
|
|
} else {
|
|
|
|
|
editor.dispatchCommand(REMOVE_LIST_COMMAND);
|
|
|
|
@ -371,7 +352,7 @@ function BlockOptionsDropdownList({
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatNumberedList = () => {
|
|
|
|
|
if (blockType !== "ol") {
|
|
|
|
|
if (blockType !== 'ol') {
|
|
|
|
|
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND);
|
|
|
|
|
} else {
|
|
|
|
|
editor.dispatchCommand(REMOVE_LIST_COMMAND);
|
|
|
|
@ -380,12 +361,12 @@ function BlockOptionsDropdownList({
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatQuote = () => {
|
|
|
|
|
if (blockType !== "quote") {
|
|
|
|
|
if (blockType !== 'quote') {
|
|
|
|
|
editor.update(() => {
|
|
|
|
|
const selection = $getSelection();
|
|
|
|
|
|
|
|
|
|
if ($isRangeSelection(selection)) {
|
|
|
|
|
$wrapNodes(selection, () => $createQuoteNode());
|
|
|
|
|
$setBlocksType(selection, () => $createQuoteNode());
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
@ -393,12 +374,12 @@ function BlockOptionsDropdownList({
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatCode = () => {
|
|
|
|
|
if (blockType !== "code") {
|
|
|
|
|
if (blockType !== 'code') {
|
|
|
|
|
editor.update(() => {
|
|
|
|
|
const selection = $getSelection();
|
|
|
|
|
|
|
|
|
|
if ($isRangeSelection(selection)) {
|
|
|
|
|
$wrapNodes(selection, () => $createCodeNode());
|
|
|
|
|
$setBlocksType(selection, () => $createCodeNode());
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
@ -406,41 +387,41 @@ function BlockOptionsDropdownList({
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="dropdown" ref={dropDownRef}>
|
|
|
|
|
<button className="item" onClick={formatParagraph}>
|
|
|
|
|
<span className="icon paragraph" />
|
|
|
|
|
<span className="text">Normal</span>
|
|
|
|
|
{blockType === "paragraph" && <span className="active" />}
|
|
|
|
|
<div className='dropdown' ref={dropDownRef}>
|
|
|
|
|
<button className='item' onClick={formatParagraph}>
|
|
|
|
|
<span className='icon paragraph' />
|
|
|
|
|
<span className='text'>Normal</span>
|
|
|
|
|
{blockType === 'paragraph' && <span className='active' />}
|
|
|
|
|
</button>
|
|
|
|
|
<button className="item" onClick={formatLargeHeading}>
|
|
|
|
|
<span className="icon large-heading" />
|
|
|
|
|
<span className="text">Heading 1</span>
|
|
|
|
|
{blockType === "h1" && <span className="active" />}
|
|
|
|
|
<button className='item' onClick={formatLargeHeading}>
|
|
|
|
|
<span className='icon large-heading' />
|
|
|
|
|
<span className='text'>Heading 1</span>
|
|
|
|
|
{blockType === 'h1' && <span className='active' />}
|
|
|
|
|
</button>
|
|
|
|
|
<button className="item" onClick={formatSmallHeading}>
|
|
|
|
|
<span className="icon small-heading" />
|
|
|
|
|
<span className="text">Heading 2</span>
|
|
|
|
|
{blockType === "h2" && <span className="active" />}
|
|
|
|
|
<button className='item' onClick={formatSmallHeading}>
|
|
|
|
|
<span className='icon small-heading' />
|
|
|
|
|
<span className='text'>Heading 2</span>
|
|
|
|
|
{blockType === 'h2' && <span className='active' />}
|
|
|
|
|
</button>
|
|
|
|
|
<button className="item" onClick={formatSmallHeading3}>
|
|
|
|
|
<span className="icon h3" />
|
|
|
|
|
<span className="text">Heading 3</span>
|
|
|
|
|
{blockType === "h3" && <span className="active" />}
|
|
|
|
|
<button className='item' onClick={formatSmallHeading3}>
|
|
|
|
|
<span className='icon h3' />
|
|
|
|
|
<span className='text'>Heading 3</span>
|
|
|
|
|
{blockType === 'h3' && <span className='active' />}
|
|
|
|
|
</button>
|
|
|
|
|
<button className="item" onClick={formatBulletList}>
|
|
|
|
|
<span className="icon bullet-list" />
|
|
|
|
|
<span className="text">Bullet List</span>
|
|
|
|
|
{blockType === "ul" && <span className="active" />}
|
|
|
|
|
<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 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 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" />
|
|
|
|
@ -451,17 +432,95 @@ function BlockOptionsDropdownList({
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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</span>
|
|
|
|
|
</DropDownItem>
|
|
|
|
|
<DropDownItem
|
|
|
|
|
onClick={() => {
|
|
|
|
|
editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined);
|
|
|
|
|
}}
|
|
|
|
|
className='item'>
|
|
|
|
|
<i className={'icon ' + (isRTL ? 'outdent' : 'indent')} />
|
|
|
|
|
<span className='text'>Indent</span>
|
|
|
|
|
</DropDownItem>
|
|
|
|
|
</DropDown>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function ToolbarPlugin() {
|
|
|
|
|
const [editor] = useLexicalComposerContext();
|
|
|
|
|
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 [blockType, setBlockType] = useState('paragraph');
|
|
|
|
|
const [selectedElementKey, setSelectedElementKey] = useState(null);
|
|
|
|
|
const [showBlockOptionsDropDown, setShowBlockOptionsDropDown] = useState(
|
|
|
|
|
false
|
|
|
|
|
);
|
|
|
|
|
const [codeLanguage, setCodeLanguage] = useState("");
|
|
|
|
|
const [showBlockOptionsDropDown, setShowBlockOptionsDropDown] = useState(false);
|
|
|
|
|
const [codeLanguage, setCodeLanguage] = useState('');
|
|
|
|
|
const [isRTL, setIsRTL] = useState(false);
|
|
|
|
|
const [isLink, setIsLink] = useState(false);
|
|
|
|
|
const [isBold, setIsBold] = useState(false);
|
|
|
|
@ -469,15 +528,43 @@ export default function ToolbarPlugin() {
|
|
|
|
|
const [isUnderline, setIsUnderline] = useState(false);
|
|
|
|
|
const [isStrikethrough, setIsStrikethrough] = useState(false);
|
|
|
|
|
const [isCode, setIsCode] = useState(false);
|
|
|
|
|
const [fontColor, setFontColor] = useState('#000');
|
|
|
|
|
const [bgColor, setBgColor] = useState('#fff');
|
|
|
|
|
const [elementFormat, setElementFormat] = useState('left');
|
|
|
|
|
|
|
|
|
|
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 element = anchorNode.getKey() === 'root' ? anchorNode : anchorNode.getTopLevelElementOrThrow();
|
|
|
|
|
const elementKey = element.getKey();
|
|
|
|
|
const elementDOM = editor.getElementByKey(elementKey);
|
|
|
|
|
if (elementDOM !== null) {
|
|
|
|
@ -487,9 +574,7 @@ export default function ToolbarPlugin() {
|
|
|
|
|
const type = parentList ? parentList.getTag() : element.getTag();
|
|
|
|
|
setBlockType(type);
|
|
|
|
|
} else {
|
|
|
|
|
const type = $isHeadingNode(element)
|
|
|
|
|
? element.getTag()
|
|
|
|
|
: element.getType();
|
|
|
|
|
const type = $isHeadingNode(element) ? element.getTag() : element.getType();
|
|
|
|
|
setBlockType(type);
|
|
|
|
|
if ($isCodeNode(element)) {
|
|
|
|
|
setCodeLanguage(element.getLanguage() || getDefaultCodeLanguage());
|
|
|
|
@ -497,11 +582,11 @@ export default function ToolbarPlugin() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Update text format
|
|
|
|
|
setIsBold(selection.hasFormat("bold"));
|
|
|
|
|
setIsItalic(selection.hasFormat("italic"));
|
|
|
|
|
setIsUnderline(selection.hasFormat("underline"));
|
|
|
|
|
setIsStrikethrough(selection.hasFormat("strikethrough"));
|
|
|
|
|
setIsCode(selection.hasFormat("code"));
|
|
|
|
|
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
|
|
|
|
@ -512,6 +597,17 @@ export default function ToolbarPlugin() {
|
|
|
|
|
} else {
|
|
|
|
|
setIsLink(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle buttons
|
|
|
|
|
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?.getFormatType() || 'left');
|
|
|
|
|
}
|
|
|
|
|
}, [editor]);
|
|
|
|
|
|
|
|
|
@ -566,117 +662,90 @@ export default function ToolbarPlugin() {
|
|
|
|
|
|
|
|
|
|
const insertLink = useCallback(() => {
|
|
|
|
|
if (!isLink) {
|
|
|
|
|
editor.dispatchCommand(TOGGLE_LINK_COMMAND, "https://");
|
|
|
|
|
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.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined);
|
|
|
|
|
}, [editor]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="toolbar" ref={toolbarRef}>
|
|
|
|
|
<div className='toolbar' ref={toolbarRef}>
|
|
|
|
|
<button
|
|
|
|
|
disabled={!canUndo}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
editor.dispatchCommand(UNDO_COMMAND);
|
|
|
|
|
}}
|
|
|
|
|
className="toolbar-item spaced"
|
|
|
|
|
aria-label="Undo"
|
|
|
|
|
>
|
|
|
|
|
<i className="format undo" />
|
|
|
|
|
className='toolbar-item spaced'
|
|
|
|
|
aria-label='Undo'>
|
|
|
|
|
<i className='format undo' />
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
disabled={!canRedo}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
editor.dispatchCommand(REDO_COMMAND);
|
|
|
|
|
}}
|
|
|
|
|
className="toolbar-item"
|
|
|
|
|
aria-label="Redo"
|
|
|
|
|
>
|
|
|
|
|
<i className="format redo" />
|
|
|
|
|
className='toolbar-item'
|
|
|
|
|
aria-label='Redo'>
|
|
|
|
|
<i className='format redo' />
|
|
|
|
|
</button>
|
|
|
|
|
<Divider />
|
|
|
|
|
{supportedBlockTypes.has(blockType) && (
|
|
|
|
|
<>
|
|
|
|
|
<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 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}
|
|
|
|
|
/>,
|
|
|
|
|
<BlockOptionsDropdownList editor={editor} blockType={blockType} toolbarRef={toolbarRef} setShowBlockOptionsDropDown={setShowBlockOptionsDropDown} />,
|
|
|
|
|
document.body
|
|
|
|
|
)}
|
|
|
|
|
<Divider />
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
{blockType === "code" ? (
|
|
|
|
|
{blockType === 'code' ? (
|
|
|
|
|
<>
|
|
|
|
|
<Select
|
|
|
|
|
className="toolbar-item code-language"
|
|
|
|
|
onChange={onCodeLanguageSelect}
|
|
|
|
|
options={codeLanguges}
|
|
|
|
|
value={codeLanguage}
|
|
|
|
|
/>
|
|
|
|
|
<i className="chevron-down inside" />
|
|
|
|
|
<Select className='toolbar-item code-language' onChange={onCodeLanguageSelect} options={codeLanguges} value={codeLanguage} />
|
|
|
|
|
<i className='chevron-down inside' />
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold");
|
|
|
|
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
|
|
|
|
|
}}
|
|
|
|
|
className={"toolbar-item spaced " + (isBold ? "active" : "")}
|
|
|
|
|
aria-label="Format Bold"
|
|
|
|
|
>
|
|
|
|
|
<i className="format bold" />
|
|
|
|
|
className={'toolbar-item spaced ' + (isBold ? 'active' : '')}
|
|
|
|
|
aria-label='Format Bold'>
|
|
|
|
|
<i className='format bold' />
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic");
|
|
|
|
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
|
|
|
|
|
}}
|
|
|
|
|
className={"toolbar-item spaced " + (isItalic ? "active" : "")}
|
|
|
|
|
aria-label="Format Italics"
|
|
|
|
|
>
|
|
|
|
|
<i className="format italic" />
|
|
|
|
|
className={'toolbar-item spaced ' + (isItalic ? 'active' : '')}
|
|
|
|
|
aria-label='Format Italics'>
|
|
|
|
|
<i className='format italic' />
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline");
|
|
|
|
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');
|
|
|
|
|
}}
|
|
|
|
|
className={"toolbar-item spaced " + (isUnderline ? "active" : "")}
|
|
|
|
|
aria-label="Format Underline"
|
|
|
|
|
>
|
|
|
|
|
<i className="format underline" />
|
|
|
|
|
className={'toolbar-item spaced ' + (isUnderline ? 'active' : '')}
|
|
|
|
|
aria-label='Format Underline'>
|
|
|
|
|
<i className='format underline' />
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough");
|
|
|
|
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');
|
|
|
|
|
}}
|
|
|
|
|
className={
|
|
|
|
|
"toolbar-item spaced " + (isStrikethrough ? "active" : "")
|
|
|
|
|
}
|
|
|
|
|
aria-label="Format Strikethrough"
|
|
|
|
|
>
|
|
|
|
|
<i className="format strikethrough" />
|
|
|
|
|
className={'toolbar-item spaced ' + (isStrikethrough ? 'active' : '')}
|
|
|
|
|
aria-label='Format Strikethrough'>
|
|
|
|
|
<i className='format strikethrough' />
|
|
|
|
|
</button>
|
|
|
|
|
{/* <button
|
|
|
|
|
onClick={() => {
|
|
|
|
@ -687,27 +756,40 @@ export default function ToolbarPlugin() {
|
|
|
|
|
>
|
|
|
|
|
<i className="format code" />
|
|
|
|
|
</button> */}
|
|
|
|
|
<button
|
|
|
|
|
onClick={insertLink}
|
|
|
|
|
className={"toolbar-item spaced " + (isLink ? "active" : "")}
|
|
|
|
|
aria-label="Insert Link"
|
|
|
|
|
>
|
|
|
|
|
<i className="format link" />
|
|
|
|
|
<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)}
|
|
|
|
|
{isLink && createPortal(<FloatingLinkEditor editor={editor} />, document.body)}
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined);
|
|
|
|
|
}}
|
|
|
|
|
className={"toolbar-item spaced "}
|
|
|
|
|
aria-label="Insert Horizontal Rule"
|
|
|
|
|
>
|
|
|
|
|
<i className="format icon horizontal-rule" />
|
|
|
|
|
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 />
|
|
|
|
|
<button
|
|
|
|
|
<ElementFormatDropdown disabled={!isEditable} value={elementFormat} editor={editor} isRTL={isRTL} />
|
|
|
|
|
{/* <button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, "left");
|
|
|
|
|
}}
|
|
|
|
@ -742,7 +824,7 @@ export default function ToolbarPlugin() {
|
|
|
|
|
aria-label="Justify Align"
|
|
|
|
|
>
|
|
|
|
|
<i className="format justify-align" />
|
|
|
|
|
</button>{" "}
|
|
|
|
|
</button> */}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|