mirror of
https://github.com/drawdb-io/drawdb.git
synced 2025-10-19 09:16:09 +00:00
Reorganize files
This commit is contained in:
36
src/components/LexicalEditor/AutoLinkPlugin.jsx
Normal file
36
src/components/LexicalEditor/AutoLinkPlugin.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { AutoLinkPlugin } from "@lexical/react/LexicalAutoLinkPlugin";
|
||||
|
||||
const URL_MATCHER =
|
||||
/((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
|
||||
|
||||
const EMAIL_MATCHER =
|
||||
/(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/;
|
||||
|
||||
const MATCHERS = [
|
||||
(text) => {
|
||||
const match = URL_MATCHER.exec(text);
|
||||
return (
|
||||
match && {
|
||||
index: match.index,
|
||||
length: match[0].length,
|
||||
text: match[0],
|
||||
url: match[0],
|
||||
}
|
||||
);
|
||||
},
|
||||
(text) => {
|
||||
const match = EMAIL_MATCHER.exec(text);
|
||||
return (
|
||||
match && {
|
||||
index: match.index,
|
||||
length: match[0].length,
|
||||
text: match[0],
|
||||
url: `mailto:${match[0]}`,
|
||||
}
|
||||
);
|
||||
},
|
||||
];
|
||||
|
||||
export default function PlaygroundAutoLinkPlugin() {
|
||||
return <AutoLinkPlugin matchers={MATCHERS} />;
|
||||
}
|
11
src/components/LexicalEditor/CodeHighlightPlugin.jsx
Normal file
11
src/components/LexicalEditor/CodeHighlightPlugin.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { registerCodeHighlighting } from "@lexical/code";
|
||||
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function CodeHighlightPlugin() {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
useEffect(() => {
|
||||
return registerCodeHighlighting(editor);
|
||||
}, [editor]);
|
||||
return null;
|
||||
}
|
68
src/components/LexicalEditor/ListMaxIndentLevelPlugin.jsx
Normal file
68
src/components/LexicalEditor/ListMaxIndentLevelPlugin.jsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { $getListDepth, $isListItemNode, $isListNode } from "@lexical/list";
|
||||
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
||||
import {
|
||||
$getSelection,
|
||||
$isElementNode,
|
||||
$isRangeSelection,
|
||||
INDENT_CONTENT_COMMAND,
|
||||
COMMAND_PRIORITY_HIGH,
|
||||
} from "lexical";
|
||||
import { useEffect } from "react";
|
||||
|
||||
function getElementNodesInSelection(selection) {
|
||||
const nodesInSelection = selection.getNodes();
|
||||
|
||||
if (nodesInSelection.length === 0) {
|
||||
return new Set([
|
||||
selection.anchor.getNode().getParentOrThrow(),
|
||||
selection.focus.getNode().getParentOrThrow(),
|
||||
]);
|
||||
}
|
||||
|
||||
return new Set(
|
||||
nodesInSelection.map((n) => ($isElementNode(n) ? n : n.getParentOrThrow()))
|
||||
);
|
||||
}
|
||||
|
||||
function isIndentPermitted(maxDepth) {
|
||||
const selection = $getSelection();
|
||||
|
||||
if (!$isRangeSelection(selection)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const elementNodesInSelection = getElementNodesInSelection(selection);
|
||||
|
||||
let totalDepth = 0;
|
||||
|
||||
for (const elementNode of elementNodesInSelection) {
|
||||
if ($isListNode(elementNode)) {
|
||||
totalDepth = Math.max($getListDepth(elementNode) + 1, totalDepth);
|
||||
} else if ($isListItemNode(elementNode)) {
|
||||
const parent = elementNode.getParent();
|
||||
if (!$isListNode(parent)) {
|
||||
throw new Error(
|
||||
"ListMaxIndentLevelPlugin: A ListItemNode must have a ListNode for a parent."
|
||||
);
|
||||
}
|
||||
|
||||
totalDepth = Math.max($getListDepth(parent) + 1, totalDepth);
|
||||
}
|
||||
}
|
||||
|
||||
return totalDepth <= maxDepth;
|
||||
}
|
||||
|
||||
export default function ListMaxIndentLevelPlugin({ maxDepth }) {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerCommand(
|
||||
INDENT_CONTENT_COMMAND,
|
||||
() => !isIndentPermitted(maxDepth ?? 7),
|
||||
COMMAND_PRIORITY_HIGH
|
||||
);
|
||||
}, [editor, maxDepth]);
|
||||
|
||||
return null;
|
||||
}
|
43
src/components/LexicalEditor/RichEditor.jsx
Normal file
43
src/components/LexicalEditor/RichEditor.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
|
||||
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
|
||||
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
|
||||
import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
|
||||
import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
|
||||
import { ListPlugin } from "@lexical/react/LexicalListPlugin";
|
||||
import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin";
|
||||
import { ClearEditorPlugin } from "@lexical/react/LexicalClearEditorPlugin";
|
||||
import { TRANSFORMERS } from "@lexical/markdown";
|
||||
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
|
||||
import ToolbarPlugin from "./ToolbarPlugin";
|
||||
import ListMaxIndentLevelPlugin from "./ListMaxIndentLevelPlugin";
|
||||
import CodeHighlightPlugin from "./CodeHighlightPlugin";
|
||||
import AutoLinkPlugin from "./AutoLinkPlugin";
|
||||
import "./styles/index.css";
|
||||
|
||||
function Placeholder({ text }) {
|
||||
return <div className="editor-placeholder">{text || ""}</div>;
|
||||
}
|
||||
|
||||
export default function RichEditor({ theme, placeholder }) {
|
||||
return (
|
||||
<div className="editor-container">
|
||||
<ToolbarPlugin theme={theme} />
|
||||
<div className="editor-inner">
|
||||
<RichTextPlugin
|
||||
contentEditable={<ContentEditable className="editor-input" />}
|
||||
placeholder={<Placeholder text={placeholder} />}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
<HistoryPlugin />
|
||||
<AutoFocusPlugin />
|
||||
<CodeHighlightPlugin />
|
||||
<ListPlugin />
|
||||
<LinkPlugin />
|
||||
<AutoLinkPlugin />
|
||||
<ListMaxIndentLevelPlugin maxDepth={7} />
|
||||
<MarkdownShortcutPlugin transformers={TRANSFORMERS} />
|
||||
<ClearEditorPlugin />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
630
src/components/LexicalEditor/ToolbarPlugin.jsx
Normal file
630
src/components/LexicalEditor/ToolbarPlugin.jsx
Normal file
@@ -0,0 +1,630 @@
|
||||
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,
|
||||
$getSelection,
|
||||
$isRangeSelection,
|
||||
$createParagraphNode,
|
||||
$getNodeByKey,
|
||||
} from "lexical";
|
||||
import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
|
||||
import {
|
||||
$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 { Dropdown } from "@douyinfe/semi-ui";
|
||||
import "./styles/index.css";
|
||||
|
||||
const LowPriority = 1;
|
||||
|
||||
const blockTypeToIcon = {
|
||||
code: "bi-code-slash",
|
||||
h1: "bi-type-h1",
|
||||
h2: "bi-type-h2",
|
||||
ol: "bi-list-ol",
|
||||
paragraph: "bi-text-paragraph",
|
||||
quote: "bi-chat-square-quote",
|
||||
ul: "bi-list-ul",
|
||||
};
|
||||
|
||||
const blockTypeToBlockName = {
|
||||
paragraph: "Paragraph",
|
||||
h1: "Large Heading",
|
||||
h2: "Small Heading",
|
||||
ul: "Bulleted List",
|
||||
ol: "Numbered List",
|
||||
code: "Code Block",
|
||||
quote: "Quote",
|
||||
};
|
||||
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Select({ onChange, className, options, value }) {
|
||||
return (
|
||||
<select className={className} onChange={onChange} value={value}>
|
||||
<option hidden={true} value="" />
|
||||
{options.map((option) => (
|
||||
<option className="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 BlockOptionsDropdownList({ editor, blockType }) {
|
||||
const formatParagraph = () => {
|
||||
if (blockType !== "paragraph") {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
$wrapNodes(selection, () => $createParagraphNode());
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const formatLargeHeading = () => {
|
||||
if (blockType !== "h1") {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
$wrapNodes(selection, () => $createHeadingNode("h1"));
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const formatSmallHeading = () => {
|
||||
if (blockType !== "h2") {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
$wrapNodes(selection, () => $createHeadingNode("h2"));
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const formatBulletList = () => {
|
||||
if (blockType !== "ul") {
|
||||
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND);
|
||||
} else {
|
||||
editor.dispatchCommand(REMOVE_LIST_COMMAND);
|
||||
}
|
||||
};
|
||||
|
||||
const formatNumberedList = () => {
|
||||
if (blockType !== "ol") {
|
||||
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND);
|
||||
} else {
|
||||
editor.dispatchCommand(REMOVE_LIST_COMMAND);
|
||||
}
|
||||
};
|
||||
|
||||
const formatQuote = () => {
|
||||
if (blockType !== "quote") {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
$wrapNodes(selection, () => $createQuoteNode());
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const formatCode = () => {
|
||||
if (blockType !== "code") {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
$wrapNodes(selection, () => $createCodeNode());
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
trigger="click"
|
||||
clickToHide
|
||||
render={
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
onClick={formatParagraph}
|
||||
icon={<i className={`bi ${blockTypeToIcon.paragraph}`} />}
|
||||
>
|
||||
Paragraph
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
onClick={formatLargeHeading}
|
||||
icon={<i className={`bi ${blockTypeToIcon.h1}`} />}
|
||||
>
|
||||
Large Heading
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
onClick={formatSmallHeading}
|
||||
icon={<i className={`bi ${blockTypeToIcon.h2}`} />}
|
||||
>
|
||||
Small Heading
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
onClick={formatBulletList}
|
||||
icon={<i className={`bi ${blockTypeToIcon.ul}`} />}
|
||||
>
|
||||
Bullet List
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
onClick={formatNumberedList}
|
||||
icon={<i className={`bi ${blockTypeToIcon.ol}`} />}
|
||||
>
|
||||
Numbered List
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
onClick={formatQuote}
|
||||
icon={<i className={`bi ${blockTypeToIcon.quote}`} />}
|
||||
>
|
||||
Quote
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
onClick={formatCode}
|
||||
icon={<i className={`bi ${blockTypeToIcon.code}`} />}
|
||||
>
|
||||
Code Block
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
}
|
||||
>
|
||||
<button
|
||||
className="flex mx-2 justify-center items-center"
|
||||
aria-label="Formatting Options"
|
||||
>
|
||||
<i className={`bi ${blockTypeToIcon[blockType]} me-3`} />
|
||||
<span className="me-3 text-sm">{blockTypeToBlockName[blockType]}</span>
|
||||
<i className="bi bi-chevron-down" />
|
||||
</button>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ToolbarPlugin() {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const toolbarRef = useRef(null);
|
||||
const [canUndo, setCanUndo] = useState(false);
|
||||
const [canRedo, setCanRedo] = useState(false);
|
||||
const [blockType, setBlockType] = useState("paragraph");
|
||||
const [selectedElementKey, setSelectedElementKey] = useState(null);
|
||||
const [codeLanguage, setCodeLanguage] = useState("");
|
||||
const [, 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 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setIsBold(selection.hasFormat("bold"));
|
||||
setIsItalic(selection.hasFormat("italic"));
|
||||
setIsUnderline(selection.hasFormat("underline"));
|
||||
setIsStrikethrough(selection.hasFormat("strikethrough"));
|
||||
setIsCode(selection.hasFormat("code"));
|
||||
setIsRTL($isParentElementRTL(selection));
|
||||
|
||||
const node = getSelectedNode(selection);
|
||||
const parent = node.getParent();
|
||||
if ($isLinkNode(parent) || $isLinkNode(node)) {
|
||||
setIsLink(true);
|
||||
} else {
|
||||
setIsLink(false);
|
||||
}
|
||||
}
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
editor.registerUpdateListener(({ editorState }) => {
|
||||
editorState.read(() => {
|
||||
updateToolbar();
|
||||
});
|
||||
}),
|
||||
editor.registerCommand(
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
() => {
|
||||
updateToolbar();
|
||||
return false;
|
||||
},
|
||||
LowPriority
|
||||
),
|
||||
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]);
|
||||
|
||||
return (
|
||||
<div className="toolbar" ref={toolbarRef}>
|
||||
<button
|
||||
disabled={!canUndo}
|
||||
onClick={() => editor.dispatchCommand(UNDO_COMMAND)}
|
||||
className="toolbar-item spaced"
|
||||
aria-label="Undo"
|
||||
>
|
||||
<i
|
||||
className={`bi bi-arrow-counterclockwise ${
|
||||
canUndo ? "" : "opacity-30"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
disabled={!canRedo}
|
||||
onClick={() => editor.dispatchCommand(REDO_COMMAND)}
|
||||
className="toolbar-item"
|
||||
aria-label="Redo"
|
||||
>
|
||||
<i className={`bi bi-arrow-clockwise ${canRedo ? "" : "opacity-30"}`} />
|
||||
</button>
|
||||
<Divider />
|
||||
<BlockOptionsDropdownList editor={editor} blockType={blockType} />
|
||||
<Divider />
|
||||
{blockType === "code" ? (
|
||||
<div className="flex items-center">
|
||||
<Select
|
||||
className="toolbar-item code-language"
|
||||
onChange={onCodeLanguageSelect}
|
||||
options={codeLanguges}
|
||||
value={codeLanguage}
|
||||
/>
|
||||
<i className="bi bi-chevron-down" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold")}
|
||||
className={"toolbar-item spaced " + (isBold ? "active" : "")}
|
||||
aria-label="Format Bold"
|
||||
>
|
||||
<i className="bi bi-type-bold" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic")
|
||||
}
|
||||
className={"toolbar-item spaced " + (isItalic ? "active" : "")}
|
||||
aria-label="Format Italics"
|
||||
>
|
||||
<i className="bi bi-type-italic" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline")
|
||||
}
|
||||
className={"toolbar-item spaced " + (isUnderline ? "active" : "")}
|
||||
aria-label="Format Underline"
|
||||
>
|
||||
<i className="bi bi-type-underline" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough")
|
||||
}
|
||||
className={
|
||||
"toolbar-item spaced " + (isStrikethrough ? "active" : "")
|
||||
}
|
||||
aria-label="Format Strikethrough"
|
||||
>
|
||||
<i className="bi bi-type-strikethrough" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code")}
|
||||
className={"toolbar-item spaced " + (isCode ? "active" : "")}
|
||||
aria-label="Insert Code"
|
||||
>
|
||||
<i className="bi bi-code-slash" />
|
||||
</button>
|
||||
<button
|
||||
onClick={insertLink}
|
||||
className={"toolbar-item spaced " + (isLink ? "active" : "")}
|
||||
aria-label="Insert Link"
|
||||
>
|
||||
<i className="bi bi-link" />
|
||||
</button>
|
||||
{isLink &&
|
||||
createPortal(
|
||||
<FloatingLinkEditor editor={editor} />,
|
||||
document.body
|
||||
)}
|
||||
<Divider />
|
||||
<button
|
||||
onClick={() =>
|
||||
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, "left")
|
||||
}
|
||||
className="toolbar-item spaced"
|
||||
aria-label="Left Align"
|
||||
>
|
||||
<i className="bi bi-text-left" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, "center")
|
||||
}
|
||||
className="toolbar-item spaced"
|
||||
aria-label="Center Align"
|
||||
>
|
||||
<i className="bi bi-text-center" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, "right");
|
||||
}}
|
||||
className="toolbar-item spaced"
|
||||
aria-label="Right Align"
|
||||
>
|
||||
<i className="bi bi-text-right" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, "justify");
|
||||
}}
|
||||
className="toolbar-item"
|
||||
aria-label="Justify Align"
|
||||
>
|
||||
<i className="bi bi-justify" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
391
src/components/LexicalEditor/styles/index.css
Normal file
391
src/components/LexicalEditor/styles/index.css
Normal file
@@ -0,0 +1,391 @@
|
||||
.ltr {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.rtl {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
margin: 16px auto 16px auto;
|
||||
border-radius: 6px;
|
||||
color: var(--semi-color-text-1);
|
||||
background-color: rgba(var(--semi-grey-1), 1);
|
||||
position: relative;
|
||||
line-height: 20px;
|
||||
font-weight: 400;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.editor-inner {
|
||||
background-color: rgba(var(--semi-grey-1), 1);
|
||||
position: relative;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.editor-input {
|
||||
min-height: 160px;
|
||||
resize: none;
|
||||
font-size: 15px;
|
||||
position: relative;
|
||||
tab-size: 1;
|
||||
outline: 0;
|
||||
padding: 15px 10px;
|
||||
}
|
||||
|
||||
.editor-placeholder {
|
||||
color: #999;
|
||||
position: absolute;
|
||||
text-overflow: ellipsis;
|
||||
top: 15px;
|
||||
left: 10px;
|
||||
font-size: 15px;
|
||||
user-select: none;
|
||||
display: inline-block;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.editor-text-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.editor-text-italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.editor-text-underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.editor-text-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.editor-text-underlineStrikethrough {
|
||||
text-decoration: underline line-through;
|
||||
}
|
||||
|
||||
.editor-text-code {
|
||||
background-color: rgba(var(--semi-grey-2), 1);
|
||||
padding: 1px 0.25rem;
|
||||
font-family: Menlo, Consolas, Monaco, monospace;
|
||||
font-size: 94%;
|
||||
}
|
||||
|
||||
.editor-link {
|
||||
color: rgb(33, 111, 219);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.editor-code {
|
||||
background-color: rgba(var(--semi-grey-0), 1);
|
||||
font-family: Menlo, Consolas, Monaco, monospace;
|
||||
display: block;
|
||||
padding: 8px 8px 8px 52px;
|
||||
line-height: 1.53;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
tab-size: 2;
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.editor-code:before {
|
||||
content: attr(data-gutter);
|
||||
position: absolute;
|
||||
background-color: rgba(var(--semi-grey-0), 1);
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-right: 1px solid rgba(var(--semi-grey-3), 1);
|
||||
padding: 8px;
|
||||
color: #777;
|
||||
white-space: pre-wrap;
|
||||
text-align: right;
|
||||
min-width: 25px;
|
||||
}
|
||||
|
||||
.editor-code:after {
|
||||
content: attr(data-highlight-language);
|
||||
top: 0;
|
||||
right: 3px;
|
||||
padding: 3px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
position: absolute;
|
||||
color: rgba(var(--semi-text-1), 1);
|
||||
}
|
||||
|
||||
.editor-tokenComment {
|
||||
color: slategray;
|
||||
}
|
||||
|
||||
.editor-tokenPunctuation {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.editor-tokenProperty {
|
||||
color: #905;
|
||||
}
|
||||
|
||||
.editor-tokenSelector {
|
||||
color: #690;
|
||||
}
|
||||
|
||||
.editor-tokenOperator {
|
||||
color: #9a6e3a;
|
||||
}
|
||||
|
||||
.editor-tokenAttr {
|
||||
color: #07a;
|
||||
}
|
||||
|
||||
.editor-tokenVariable {
|
||||
color: #e90;
|
||||
}
|
||||
|
||||
.editor-tokenFunction {
|
||||
color: #dd4a68;
|
||||
}
|
||||
|
||||
.editor-paragraph {
|
||||
margin: 0;
|
||||
margin-bottom: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.editor-paragraph:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.editor-heading-h1 {
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
margin-bottom: 12px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.editor-heading-h2 {
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
margin-top: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.editor-quote {
|
||||
margin: 0;
|
||||
margin-left: 20px;
|
||||
font-size: 15px;
|
||||
color: rgb(101, 103, 107);
|
||||
border-left-color: rgb(206, 208, 212);
|
||||
border-left-width: 4px;
|
||||
border-left-style: solid;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.editor-list-ol {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-left: 16px;
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.editor-list-ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-left: 16px;
|
||||
list-style-type: circle;
|
||||
}
|
||||
|
||||
.editor-listitem {
|
||||
margin: 8px 32px 8px 32px;
|
||||
}
|
||||
|
||||
.editor-nested-listitem {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
pre::-webkit-scrollbar {
|
||||
background: transparent;
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
pre::-webkit-scrollbar-thumb {
|
||||
background: #999;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1px;
|
||||
background-color: rgba(var(--semi-grey-1), 1);
|
||||
padding: 4px;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
border-bottom: rgba(var(--semi-grey-2), 1) solid 2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.toolbar button.toolbar-item {
|
||||
border: 0;
|
||||
display: flex;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.toolbar button.toolbar-item:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.toolbar button.toolbar-item.spaced {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.toolbar button.toolbar-item i.format {
|
||||
background-size: contain;
|
||||
display: inline-block;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
margin-top: 2px;
|
||||
vertical-align: -0.25em;
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.toolbar button.toolbar-item:disabled i.format {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.toolbar button.toolbar-item.active {
|
||||
background-color: rgba(var(--semi-grey-2), 1);
|
||||
}
|
||||
|
||||
.toolbar button.toolbar-item.active i {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.toolbar .toolbar-item:hover:not([disabled]) {
|
||||
background-color: rgba(var(--semi-grey-2), 1);
|
||||
}
|
||||
|
||||
.toolbar .divider {
|
||||
width: 2px;
|
||||
background-color: rgba(var(--semi-grey-2), 1);
|
||||
margin: 4px 4px;
|
||||
}
|
||||
|
||||
.toolbar select.toolbar-item {
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
vertical-align: middle;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
font-size: 14px;
|
||||
background-color: rgba(var(--semi-grey-1), 1);
|
||||
text-overflow: ellipsis;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.toolbar select.code-language {
|
||||
text-transform: capitalize;
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
.toolbar .toolbar-item .text {
|
||||
line-height: 20px;
|
||||
width: 200px;
|
||||
vertical-align: middle;
|
||||
font-size: 14px;
|
||||
color: rgba(var(--semi-text-1), 1);
|
||||
text-overflow: ellipsis;
|
||||
width: 70px;
|
||||
overflow: hidden;
|
||||
height: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.toolbar .toolbar-item .icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
user-select: none;
|
||||
margin-right: 8px;
|
||||
line-height: 16px;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.link-editor {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
top: -10000px;
|
||||
left: -10000px;
|
||||
margin-top: -6px;
|
||||
max-width: 300px;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
background-color: rgba(var(--semi-grey-1), 1);
|
||||
box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 6px;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.link-editor .link-input {
|
||||
display: block;
|
||||
width: calc(100% - 24px);
|
||||
box-sizing: border-box;
|
||||
margin: 8px 12px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
background-color: rgba(var(--semi-grey-2), 1);
|
||||
font-size: 15px;
|
||||
color: var(--semi-color-text-1);
|
||||
border: 0;
|
||||
outline: 0;
|
||||
position: relative;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.link-editor .link-input a {
|
||||
color: rgb(33, 111, 219);
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
margin-right: 30px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.link-editor .link-input a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.link-editor .button {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
padding: 6px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.link-editor .button.hovered {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.link-editor .button i,
|
||||
.actions i {
|
||||
background-size: contain;
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
vertical-align: -0.25em;
|
||||
}
|
Reference in New Issue
Block a user