diff --git a/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/CodeOptionsPlugin/CodeOptions.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/CodeOptionsPlugin/CodeOptions.tsx new file mode 100644 index 000000000..ef240210a --- /dev/null +++ b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/CodeOptionsPlugin/CodeOptions.tsx @@ -0,0 +1,109 @@ +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { $findMatchingParent, mergeRegister } from '@lexical/utils' +import { $getNodeByKey, $getSelection, $isRangeSelection, $isRootOrShadowRoot, NodeKey } from 'lexical' +import { useCallback, useEffect, useState } from 'react' +import { $isCodeNode, CODE_LANGUAGE_MAP, CODE_LANGUAGE_FRIENDLY_NAME_MAP, normalizeCodeLang } from '@lexical/code' +import Dropdown from '@/Components/Dropdown/Dropdown' + +function getCodeLanguageOptions(): [string, string][] { + const options: [string, string][] = [] + + for (const [lang, friendlyName] of Object.entries(CODE_LANGUAGE_FRIENDLY_NAME_MAP)) { + options.push([lang, friendlyName]) + } + + return options +} + +const CODE_LANGUAGE_OPTIONS = getCodeLanguageOptions() + +const CodeOptionsPlugin = () => { + const [editor] = useLexicalComposerContext() + + const [isCode, setIsCode] = useState(false) + const [codeLanguage, setCodeLanguage] = useState('') + const [selectedElementKey, setSelectedElementKey] = useState(null) + + const updateToolbar = useCallback(() => { + const selection = $getSelection() + if (!$isRangeSelection(selection)) { + return + } + + const anchorNode = selection.anchor.getNode() + let element = + anchorNode.getKey() === 'root' + ? anchorNode + : $findMatchingParent(anchorNode, (e) => { + const parent = e.getParent() + return parent !== null && $isRootOrShadowRoot(parent) + }) + + if (element === null) { + element = anchorNode.getTopLevelElementOrThrow() + } + + const elementKey = element.getKey() + const elementDOM = editor.getElementByKey(elementKey) + + if (elementDOM !== null) { + setSelectedElementKey(elementKey) + if ($isCodeNode(element)) { + setIsCode(true) + const language = element.getLanguage() as keyof typeof CODE_LANGUAGE_MAP + setCodeLanguage(language ? CODE_LANGUAGE_MAP[language] || language : '') + } else { + setIsCode(false) + } + } + }, [editor]) + + useEffect(() => { + return mergeRegister( + editor.registerUpdateListener(({ editorState }) => { + editorState.read(() => { + updateToolbar() + }) + }), + ) + }, [editor, updateToolbar]) + + const onCodeLanguageSelect = useCallback( + (value: string) => { + editor.update(() => { + if (selectedElementKey !== null) { + const node = $getNodeByKey(selectedElementKey) + if ($isCodeNode(node)) { + node.setLanguage(value) + } + } + }) + }, + [editor, selectedElementKey], + ) + + if (!isCode) { + return null + } + + return ( + <> +
+ ({ + label, + value, + }))} + value={normalizeCodeLang(codeLanguage)} + onChange={(value: string) => { + onCodeLanguageSelect(value) + }} + /> +
+ + ) +} + +export default CodeOptionsPlugin diff --git a/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperEditor.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperEditor.tsx index 24628fc40..ce5847b17 100644 --- a/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperEditor.tsx +++ b/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperEditor.tsx @@ -43,6 +43,7 @@ import { SearchPlugin } from './Plugins/SearchPlugin/SearchPlugin' import ModalOverlay from '@/Components/Modal/ModalOverlay' import MobileToolbarPlugin from './Plugins/MobileToolbarPlugin/MobileToolbarPlugin' import { SuperEditorNodes } from './SuperEditorNodes' +import CodeOptionsPlugin from './Plugins/CodeOptionsPlugin/CodeOptions' export const SuperNotePreviewCharLimit = 160 @@ -205,6 +206,7 @@ export const SuperEditor: FunctionComponent = ({ +