import React, {Component} from 'react';
import PropTypes from 'prop-types';
import WebView from 'react-native-webview';
import {MessageConverter} from './WebviewMessageHandler';
import {actions, messages} from './const';
import {Modal, View, Text, StyleSheet, TextInput, TouchableOpacity, Platform, PixelRatio, Keyboard, Dimensions} from 'react-native';
const PlatformIOS = Platform.OS === 'ios';
export default class RichTextEditor extends Component {
static propTypes = {
initialTitleHTML: PropTypes.string,
initialContentHTML: PropTypes.string,
titlePlaceholder: PropTypes.string,
contentPlaceholder: PropTypes.string,
editorInitializedCallback: PropTypes.func,
customCSS: PropTypes.string,
hiddenTitle: PropTypes.bool,
enableOnChange: PropTypes.bool,
footerHeight: PropTypes.number,
contentInset: PropTypes.object
};
static defaultProps = {
contentInset: {},
style: {}
};
constructor(props) {
super(props);
this._sendAction = this._sendAction.bind(this);
this.registerToolbar = this.registerToolbar.bind(this);
this.onMessage = this.onMessage.bind(this);
this._onKeyboardWillShow = this._onKeyboardWillShow.bind(this);
this._onKeyboardWillHide = this._onKeyboardWillHide.bind(this);
this.state = {
selectionChangeListeners: [],
onChange: [],
isShowLinkDialog: false,
linkInitialUrl: '',
linkTitle: '',
linkUrl: '',
keyboardHeight: 0
};
this._selectedTextChangeListeners = [];
}
componentDidMount() {
if(PlatformIOS) {
this.keyboardEventListeners = [
Keyboard.addListener('keyboardWillShow', this._onKeyboardWillShow),
Keyboard.addListener('keyboardWillHide', this._onKeyboardWillHide)
];
} else {
this.keyboardEventListeners = [
Keyboard.addListener('keyboardDidShow', this._onKeyboardWillShow),
Keyboard.addListener('keyboardDidHide', this._onKeyboardWillHide)
];
if (this.props.autoFocus) {
this.webview.requestFocus();
}
}
}
componentWillUnmount() {
this.keyboardEventListeners.forEach((eventListener) => eventListener.remove());
}
_onKeyboardWillShow(event) {
const newKeyboardHeight = event.endCoordinates.height;
if (this.state.keyboardHeight === newKeyboardHeight) {
return;
}
if (newKeyboardHeight) {
this.setEditorAvailableHeightBasedOnKeyboardHeight(newKeyboardHeight);
}
this.setState({keyboardHeight: newKeyboardHeight});
}
_onKeyboardWillHide(event) {
this.setState({keyboardHeight: 0});
}
setEditorAvailableHeightBasedOnKeyboardHeight(keyboardHeight) {
const {top = 0, bottom = 0} = this.props.contentInset;
const {marginTop = 0, marginBottom = 0} = this.props.style;
const {heightToScreenTop = 0} = this.props;
const spacing = marginTop + marginBottom + top + bottom;
const editorAvailableHeight = Dimensions.get('window').height - keyboardHeight - spacing - heightToScreenTop;
this.setEditorHeight(editorAvailableHeight);
}
onMessage({ nativeEvent }){
const { data: str } = nativeEvent;
try {
const message = JSON.parse(str);
switch (message.type) {
case messages.TITLE_HTML_RESPONSE:
if (this.titleResolve) {
this.titleResolve(message.data);
this.titleResolve = undefined;
this.titleReject = undefined;
if (this.pendingTitleHtml) {
clearTimeout(this.pendingTitleHtml);
this.pendingTitleHtml = undefined;
}
}
break;
case messages.TITLE_TEXT_RESPONSE:
if (this.titleTextResolve) {
this.titleTextResolve(message.data);
this.titleTextResolve = undefined;
this.titleTextReject = undefined;
if (this.pendingTitleText) {
clearTimeout(this.pendingTitleText);
this.pendingTitleText = undefined;
}
}
break;
case messages.CONTENT_HTML_RESPONSE:
if (this.contentResolve) {
this.contentResolve(message.data);
this.contentResolve = undefined;
this.contentReject = undefined;
if (this.pendingContentHtml) {
clearTimeout(this.pendingContentHtml);
this.pendingContentHtml = undefined;
}
}
break;
case messages.SELECTED_TEXT_RESPONSE:
if (this.selectedTextResolve) {
this.selectedTextResolve(message.data);
this.selectedTextResolve = undefined;
this.selectedTextReject = undefined;
if (this.pendingSelectedText) {
clearTimeout(this.pendingSelectedText);
this.pendingSelectedText = undefined;
}
}
break;
case messages.ZSS_INITIALIZED:
if (this.props.customCSS) {
this.setCustomCSS(this.props.customCSS);
}
this.setTitlePlaceholder(this.props.titlePlaceholder);
this.setContentPlaceholder(this.props.contentPlaceholder);
this.setTitleHTML(this.props.initialTitleHTML || '');
this.setContentHTML(this.props.initialContentHTML || '');
!this.props.hiddenTitle && this.showTitle();
this.props.enableOnChange && this.enableOnChange();
this.props.editorInitializedCallback && this.props.editorInitializedCallback();
break;
case messages.LINK_TOUCHED:
const {title, url} = message.data;
this.showLinkDialog(title, url);
break;
case messages.LOG:
console.log('FROM ZSS', message.data);
break;
case messages.SCROLL:
this.webview.setNativeProps({contentOffset: {y: message.data}});
break;
case messages.TITLE_FOCUSED:
this.titleFocusHandler && this.titleFocusHandler();
break;
case messages.CONTENT_FOCUSED:
this.contentFocusHandler && this.contentFocusHandler();
break;
case messages.CONTENT_BLUR:
this.contentBlurHandler && this.contentBlurHandler();
break;
case messages.ONCHANGE_EMPTY_OR_NOT:
this.onChangeEmptyOrNot && this.onChangeEmptyOrNot(message.isEmpty);
break;
case messages.SELECTION_CHANGE: {
const items = message.data.items;
this.state.selectionChangeListeners.map((listener) => {
listener(items);
});
break;
}
case messages.CONTENT_CHANGE: {
const content = message.data.content;
this.state.onChange.map((listener) => listener(content));
break;
}
case messages.SELECTED_TEXT_CHANGED: {
const selectedText = message.data;
this._selectedTextChangeListeners.forEach((listener) => {
listener(selectedText);
});
break;
}
}
} catch(e) {
//alert('NON JSON MESSAGE');
}
}
_renderLinkModal() {
const {linkOption,optionalText} = this.props;
return (
this.setState({isShowLinkDialog: false})}
>
{linkOption.titleText}
this.setState({linkTitle: text})}
value={this.state.linkTitle}
placeholder={optionalText}
/>
{linkOption.urlText}
this.setState({linkUrl: text})}
value={this.state.linkUrl}
keyboardType="url"
autoCapitalize="none"
autoCorrect={false}
/>
{this._renderModalButtons()}
);
}
_hideModal() {
this.setState({
isShowLinkDialog: false,
linkInitialUrl: '',
linkTitle: '',
linkUrl: ''
})
}
_renderModalButtons() {
const insertUpdateDisabled = this.state.linkUrl.trim().length <= 0;
const containerPlatformStyle = {justifyContent: 'space-between'};
const buttonPlatformStyle = {flex: 1, height: 45, justifyContent: 'center'};
const { linkOption } = this.props;
return (
this._hideModal()}
style={buttonPlatformStyle}
>
{linkOption.cancelText}
{
if (this._linkIsNew()) {
this.insertLink(this.state.linkUrl, this.state.linkTitle);
} else {
this.updateLink(this.state.linkUrl, this.state.linkTitle);
}
this._hideModal();
}}
disabled={insertUpdateDisabled}
style={buttonPlatformStyle}
>
{this._linkIsNew() ? linkOption.insertText : linkOption.updateText}
);
}
_linkIsNew() {
return !this.state.linkInitialUrl;
}
render() {
//in release build, external html files in Android can't be required, so they must be placed in the assets folder and accessed via uri
const pageSource = PlatformIOS ? require('./editor.html') : { uri: 'file:///android_asset/editor.html' };
return (
{this.webview = r}}
onMessage={(message) => this.onMessage(message)}
source={pageSource}
onLoad={() => this.init()}
/>
{this._renderLinkModal()}
);
}
_sendAction(action, data) {
let jsToBeExecutedOnPage = MessageConverter({ type: action, data });
this.webview.injectJavaScript(jsToBeExecutedOnPage + ';true;');
}
//-------------------------------------------------------------------------------
//--------------- Public API
showLinkDialog(optionalTitle = '', optionalUrl = '', notCheckedUrlCallback) {
this.notCheckedUrlCallback = notCheckedUrlCallback || this.notCheckedUrlCallback;
this.prepareInsert();
this.setState({
linkInitialUrl: optionalUrl,
linkTitle: optionalTitle,
linkUrl: optionalUrl,
isShowLinkDialog: true
});
}
focusTitle() {
this._sendAction(actions.focusTitle);
}
focusContent() {
this._sendAction(actions.focusContent);
}
registerToolbar(listener) {
this.setState({
selectionChangeListeners: [...this.state.selectionChangeListeners, listener]
});
}
enableOnChange() {
this._sendAction(actions.enableOnChange);
}
registerContentChangeListener(listener) {
this.setState({
onChange: [...this.state.onChange, listener]
});
}
setTitleHTML(html) {
this._sendAction(actions.setTitleHtml, html);
}
hideTitle() {
this._sendAction(actions.hideTitle);
}
showTitle() {
this._sendAction(actions.showTitle);
}
toggleTitle() {
this._sendAction(actions.toggleTitle);
}
setContentHTML(html) {
this._sendAction(actions.setContentHtml, html);
}
blurTitleEditor() {
this._sendAction(actions.blurTitleEditor);
}
blurContentEditor() {
this._sendAction(actions.blurContentEditor);
}
setBold() {
this._sendAction(actions.setBold);
}
setItalic() {
this._sendAction(actions.setItalic);
}
setUnderline() {
this._sendAction(actions.setUnderline);
}
heading1() {
this._sendAction(actions.heading1);
}
heading2() {
this._sendAction(actions.heading2);
}
heading3() {
this._sendAction(actions.heading3);
}
heading4() {
this._sendAction(actions.heading4);
}
heading5() {
this._sendAction(actions.heading5);
}
heading6() {
this._sendAction(actions.heading6);
}
setParagraph() {
this._sendAction(actions.setParagraph);
}
removeFormat() {
this._sendAction(actions.removeFormat);
}
alignLeft() {
this._sendAction(actions.alignLeft);
}
alignCenter() {
this._sendAction(actions.alignCenter);
}
alignRight() {
this._sendAction(actions.alignRight);
}
alignFull() {
this._sendAction(actions.alignFull);
}
insertBulletsList() {
this._sendAction(actions.insertBulletsList);
}
insertOrderedList() {
this._sendAction(actions.insertOrderedList);
}
insertLink(url, title) {
// 不填写标题,则采用链接作为url
title = title || url;
// 如果前没有http://,则需要加入
if (!(/^(http:\/\/|https:\/\/)/.test(url))) {
url = `http://${url}`;
}
this._sendAction(actions.insertLink, {url, title});
// if (/^(http:\/\/|https:\/\/)/.test(url)) {
// this._sendAction(actions.insertLink, {url, title: title || url});
// } else {
// this.notCheckedUrlCallback && this.notCheckedUrlCallback()
// }
}
updateLink(url, title) {
// 不填写标题,则采用链接作为url
title = title || url;
// 如果前没有http://,则需要加入
if (!(/^(http:\/\/|https:\/\/)/.test(url))) {
url = `http://${url}`;
}
this._sendAction(actions.updateLink, {url, title});
// if (/^(http:\/\/|https:\/\/)/.test(url)) {
// this._sendAction(actions.updateLink, {url, title: title || url});
// } else {
// this.notCheckedUrlCallback && this.notCheckedUrlCallback()
// }
}
insertImage(url) {
this._sendAction(actions.insertImage, url);
}
insertEmoji(url, tag) {
this._sendAction(actions.insertEmoji, { tag, url });
}
deleteEmoji(url) {
this._sendAction(actions.deleteEmoji, url);
}
setSubscript() {
this._sendAction(actions.setSubscript);
}
setSuperscript() {
this._sendAction(actions.setSuperscript);
}
setStrikethrough() {
this._sendAction(actions.setStrikethrough);
}
setHR() {
this._sendAction(actions.setHR);
}
setIndent() {
this._sendAction(actions.setIndent);
}
setOutdent() {
this._sendAction(actions.setOutdent);
}
setBackgroundColor(color) {
this._sendAction(actions.setBackgroundColor, color);
}
setTextColor(color) {
this._sendAction(actions.setTextColor, color);
}
setTitlePlaceholder(placeholder) {
this._sendAction(actions.setTitlePlaceholder, placeholder);
}
setContentPlaceholder(placeholder) {
this._sendAction(actions.setContentPlaceholder, placeholder);
}
setCustomCSS(css) {
this._sendAction(actions.setCustomCSS, css);
}
prepareInsert(showCaretPlaceholder) {
this._sendAction(actions.prepareInsert, showCaretPlaceholder);
}
restoreSelection() {
this._sendAction(actions.restoreSelection);
}
init() {
this._sendAction(actions.init);
this.setPlatform();
if (this.props.footerHeight) {
this.setFooterHeight();
}
}
setEditorHeight(height) {
this._sendAction(actions.setEditorHeight, height);
}
setFooterHeight() {
this._sendAction(actions.setFooterHeight, this.props.footerHeight);
}
setPlatform() {
this._sendAction(actions.setPlatform, Platform.OS);
}
async getTitleHtml() {
return new Promise((resolve, reject) => {
this.titleResolve = resolve;
this.titleReject = reject;
this._sendAction(actions.getTitleHtml);
this.pendingTitleHtml = setTimeout(() => {
if (this.titleReject) {
this.titleReject('timeout')
}
}, 5000);
});
}
async getTitleText() {
return new Promise((resolve, reject) => {
this.titleTextResolve = resolve;
this.titleTextReject = reject;
this._sendAction(actions.getTitleText);
this.pendingTitleText = setTimeout(() => {
if (this.titleTextReject) {
this.titleTextReject('timeout');
}
}, 5000);
});
}
async getContentHtml() {
return new Promise((resolve, reject) => {
this.contentResolve = resolve;
this.contentReject = reject;
this._sendAction(actions.getContentHtml);
this.pendingContentHtml = setTimeout(() => {
if (this.contentReject) {
this.contentReject('timeout')
}
}, 5000);
});
}
async getSelectedText() {
return new Promise((resolve, reject) => {
this.selectedTextResolve = resolve;
this.selectedTextReject = reject;
this._sendAction(actions.getSelectedText);
this.pendingSelectedText = setTimeout(() => {
if (this.selectedTextReject) {
this.selectedTextReject('timeout')
}
}, 5000);
});
}
setTitleFocusHandler(callbackHandler) {
this.titleFocusHandler = callbackHandler;
this._sendAction(actions.setTitleFocusHandler);
}
setContentFocusHandler(callbackHandler) {
this.contentFocusHandler = callbackHandler;
this._sendAction(actions.setContentFocusHandler);
}
setContentBlurHandler(callbackHandler) {
this.contentBlurHandler = callbackHandler;
this._sendAction(actions.setContentBlurHandler);
}
setOnChangeEmptyOrNot(callbackHandler) {
this.onChangeEmptyOrNot = callbackHandler;
this._sendAction(actions.setOnChangeEmptyOrNot);
}
addSelectedTextChangeListener(listener) {
this._selectedTextChangeListeners.push(listener);
}
}
const styles = StyleSheet.create({
modal: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.5)'
},
innerModal: {
backgroundColor: 'rgba(255, 255, 255, 0.9)',
paddingTop: 20,
paddingBottom: 0,
paddingLeft: 20,
paddingRight: 20,
alignSelf: 'stretch',
margin: 40,
borderRadius: 8
},
button: {
fontSize: 16,
color: '#4a4a4a',
textAlign: 'center'
},
inputWrapper: {
marginTop: 5,
marginBottom: 10,
borderBottomColor: '#4a4a4a',
borderBottomWidth: StyleSheet.hairlineWidth,
justifyContent :'flex-end',
height: 30,
paddingBottom: 1,
},
inputTitle: {
color: '#4a4a4a'
},
input: {
padding: 0,
margin: 0,
includeFontPadding:false,
},
lineSeparator: {
height: 1 / PixelRatio.get(),
backgroundColor: '#d5d5d5',
marginLeft: -20,
marginRight: -20,
marginTop: 15
}
});