import React, {Component, PropTypes} from 'react';
import WebViewBridge from 'react-native-webview-bridge-updated';
import {InjectedMessageHandler} from './WebviewMessageHandler';
import {actions, messages} from './const';
import {Modal, View, Text, StyleSheet, TextInput, TouchableOpacity, Platform, PixelRatio, Keyboard} from 'react-native';
const injectScript = `
(function () {
${InjectedMessageHandler}
}());
`;
const PlatfomIOS = 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
};
constructor(props) {
super(props);
this._sendAction = this._sendAction.bind(this);
this.registerToolbar = this.registerToolbar.bind(this);
this.onBridgeMessage = this.onBridgeMessage.bind(this);
this._onKeyboardWillShow = this._onKeyboardWillShow.bind(this);
this._onKeyboardWillHide = this._onKeyboardWillHide.bind(this);
this.state = {
listeners: [],
showLinkDialog: false,
linkTitle: '',
linkUrl: '',
keyboardHeight: 0
};
}
componentWillMount() {
if(PlatfomIOS) {
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)
];
}
}
componentWillUnmount() {
this.keyboardEventListeners.forEach((eventListener) => eventListener.remove());
}
_onKeyboardWillShow(event) {
console.log('!!!!', event);
const newKeyboardHeight = event.endCoordinates.height;
if (this.state.keyboardHeight === newKeyboardHeight) {
return;
}
this.setState({keyboardHeight: newKeyboardHeight});
}
_onKeyboardWillHide(event) {
this.setState({keyboardHeight: 0});
}
onBridgeMessage(str){
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.ZSS_INITIALIZED:
if (this.props.customCSS) {
this.setCustomCSS(this.props.customCSS);
}
this.setTitlePlaceholder(this.props.titlePlaceholder);
this.setContentPlaceholder(this.props.contentPlaceholder);
this.setTitleHTML(this.htmlEcodeString(this.props.initialTitleHTML));
this.setContentHTML(this.htmlEcodeString(this.props.initialContentHTML));
this.props.editorInitializedCallback && this.props.editorInitializedCallback();
break;
case messages.LOG:
console.log('FROM ZSS', message.data);
break;
case messages.SCROLL:
this.webviewBridge.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.SELECTION_CHANGE:
const items = message.data.items;
this.state.listeners.map((listener) => listener(items));
break
}
} catch(e) {
//alert('NON JSON MESSAGE');
}
}
_renderLinkModal() {
return (
this.setState({showLinkDialog: false})}
>
Title
this.setState({linkTitle: text})}
value={this.state.linkTitle}
/>
URL
this.setState({linkUrl: text})}
value={this.state.linkUrl}
keyboardType="url"
autoCapitalize="none"
autoCorrect={false}
/>
{PlatfomIOS && }
{this._renderModalButtons()}
);
}
_hideModal() {
this.setState({
showLinkDialog: false,
linkTitle: '',
linkUrl: ''
})
}
_renderModalButtons() {
const insertDisabled = this.state.linkTitle.trim().length <= 0 || this.state.linkUrl.trim().length <= 0;
const containerPlatformStyle = PlatfomIOS ? {justifyContent: 'space-between'} : {paddingTop: 15};
const buttonPlatformStyle = PlatfomIOS ? {flex: 1, height: 45, justifyContent: 'center'} : {};
return (
{!PlatfomIOS && }
this._hideModal()}
style={buttonPlatformStyle}
>
{PlatfomIOS ? 'Cancel' : 'CANCEL'}
{
this.insertLink(this.state.linkUrl, this.state.linkTitle);
this._hideModal();
}}
disabled={insertDisabled}
style={buttonPlatformStyle}
>
{PlatfomIOS ? 'Insert' : 'INSERT'}
);
}
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 = PlatfomIOS ? require('./editor.html') : { uri: 'file:///android_asset/editor.html' };
return (
{this.webviewBridge = r}}
onBridgeMessage={(message) => this.onBridgeMessage(message)}
injectedJavaScript={injectScript}
source={pageSource}
onLoad={() => this.init()}
onShouldStartLoadWithRequest={(event) => {return this._onShouldStartLoadWithRequest(event)}}
/>
{this._renderLinkModal()}
);
}
_onShouldStartLoadWithRequest(event) {
return (event.url.indexOf('editor.html') != -1);
}
escapeJSONString = function(string) {
return string
.replace(/[\\]/g, '\\\\')
.replace(/[\"]/g, '\\\"')
.replace(/[\/]/g, '\\/')
.replace(/[\b]/g, '\\b')
.replace(/[\f]/g, '\\f')
.replace(/[\n]/g, '\\n')
.replace(/[\r]/g, '\\r')
.replace(/[\t]/g, '\\t');
};
htmlEcodeString = function (string) {
//for some reason there's an issue only with apostrophes
return string
.replace(/'/g, ''');
}
_sendAction(action, data) {
let jsonString = JSON.stringify({type: action, data});
jsonString = this.escapeJSONString(jsonString);
this.webviewBridge.sendToBridge(jsonString);
}
//-------------------------------------------------------------------------------
//--------------- Public API
showLinkDialog() {
this.setState({
showLinkDialog: true
});
}
focusTitle() {
this._sendAction(actions.focusTitle);
}
focusContent() {
this._sendAction(actions.focusContent);
}
registerToolbar(listener) {
this.setState({
listeners: [...this.state.listeners, listener]
});
}
setTitleHTML(html) {
this._sendAction(actions.setTitleHtml, html);
}
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) {
this._sendAction(actions.insertLink, {url, title: this.htmlEcodeString(title)});
}
insertImage(url, alt) {
this._sendAction(actions.insertImage, {url, alt});
this.prepareInsert(); //This must be called BEFORE insertImage. But WebViewBridge uses a stack :/
}
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() {
this._sendAction(actions.prepareInsert);
}
restoreSelection() {
this._sendAction(actions.restoreSelection);
}
init() {
this._sendAction(actions.init);
}
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);
});
}
setTitleFocusHandler(callbackHandler) {
this.titleFocusHandler = callbackHandler;
this._sendAction(actions.setTitleFocusHandler);
}
setContentFocusHandler(callbackHandler) {
this.contentFocusHandler = callbackHandler;
this._sendAction(actions.setContentFocusHandler);
}
}
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: PlatfomIOS ? 0 : 20,
paddingLeft: 20,
paddingRight: 20,
alignSelf: 'stretch',
margin: 40,
borderRadius: PlatfomIOS ? 8 : 2
},
button: {
fontSize: 16,
color: '#4a4a4a',
textAlign: 'center'
},
inputWrapper: {
marginTop: 5,
marginBottom: 10,
borderBottomColor: '#4a4a4a',
borderBottomWidth: PlatfomIOS ? 1 / PixelRatio.get() : 0
},
inputTitle: {
color: '#4a4a4a'
},
lineSeparator: {
height: 1 / PixelRatio.get(),
backgroundColor: '#d5d5d5',
marginLeft: -20,
marginRight: -20,
marginTop: 20
}
});