From b0ff77d6fcdcb3a70cbfd1ef054d2c23d6d4ba91 Mon Sep 17 00:00:00 2001 From: Yume <2839681263@qq.com> Date: Thu, 29 May 2025 15:01:40 +0800 Subject: [PATCH 1/2] feat(UI): add blob page --- archived/moon/.env.local | 6 +- .../CodeView/BlobView/CodeContent.module.css | 34 + .../CodeView/BlobView/CodeContent.tsx | 123 +++ .../components/CodeView/BlobView/Editor.tsx | 80 ++ .../CodeView/BlobView/ExampleTheme.js | 69 ++ .../CodeView/BlobView/ToolbarPlugin.js | 609 +++++++++++++++ .../components/CodeView/BlobView/editor.css | 721 ++++++++++++++++++ .../web/components/CodeView/CodeTable.tsx | 10 +- moon/apps/web/hooks/useGetBlob.ts | 12 + moon/apps/web/package.json | 7 + .../web/pages/[org]/code/blob/[...path].tsx | 73 ++ moon/apps/web/public/icons/LICENSE.md | 5 + .../apps/web/public/icons/arrow-clockwise.svg | 4 + .../public/icons/arrow-counterclockwise.svg | 4 + moon/apps/web/public/icons/backtop.svg | 1 + .../web/public/icons/chat-square-quote.svg | 4 + moon/apps/web/public/icons/chevron-down.svg | 3 + moon/apps/web/public/icons/code.svg | 1 + moon/apps/web/public/icons/commit.svg | 1 + moon/apps/web/public/icons/file.svg | 1 + moon/apps/web/public/icons/folder.svg | 1 + .../web/public/icons/git-pull-request.svg | 1 + moon/apps/web/public/icons/headImage.svg | 1 + moon/apps/web/public/icons/help.svg | 1 + moon/apps/web/public/icons/issues.svg | 1 + moon/apps/web/public/icons/journal-code.svg | 5 + moon/apps/web/public/icons/journal-text.svg | 5 + moon/apps/web/public/icons/justify.svg | 3 + moon/apps/web/public/icons/link.svg | 4 + moon/apps/web/public/icons/list-ol.svg | 4 + moon/apps/web/public/icons/list-ul.svg | 3 + moon/apps/web/public/icons/markdown.svg | 1 + moon/apps/web/public/icons/notification.svg | 1 + moon/apps/web/public/icons/pencil-fill.svg | 3 + moon/apps/web/public/icons/text-center.svg | 3 + moon/apps/web/public/icons/text-left.svg | 3 + moon/apps/web/public/icons/text-paragraph.svg | 3 + moon/apps/web/public/icons/text-right.svg | 3 + moon/apps/web/public/icons/type-bold.svg | 3 + moon/apps/web/public/icons/type-h1.svg | 3 + moon/apps/web/public/icons/type-h2.svg | 3 + moon/apps/web/public/icons/type-h3.svg | 3 + moon/apps/web/public/icons/type-italic.svg | 3 + .../web/public/icons/type-strikethrough.svg | 3 + moon/apps/web/public/icons/type-underline.svg | 3 + moon/pnpm-lock.yaml | 32 + 46 files changed, 1865 insertions(+), 2 deletions(-) create mode 100644 moon/apps/web/components/CodeView/BlobView/CodeContent.module.css create mode 100644 moon/apps/web/components/CodeView/BlobView/CodeContent.tsx create mode 100644 moon/apps/web/components/CodeView/BlobView/Editor.tsx create mode 100644 moon/apps/web/components/CodeView/BlobView/ExampleTheme.js create mode 100644 moon/apps/web/components/CodeView/BlobView/ToolbarPlugin.js create mode 100644 moon/apps/web/components/CodeView/BlobView/editor.css create mode 100644 moon/apps/web/hooks/useGetBlob.ts create mode 100644 moon/apps/web/pages/[org]/code/blob/[...path].tsx create mode 100644 moon/apps/web/public/icons/LICENSE.md create mode 100644 moon/apps/web/public/icons/arrow-clockwise.svg create mode 100644 moon/apps/web/public/icons/arrow-counterclockwise.svg create mode 100644 moon/apps/web/public/icons/backtop.svg create mode 100644 moon/apps/web/public/icons/chat-square-quote.svg create mode 100644 moon/apps/web/public/icons/chevron-down.svg create mode 100644 moon/apps/web/public/icons/code.svg create mode 100644 moon/apps/web/public/icons/commit.svg create mode 100644 moon/apps/web/public/icons/file.svg create mode 100644 moon/apps/web/public/icons/folder.svg create mode 100644 moon/apps/web/public/icons/git-pull-request.svg create mode 100644 moon/apps/web/public/icons/headImage.svg create mode 100644 moon/apps/web/public/icons/help.svg create mode 100644 moon/apps/web/public/icons/issues.svg create mode 100644 moon/apps/web/public/icons/journal-code.svg create mode 100644 moon/apps/web/public/icons/journal-text.svg create mode 100644 moon/apps/web/public/icons/justify.svg create mode 100644 moon/apps/web/public/icons/link.svg create mode 100644 moon/apps/web/public/icons/list-ol.svg create mode 100644 moon/apps/web/public/icons/list-ul.svg create mode 100644 moon/apps/web/public/icons/markdown.svg create mode 100644 moon/apps/web/public/icons/notification.svg create mode 100644 moon/apps/web/public/icons/pencil-fill.svg create mode 100644 moon/apps/web/public/icons/text-center.svg create mode 100644 moon/apps/web/public/icons/text-left.svg create mode 100644 moon/apps/web/public/icons/text-paragraph.svg create mode 100644 moon/apps/web/public/icons/text-right.svg create mode 100644 moon/apps/web/public/icons/type-bold.svg create mode 100644 moon/apps/web/public/icons/type-h1.svg create mode 100644 moon/apps/web/public/icons/type-h2.svg create mode 100644 moon/apps/web/public/icons/type-h3.svg create mode 100644 moon/apps/web/public/icons/type-italic.svg create mode 100644 moon/apps/web/public/icons/type-strikethrough.svg create mode 100644 moon/apps/web/public/icons/type-underline.svg diff --git a/archived/moon/.env.local b/archived/moon/.env.local index 4a31f2162..15cb2b49c 100644 --- a/archived/moon/.env.local +++ b/archived/moon/.env.local @@ -1 +1,5 @@ -NEXT_PUBLIC_GOOGLE_ANALYTICS_ID=G-WCSCZGFL72 \ No newline at end of file +NEXT_PUBLIC_GOOGLE_ANALYTICS_ID=G-WCSCZGFL72 +MEGA_HOST=http://git.gitmega.com +MEGA_INTERNAL_HOST=http://git.gitmega.com +# MEGA_HOST=http://localhost:8000 +# MEGA_INTERNAL_HOST=http://localhost:8000 \ No newline at end of file diff --git a/moon/apps/web/components/CodeView/BlobView/CodeContent.module.css b/moon/apps/web/components/CodeView/BlobView/CodeContent.module.css new file mode 100644 index 000000000..9469cdf76 --- /dev/null +++ b/moon/apps/web/components/CodeView/BlobView/CodeContent.module.css @@ -0,0 +1,34 @@ + +.codeLineNumber { + margin-left: 15px; + margin-right: 25px; +} + +.viewChangeTab { + background-color: rgba(53, 53, 53, 0.103); + display: flex; + position: absolute; + height: 50px; + border-top-left-radius: 1rem; + border-top-right-radius: 1rem; +} + +.viewChangeTabButton { + padding-left: 20px; + padding-right: 20px; + height: 100%; + border-radius: 1rem; + border: none; + background-color: transparent; +} + +.viewChangeTabButton:hover, +.viewChangeTabButton:checked { + background-color: rgba(0, 0, 0, 0.121); +} + +.fileCodeContainer { + border-radius: 0.5rem; + margin-top: 10px; + +} \ No newline at end of file diff --git a/moon/apps/web/components/CodeView/BlobView/CodeContent.tsx b/moon/apps/web/components/CodeView/BlobView/CodeContent.tsx new file mode 100644 index 000000000..831d15e87 --- /dev/null +++ b/moon/apps/web/components/CodeView/BlobView/CodeContent.tsx @@ -0,0 +1,123 @@ +import Editor from '@/components/CodeView/BlobView/Editor' + +import 'github-markdown-css/github-markdown-light.css' + +import { useEffect, useRef, useState } from 'react' +import { Highlight, themes } from 'prism-react-renderer' +import { createRoot } from 'react-dom/client' + +import styles from './CodeContent.module.css' + +// @ts-ignore +const CodeContent = ({ fileContent }: string) => { + const [showEditor, setShowEditor] = useState(false) + const [lfs, setLfs] = useState(false) + + useEffect(() => { + if (isLfsContent(fileContent)) { + setLfs(true) + } + }, [fileContent]) + + const lineRef = useRef([]) + // @ts-ignore + const handleLineNumberClick = (lineIndex) => { + setShowEditor(!showEditor) + const codeLineNumber = lineRef.current[lineIndex] + + if (showEditor) { + const editorContainer = document.createElement('div') + + editorContainer.className = 'editor-container' + + const root = createRoot(editorContainer) + + root.render() + if (codeLineNumber && codeLineNumber.parentNode) { + codeLineNumber.parentNode.insertBefore(editorContainer, codeLineNumber.nextSibling) + } + } else { + const editorContainer = document.querySelector('.editor-container') + + if (editorContainer && editorContainer.parentNode) { + editorContainer.parentNode.removeChild(editorContainer) + } + } + } + + function isLfsContent(content: string): boolean { + const lines = content.split('\n') + let foundVersion = false + let foundOid = false + let foundSize = false + + for (const line of lines) { + if (line.startsWith('version ')) { + foundVersion = true + } else if (line.startsWith('oid sha256:')) { + foundOid = true + } else if (line.startsWith('size ')) { + foundSize = true + } + if (foundVersion && foundOid && foundSize) { + return true + } + } + return false + } + + return ( +
+
+ + +
+ + + {({ style, tokens, getLineProps, getTokenProps }) => ( +
+            {!lfs &&
+              tokens.map((line, i) => (
+                
lineRef.current[i] = el as HTMLDivElement} + > + + {i + 1} + {line.map((token, key) => ( + + ))} +
+ ))} + {lfs && (Sorry about that, but we can’t show files that are this big right now.)} +
+ )} +
+
+ ) +} + +export default CodeContent diff --git a/moon/apps/web/components/CodeView/BlobView/Editor.tsx b/moon/apps/web/components/CodeView/BlobView/Editor.tsx new file mode 100644 index 000000000..43846df1e --- /dev/null +++ b/moon/apps/web/components/CodeView/BlobView/Editor.tsx @@ -0,0 +1,80 @@ +import { useEffect } from "react"; +import { CodeHighlightNode, CodeNode, registerCodeHighlighting } from "@lexical/code"; +import { AutoLinkNode, LinkNode } from "@lexical/link"; +import { ListItemNode, ListNode } from "@lexical/list"; +import { TRANSFORMERS } from "@lexical/markdown"; +import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin"; +import { LexicalComposer } from "@lexical/react/LexicalComposer"; +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { ContentEditable } from "@lexical/react/LexicalContentEditable"; +import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin"; +import { ListPlugin } from "@lexical/react/LexicalListPlugin"; +import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin"; +import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin"; +import { HeadingNode, QuoteNode } from "@lexical/rich-text"; +import { TableCellNode, TableNode, TableRowNode } from "@lexical/table"; + +import "./editor.css"; +import ExampleTheme from "./ExampleTheme"; +import ToolbarPlugin from "./ToolbarPlugin"; + + +function Placeholder() { + return
Writing your comment...
; +} + +function CodeHighlightPlugin() { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + return registerCodeHighlighting(editor); + }, [editor]); + return null; +} + +const editorConfig = { + theme: ExampleTheme, + // Handling of errors during update + onError(error: any) { + throw error + }, + // Any custom nodes go here + nodes: [ + HeadingNode, + ListNode, + ListItemNode, + QuoteNode, + CodeNode, + CodeHighlightNode, + TableNode, + TableCellNode, + TableRowNode, + AutoLinkNode, + LinkNode + ] +} + + +export default function Editor() { + return ( + // @ts-ignore + +
+ +
+ } + placeholder={} + // @ts-ignore + ErrorBoundary={undefined} + /> + + + + + +
+
+
+ ); +} diff --git a/moon/apps/web/components/CodeView/BlobView/ExampleTheme.js b/moon/apps/web/components/CodeView/BlobView/ExampleTheme.js new file mode 100644 index 000000000..f0911a719 --- /dev/null +++ b/moon/apps/web/components/CodeView/BlobView/ExampleTheme.js @@ -0,0 +1,69 @@ +const exampleTheme = { + ltr: "ltr", + rtl: "rtl", + placeholder: "editor-placeholder", + paragraph: "editor-paragraph", + quote: "editor-quote", + heading: { + h1: "editor-heading-h1", + h2: "editor-heading-h2", + h3: "editor-heading-h3", + h4: "editor-heading-h4", + h5: "editor-heading-h5" + }, + list: { + nested: { + listitem: "editor-nested-listitem" + }, + ol: "editor-list-ol", + ul: "editor-list-ul", + listitem: "editor-listitem" + }, + image: "editor-image", + link: "editor-link", + text: { + bold: "editor-text-bold", + italic: "editor-text-italic", + overflowed: "editor-text-overflowed", + hashtag: "editor-text-hashtag", + underline: "editor-text-underline", + strikethrough: "editor-text-strikethrough", + underlineStrikethrough: "editor-text-underlineStrikethrough", + code: "editor-text-code" + }, + code: "editor-code", + codeHighlight: { + atrule: "editor-tokenAttr", + attr: "editor-tokenAttr", + boolean: "editor-tokenProperty", + builtin: "editor-tokenSelector", + cdata: "editor-tokenComment", + char: "editor-tokenSelector", + class: "editor-tokenFunction", + "class-name": "editor-tokenFunction", + comment: "editor-tokenComment", + constant: "editor-tokenProperty", + deleted: "editor-tokenProperty", + doctype: "editor-tokenComment", + entity: "editor-tokenOperator", + function: "editor-tokenFunction", + important: "editor-tokenVariable", + inserted: "editor-tokenSelector", + keyword: "editor-tokenAttr", + namespace: "editor-tokenVariable", + number: "editor-tokenProperty", + operator: "editor-tokenOperator", + prolog: "editor-tokenComment", + property: "editor-tokenProperty", + punctuation: "editor-tokenPunctuation", + regex: "editor-tokenVariable", + selector: "editor-tokenSelector", + string: "editor-tokenSelector", + symbol: "editor-tokenProperty", + tag: "editor-tokenProperty", + url: "editor-tokenOperator", + variable: "editor-tokenVariable" + } +}; + +export default exampleTheme; diff --git a/moon/apps/web/components/CodeView/BlobView/ToolbarPlugin.js b/moon/apps/web/components/CodeView/BlobView/ToolbarPlugin.js new file mode 100644 index 000000000..a134fed93 --- /dev/null +++ b/moon/apps/web/components/CodeView/BlobView/ToolbarPlugin.js @@ -0,0 +1,609 @@ +import { + $createCodeNode, + $isCodeNode, + getCodeLanguages, + getDefaultCodeLanguage +} from "@lexical/code"; +import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link"; +import { + $isListNode, + INSERT_ORDERED_LIST_COMMAND, + INSERT_UNORDERED_LIST_COMMAND, + ListNode, + REMOVE_LIST_COMMAND +} from "@lexical/list"; +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { + $createHeadingNode, + $createQuoteNode, + $isHeadingNode +} from "@lexical/rich-text"; +import { $isAtNodeEnd, $wrapNodes } from "@lexical/selection"; +import { $getNearestNodeOfType, mergeRegister } from "@lexical/utils"; +import { + $createParagraphNode, + $getNodeByKey, + $getSelection, + $isRangeSelection, + FORMAT_TEXT_COMMAND, + SELECTION_CHANGE_COMMAND +} from "lexical"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { createPortal } from "react-dom"; + +const LowPriority = 1; + +const supportedBlockTypes = new Set([ + "paragraph", + "quote", + "code", + "h1", + "h2", + "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" +}; + +function Divider() { + return
; +} + +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 ( +
+ {isEditMode ? ( + { + 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); + } + }} + /> + ) : ( + <> +
+ + {linkUrl} + +
event.preventDefault()} + onClick={() => { + setEditMode(true); + }} + /> +
+ + )} +
+ ); +} + +function Select({ onChange, className, options, value }) { + return ( + + ); +} + +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, + 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)) { + $wrapNodes(selection, () => $createParagraphNode()); + } + }); + } + setShowBlockOptionsDropDown(false); + }; + + const formatLargeHeading = () => { + if (blockType !== "h1") { + editor.update(() => { + const selection = $getSelection(); + + if ($isRangeSelection(selection)) { + $wrapNodes(selection, () => $createHeadingNode("h1")); + } + }); + } + setShowBlockOptionsDropDown(false); + }; + + const formatSmallHeading = () => { + if (blockType !== "h2") { + editor.update(() => { + const selection = $getSelection(); + + if ($isRangeSelection(selection)) { + $wrapNodes(selection, () => $createHeadingNode("h2")); + } + }); + } + 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)) { + $wrapNodes(selection, () => $createQuoteNode()); + } + }); + } + setShowBlockOptionsDropDown(false); + }; + + const formatCode = () => { + if (blockType !== "code") { + editor.update(() => { + const selection = $getSelection(); + + if ($isRangeSelection(selection)) { + $wrapNodes(selection, () => $createCodeNode()); + } + }); + } + setShowBlockOptionsDropDown(false); + }; + + return ( +
+ + + + + + + +
+ ); +} + +export default function ToolbarPlugin() { + const [editor] = useLexicalComposerContext(); + const toolbarRef = useRef(null); + const [blockType, setBlockType] = useState("paragraph"); + const [selectedElementKey, setSelectedElementKey] = useState(null); + const [showBlockOptionsDropDown, setShowBlockOptionsDropDown] = useState( + false + ); + const [codeLanguage, setCodeLanguage] = useState(""); + 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()); + } + } + } + // Update text format + setIsBold(selection.hasFormat("bold")); + setIsItalic(selection.hasFormat("italic")); + setIsUnderline(selection.hasFormat("underline")); + setIsStrikethrough(selection.hasFormat("strikethrough")); + setIsCode(selection.hasFormat("code")); + + // Update links + 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, + (_payload, newEditor) => { + updateToolbar(); + 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 ( +
+ {supportedBlockTypes.has(blockType) && ( + <> + + {showBlockOptionsDropDown && + createPortal( + , + document.body + )} + + + )} + {blockType === "code" ? ( + <> +