@@ -56,6 +56,7 @@ import {
5656 UNDO_COMMAND ,
5757 type EditorState ,
5858 type LexicalEditor ,
59+ $createTextNode ,
5960} from 'lexical' ;
6061import IconButton from './IconButton' ;
6162import ContextMenuComponent , { type MenuItem as ContextMenuItem } from './ContextMenu' ;
@@ -285,19 +286,26 @@ const ToolbarPlugin: React.FC<{
285286 } ) ;
286287 } , [ editor ] ) ;
287288
288- const toggleLink = useCallback ( ( ) => {
289- if ( readOnly ) {
290- return ;
291- }
292- if ( isLink ) {
293- editor . dispatchCommand ( TOGGLE_LINK_COMMAND , null ) ;
294- return ;
295- }
296- const url = window . prompt ( 'Enter URL' ) ;
297- if ( url ) {
298- editor . dispatchCommand ( TOGGLE_LINK_COMMAND , url ) ;
299- }
300- } , [ editor , isLink , readOnly ] ) ;
289+ const toggleLink = useCallback ( ( ) => {
290+ if ( readOnly ) {
291+ return ;
292+ }
293+ if ( isLink ) {
294+ editor . dispatchCommand ( TOGGLE_LINK_COMMAND , null ) ;
295+ return ;
296+ }
297+
298+ const promptFn = typeof window . prompt === 'function' ? window . prompt . bind ( window ) : null ;
299+ if ( ! promptFn ) {
300+ console . warn ( 'Link insertion prompt is unavailable in this environment.' ) ;
301+ return ;
302+ }
303+
304+ const url = promptFn ( 'Enter URL' ) ;
305+ if ( url ) {
306+ editor . dispatchCommand ( TOGGLE_LINK_COMMAND , url ) ;
307+ }
308+ } , [ editor , isLink , readOnly ] ) ;
301309
302310 const insertImage = useCallback (
303311 ( payload : ImagePayload ) => {
@@ -617,34 +625,7 @@ const HtmlContentSynchronizer: React.FC<{ html: string; lastAppliedHtmlRef: Reac
617625 const [ editor ] = useLexicalComposerContext ( ) ;
618626
619627 useEffect ( ( ) => {
620- const normalizedIncoming = html . trim ( ) ;
621-
622- editor . update ( ( ) => {
623- const root = $getRoot ( ) ;
624- const currentHtml = $generateHtmlFromNodes ( editor ) . trim ( ) ;
625-
626- if ( currentHtml === normalizedIncoming ) {
627- lastAppliedHtmlRef . current = normalizedIncoming ;
628- return ;
629- }
630-
631- if ( normalizedIncoming === lastAppliedHtmlRef . current && currentHtml !== '' ) {
632- return ;
633- }
634-
635- root . clear ( ) ;
636-
637- if ( ! normalizedIncoming ) {
638- lastAppliedHtmlRef . current = '' ;
639- return ;
640- }
641-
642- const parser = new DOMParser ( ) ;
643- const dom = parser . parseFromString ( normalizedIncoming , 'text/html' ) ;
644- const nodes = $generateNodesFromDOM ( editor , dom ) ;
645- nodes . forEach ( node => root . append ( node ) ) ;
646- lastAppliedHtmlRef . current = normalizedIncoming ;
647- } ) ;
628+ applyHtmlToEditor ( editor , html , lastAppliedHtmlRef ) ;
648629 } , [ editor , html , lastAppliedHtmlRef ] ) ;
649630
650631 return null ;
@@ -799,6 +780,68 @@ const ClipboardImagePlugin: React.FC<{ readOnly: boolean }> = ({ readOnly }) =>
799780 return null ;
800781} ;
801782
783+ const sanitizeDomFromHtml = ( html : string ) : Document => {
784+ const parser = new DOMParser ( ) ;
785+ const dom = parser . parseFromString ( html , 'text/html' ) ;
786+ dom . querySelectorAll ( 'script,style' ) . forEach ( node => node . remove ( ) ) ;
787+ return dom ;
788+ } ;
789+
790+ const fallbackToPlainText = ( text : string ) => {
791+ const parser = new DOMParser ( ) ;
792+ const dom = parser . parseFromString ( text , 'text/html' ) ;
793+ return ( dom . body . textContent || '' ) . trim ( ) ;
794+ } ;
795+
796+ const applyHtmlToEditor = (
797+ editor : LexicalEditor ,
798+ html : string ,
799+ lastAppliedHtmlRef : React . MutableRefObject < string > ,
800+ ) => {
801+ const normalizedIncoming = html . trim ( ) ;
802+
803+ editor . update ( ( ) => {
804+ const root = $getRoot ( ) ;
805+ const currentHtml = $generateHtmlFromNodes ( editor ) . trim ( ) ;
806+
807+ if ( currentHtml === normalizedIncoming ) {
808+ lastAppliedHtmlRef . current = normalizedIncoming ;
809+ return ;
810+ }
811+
812+ if ( normalizedIncoming === lastAppliedHtmlRef . current && currentHtml !== '' ) {
813+ return ;
814+ }
815+
816+ root . clear ( ) ;
817+
818+ if ( ! normalizedIncoming ) {
819+ lastAppliedHtmlRef . current = '' ;
820+ root . append ( $createParagraphNode ( ) ) ;
821+ return ;
822+ }
823+
824+ try {
825+ const dom = sanitizeDomFromHtml ( normalizedIncoming ) ;
826+ const nodes = $generateNodesFromDOM ( editor , dom ) ;
827+ if ( nodes . length === 0 ) {
828+ const paragraph = $createParagraphNode ( ) ;
829+ paragraph . append ( $createTextNode ( '' ) ) ;
830+ root . append ( paragraph ) ;
831+ } else {
832+ nodes . forEach ( node => root . append ( node ) ) ;
833+ }
834+ lastAppliedHtmlRef . current = normalizedIncoming ;
835+ } catch ( error ) {
836+ console . error ( 'Failed to sync HTML content into the rich text editor.' , error ) ;
837+ const paragraph = $createParagraphNode ( ) ;
838+ paragraph . append ( $createTextNode ( fallbackToPlainText ( normalizedIncoming ) ) ) ;
839+ root . append ( paragraph ) ;
840+ lastAppliedHtmlRef . current = paragraph . getTextContent ( ) ;
841+ }
842+ } ) ;
843+ } ;
844+
802845const RichTextEditor = forwardRef < RichTextEditorHandle , RichTextEditorProps > (
803846 ( { html, onChange, readOnly = false , onScroll, onFocusChange } , ref ) => {
804847 const scrollContainerRef = useRef < HTMLDivElement > ( null ) ;
@@ -860,13 +903,17 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(
860903 const handleChange = useCallback (
861904 ( editorState : EditorState , editor : LexicalEditor ) => {
862905 editorState . read ( ( ) => {
863- const generated = $generateHtmlFromNodes ( editor ) ;
864- const normalized = generated . trim ( ) ;
865- if ( normalized === lastAppliedHtmlRef . current ) {
866- return ;
906+ try {
907+ const generated = $generateHtmlFromNodes ( editor ) ;
908+ const normalized = generated . trim ( ) ;
909+ if ( normalized === lastAppliedHtmlRef . current ) {
910+ return ;
911+ }
912+ lastAppliedHtmlRef . current = normalized ;
913+ onChange ( normalized ) ;
914+ } catch ( error ) {
915+ console . error ( 'Failed to serialize rich text content to HTML.' , error ) ;
867916 }
868- lastAppliedHtmlRef . current = normalized ;
869- onChange ( normalized ) ;
870917 } ) ;
871918 } ,
872919 [ onChange ] ,
@@ -911,7 +958,7 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(
911958 editable : ! readOnly ,
912959 theme : RICH_TEXT_THEME ,
913960 onError : ( error : Error ) => {
914- throw error ;
961+ console . error ( 'Rich text editor encountered an error.' , error ) ;
915962 } ,
916963 nodes : [ HeadingNode , QuoteNode , ListNode , ListItemNode , LinkNode , ImageNode ] ,
917964 editorState : ( editor : LexicalEditor ) => {
@@ -920,15 +967,7 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(
920967 lastAppliedHtmlRef . current = '' ;
921968 return ;
922969 }
923- const parser = new DOMParser ( ) ;
924- const dom = parser . parseFromString ( initialHtml , 'text/html' ) ;
925- editor . update ( ( ) => {
926- const root = $getRoot ( ) ;
927- root . clear ( ) ;
928- const nodes = $generateNodesFromDOM ( editor , dom ) ;
929- nodes . forEach ( node => root . append ( node ) ) ;
930- lastAppliedHtmlRef . current = initialHtml ;
931- } ) ;
970+ applyHtmlToEditor ( editor , initialHtml , lastAppliedHtmlRef ) ;
932971 } ,
933972 } ) ,
934973 [ readOnly ] ,
0 commit comments