diff --git a/README.md b/README.md index c7d87f4..9c0cf2b 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ class Example extends Component { }; render() { - return diff --git a/demo/src/index.js b/demo/src/index.js index 429fb01..be5acbd 100644 --- a/demo/src/index.js +++ b/demo/src/index.js @@ -2,30 +2,41 @@ import React, { useState } from "react"; import { render } from "react-dom"; import Editor from "../../src"; -import styles from "../../src/styles.module.css"; -import { Button, ConfigProvider, Divider, Drawer, FloatButton, Select, Space, Switch } from "antd"; +import { + Button, + ConfigProvider, + Divider, + Drawer, + FloatButton, + Select, + Space, + Switch, +} from "antd"; import Icons from "../../src/icons"; import i18n from "../../src/i18n"; import punctuationCorrections from "./punctuationCorrections"; -import wordList from './wordList'; -import autoCorrection from './autoCorrection'; +import wordList from "./wordList"; +import autoCorrection from "./autoCorrection"; const urduFonts = [ - { value: 'AlviLahoriNastaleeq', label: 'Alvi Lahori Nastaleeq' }, - { value: 'FajerNooriNastalique', label: 'Fajer Noori Nastalique' }, - { value: 'gulzar-nastalique', label: 'Gulzar Nastalique' }, - { value: 'EmadNastaleeq', label: 'Emad Nastaleeq' }, - { value: 'NafeesWebNaskh', label: 'Nafees Web Naskh' }, - { value: 'NafeesNastaleeq', label: 'Nafees Nastaleeq' }, - { value: 'MehrNastaleeq', label: 'Mehr Nastaleeq' }, - { value: 'AdobeArabic', label: 'Adobe Arabic' }, - { value: 'Dubai', label: 'Dubai' }, - { value: 'Noto Naskh Arabic', label: 'Noto Naskh Arabic' }, - { value: 'Noto Nastaliq Urdu', label: 'Noto Nastaliq Urdu' }, - { value: 'Jameel Noori Nastaleeq', label: 'Jameel Noori Nastaleeq' }, - { value: 'jameel-khushkhati', label: 'Jameel Khushkhati' }, - { value: 'JameelNooriNastaleeqKasheeda', label: 'Jameel Noori Nastaleeq Kasheeda' } + { value: "AlviLahoriNastaleeq", label: "Alvi Lahori Nastaleeq" }, + { value: "FajerNooriNastalique", label: "Fajer Noori Nastalique" }, + { value: "gulzar-nastalique", label: "Gulzar Nastalique" }, + { value: "EmadNastaleeq", label: "Emad Nastaleeq" }, + { value: "NafeesWebNaskh", label: "Nafees Web Naskh" }, + { value: "NafeesNastaleeq", label: "Nafees Nastaleeq" }, + { value: "MehrNastaleeq", label: "Mehr Nastaleeq" }, + { value: "AdobeArabic", label: "Adobe Arabic" }, + { value: "Dubai", label: "Dubai" }, + { value: "Noto Naskh Arabic", label: "Noto Naskh Arabic" }, + { value: "Noto Nastaliq Urdu", label: "Noto Nastaliq Urdu" }, + { value: "Jameel Noori Nastaleeq", label: "Jameel Noori Nastaleeq" }, + { value: "jameel-khushkhati", label: "Jameel Khushkhati" }, + { + value: "JameelNooriNastaleeqKasheeda", + label: "Jameel Noori Nastaleeq Kasheeda", + }, ]; const Demo = () => { const [open, setOpen] = useState(false); @@ -35,7 +46,7 @@ const Demo = () => { language: "ur", toolbar: { fonts: urduFonts, - defaultFont: 'MehrNastaleeq', + defaultFont: "MehrNastaleeq", showAlignment: true, showBlockFormat: true, showFontFormat: true, @@ -47,14 +58,15 @@ const Demo = () => { showSave: true, }, spellchecker: { - enabled : true, + enabled: true, punctuationCorrections: (lang) => punctuationCorrections[lang], autoCorrections: (lang) => autoCorrection[lang], - wordList : (lang) => wordList[lang] + wordList: (lang) => wordList[lang], }, format: "raw", }); - const locale = i18n[configuration.language]; + const lang = configuration?.language ?? "en"; + const locale = i18n[lang]; const showDrawer = () => { setOpen(true); }; @@ -69,157 +81,186 @@ const Demo = () => { جہاں زاد `); - } - else - { - setValue('{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"حسن کوزہ گر","type":"text","version":1}],"direction":"rtl","format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"جہاں زاد","type":"text","version":1}],"direction":"rtl","format":"","indent":0,"type":"paragraph","version":1}],"direction":"rtl","format":"","indent":0,"type":"root","version":1}}'); + } else { + setValue( + '{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"حسن کوزہ گر","type":"text","version":1}],"direction":"rtl","format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"جہاں زاد","type":"text","version":1}],"direction":"rtl","format":"","indent":0,"type":"paragraph","version":1}],"direction":"rtl","format":"","indent":0,"type":"root","version":1}}' + ); } } else { setValue("حسن کوزہ گر"); } - } + }; const changeLanguage = (value) => { - setConfiguration((e) => ({ ...e, language: value, toolbar: { ...e.toolbar, fonts: value == "ur" ? urduFonts : null }})) - } + setConfiguration((e) => ({ + ...e, + language: value, + toolbar: { ...e.toolbar, fonts: value == "ur" ? urduFonts : null }, + })); + }; + const onSave = (contents) => { + console.log("OnSave: ", contents); + }; return ( - -
- console.log('OnChange: ', val)} - onSave={(contents) => console.log('OnSave: ', contents)} - /> -
- } onClick={showDrawer} /> - } - placement="right" - onClose={onClose} - open={open} + + - - - setConfiguration((e) => ({ ...e, richText: checked })) - } +
+ console.log("OnChange: ", val)} + onSave={onSave} /> - - setConfiguration((e) => ({ ...e, format: value })) - } - options={[ - { - value: "raw", - label: "Raw", - }, - { - value: "markdown", - label: "Markdown", - }, - ]} - /> - - - setConfiguration((e) => ({ ...e, toolbar: {... e.toolbar, showAlignment : checked } })) - } - /> - + } onClick={showDrawer} /> + } + placement="right" + onClose={onClose} + open={open} + > + + + setConfiguration((e) => ({ ...e, richText: checked })) + } + /> + + setConfiguration((e) => ({ ...e, format: value })) + } + options={[ + { + value: "raw", + label: "Raw", + }, + { + value: "markdown", + label: "Markdown", + }, + ]} + /> + + + setConfiguration((e) => ({ + ...e, + toolbar: { ...e.toolbar, showAlignment: checked }, + })) + } + /> + - setConfiguration((e) => ({ ...e, toolbar: {... e.toolbar, showUndoRedo : checked } })) + setConfiguration((e) => ({ + ...e, + toolbar: { ...e.toolbar, showUndoRedo: checked }, + })) } /> - - setConfiguration((e) => ({ ...e, toolbar: {... e.toolbar, showBlockFormat : checked } })) + setConfiguration((e) => ({ + ...e, + toolbar: { ...e.toolbar, showBlockFormat: checked }, + })) } /> - - setConfiguration((e) => ({ ...e, toolbar: {... e.toolbar, showInsert : checked } })) + setConfiguration((e) => ({ + ...e, + toolbar: { ...e.toolbar, showInsert: checked }, + })) } /> - - setConfiguration((e) => ({ ...e, toolbar: {... e.toolbar, showFontFormat : checked } })) + setConfiguration((e) => ({ + ...e, + toolbar: { ...e.toolbar, showFontFormat: checked }, + })) } /> - - setConfiguration((e) => ({ ...e, toolbar: {... e.toolbar, showExtraFormat : checked } })) + setConfiguration((e) => ({ + ...e, + toolbar: { ...e.toolbar, showExtraFormat: checked }, + })) } /> - - setConfiguration((e) => ({ ...e, toolbar: {... e.toolbar, showInsertLink : checked } })) + setConfiguration((e) => ({ + ...e, + toolbar: { ...e.toolbar, showInsertLink: checked }, + })) } /> - - + - setConfiguration((e) => ({ ...e, spellchecker: {... e.spellchecker, enabled : checked } })) + setConfiguration((e) => ({ + ...e, + spellchecker: { ...e.spellchecker, enabled: checked }, + })) } /> - - - - - + + + + + + ); }; diff --git a/src/icons/index.js b/src/icons/index.js index 2dee834..c68f4f3 100644 --- a/src/icons/index.js +++ b/src/icons/index.js @@ -1,49 +1,544 @@ -import React from 'react'; +import React from "react"; const Icons = { - Undo: () => (), - Redo: () => (), - Bold: () => (), - Italic: () => (), - Underline: () => (), - Strikethrough: () => (), - SuperScript: () => (), - SubScript: () => (), - Font: () => (), - FontSize: () => (), - Paragraph: () => (), - TextHeading1: () => (), - TextHeading2: () => (), - TextHeading3: () => (), - TextHeading4: () => (), - TextHeading5: () => (), - TextHeading6: () => (), - BulletList: () => (), - NumberList: () => (), - CheckList: () => (), - Quote: () => (), - Code: () => (), - AlignLeft: () => (), - AlignRight: () => (), - AlignMiddle: () => (), - AlignJustify: () => (), - IndentIncrease: () => (), - IndentDecrease: () => (), - Link: () => (), - Plus: () => (), - HorizontalRule: () => (), - Image: () => (), - Down: () => (), - Setting: () => (), - Save: () => (), - Trash: () => (), - Edit: () => (), - OK: () => (), - Cancel: () => (), - KebabMenu: () => (), - Tools: () => (), - SpellChecker: () => (), - Punctuation: () => (), - AutoCorrect: () => () + Undo: () => ( + + + + ), + Redo: () => ( + + + + ), + Bold: () => ( + + + + ), + Italic: () => ( + + + + ), + Underline: () => ( + + + + ), + Strikethrough: () => ( + + + + ), + SuperScript: () => ( + + + + + ), + SubScript: () => ( + + + + + ), + Font: () => ( + + + + ), + FontSize: () => ( + + + + ), + Paragraph: () => ( + + + + ), + TextHeading1: () => ( + + + + ), + TextHeading2: () => ( + + + + ), + TextHeading3: () => ( + + + + ), + TextHeading4: () => ( + + + + ), + TextHeading5: () => ( + + + + ), + TextHeading6: () => ( + + + + ), + BulletList: () => ( + + + + ), + NumberList: () => ( + + + + + ), + CheckList: () => ( + + + + + ), + Quote: () => ( + + + + + ), + Code: () => ( + + + + ), + AlignLeft: () => ( + + + + ), + AlignRight: () => ( + + + + ), + AlignMiddle: () => ( + + + + ), + AlignJustify: () => ( + + + + ), + IndentIncrease: () => ( + + + + ), + IndentDecrease: () => ( + + + + ), + Link: () => ( + + + + + ), + Plus: () => ( + + + + ), + HorizontalRule: () => ( + + + + ), + Image: () => ( + + + + + ), + Down: () => ( + + + + ), + Setting: () => ( + + + + ), + Save: () => ( + + + + ), + Trash: () => ( + + + + ), + Edit: () => ( + + + + + ), + OK: () => ( + + + + + ), + Cancel: () => ( + + + + + ), + Tools: () => ( + + + + ), + SpellChecker: () => ( + + + + + + ), + Punctuation: () => ( + + + + + + + ), + AutoCorrect: () => ( + + + + ), }; -export default Icons +export default Icons; diff --git a/src/index.js b/src/index.js index 5f7d70d..d699f8e 100644 --- a/src/index.js +++ b/src/index.js @@ -1,38 +1,35 @@ -import React, {useEffect, useState } from 'react' +import React, { useEffect, useState } from "react"; // ------------------------------------------------------ import { LexicalComposer } from "@lexical/react/LexicalComposer"; import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin"; import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin"; -import { CheckListPlugin } from '@lexical/react/LexicalCheckListPlugin'; -import { ListPlugin } from '@lexical/react/LexicalListPlugin'; +import { CheckListPlugin } from "@lexical/react/LexicalCheckListPlugin"; +import { ListPlugin } from "@lexical/react/LexicalListPlugin"; import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin"; import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary"; -import { - $convertFromMarkdownString, - TRANSFORMERS, -} from '@lexical/markdown'; -import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin'; -import {CAN_USE_DOM} from './utils/canUseDOM'; +import { $convertFromMarkdownString, TRANSFORMERS } from "@lexical/markdown"; +import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin"; +import { CAN_USE_DOM } from "./utils/canUseDOM"; // ------------------------------------------------------ import ToolbarPlugin from "./plugins/toolbarPlugin"; -import AutoLinkPlugin from './plugins/autoLink.Plugin'; -import { HorizontalRulePlugin } from './plugins/horizontalRulePlugin'; -import LinkPlugin from './plugins/link.Plugin'; -import FloatingLinkEditorPlugin from './plugins/floatingLinkEditorPlugin'; -import DraggableBlockPlugin from './plugins/draggableBlockPlugin'; -import FloatingTextFormatToolbarPlugin from './plugins/FloatingTextFormatToolbarPlugin'; +import AutoLinkPlugin from "./plugins/autoLink.Plugin"; +import { HorizontalRulePlugin } from "./plugins/horizontalRulePlugin"; +import LinkPlugin from "./plugins/link.Plugin"; +import FloatingLinkEditorPlugin from "./plugins/floatingLinkEditorPlugin"; +import DraggableBlockPlugin from "./plugins/draggableBlockPlugin"; +import FloatingTextFormatToolbarPlugin from "./plugins/FloatingTextFormatToolbarPlugin"; import EditorNodes from "./nodes"; -import EditorTheme from './themes/editorTheme' -import ContentEditable from './ui/contentEditable'; +import EditorTheme from "./themes/editorTheme"; +import ContentEditable from "./ui/contentEditable"; // ------------------------------------------------------ -import i18n from './i18n'; -import styles from './styles.module.css'; -import SavePlugin from './plugins/savePlugin'; -import SpellCheckerPlugin from './plugins/spellchecker'; -import { ControlledValuePlugin } from './plugins/controlledValuePlugin'; +import i18n from "./i18n"; +import "./styles.css"; +import SavePlugin from "./plugins/savePlugin"; +import SpellCheckerPlugin from "./plugins/spellchecker"; +import { ControlledValuePlugin } from "./plugins/controlledValuePlugin"; // ------------------------------------------------------ const EMPTY_CONTENT = @@ -46,20 +43,25 @@ function onError(error) { // ------------------------------------------------------ function Placeholder({ children }) { - return
{children}
; + return ( +
+ {children} +
+ ); } // ------------------------------------------------------ -export default ({ value = null, +export default ({ + value = null, onChange, onSave, configuration = { - richText : false, + richText: false, format: "raw", - language : "en", - placeholder : null, - toolbar : { - fonts : null, + language: "en", + placeholder: null, + toolbar: { + fonts: null, defaultFont: null, showAlignment: true, showBlockFormat: true, @@ -71,22 +73,30 @@ export default ({ value = null, showInsertLink: true, showSave: false, }, - spellchecker : { + spellchecker: { enabled: false, language: "en", punctuationCorrections: null, autoCorrections: null, wordList: null, - } - } + }, + }, }) => { const locale = i18n[configuration.language]; const isRtl = configuration.language == "ur" ? true : false; const [isSmallWidthViewport, setIsSmallWidthViewport] = useState(false); - const editorState = value === EMPTY_CONTENT ? configuration.format == 'markdown'? ' ' : EMPTY_CONTENT : value; + const editorState = + value === EMPTY_CONTENT + ? configuration.format == "markdown" + ? " " + : EMPTY_CONTENT + : value === EMPTY_CONTENT; const initialConfig = { namespace: "MyEditor", - editorState: () => configuration.format === 'markdown' ? $convertFromMarkdownString(editorState, TRANSFORMERS) : editorState, + editorState: () => + configuration.format === "markdown" + ? $convertFromMarkdownString(editorState, TRANSFORMERS) + : editorState, nodes: [...EditorNodes], theme: EditorTheme, onError, @@ -102,72 +112,102 @@ export default ({ value = null, useEffect(() => { const updateViewPortWidth = () => { const isNextSmallWidthViewport = - CAN_USE_DOM && window.matchMedia('(max-width: 1025px)').matches; + CAN_USE_DOM && window.matchMedia("(max-width: 1025px)").matches; if (isNextSmallWidthViewport !== isSmallWidthViewport) { setIsSmallWidthViewport(isNextSmallWidthViewport); } }; updateViewPortWidth(); - window.addEventListener('resize', updateViewPortWidth); + window.addEventListener("resize", updateViewPortWidth); return () => { - window.removeEventListener('resize', updateViewPortWidth); + window.removeEventListener("resize", updateViewPortWidth); }; }, [isSmallWidthViewport]); return ( -
- - { configuration.richText && } - { configuration.richText ? <> - -
- + + {configuration.richText && ( + + )} +
+ {configuration.richText ? ( + <> + +
+ +
-
- } - placeholder={{configuration.placeholder ?? locale.resources.placeholder }} - ErrorBoundary={LexicalErrorBoundary} - /> - - - - - - {floatingAnchorElem && !isSmallWidthViewport && ( + } + placeholder={ + + {configuration.placeholder ?? locale.resources.placeholder} + + } + ErrorBoundary={LexicalErrorBoundary} + /> + + + + + + {floatingAnchorElem && !isSmallWidthViewport && ( <> - + )} - - : - } - placeholder={{configuration.placeholder ?? locale.resources.placeholder}} - ErrorBoundary={LexicalErrorBoundary} - /> } + + ) : ( + } + placeholder={ + + {configuration.placeholder ?? locale.resources.placeholder} + + } + ErrorBoundary={LexicalErrorBoundary} + /> + )} - - + + - {configuration.format == "markdown" && } -
-
+ format={configuration.format} + isRichtext={configuration.richText} + /> + {configuration.format == "markdown" && ( + + )} +
+ ); }; diff --git a/src/plugins/FloatingTextFormatToolbarPlugin/index.js b/src/plugins/FloatingTextFormatToolbarPlugin/index.js index bcb5759..ea441ac 100644 --- a/src/plugins/FloatingTextFormatToolbarPlugin/index.js +++ b/src/plugins/FloatingTextFormatToolbarPlugin/index.js @@ -1,9 +1,9 @@ -import './index.css'; +import "./index.css"; -import {$isCodeHighlightNode} from '@lexical/code'; -import {$isLinkNode, TOGGLE_LINK_COMMAND} from '@lexical/link'; -import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; -import {mergeRegister} from '@lexical/utils'; +import { $isCodeHighlightNode } from "@lexical/code"; +import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link"; +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { mergeRegister } from "@lexical/utils"; import { $getSelection, $isParagraphNode, @@ -12,19 +12,18 @@ import { COMMAND_PRIORITY_LOW, FORMAT_TEXT_COMMAND, SELECTION_CHANGE_COMMAND, -} from 'lexical'; -import {useCallback, useEffect, useRef, useState} from 'react'; -import * as React from 'react'; -import {createPortal} from 'react-dom'; - -import {getDOMRangeRect} from '../../utils/getDOMRangeRect'; -import {getSelectedNode} from '../../utils/getSelectedNode'; -import {setFloatingElemPosition} from '../../utils/setFloatingElemPosition'; +} from "lexical"; +import { useCallback, useEffect, useRef, useState } from "react"; +import * as React from "react"; +import { createPortal } from "react-dom"; + +import { getDOMRangeRect } from "../../utils/getDOMRangeRect"; +import { getSelectedNode } from "../../utils/getSelectedNode"; +import { setFloatingElemPosition } from "../../utils/setFloatingElemPosition"; //---------------------------------------------- -import Icons from '../../icons'; -import { Button } from 'antd'; -import CheckButton from '../../components/checkButton'; +import Icons from "../../icons"; +import CheckButton from "../../components/checkButton"; //---------------------------------------------- function TextFormatFloatingToolbar({ editor, @@ -37,13 +36,14 @@ function TextFormatFloatingToolbar({ isCode, isStrikethrough, isSubscript, - isSuperscript + isSuperscript, + isRtl, }) { const popupCharStylesEditorRef = useRef(null); const insertLink = useCallback(() => { if (!isLink) { - editor.dispatchCommand(TOGGLE_LINK_COMMAND, 'https://'); + editor.dispatchCommand(TOGGLE_LINK_COMMAND, "https://"); } else { editor.dispatchCommand(TOGGLE_LINK_COMMAND, null); } @@ -54,34 +54,34 @@ function TextFormatFloatingToolbar({ popupCharStylesEditorRef?.current && (e.buttons === 1 || e.buttons === 3) ) { - if (popupCharStylesEditorRef.current.style.pointerEvents !== 'none') { + if (popupCharStylesEditorRef.current.style.pointerEvents !== "none") { const x = e.clientX; const y = e.clientY; const elementUnderMouse = document.elementFromPoint(x, y); if (!popupCharStylesEditorRef.current.contains(elementUnderMouse)) { // Mouse is not over the target element => not a normal click, but probably a drag - popupCharStylesEditorRef.current.style.pointerEvents = 'none'; + popupCharStylesEditorRef.current.style.pointerEvents = "none"; } } } } function mouseUpListener(e) { if (popupCharStylesEditorRef?.current) { - if (popupCharStylesEditorRef.current.style.pointerEvents !== 'auto') { - popupCharStylesEditorRef.current.style.pointerEvents = 'auto'; + if (popupCharStylesEditorRef.current.style.pointerEvents !== "auto") { + popupCharStylesEditorRef.current.style.pointerEvents = "auto"; } } } useEffect(() => { if (popupCharStylesEditorRef?.current) { - document.addEventListener('mousemove', mouseMoveListener); - document.addEventListener('mouseup', mouseUpListener); + document.addEventListener("mousemove", mouseMoveListener); + document.addEventListener("mouseup", mouseUpListener); return () => { - document.removeEventListener('mousemove', mouseMoveListener); - document.removeEventListener('mouseup', mouseUpListener); + document.removeEventListener("mousemove", mouseMoveListener); + document.removeEventListener("mouseup", mouseUpListener); }; } }, [popupCharStylesEditorRef]); @@ -111,6 +111,9 @@ function TextFormatFloatingToolbar({ popupCharStylesEditorElem, anchorElem, isLink, + 0, + 0, + isRtl ); } }, [editor, anchorElem, isLink]); @@ -124,15 +127,15 @@ function TextFormatFloatingToolbar({ }); }; - window.addEventListener('resize', update); + window.addEventListener("resize", update); if (scrollerElem) { - scrollerElem.addEventListener('scroll', update); + scrollerElem.addEventListener("scroll", update); } return () => { - window.removeEventListener('resize', update); + window.removeEventListener("resize", update); if (scrollerElem) { - scrollerElem.removeEventListener('scroll', update); + scrollerElem.removeEventListener("scroll", update); } }; }, [editor, updateTextFormatFloatingToolbar, anchorElem]); @@ -142,7 +145,7 @@ function TextFormatFloatingToolbar({ updateTextFormatFloatingToolbar(); }); return mergeRegister( - editor.registerUpdateListener(({editorState}) => { + editor.registerUpdateListener(({ editorState }) => { editorState.read(() => { updateTextFormatFloatingToolbar(); }); @@ -154,8 +157,8 @@ function TextFormatFloatingToolbar({ updateTextFormatFloatingToolbar(); return false; }, - COMMAND_PRIORITY_LOW, - ), + COMMAND_PRIORITY_LOW + ) ); }, [editor, updateTextFormatFloatingToolbar]); @@ -166,7 +169,7 @@ function TextFormatFloatingToolbar({ { - editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold'); + editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold"); }} checked={isBold} aria-label="Format text as bold" @@ -175,7 +178,7 @@ function TextFormatFloatingToolbar({ { - editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic'); + editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic"); }} checked={isItalic} aria-label="Format text as italics" @@ -184,43 +187,45 @@ function TextFormatFloatingToolbar({ { - editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline'); + editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline"); }} checked={isUnderline} aria-label="Format text to underlined" icon={} /> - { configuration.toolbar.showExtraFormat && <> - { - editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough'); - }} - checked={isStrikethrough} - aria-label="Format text with a strikethrough" - icon={} - /> - { - editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript'); - }} - checked={isSubscript} - title="Subscript" - aria-label="Format Subscript" - icon={} - /> - { - editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript'); - }} - checked={isSuperscript} - title="Superscript" - aria-label="Format Superscript" - icon={} - /> - } + {configuration.toolbar.showExtraFormat && ( + <> + { + editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough"); + }} + checked={isStrikethrough} + aria-label="Format text with a strikethrough" + icon={} + /> + { + editor.dispatchCommand(FORMAT_TEXT_COMMAND, "subscript"); + }} + checked={isSubscript} + title="Subscript" + aria-label="Format Subscript" + icon={} + /> + { + editor.dispatchCommand(FORMAT_TEXT_COMMAND, "superscript"); + }} + checked={isSuperscript} + title="Superscript" + aria-label="Format Superscript" + icon={} + /> + + )} {/* { @@ -230,14 +235,17 @@ function TextFormatFloatingToolbar({ aria-label="Insert code block" icon={} /> */} - { configuration.toolbar.showInsertLink && <> - } /> - } + {configuration.toolbar.showInsertLink && ( + <> + } + /> + + )} )} @@ -282,13 +290,13 @@ function useFloatingTextFormatToolbar(editor, anchorElem, configuration) { const node = getSelectedNode(selection); // Update text format - setIsBold(selection.hasFormat('bold')); - setIsItalic(selection.hasFormat('italic')); - setIsUnderline(selection.hasFormat('underline')); - setIsStrikethrough(selection.hasFormat('strikethrough')); - setIsSubscript(selection.hasFormat('subscript')); - setIsSuperscript(selection.hasFormat('superscript')); - setIsCode(selection.hasFormat('code')); + setIsBold(selection.hasFormat("bold")); + setIsItalic(selection.hasFormat("italic")); + setIsUnderline(selection.hasFormat("underline")); + setIsStrikethrough(selection.hasFormat("strikethrough")); + setIsSubscript(selection.hasFormat("subscript")); + setIsSuperscript(selection.hasFormat("superscript")); + setIsCode(selection.hasFormat("code")); // Update links const parent = node.getParent(); @@ -300,15 +308,15 @@ function useFloatingTextFormatToolbar(editor, anchorElem, configuration) { if ( !$isCodeHighlightNode(selection.anchor.getNode()) && - selection.getTextContent() !== '' + selection.getTextContent() !== "" ) { setIsText($isTextNode(node) || $isParagraphNode(node)); } else { setIsText(false); } - const rawTextContent = selection.getTextContent().replace(/\n/g, ''); - if (!selection.isCollapsed() && rawTextContent === '') { + const rawTextContent = selection.getTextContent().replace(/\n/g, ""); + if (!selection.isCollapsed() && rawTextContent === "") { setIsText(false); return; } @@ -316,9 +324,9 @@ function useFloatingTextFormatToolbar(editor, anchorElem, configuration) { }, [editor]); useEffect(() => { - document.addEventListener('selectionchange', updatePopup); + document.addEventListener("selectionchange", updatePopup); return () => { - document.removeEventListener('selectionchange', updatePopup); + document.removeEventListener("selectionchange", updatePopup); }; }, [updatePopup]); @@ -331,7 +339,7 @@ function useFloatingTextFormatToolbar(editor, anchorElem, configuration) { if (editor.getRootElement() === null) { setIsText(false); } - }), + }) ); }, [editor, updatePopup]); @@ -353,13 +361,13 @@ function useFloatingTextFormatToolbar(editor, anchorElem, configuration) { isUnderline={isUnderline} isCode={isCode} />, - anchorElem, + anchorElem ); } export default function FloatingTextFormatToolbarPlugin({ anchorElem = document.body, - configuration + configuration, }) { const [editor] = useLexicalComposerContext(); return useFloatingTextFormatToolbar(editor, anchorElem, configuration); diff --git a/src/plugins/controlledValuePlugin.js b/src/plugins/controlledValuePlugin.js index e0ce28b..c9a2cbc 100644 --- a/src/plugins/controlledValuePlugin.js +++ b/src/plugins/controlledValuePlugin.js @@ -3,20 +3,27 @@ import React, { useEffect } from "react"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin"; -import { $convertFromMarkdownString, $convertToMarkdownString, TRANSFORMERS } from "@lexical/markdown"; +import { + $convertFromMarkdownString, + $convertToMarkdownString, + TRANSFORMERS, +} from "@lexical/markdown"; -export const ControlledValuePlugin = ({ value, onChange, isRichtext, format }) => { +export const ControlledValuePlugin = ({ + value, + onChange, + isRichtext, + format, +}) => { useAdoptPlaintextValue(value, isRichtext, format); const handleChange = (editorState, editor) => { editorState.read(() => { if (format === "markdown") { - editor.update(() => { - const markdown = $convertToMarkdownString(TRANSFORMERS); - if (onChange) { - onChange(markdown); - } - }); + const markdown = $convertToMarkdownString(TRANSFORMERS); + if (onChange) { + onChange(markdown); + } } else { const editorState = editor.getEditorState(); const json = editorState.toJSON(); @@ -34,22 +41,26 @@ export const useAdoptPlaintextValue = (value, isRichText, format) => { const [editor] = useLexicalComposerContext(); useEffect(() => { - if (value) - { - if (isRichText) - { + if (value) { + if (isRichText) { editor.update(() => { if (format === "markdown") { $convertFromMarkdownString(value, TRANSFORMERS); } else { - const editorState = editor.parseEditorState(value) + const editorState = editor.parseEditorState(value); editor.setEditorState(editorState); } }); - } - else { - // TODO: implement plain text updates - } + } else { + editor.update(() => { + const root = $getRoot(); + const selection = $getSelection(); + const paragraphNode = $createParagraphNode(); + const textNode = $createTextNode(value); + paragraphNode.append(textNode); + root.append(paragraphNode); + }); + } } }, [editor, value]); }; diff --git a/src/plugins/draggableBlockPlugin/index.css b/src/plugins/draggableBlockPlugin/index.css index 6f8cdd5..712e4ef 100644 --- a/src/plugins/draggableBlockPlugin/index.css +++ b/src/plugins/draggableBlockPlugin/index.css @@ -9,10 +9,16 @@ will-change: transform; } +.rtl .draggable-block-menu { + left: auto; + right: 0; +} + .draggable-block-menu .icon { width: 16px; height: 16px; opacity: 0.3; + background-image: url('data:image/svg+xml;utf8,'); } .draggable-block-menu:active { @@ -33,3 +39,9 @@ opacity: 0; will-change: transform; } + + +.rtl .draggable-block-target-line { + left: auto; + right: 0; +} diff --git a/src/plugins/draggableBlockPlugin/index.js b/src/plugins/draggableBlockPlugin/index.js index c61ae85..d0b77de 100644 --- a/src/plugins/draggableBlockPlugin/index.js +++ b/src/plugins/draggableBlockPlugin/index.js @@ -1,8 +1,8 @@ -import './index.css'; +import "./index.css"; -import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; -import {eventFiles} from '@lexical/rich-text'; -import {mergeRegister} from '@lexical/utils'; +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { eventFiles } from "@lexical/rich-text"; +import { mergeRegister } from "@lexical/utils"; import { $getNearestNodeFromDOMNode, $getNodeByKey, @@ -11,20 +11,20 @@ import { COMMAND_PRIORITY_LOW, DRAGOVER_COMMAND, DROP_COMMAND, -} from 'lexical'; -import * as React from 'react'; -import {useEffect, useRef, useState} from 'react'; -import {createPortal} from 'react-dom'; +} from "lexical"; +import * as React from "react"; +import { useEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; -import {isHTMLElement} from '../../utils/guard'; -import {Point} from '../../utils/point'; -import {Rect} from '../../utils/rect'; -import Icons from '../../icons'; +import { isHTMLElement } from "../../utils/guard"; +import { Point } from "../../utils/point"; +import { Rect } from "../../utils/rect"; +import Icons from "../../icons"; const SPACE = 4; const TARGET_LINE_HALF_HEIGHT = 2; -const DRAGGABLE_BLOCK_MENU_CLASSNAME = 'draggable-block-menu'; -const DRAG_DATA_FORMAT = 'application/x-lexical-drag-block'; +const DRAGGABLE_BLOCK_MENU_CLASSNAME = "draggable-block-menu"; +const DRAG_DATA_FORMAT = "application/x-lexical-drag-block"; const TEXT_BOX_HORIZONTAL_PADDING = 28; const Downward = 1; @@ -49,36 +49,31 @@ function getTopLevelNodeKeys(editor) { } function getCollapsedMargins(elem) { - const getMargin = ( element, margin) => + const getMargin = (element, margin) => element ? parseFloat(window.getComputedStyle(element)[margin]) : 0; - const {marginTop, marginBottom} = window.getComputedStyle(elem); + const { marginTop, marginBottom } = window.getComputedStyle(elem); const prevElemSiblingMarginBottom = getMargin( elem.previousElementSibling, - 'marginBottom', + "marginBottom" ); const nextElemSiblingMarginTop = getMargin( elem.nextElementSibling, - 'marginTop', + "marginTop" ); const collapsedTopMargin = Math.max( parseFloat(marginTop), - prevElemSiblingMarginBottom, + prevElemSiblingMarginBottom ); const collapsedBottomMargin = Math.max( parseFloat(marginBottom), - nextElemSiblingMarginTop, + nextElemSiblingMarginTop ); - return {marginBottom: collapsedBottomMargin, marginTop: collapsedTopMargin}; + return { marginBottom: collapsedBottomMargin, marginTop: collapsedTopMargin }; } -function getBlockElement( - anchorElem, - editor, - event, - useEdgeAsDefault = false, -) { +function getBlockElement(anchorElem, editor, event, useEdgeAsDefault = false) { const anchorElementRect = anchorElem.getBoundingClientRect(); const topLevelNodeKeys = getTopLevelNodeKeys(editor); @@ -120,7 +115,7 @@ function getBlockElement( } const point = new Point(event.x, event.y); const domRect = Rect.fromDOM(elem); - const {marginTop, marginBottom} = getCollapsedMargins(elem); + const { marginTop, marginBottom } = getCollapsedMargins(elem); const rect = domRect.generateNewRect({ bottom: domRect.bottom + marginBottom, @@ -131,7 +126,7 @@ function getBlockElement( const { result, - reason: {isOnTopSide, isOnBottomSide}, + reason: { isOnTopSide, isOnBottomSide }, } = rect.contains(point); if (result) { @@ -162,14 +157,14 @@ function isOnMenu(element) { return !!element.closest(`.${DRAGGABLE_BLOCK_MENU_CLASSNAME}`); } -function setMenuPosition( - targetElem, - floatingElem, - anchorElem, -) { +function setMenuPosition(targetElem, floatingElem, anchorElem, isRtl) { if (!targetElem) { - floatingElem.style.opacity = '0'; - floatingElem.style.transform = 'translate(-10000px, -10000px)'; + floatingElem.style.opacity = "0"; + if (isRtl) { + floatingElem.style.transform = "translate(10000px, -10000px)"; + } else { + floatingElem.style.transform = "translate(-10000px, -10000px)"; + } return; } @@ -185,15 +180,15 @@ function setMenuPosition( const left = SPACE; - floatingElem.style.opacity = '1'; + floatingElem.style.opacity = "1"; floatingElem.style.transform = `translate(${left}px, ${top}px)`; } function setDragImage(dataTransfer, draggableBlockElem) { - const {transform} = draggableBlockElem.style; + const { transform } = draggableBlockElem.style; // Remove dragImage borders - draggableBlockElem.style.transform = 'translateZ(0)'; + draggableBlockElem.style.transform = "translateZ(0)"; dataTransfer.setDragImage(draggableBlockElem, 0, 0); setTimeout(() => { @@ -201,13 +196,13 @@ function setDragImage(dataTransfer, draggableBlockElem) { }); } -function setTargetLine( targetLineElem, targetBlockElem, mouseY, anchorElem ) { - const {top: targetBlockElemTop, height: targetBlockElemHeight} = +function setTargetLine(targetLineElem, targetBlockElem, mouseY, anchorElem) { + const { top: targetBlockElemTop, height: targetBlockElemHeight } = targetBlockElem.getBoundingClientRect(); - const {top: anchorTop, width: anchorWidth} = + const { top: anchorTop, width: anchorWidth } = anchorElem.getBoundingClientRect(); - const {marginTop, marginBottom} = getCollapsedMargins(targetBlockElem); + const { marginTop, marginBottom } = getCollapsedMargins(targetBlockElem); let lineTop = targetBlockElemTop; if (mouseY >= targetBlockElemTop) { lineTop += targetBlockElemHeight + marginBottom / 2; @@ -222,17 +217,21 @@ function setTargetLine( targetLineElem, targetBlockElem, mouseY, anchorElem ) { targetLineElem.style.width = `${ anchorWidth - (TEXT_BOX_HORIZONTAL_PADDING - SPACE) * 2 }px`; - targetLineElem.style.opacity = '.4'; + targetLineElem.style.opacity = ".4"; } -function hideTargetLine(targetLineElem) { +function hideTargetLine(targetLineElem, isRtl) { if (targetLineElem) { - targetLineElem.style.opacity = '0'; - targetLineElem.style.transform = 'translate(-10000px, -10000px)'; + targetLineElem.style.opacity = "0"; + if (isRtl) { + targetLineElem.style.transform = "translate(10000px, -10000px)"; + } else { + targetLineElem.style.transform = "translate(-10000px, -10000px)"; + } } } -function useDraggableBlockMenu(editor, anchorElem, isEditable) { +function useDraggableBlockMenu(editor, anchorElem, isEditable, isRtl) { const scrollerElem = anchorElem.parentElement; const menuRef = useRef(null); @@ -261,18 +260,18 @@ function useDraggableBlockMenu(editor, anchorElem, isEditable) { setDraggableBlockElem(null); } - scrollerElem?.addEventListener('mousemove', onMouseMove); - scrollerElem?.addEventListener('mouseleave', onMouseLeave); + scrollerElem?.addEventListener("mousemove", onMouseMove); + scrollerElem?.addEventListener("mouseleave", onMouseLeave); return () => { - scrollerElem?.removeEventListener('mousemove', onMouseMove); - scrollerElem?.removeEventListener('mouseleave', onMouseLeave); + scrollerElem?.removeEventListener("mousemove", onMouseMove); + scrollerElem?.removeEventListener("mouseleave", onMouseLeave); }; }, [scrollerElem, anchorElem, editor]); useEffect(() => { if (menuRef.current) { - setMenuPosition(draggableBlockElem, menuRef.current, anchorElem); + setMenuPosition(draggableBlockElem, menuRef.current, anchorElem, isRtl); } }, [anchorElem, draggableBlockElem]); @@ -285,7 +284,7 @@ function useDraggableBlockMenu(editor, anchorElem, isEditable) { if (isFileTransfer) { return false; } - const {pageY, target} = event; + const { pageY, target } = event; if (!isHTMLElement(target)) { return false; } @@ -308,8 +307,8 @@ function useDraggableBlockMenu(editor, anchorElem, isEditable) { if (isFileTransfer) { return false; } - const {target, dataTransfer, pageY} = event; - const dragData = dataTransfer?.getData(DRAG_DATA_FORMAT) || ''; + const { target, dataTransfer, pageY } = event; + const dragData = dataTransfer?.getData(DRAG_DATA_FORMAT) || ""; const draggedNode = $getNodeByKey(dragData); if (!draggedNode) { return false; @@ -345,15 +344,15 @@ function useDraggableBlockMenu(editor, anchorElem, isEditable) { (event) => { return onDragover(event); }, - COMMAND_PRIORITY_LOW, + COMMAND_PRIORITY_LOW ), editor.registerCommand( DROP_COMMAND, (event) => { return onDrop(event); }, - COMMAND_PRIORITY_HIGH, - ), + COMMAND_PRIORITY_HIGH + ) ); }, [anchorElem, editor]); @@ -363,7 +362,7 @@ function useDraggableBlockMenu(editor, anchorElem, isEditable) { return; } setDragImage(dataTransfer, draggableBlockElem); - let nodeKey = ''; + let nodeKey = ""; editor.update(() => { const node = $getNearestNodeFromDOMNode(draggableBlockElem); if (node) { @@ -376,7 +375,7 @@ function useDraggableBlockMenu(editor, anchorElem, isEditable) { function onDragEnd() { isDraggingBlockRef.current = false; - hideTargetLine(targetLineRef.current); + hideTargetLine(targetLineRef.current, isRtl); } return createPortal( @@ -386,20 +385,20 @@ function useDraggableBlockMenu(editor, anchorElem, isEditable) { ref={menuRef} draggable={true} onDragStart={onDragStart} - onDragEnd={onDragEnd}> -
- -
+ onDragEnd={onDragEnd} + > +
, - anchorElem, + anchorElem ); } export default function DraggableBlockPlugin({ anchorElem = document.body, + isRtl, }) { const [editor] = useLexicalComposerContext(); - return useDraggableBlockMenu(editor, anchorElem, editor._editable); + return useDraggableBlockMenu(editor, anchorElem, editor._editable, isRtl); } diff --git a/src/plugins/floatingLinkEditorPlugin/index.js b/src/plugins/floatingLinkEditorPlugin/index.js index bc24c9b..fb04146 100644 --- a/src/plugins/floatingLinkEditorPlugin/index.js +++ b/src/plugins/floatingLinkEditorPlugin/index.js @@ -1,12 +1,12 @@ -import './index.css'; +import "./index.css"; import { $createLinkNode, $isAutoLinkNode, $isLinkNode, TOGGLE_LINK_COMMAND, -} from '@lexical/link'; -import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; -import {$findMatchingParent, mergeRegister} from '@lexical/utils'; +} from "@lexical/link"; +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { $findMatchingParent, mergeRegister } from "@lexical/utils"; import { $getSelection, $isLineBreakNode, @@ -17,16 +17,16 @@ import { COMMAND_PRIORITY_LOW, KEY_ESCAPE_COMMAND, SELECTION_CHANGE_COMMAND, -} from 'lexical'; -import {useCallback, useEffect, useRef, useState} from 'react'; -import * as React from 'react'; -import {createPortal} from 'react-dom'; +} from "lexical"; +import { useCallback, useEffect, useRef, useState } from "react"; +import * as React from "react"; +import { createPortal } from "react-dom"; -import {getSelectedNode} from '../../utils/getSelectedNode'; -import {setFloatingElemPositionForLinkEditor} from '../../utils/setFloatingElemPositionForLinkEditor'; -import {sanitizeUrl} from '../../utils/url'; -import { Button, Input } from 'antd'; -import Icons from '../../icons'; +import { getSelectedNode } from "../../utils/getSelectedNode"; +import { setFloatingElemPositionForLinkEditor } from "../../utils/setFloatingElemPositionForLinkEditor"; +import { sanitizeUrl } from "../../utils/url"; +import { Button, Input } from "antd"; +import Icons from "../../icons"; function FloatingLinkEditor({ editor, @@ -35,14 +35,13 @@ function FloatingLinkEditor({ anchorElem, isLinkEditMode, setIsLinkEditMode, + isRtl, }) { const editorRef = useRef(null); const inputRef = useRef(null); - const [linkUrl, setLinkUrl] = useState(''); - const [editedLinkUrl, setEditedLinkUrl] = useState('https://'); - const [lastSelection, setLastSelection] = useState( - null, - ); + const [linkUrl, setLinkUrl] = useState(""); + const [editedLinkUrl, setEditedLinkUrl] = useState("https://"); + const [lastSelection, setLastSelection] = useState(null); const updateLinkEditor = useCallback(() => { const selection = $getSelection(); @@ -55,7 +54,7 @@ function FloatingLinkEditor({ } else if ($isLinkNode(node)) { setLinkUrl(node.getURL()); } else { - setLinkUrl(''); + setLinkUrl(""); } if (isLinkEditMode) { setEditedLinkUrl(linkUrl); @@ -78,19 +77,30 @@ function FloatingLinkEditor({ rootElement.contains(nativeSelection.anchorNode) && editor.isEditable() ) { - const domRect = nativeSelection.focusNode?.parentElement?.getBoundingClientRect(); + const domRect = + nativeSelection.focusNode?.parentElement?.getBoundingClientRect(); if (domRect) { domRect.y += 40; - setFloatingElemPositionForLinkEditor(domRect, editorElem, anchorElem); + setFloatingElemPositionForLinkEditor( + domRect, + editorElem, + anchorElem, + isRtl + ); } setLastSelection(selection); - } else if (!activeElement || activeElement.className !== 'link-input') { + } else if (!activeElement || activeElement.className !== "link-input") { if (rootElement !== null) { - setFloatingElemPositionForLinkEditor(null, editorElem, anchorElem); + setFloatingElemPositionForLinkEditor( + null, + editorElem, + anchorElem, + isRtl + ); } setLastSelection(null); setIsLinkEditMode(false); - setLinkUrl(''); + setLinkUrl(""); } return true; @@ -105,24 +115,24 @@ function FloatingLinkEditor({ }); }; - window.addEventListener('resize', update); + window.addEventListener("resize", update); if (scrollerElem) { - scrollerElem.addEventListener('scroll', update); + scrollerElem.addEventListener("scroll", update); } return () => { - window.removeEventListener('resize', update); + window.removeEventListener("resize", update); if (scrollerElem) { - scrollerElem.removeEventListener('scroll', update); + scrollerElem.removeEventListener("scroll", update); } }; }, [anchorElem.parentElement, editor, updateLinkEditor]); useEffect(() => { return mergeRegister( - editor.registerUpdateListener(({editorState}) => { + editor.registerUpdateListener(({ editorState }) => { editorState.read(() => { updateLinkEditor(); }); @@ -134,7 +144,7 @@ function FloatingLinkEditor({ updateLinkEditor(); return true; }, - COMMAND_PRIORITY_LOW, + COMMAND_PRIORITY_LOW ), editor.registerCommand( KEY_ESCAPE_COMMAND, @@ -145,8 +155,8 @@ function FloatingLinkEditor({ } return false; }, - COMMAND_PRIORITY_HIGH, - ), + COMMAND_PRIORITY_HIGH + ) ); }, [editor, updateLinkEditor, setIsLink, isLink]); @@ -163,10 +173,10 @@ function FloatingLinkEditor({ }, [isLinkEditMode, isLink]); const monitorInputInteraction = (event) => { - if (event.key === 'Enter') { + if (event.key === "Enter") { event.preventDefault(); handleLinkSubmission(); - } else if (event.key === 'Escape') { + } else if (event.key === "Escape") { event.preventDefault(); setIsLinkEditMode(false); } @@ -174,7 +184,7 @@ function FloatingLinkEditor({ const handleLinkSubmission = () => { if (lastSelection !== null) { - if (linkUrl !== '') { + if (linkUrl !== "") { editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl(editedLinkUrl)); editor.update(() => { const selection = $getSelection(); @@ -200,16 +210,26 @@ function FloatingLinkEditor({ {!isLink ? null : isLinkEditMode ? (
-
) : (
- -
+ }} + /> +
)} ); @@ -255,6 +289,7 @@ function useFloatingLinkEditorToolbar( anchorElem, isLinkEditMode, setIsLinkEditMode, + isRtl ) { const [activeEditor, setActiveEditor] = useState(editor); const [isLink, setIsLink] = useState(false); @@ -267,7 +302,7 @@ function useFloatingLinkEditorToolbar( const focusLinkNode = $findMatchingParent(focusNode, $isLinkNode); const focusAutoLinkNode = $findMatchingParent( focusNode, - $isAutoLinkNode, + $isAutoLinkNode ); if (!(focusLinkNode || focusAutoLinkNode)) { setIsLink(false); @@ -294,7 +329,7 @@ function useFloatingLinkEditorToolbar( } } return mergeRegister( - editor.registerUpdateListener(({editorState}) => { + editor.registerUpdateListener(({ editorState }) => { editorState.read(() => { updateToolbar(); }); @@ -306,7 +341,7 @@ function useFloatingLinkEditorToolbar( setActiveEditor(newEditor); return false; }, - COMMAND_PRIORITY_CRITICAL, + COMMAND_PRIORITY_CRITICAL ), editor.registerCommand( CLICK_COMMAND, @@ -316,14 +351,14 @@ function useFloatingLinkEditorToolbar( const node = getSelectedNode(selection); const linkNode = $findMatchingParent(node, $isLinkNode); if ($isLinkNode(linkNode) && (payload.metaKey || payload.ctrlKey)) { - window.open(linkNode.getURL(), '_blank'); + window.open(linkNode.getURL(), "_blank"); return true; } } return false; }, - COMMAND_PRIORITY_LOW, - ), + COMMAND_PRIORITY_LOW + ) ); }, [editor]); @@ -335,8 +370,9 @@ function useFloatingLinkEditorToolbar( setIsLink={setIsLink} isLinkEditMode={isLinkEditMode} setIsLinkEditMode={setIsLinkEditMode} + isRtl={isRtl} />, - anchorElem, + anchorElem ); } @@ -344,6 +380,7 @@ export default function FloatingLinkEditorPlugin({ anchorElem = document.body, isLinkEditMode, setIsLinkEditMode, + isRtl, }) { const [editor] = useLexicalComposerContext(); return useFloatingLinkEditorToolbar( @@ -351,5 +388,6 @@ export default function FloatingLinkEditorPlugin({ anchorElem, isLinkEditMode, setIsLinkEditMode, + isRtl ); } diff --git a/src/plugins/savePlugin.js b/src/plugins/savePlugin.js index e542cb3..3bf898d 100644 --- a/src/plugins/savePlugin.js +++ b/src/plugins/savePlugin.js @@ -1,21 +1,19 @@ -import {useEffect, useCallback} from 'react'; +import { useEffect, useCallback } from "react"; // ------------------------------------------------------ -import { $convertToMarkdownString, TRANSFORMERS } from '@lexical/markdown'; -import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; -import { - COMMAND_PRIORITY_LOW, -} from 'lexical'; +import { $convertToMarkdownString, TRANSFORMERS } from "@lexical/markdown"; +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { COMMAND_PRIORITY_LOW } from "lexical"; // ------------------------------------------------------ -import { SAVE_COMMAND } from '../commands/saveCommand'; +import { SAVE_COMMAND } from "../commands/saveCommand"; // ------------------------------------------------------ function SavePlugin({ format, onSave }) { const [editor] = useLexicalComposerContext(); - const saveCallback = () => { + const saveCallback = useCallback(() => { if (format === "markdown") { editor.update(() => { const markdown = $convertToMarkdownString(TRANSFORMERS); - if (onSave) { + if (onSave) { onSave(markdown); } }); @@ -26,16 +24,14 @@ function SavePlugin({ format, onSave }) { onSave(json); } } - }; + }, [format, onSave]); useEffect(() => { - editor.registerCommand( - SAVE_COMMAND, - saveCallback, - COMMAND_PRIORITY_LOW, - ); - - }, [editor]); + if (editor._commands.has(SAVE_COMMAND)) { + editor._commands.delete(SAVE_COMMAND); + } + editor.registerCommand(SAVE_COMMAND, saveCallback, COMMAND_PRIORITY_LOW); + }, [editor, format, saveCallback]); return null; } diff --git a/src/plugins/toolbarPlugin/fontDropdown.js b/src/plugins/toolbarPlugin/fontDropdown.js index 36fce79..e271d7d 100644 --- a/src/plugins/toolbarPlugin/fontDropdown.js +++ b/src/plugins/toolbarPlugin/fontDropdown.js @@ -1,6 +1,6 @@ -import React from 'react'; +import React from "react"; import { Button, Dropdown, Select, Space } from "antd"; -import Icons from '../../icons'; +import Icons from "../../icons"; // -------------------------------------------------- @@ -14,39 +14,44 @@ const FONT_FAMILY_OPTIONS = [ ]; export const defaultFont = (configuration) => { - var fonts = configuration?.toolbar?.fonts || FONT_FAMILY_OPTIONS; - return configuration?.toolbar?.defaultFont - ? fonts.find(x => x.value === configuration?.toolbar?.defaultFont) || fonts[0] - : fonts[0]; -} - + var fonts = configuration?.toolbar?.fonts ?? FONT_FAMILY_OPTIONS; + var retVal = configuration?.toolbar?.defaultFont + ? fonts.find((x) => x.value === configuration?.toolbar?.defaultFont) || + fonts[0] + : fonts[0]; + return retVal; +}; // -------------------------------------------------- const FontDropDown = ({ fonts, value, onChange = () => {} }) => { - const configuredFonts = (fonts && fonts.length > 0 ? fonts : FONT_FAMILY_OPTIONS); - const selected = () => value && configuredFonts.find(x => x.value === value) || configuredFonts[0]; + const configuredFonts = + fonts && fonts.length > 0 ? fonts : FONT_FAMILY_OPTIONS; + const selected = () => + (value && configuredFonts.find((x) => x.value === value)) || + configuredFonts[0]; const onFontSelect = (item) => { onChange(item.key); - } - const items = configuredFonts - .map(i => ({ + }; + const items = configuredFonts.map((i) => ({ key: i.value, label: i.label, })); return ( - + + ); }; diff --git a/src/plugins/toolbarPlugin/index.css b/src/plugins/toolbarPlugin/index.css new file mode 100644 index 0000000..e69de29 diff --git a/src/plugins/toolbarPlugin/index.js b/src/plugins/toolbarPlugin/index.js index e96055e..545b1c2 100644 --- a/src/plugins/toolbarPlugin/index.js +++ b/src/plugins/toolbarPlugin/index.js @@ -1,6 +1,6 @@ -import React from 'react'; +import React from "react"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; -import useLexicalEditable from '@lexical/react/useLexicalEditable'; +import useLexicalEditable from "@lexical/react/useLexicalEditable"; import { $getSelection, $isRangeSelection, @@ -16,15 +16,9 @@ import { KEY_MODIFIER_COMMAND, $getRoot, } from "lexical"; -import { - $isCodeNode, - CODE_LANGUAGE_MAP, -} from '@lexical/code'; -import { - $isListNode, - ListNode, -} from '@lexical/list'; -import {$isLinkNode, TOGGLE_LINK_COMMAND} from '@lexical/link'; +import { $isCodeNode, CODE_LANGUAGE_MAP } from "@lexical/code"; +import { $isListNode, ListNode } from "@lexical/list"; +import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link"; import { $getSelectionStyleValueForProperty, $isParentElementRTL, @@ -34,26 +28,27 @@ import { $findMatchingParent, $getNearestNodeOfType, mergeRegister, -} from '@lexical/utils'; -import {$isTableNode} from '@lexical/table'; -import { $isHeadingNode } from '@lexical/rich-text'; +} from "@lexical/utils"; +import { $isTableNode } from "@lexical/table"; +import { $isHeadingNode } from "@lexical/rich-text"; import { useCallback, useEffect, useState } from "react"; // ----------------------------------------------------------- import { Button, Divider, InputNumber, Tooltip } from "antd"; // ----------------------------------------------------------- -import { sanitizeUrl } from '../../utils/url'; -import { getSelectedNode } from '../../utils/getSelectedNode'; +import { sanitizeUrl } from "../../utils/url"; +import { getSelectedNode } from "../../utils/getSelectedNode"; import FontDropDown, { defaultFont } from "./fontDropdown"; -import BlockFormatDropDown, { blockTypeToBlockName } from './blockFormatDropDown'; +import BlockFormatDropDown, { + blockTypeToBlockName, +} from "./blockFormatDropDown"; import InsertDropDown from "./insertDropDown"; import ToolsDropdown from "./toolsDropDown"; -import Icons from '../../icons' +import Icons from "../../icons"; import CheckButton from "../../components/checkButton"; import AlignFormatDropDown from "./alignFormatDropDown"; -import styles from "../../styles.module.css"; -import { SAVE_COMMAND } from '../../commands/saveCommand'; +import { SAVE_COMMAND } from "../../commands/saveCommand"; // ----------------------------------------------------------- const ToolbarPlugin = ({ configuration, setIsLinkEditMode, locale }) => { @@ -67,28 +62,29 @@ const ToolbarPlugin = ({ configuration, setIsLinkEditMode, locale }) => { const [isSubscript, setIsSubscript] = useState(false); const [isSuperscript, setIsSuperscript] = useState(false); const [isCode, setIsCode] = useState(false); - const [rootType, setRootType] = useState('root'); - const [blockType, setBlockType] = useState('paragraph'); + const [rootType, setRootType] = useState("root"); + const [blockType, setBlockType] = useState("paragraph"); const [selectedElementKey, setSelectedElementKey] = useState(null); const [canUndo, setCanUndo] = useState(false); const [canRedo, setCanRedo] = useState(false); const [fontSize, setFontSize] = useState(15); const [fontColor, setFontColor] = useState("#000"); const [bgColor, setBgColor] = useState("#fff"); - const [fontFamily, setFontFamily] = useState(configuration.toolbar.defaultFont); - const [codeLanguage, setCodeLanguage] = useState(''); + const [fontFamily, setFontFamily] = useState( + configuration.toolbar.defaultFont ?? defaultFont(configuration) + ); + const [codeLanguage, setCodeLanguage] = useState(""); const [isLink, setIsLink] = useState(false); const isEditable = useLexicalEditable(); - // TODO: Set Default Font useEffect(() => { editor.update(() => { if (!editor || !configuration) return; - $getRoot()?.getChildAtIndex(0)?.select(); + $getRoot().select(); const selection = $getSelection(); if (selection) { $patchStyleText(selection, { - 'font-family': defaultFont(configuration?.toolbar?.fonts), + "font-family": fontFamily, }); } }); @@ -99,7 +95,7 @@ const ToolbarPlugin = ({ configuration, setIsLinkEditMode, locale }) => { if ($isRangeSelection(selection)) { const anchorNode = selection.anchor.getNode(); let element = - anchorNode.getKey() === 'root' + anchorNode.getKey() === "root" ? anchorNode : $findMatchingParent(anchorNode, (e) => { const parent = e.getParent(); @@ -114,13 +110,13 @@ const ToolbarPlugin = ({ configuration, setIsLinkEditMode, locale }) => { const elementDOM = activeEditor.getElementByKey(elementKey); // Update text format - setIsBold(selection.hasFormat('bold')); - setIsItalic(selection.hasFormat('italic')); - setIsUnderline(selection.hasFormat('underline')); - setIsStrikethrough(selection.hasFormat('strikethrough')); - setIsSubscript(selection.hasFormat('subscript')); - setIsSuperscript(selection.hasFormat('superscript')); - setIsCode(selection.hasFormat('code')); + setIsBold(selection.hasFormat("bold")); + setIsItalic(selection.hasFormat("italic")); + setIsUnderline(selection.hasFormat("underline")); + setIsStrikethrough(selection.hasFormat("strikethrough")); + setIsSubscript(selection.hasFormat("subscript")); + setIsSuperscript(selection.hasFormat("superscript")); + setIsCode(selection.hasFormat("code")); setIsRTL($isParentElementRTL(selection)); // Update links @@ -134,18 +130,15 @@ const ToolbarPlugin = ({ configuration, setIsLinkEditMode, locale }) => { const tableNode = $findMatchingParent(node, $isTableNode); if ($isTableNode(tableNode)) { - setRootType('table'); + setRootType("table"); } else { - setRootType('root'); + setRootType("root"); } if (elementDOM !== null) { setSelectedElementKey(elementKey); if ($isListNode(element)) { - const parentList = $getNearestNodeOfType( - anchorNode, - ListNode, - ); + const parentList = $getNearestNodeOfType(anchorNode, ListNode); const type = parentList ? parentList.getListType() : element.getListType(); @@ -154,14 +147,13 @@ const ToolbarPlugin = ({ configuration, setIsLinkEditMode, locale }) => { const type = $isHeadingNode(element) ? element.getTag() : element.getType(); - if (type in blockTypeToBlockName ) { + if (type in blockTypeToBlockName) { setBlockType(type); } if ($isCodeNode(element)) { - const language = - element.getLanguage(); + const language = element.getLanguage(); setCodeLanguage( - language ? CODE_LANGUAGE_MAP[language] || language : '', + language ? CODE_LANGUAGE_MAP[language] || language : "" ); return; } @@ -170,20 +162,26 @@ const ToolbarPlugin = ({ configuration, setIsLinkEditMode, locale }) => { // Handle buttons setFontSize( - parseInt($getSelectionStyleValueForProperty(selection, 'font-size', '15px').replace('px','')) + parseInt( + $getSelectionStyleValueForProperty( + selection, + "font-size", + "15px" + ).replace("px", "") + ) ); setFontColor( - $getSelectionStyleValueForProperty(selection, 'color', '#000'), + $getSelectionStyleValueForProperty(selection, "color", "#000") ); setBgColor( $getSelectionStyleValueForProperty( selection, - 'background-color', - '#fff', - ), + "background-color", + "#fff" + ) ); setFontFamily( - $getSelectionStyleValueForProperty(selection, 'font-family', defaultFont({ configuration }).value) + $getSelectionStyleValueForProperty(selection, "font-family", fontFamily) ); } }, [activeEditor]); @@ -231,18 +229,18 @@ const ToolbarPlugin = ({ configuration, setIsLinkEditMode, locale }) => { KEY_MODIFIER_COMMAND, (payload) => { const event = payload; - const {code, ctrlKey, metaKey} = event; + const { code, ctrlKey, metaKey } = event; - if (code === 'KeyK' && (ctrlKey || metaKey)) { + if (code === "KeyK" && (ctrlKey || metaKey)) { event.preventDefault(); return activeEditor.dispatchCommand( TOGGLE_LINK_COMMAND, - sanitizeUrl('https://'), + sanitizeUrl("https://") ); } return false; }, - COMMAND_PRIORITY_NORMAL, + COMMAND_PRIORITY_NORMAL ); }, [activeEditor, isLink]); @@ -253,7 +251,7 @@ const ToolbarPlugin = ({ configuration, setIsLinkEditMode, locale }) => { $patchStyleText(selection, { "font-family": font, }); - } + } }); }; @@ -271,7 +269,7 @@ const ToolbarPlugin = ({ configuration, setIsLinkEditMode, locale }) => { const insertLink = useCallback(() => { if (!isLink) { setIsLinkEditMode(true); - editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl('https://')); + editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl("https://")); } else { setIsLinkEditMode(false); editor.dispatchCommand(TOGGLE_LINK_COMMAND, null); @@ -279,59 +277,169 @@ const ToolbarPlugin = ({ configuration, setIsLinkEditMode, locale }) => { }, [editor, isLink]); return ( -
- { configuration.toolbar.showSave && -
); }; diff --git a/src/styles.module.css b/src/styles.css similarity index 96% rename from src/styles.module.css rename to src/styles.css index 8d8809d..67f4b41 100644 --- a/src/styles.module.css +++ b/src/styles.css @@ -24,20 +24,21 @@ code { } .editorPlaceholder { + font-size: 15px; color: #999; overflow: hidden; position: absolute; text-overflow: ellipsis; - top: 64px; - left: 18px; - font-size: 15px; + top: 8px; + left: 28px; + right: 28px; user-select: none; + white-space: nowrap; display: inline-block; pointer-events: none; } .rtl .editorPlaceholder { - right: 18px; left: auto; } diff --git a/src/utils/setFloatingElemPosition.js b/src/utils/setFloatingElemPosition.js index 436b560..f5afcad 100644 --- a/src/utils/setFloatingElemPosition.js +++ b/src/utils/setFloatingElemPosition.js @@ -8,12 +8,17 @@ export function setFloatingElemPosition( isLink, verticalGap, horizontalOffset, + isRtl ) { const scrollerElem = anchorElem.parentElement; if (targetRect === null || !scrollerElem) { - floatingElem.style.opacity = '0'; - floatingElem.style.transform = 'translate(-10000px, -10000px)'; + floatingElem.style.opacity = "0"; + if (isRtl) { + floatingElem.style.transform = "translate(10000px, -10000px)"; + } else { + floatingElem.style.transform = "translate(-10000px, -10000px)"; + } return; } @@ -39,6 +44,6 @@ export function setFloatingElemPosition( top -= anchorElementRect.top; left -= anchorElementRect.left; - floatingElem.style.opacity = '1'; + floatingElem.style.opacity = "1"; floatingElem.style.transform = `translate(${left}px, ${top}px)`; } diff --git a/src/utils/setFloatingElemPositionForLinkEditor.js b/src/utils/setFloatingElemPositionForLinkEditor.js index 514d8f8..01006d1 100644 --- a/src/utils/setFloatingElemPositionForLinkEditor.js +++ b/src/utils/setFloatingElemPositionForLinkEditor.js @@ -7,12 +7,17 @@ export function setFloatingElemPositionForLinkEditor( anchorElem, verticalGap = VERTICAL_GAP, horizontalOffset = HORIZONTAL_OFFSET, + isRtl ) { const scrollerElem = anchorElem.parentElement; if (targetRect === null || !scrollerElem) { - floatingElem.style.opacity = '0'; - floatingElem.style.transform = 'translate(-10000px, -10000px)'; + floatingElem.style.opacity = "0"; + if (isRtl) { + floatingElem.style.transform = "translate(10000px, -10000px)"; + } else { + floatingElem.style.transform = "translate(-10000px, -10000px)"; + } return; } @@ -34,6 +39,6 @@ export function setFloatingElemPositionForLinkEditor( top -= anchorElementRect.top; left -= anchorElementRect.left; - floatingElem.style.opacity = '1'; + floatingElem.style.opacity = "1"; floatingElem.style.transform = `translate(${left}px, ${top}px)`; }