import { useRef , useState , useEffect , forwardRef , Ref } from 'react' ; import { c } from 'ttag' ; import { isMac } from '@proton/shared/lib/helpers/browser' ; import { message } from '@proton/shared/lib/sanitize' ; import { useHandler , useModals , useNotifications } from '../../hooks' ; import { getSquireRef , setSquireRef , initSquire , toggleEllipsisButton } from './squireConfig' ; import { getLinkAtCursor , makeLink , pasteFileHandler , scrollIntoViewIfNeeded } from './squireActions' ; import { SquireEditorMetadata , SquireType , FontData } from './interface' ; import { InlineLinkButton } from '../button' ; import InsertLinkModal from './modals/InsertLinkModal' ; const isHTMLEmpty = ( html ) => ! html || html === '<div><br /></div>' || html === '<div><br></div>' ; interface Props { id ? : string ; placeholder ? : string ; metadata : SquireEditorMetadata ; onReady : ( ) => void ; onFocus : ( ) => void ; onInput : ( value : string ) => void ; onAddImages : ( files : File [ ] ) => void ; showEllipseButton : boolean ; onEllipseClick : ( ) => void ; keydownHandler ? : ( e : KeyboardEvent ) => void ; defaultFont ? : FontData ; } /**
 * This component is *Uncontrolled*
 * https://reactjs.org/docs/uncontrolled-components.html
 * There is issues when trying to synchronize input value to the current content of the editor
 * Uncontrolled components is prefered in this case
 */ const SquireIframe = ( { placeholder , metadata , onReady , onFocus , onInput , onAddImages , showEllipseButton , onEllipseClick , keydownHandler , defaultFont , ... rest } , ref ) => { const { createNotification } = useNotifications ( ) ; const [ iframeReady , setIframeReady ] = useState ( false ) ; const [ squireReady , setSquireReady ] = useState ( false ) ; const [ isEmpty , setIsEmpty ] = useState ( false ) ; const iframeRef = useRef < HTMLIFrameElement > ( null ) ; useEffect ( ( ) => { const handleLoad = ( ) => setIframeReady ( true ) ; const iframeDoc = iframeRef . current ?. contentDocument && iframeRef . current ?. contentWindow ?. document ; if ( iframeDoc ?. readyState === 'complete' ) { handleLoad ( ) ; } iframeRef . current ?. addEventListener ( 'load' , handleLoad ) ; return ( ) => iframeRef . current ?. removeEventListener ( 'load' , handleLoad ) ; } , [ ] ) ; useEffect ( ( ) => { const init = async ( iframeDoc ) => { try { const squire = await initSquire ( iframeDoc , metadata . supportImages , onEllipseClick , defaultFont ) ; setSquireRef ( ref , squire ) ; setSquireReady ( true ) ; onReady ( ) ; } catch ( error ) { const text = c ( 'Error' ) . t `The composer failed to load.` ; console . error ( text , error ) ; createNotification ( { type : 'error' , text : ( < > { text } < InlineLinkButton className = "text-bold" onClick = { ( ) => { window . location . reload ( ) ; } } > { c ( 'Error' ) . t `Please refresh the page.` } < /InlineLinkButton> < />), expiration : 20000 , } ) ; } } ; if ( iframeReady && ! squireReady ) { const iframeDoc = iframeRef . current ?. contentWindow ?. document as Document ; void init ( iframeDoc ) ; } } , [ iframeReady ] ) ; useEffect ( ( ) => { if ( squireReady ) { toggleEllipsisButton ( iframeRef . current ?. contentWindow ?. document as Document , showEllipseButton ) ; } } , [ squireReady , showEllipseButton ] ) ; const handleFocus = useHandler ( ( ) => { onFocus ( ) ; document . dispatchEvent ( new CustomEvent ( 'dropdownclose' ) ) ; } ) ; const handleInput = useHandler ( ( ) => { const content = getSquireRef ( ref ) . getHTML ( ) ; setIsEmpty ( isHTMLEmpty ( content ) ) ; onInput ( content ) ; } ) ; const handlePaste = useHandler ( ( event ) => { setTimeout ( ( ) => { handleInput ( ) ; } , 100 ) ; if ( metadata . supportImages ) { pasteFileHandler ( event , onAddImages ) ; } } ) ; const handleCursor = ( ) => scrollIntoViewIfNeeded ( getSquireRef ( ref ) ) ; const handlePassDragEvents = useHandler ( ( event ) => { const newEvent = new CustomEvent ( event . type , { bubbles : true } ) as any ; newEvent . dataTransfer = event . dataTransfer ; iframeRef . current ?. dispatchEvent ( newEvent ) ; } ) ; const { createModal } = useModals ( ) ; const handleLink = ( squire ) => { const link = getLinkAtCursor ( squire ) ; createModal ( < InsertLinkModal inputLink = { link } onSubmit = { ( linkData ) => makeLink ( squire , linkData ) } />); } ; const handleKeyDown = ( e , squire ) => { const ctrlOrMetaKey = isMac ( ) ? e . metaKey : e . ctrlKey ; if ( e . key === 'k' && ctrlOrMetaKey ) { e . preventDefault ( ) ; e . stopPropagation ( ) ; handleLink ( squire ) ; return ; } keydownHandler ?. ( e ) ; } ; const handleDrop = ( e , squire ) => { if ( ! e . dataTransfer ) { return ; } const hasHtml = e . dataTransfer . types . some ( ( type ) => type === 'text/html' ) ; if ( hasHtml || ! metadata . supportImages ) { const data = e . dataTransfer . getData ( 'text/html' ) ; e . preventDefault ( ) ; squire . insertHTML ( message ( data ) ) ; } } ; useEffect ( ( ) => { if ( squireReady ) { const squire = getSquireRef ( ref ) ; const iframeDocumentRef = iframeRef . current ?. contentDocument ; const keydownHandler = ( e ) => handleKeyDown ( e , squire ) ; const dropHandler = ( e ) => handleDrop ( e , squire ) ; squire . addEventListener ( 'focus' , handleFocus ) ; squire . addEventListener ( 'input' , handleInput ) ; squire . addEventListener ( 'paste' , handlePaste ) ; iframeDocumentRef ?. addEventListener ( 'dragenter' , handlePassDragEvents ) ; iframeDocumentRef ?. addEventListener ( 'dragleave' , handlePassDragEvents ) ; iframeDocumentRef ?. addEventListener ( 'dragover' , handlePassDragEvents ) ; iframeDocumentRef ?. addEventListener ( 'drop' , dropHandler ) ; squire . addEventListener ( 'cursor' , handleCursor ) ; squire . addEventListener ( 'keyup' , handleInput ) ; squire . addEventListener ( 'keydown' , keydownHandler ) ; return ( ) => { squire . removeEventListener ( 'focus' , handleFocus ) ; squire . removeEventListener ( 'input' , handleInput ) ; squire . removeEventListener ( 'paste' , handlePaste ) ; iframeDocumentRef ?. removeEventListener ( 'dragenter' , handlePassDragEvents ) ; iframeDocumentRef ?. removeEventListener ( 'dragleave' , handlePassDragEvents ) ; iframeDocumentRef ?. removeEventListener ( 'dragover' , handlePassDragEvents ) ; iframeDocumentRef ?. removeEventListener ( 'drop' , dropHandler ) ; squire . removeEventListener ( 'cursor' , handleCursor ) ; squire . removeEventListener ( 'keyup' , handleInput ) ; squire . removeEventListener ( 'keydown' , keydownHandler ) ; } ; } } , [ squireReady ] ) ; return ( < div className = "editor-squire-wrapper fill w100 h100 scroll-if-needed flex-item-fluid relative" > { placeholder && isEmpty && < div className = "absolute-only no-pointer-events placeholder" > { placeholder } < /div>} < iframe title = "Editor" ref = { iframeRef } frameBorder = "0" className = "w100 h100 squireIframe" data - testid = "squire-iframe" { ... rest } /> < /div>); } ; export default forwardRef ( SquireIframe ) ;