import * as React from 'react' ; import { WebView , WebViewMessageEvent , WebViewProps , } from 'react-native-webview' ; import { View , Text , StyleSheet , StyleProp , ViewStyle , Platform , } from 'react-native' ; import { createHtml } from '../utils/editor-utils' ; import type { CustomFont , EditorMessage , EditorResponse , QuillConfig , } from '../types' ; import type { EditorEventHandler , EditorEventType , SelectionChangeData , EditorChangeData , TextChangeData , HtmlChangeData , DimensionsChangeData , } from '../constants/editor-event' ; import { Loading } from './loading' ; export interface EditorState { webviewContent : string | null ; height ? : number ; } export interface EditorProps { autoSize ? : boolean ; style ? : StyleProp < ViewStyle > ; quill ? : QuillConfig ; customFonts ? : Array < CustomFont > ; defaultFontFamily ? : string ; initialHtml ? : string ; customStyles ? : string [ ] ; import3rdParties ? : 'local' | 'cdn' ; containerId ? : string ; theme ? : { background : string ; color : string ; placeholder : string ; } ; loading ? : string | React . ReactNode ; container ? : boolean | React . ComponentType ; onSelectionChange ? : ( data : SelectionChangeData ) => void ; onTextChange ? : ( data : TextChangeData ) => void ; onHtmlChange ? : ( data : HtmlChangeData ) => void ; onEditorChange ? : ( data : EditorChangeData ) => void ; onDimensionsChange ? : ( data : DimensionsChangeData ) => void ; webview ? : WebViewProps ; onBlur ? : ( ) => void ; onFocus ? : ( ) => void ; customJS ? : string ; } export default class QuillEditor extends React . Component < EditorProps , EditorState > { private _webview : React . RefObject < WebView > ; private _handlers : Array < { event : EditorEventType ; handler : EditorEventHandler ; } > ; private _promises : Array < EditorResponse > ; constructor ( props ) { super ( props ) ; this . _webview = React . createRef ( ) ; this . state = { webviewContent : this . getInitalHtml ( ) , } ; this . _handlers = [ ] ; this . _promises = [ ] ; const { onSelectionChange , onEditorChange , onTextChange , onHtmlChange , onDimensionsChange , onBlur , onFocus , } = this . props ; if ( onSelectionChange ) { this . on ( 'selection-change' , onSelectionChange ) ; } if ( onEditorChange ) { this . on ( 'editor-change' , onEditorChange ) ; } if ( onTextChange ) { this . on ( 'text-change' , onTextChange ) ; } if ( onHtmlChange ) { this . on ( 'html-change' , onHtmlChange ) ; } if ( onDimensionsChange ) { this . on ( 'dimensions-change' , onDimensionsChange ) ; } if ( onBlur ) { this . on ( 'blur' , onBlur ) ; } if ( onFocus ) { this . on ( 'focus' , onFocus ) ; } } private getInitalHtml = ( ) : string => { const { initialHtml = '' , import3rdParties = 'local' , containerId = 'standalone-container' , theme = { background : 'white' , color : 'rgb(32, 35, 42)' , placeholder : 'rgba(0,0,0,0.6)' , } , quill = { id : 'editor-container' , placeholder : 'write here!' , modules : { toolbar : false , } , theme : 'snow' , } , customFonts = [ ] , customStyles = [ ] , defaultFontFamily = undefined , customJS = '' , } = this . props ; return createHtml ( { initialHtml , autoSize : this . props . autoSize , placeholder : quill . placeholder , theme : quill . theme ? quill . theme : 'snow' , toolbar : JSON . stringify ( quill . modules ?. toolbar ) , clipboard : quill . modules ?. clipboard , libraries : import3rdParties , editorId : quill . id ? quill . id : 'editor-container' , defaultFontFamily , containerId , color : theme . color , fonts : customFonts , backgroundColor : theme . background , placeholderColor : theme . placeholder , customStyles , customJS , } ) ; } ; private getKey ( ) { var timestamp = new Date ( ) . getUTCMilliseconds ( ) ; return `${ timestamp }${ Math . random ( ) }` ; } private postAwait < T > ( data ) { const key = this . getKey ( ) ; let resolveFn ; resolveFn = ( ) => { } ; const promise = new Promise < T > ( ( resolve ) => { resolveFn = resolve ; } ) ; const resp = { key , resolve : resolveFn , } ; this . _promises . push ( resp ) ; this . post ( { ... data , key } ) ; return promise ; } private post = ( obj ) => { const jsonString = JSON . stringify ( obj ) ; this . _webview . current ?. postMessage ( jsonString ) ; } ; private toMessage = ( data ) : EditorMessage => { const message = JSON . parse ( data ) ; return message ; } ; private onMessage = ( event ) => { const message = this . toMessage ( event . nativeEvent . data ) ; const { autoSize } = this . props ; const response = message . key ? this . _promises . find ( ( x ) => x . key === message . key ) : undefined ; switch ( message . type ) { case 'dimensions-change' : if ( autoSize === true ) this . setState ( { height : message . data . height } ) ; this . _handlers . filter ( ( x ) => x . event === message . type ) . forEach ( ( item ) => item . handler ( message . data ) ) ; break ; case 'format-change' : case 'text-change' : case 'selection-change' : case 'html-change' : case 'editor-change' : case 'blur' : case 'focus' : this . _handlers . filter ( ( x ) => x . event === message . type ) . forEach ( ( item ) => item . handler ( message . data ) ) ; break ; case 'has-focus' : case 'get-contents' : case 'get-text' : case 'get-length' : case 'get-bounds' : case 'get-selection' : case 'get-dimensions' : case 'get-html' : if ( response ) { response . resolve ( message . data ) ; this . _promises = this . _promises . filter ( ( x ) => x . key !== message . key ) ; } break ; default : if ( this . props . webview ?. onMessage ) { this . props . webview ?. onMessage ( event ) ; } } } ; blur = ( ) => { this . post ( { command : 'blur' } ) ; } ; focus = ( ) => { this . post ( { command : 'focus' } ) ; if ( Platform . OS === 'android' ) { this . _webview . current ?. requestFocus ( ) ; } } ; hasFocus = ( ) : Promise < boolean > => { return this . postAwait < any > ( { command : 'hasFocus' } ) ; } ; enable = ( enable = true ) => { this . post ( { command : 'enable' , value : enable } ) ; } ; disable = ( ) => { this . post ( { command : 'enable' , value : false } ) ; } ; update = ( ) => { this . post ( { command : 'update' } ) ; } ; format = ( name , value ) => { this . post ( { command : 'format' , name , value } ) ; } ; deleteText = ( index , length ) => { this . post ( { command : 'deleteText' , index , length } ) ; } ; getDimensions = ( ) : Promise < any > => { return this . postAwait < any > ( { command : 'getDimensions' } ) ; } ; getContents = ( index ? , length ? ) : Promise < any > => { return this . postAwait < any > ( { command : 'getContents' , index , length } ) ; } ; getHtml = ( ) : Promise < any > => { return this . postAwait < any > ( { command : 'getHtml' } ) ; } ; getLength = ( ) : Promise < any > => { return this . postAwait < any > ( { command : 'getLength' } ) ; } ; getText = ( index ? , length ? ) : Promise < any > => { return this . postAwait < any > ( { command : 'getText' , index , length } ) ; } ; getBounds = ( index , length ? ) : Promise < any > => { return this . postAwait < any > ( { command : 'getBounds' , index , length } ) ; } ; getSelection = ( focus = false ) : Promise < any > => { return this . postAwait < any > ( { command : 'getSelection' , focus } ) ; } ; setSelection = ( index , length ? , source ? ) => { this . post ( { command : 'setSelection' , index , length , source } ) ; } ; insertEmbed = ( index , type , value ) => { this . post ( { command : 'insertEmbed' , index , type , value } ) ; } ; insertText = ( index , text , formats ? ) => { this . post ( { command : 'insertText' , index , text , formats } ) ; } ; setContents = ( delta ) => { this . post ( { command : 'setContents' , delta } ) ; } ; setText = ( text ) => { this . post ( { command : 'setText' , text } ) ; } ; updateContents = ( delta ) => { this . post ( { command : 'updateContents' , delta } ) ; } ; on = ( event , handler ) => { this . _handlers . push ( { event , handler } ) ; } ; off = ( event , handler ) => { const index = this . _handlers . findIndex ( ( x ) => x . event === event && x . handler === handler ) ; if ( index > - 1 ) { this . _handlers . splice ( index , 1 ) ; } } ; dangerouslyPasteHTML = ( index , html ) => { this . post ( { command : 'dangerouslyPasteHTML' , index , html } ) ; } ; renderWebview = ( content , style , props = { } ) => ( < WebView scrollEnabled = { false } hideKeyboardAccessoryView = { true } keyboardDisplayRequiresUserAction = { false } originWhitelist = { [ '*' ] } style = { style } onError = { ( syntheticEvent ) => { const { nativeEvent } = syntheticEvent ; console . warn ( 'WebView error: ' , nativeEvent ) ; } } allowFileAccess = { true } domStorageEnabled = { false } automaticallyAdjustContentInsets = { true } bounces = { false } dataDetectorTypes = "none" { ... props } javaScriptEnabled = { true } source = { { html : content } } ref = { this . _webview } onMessage = { this . onMessage } / > ) ; render ( ) { const { webviewContent , height } = this . state ; const { style , webview , container = false , loading = 'Please Wait ...' , autoSize = false , } = this . props ; if ( container === false ) { if ( ! webviewContent ) return < Text > Please wait ... < /Text>; return this . renderWebview ( webviewContent , style , webview ) ; } else { const ContainerComponent = container === true ? View : container ; return ( < ContainerComponent style = { [ style , autoSize && height ? { height } : { } ] } > { webviewContent ? ( this . renderWebview ( webviewContent , styles . webView , webview ) ) : typeof loading === 'string' ? ( < Loading text = { loading } / > ) : ( loading ) } < /ContainerComponent>); } } } let styles = StyleSheet . create ( { webView : { flexGrow : 1 , borderWidth : 0 , } , } ) ;