123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531 |
- 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 (
- <Modal
- animationType={"fade"}
- transparent
- visible={this.state.showLinkDialog}
- onRequestClose={() => this.setState({showLinkDialog: false})}
- >
- <View style={styles.modal}>
- <View style={[styles.innerModal, {marginBottom: this.state.keyboardHeight}]}>
- <Text style={styles.inputTitle}>Title</Text>
- <View style={styles.inputWrapper}>
- <TextInput
- style={{height: 20}}
- onChangeText={(text) => this.setState({linkTitle: text})}
- value={this.state.linkTitle}
- />
- </View>
- <Text style={[styles.inputTitle ,{marginTop: 10}]}>URL</Text>
- <View style={styles.inputWrapper}>
- <TextInput
- style={{height: 20}}
- onChangeText={(text) => this.setState({linkUrl: text})}
- value={this.state.linkUrl}
- keyboardType="url"
- autoCapitalize="none"
- autoCorrect={false}
- />
- </View>
- {PlatfomIOS && <View style={styles.lineSeparator}/>}
- {this._renderModalButtons()}
- </View>
- </View>
- </Modal>
- );
- }
-
- _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 (
- <View style={[{alignSelf: 'stretch', flexDirection: 'row'}, containerPlatformStyle]}>
- {!PlatfomIOS && <View style={{flex: 1}}/>}
- <TouchableOpacity
- onPress={() => this._hideModal()}
- style={buttonPlatformStyle}
- >
- <Text style={[styles.button, {paddingRight: 10}]}>
- {PlatfomIOS ? 'Cancel' : 'CANCEL'}
- </Text>
- </TouchableOpacity>
- <TouchableOpacity
- onPress={() => {
- this.insertLink(this.state.linkUrl, this.state.linkTitle);
- this._hideModal();
- }}
- disabled={insertDisabled}
- style={buttonPlatformStyle}
- >
- <Text style={[styles.button, {opacity: insertDisabled ? 0.5 : 1}]}>
- {PlatfomIOS ? 'Insert' : 'INSERT'}
- </Text>
- </TouchableOpacity>
- </View>
- );
- }
-
- 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 (
- <View style={{flex: 1}}>
- <WebViewBridge
- style={{flex: 1}}
- {...this.props}
- hideKeyboardAccessoryView={true}
- ref={(r) => {this.webviewBridge = r}}
- onBridgeMessage={(message) => this.onBridgeMessage(message)}
- injectedJavaScript={injectScript}
- source={pageSource}
- onLoad={() => this.init()}
- onShouldStartLoadWithRequest={(event) => {return this._onShouldStartLoadWithRequest(event)}}
- />
- {this._renderLinkModal()}
- </View>
- );
- }
-
- _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
- }
- });
|