No Description

RichTextEditor.js 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638
  1. import React, {Component} from 'react';
  2. import PropTypes from 'prop-types';
  3. import WebView from 'react-native-webview';
  4. import {MessageConverter} from './WebviewMessageHandler';
  5. import {actions, messages} from './const';
  6. import {Modal, View, Text, StyleSheet, TextInput, TouchableOpacity, Platform, PixelRatio, Keyboard, Dimensions} from 'react-native';
  7. const PlatformIOS = Platform.OS === 'ios';
  8. export default class RichTextEditor extends Component {
  9. static propTypes = {
  10. initialTitleHTML: PropTypes.string,
  11. initialContentHTML: PropTypes.string,
  12. titlePlaceholder: PropTypes.string,
  13. contentPlaceholder: PropTypes.string,
  14. editorInitializedCallback: PropTypes.func,
  15. customCSS: PropTypes.string,
  16. hiddenTitle: PropTypes.bool,
  17. enableOnChange: PropTypes.bool,
  18. footerHeight: PropTypes.number,
  19. contentInset: PropTypes.object
  20. };
  21. static defaultProps = {
  22. contentInset: {},
  23. style: {}
  24. };
  25. constructor(props) {
  26. super(props);
  27. this._sendAction = this._sendAction.bind(this);
  28. this.registerToolbar = this.registerToolbar.bind(this);
  29. this.onMessage = this.onMessage.bind(this);
  30. this._onKeyboardWillShow = this._onKeyboardWillShow.bind(this);
  31. this._onKeyboardWillHide = this._onKeyboardWillHide.bind(this);
  32. this.state = {
  33. selectionChangeListeners: [],
  34. onChange: [],
  35. isShowLinkDialog: false,
  36. linkInitialUrl: '',
  37. linkTitle: '',
  38. linkUrl: '',
  39. keyboardHeight: 0
  40. };
  41. this._selectedTextChangeListeners = [];
  42. }
  43. componentDidMount() {
  44. if(PlatformIOS) {
  45. this.keyboardEventListeners = [
  46. Keyboard.addListener('keyboardWillShow', this._onKeyboardWillShow),
  47. Keyboard.addListener('keyboardWillHide', this._onKeyboardWillHide)
  48. ];
  49. } else {
  50. this.keyboardEventListeners = [
  51. Keyboard.addListener('keyboardDidShow', this._onKeyboardWillShow),
  52. Keyboard.addListener('keyboardDidHide', this._onKeyboardWillHide)
  53. ];
  54. }
  55. }
  56. componentWillUnmount() {
  57. this.keyboardEventListeners.forEach((eventListener) => eventListener.remove());
  58. }
  59. _onKeyboardWillShow(event) {
  60. const newKeyboardHeight = event.endCoordinates.height;
  61. if (this.state.keyboardHeight === newKeyboardHeight) {
  62. return;
  63. }
  64. if (newKeyboardHeight) {
  65. this.setEditorAvailableHeightBasedOnKeyboardHeight(newKeyboardHeight);
  66. }
  67. this.setState({keyboardHeight: newKeyboardHeight});
  68. }
  69. _onKeyboardWillHide(event) {
  70. this.setState({keyboardHeight: 0});
  71. }
  72. setEditorAvailableHeightBasedOnKeyboardHeight(keyboardHeight) {
  73. const {top = 0, bottom = 0} = this.props.contentInset;
  74. const {marginTop = 0, marginBottom = 0} = this.props.style;
  75. const spacing = marginTop + marginBottom + top + bottom;
  76. const editorAvailableHeight = Dimensions.get('window').height - keyboardHeight - spacing;
  77. this.setEditorHeight(editorAvailableHeight);
  78. }
  79. onMessage({ nativeEvent }){
  80. const { data: str } = nativeEvent;
  81. try {
  82. const message = JSON.parse(str);
  83. switch (message.type) {
  84. case messages.TITLE_HTML_RESPONSE:
  85. if (this.titleResolve) {
  86. this.titleResolve(message.data);
  87. this.titleResolve = undefined;
  88. this.titleReject = undefined;
  89. if (this.pendingTitleHtml) {
  90. clearTimeout(this.pendingTitleHtml);
  91. this.pendingTitleHtml = undefined;
  92. }
  93. }
  94. break;
  95. case messages.TITLE_TEXT_RESPONSE:
  96. if (this.titleTextResolve) {
  97. this.titleTextResolve(message.data);
  98. this.titleTextResolve = undefined;
  99. this.titleTextReject = undefined;
  100. if (this.pendingTitleText) {
  101. clearTimeout(this.pendingTitleText);
  102. this.pendingTitleText = undefined;
  103. }
  104. }
  105. break;
  106. case messages.CONTENT_HTML_RESPONSE:
  107. if (this.contentResolve) {
  108. this.contentResolve(message.data);
  109. this.contentResolve = undefined;
  110. this.contentReject = undefined;
  111. if (this.pendingContentHtml) {
  112. clearTimeout(this.pendingContentHtml);
  113. this.pendingContentHtml = undefined;
  114. }
  115. }
  116. break;
  117. case messages.SELECTED_TEXT_RESPONSE:
  118. if (this.selectedTextResolve) {
  119. this.selectedTextResolve(message.data);
  120. this.selectedTextResolve = undefined;
  121. this.selectedTextReject = undefined;
  122. if (this.pendingSelectedText) {
  123. clearTimeout(this.pendingSelectedText);
  124. this.pendingSelectedText = undefined;
  125. }
  126. }
  127. break;
  128. case messages.ZSS_INITIALIZED:
  129. if (this.props.customCSS) {
  130. this.setCustomCSS(this.props.customCSS);
  131. }
  132. this.setTitlePlaceholder(this.props.titlePlaceholder);
  133. this.setContentPlaceholder(this.props.contentPlaceholder);
  134. this.setTitleHTML(this.props.initialTitleHTML || '');
  135. this.setContentHTML(this.props.initialContentHTML || '');
  136. !this.props.hiddenTitle && this.showTitle();
  137. this.props.enableOnChange && this.enableOnChange();
  138. this.props.editorInitializedCallback && this.props.editorInitializedCallback();
  139. break;
  140. case messages.LINK_TOUCHED:
  141. const {title, url} = message.data;
  142. this.showLinkDialog(title, url);
  143. break;
  144. case messages.LOG:
  145. console.log('FROM ZSS', message.data);
  146. break;
  147. case messages.SCROLL:
  148. this.webview.setNativeProps({contentOffset: {y: message.data}});
  149. break;
  150. case messages.TITLE_FOCUSED:
  151. this.titleFocusHandler && this.titleFocusHandler();
  152. break;
  153. case messages.CONTENT_FOCUSED:
  154. this.contentFocusHandler && this.contentFocusHandler();
  155. break;
  156. case messages.SELECTION_CHANGE: {
  157. const items = message.data.items;
  158. this.state.selectionChangeListeners.map((listener) => {
  159. listener(items);
  160. });
  161. break;
  162. }
  163. case messages.CONTENT_CHANGE: {
  164. const content = message.data.content;
  165. this.state.onChange.map((listener) => listener(content));
  166. break;
  167. }
  168. case messages.SELECTED_TEXT_CHANGED: {
  169. const selectedText = message.data;
  170. this._selectedTextChangeListeners.forEach((listener) => {
  171. listener(selectedText);
  172. });
  173. break;
  174. }
  175. }
  176. } catch(e) {
  177. //alert('NON JSON MESSAGE');
  178. }
  179. }
  180. _renderLinkModal() {
  181. const {linkOption} = this.props;
  182. return (
  183. <Modal
  184. animationType={"fade"}
  185. transparent
  186. visible={this.state.isShowLinkDialog}
  187. onRequestClose={() => this.setState({isShowLinkDialog: false})}
  188. >
  189. <View style={styles.modal}>
  190. <View style={[styles.innerModal, {marginBottom: PlatformIOS ? this.state.keyboardHeight : 0}]}>
  191. <Text style={styles.inputTitle}>{linkOption.titleText}</Text>
  192. <View style={styles.inputWrapper}>
  193. <TextInput
  194. style={styles.input}
  195. onChangeText={(text) => this.setState({linkTitle: text})}
  196. value={this.state.linkTitle}
  197. />
  198. </View>
  199. <Text style={[styles.inputTitle ,{marginTop: 10}]}>{linkOption.urlText}</Text>
  200. <View style={styles.inputWrapper}>
  201. <TextInput
  202. style={styles.input}
  203. onChangeText={(text) => this.setState({linkUrl: text})}
  204. value={this.state.linkUrl}
  205. keyboardType="url"
  206. autoCapitalize="none"
  207. autoCorrect={false}
  208. />
  209. </View>
  210. {PlatformIOS && <View style={styles.lineSeparator}/>}
  211. {this._renderModalButtons()}
  212. </View>
  213. </View>
  214. </Modal>
  215. );
  216. }
  217. _hideModal() {
  218. this.setState({
  219. isShowLinkDialog: false,
  220. linkInitialUrl: '',
  221. linkTitle: '',
  222. linkUrl: ''
  223. })
  224. }
  225. _renderModalButtons() {
  226. const insertUpdateDisabled = this.state.linkTitle.trim().length <= 0 || this.state.linkUrl.trim().length <= 0;
  227. const containerPlatformStyle = PlatformIOS ? {justifyContent: 'space-between'} : {paddingTop: 15};
  228. const buttonPlatformStyle = PlatformIOS ? {flex: 1, height: 45, justifyContent: 'center'} : {};
  229. const { linkOption } = this.props;
  230. return (
  231. <View style={[{alignSelf: 'stretch', flexDirection: 'row'}, containerPlatformStyle]}>
  232. {!PlatformIOS && <View style={{flex: 1}}/>}
  233. <TouchableOpacity
  234. onPress={() => this._hideModal()}
  235. style={buttonPlatformStyle}
  236. >
  237. <Text style={[styles.button, {paddingRight: 10}]}>
  238. {linkOption.cancelText}
  239. </Text>
  240. </TouchableOpacity>
  241. <TouchableOpacity
  242. onPress={() => {
  243. if (this._linkIsNew()) {
  244. this.insertLink(this.state.linkUrl, this.state.linkTitle);
  245. } else {
  246. this.updateLink(this.state.linkUrl, this.state.linkTitle);
  247. }
  248. this._hideModal();
  249. }}
  250. disabled={insertUpdateDisabled}
  251. style={buttonPlatformStyle}
  252. >
  253. <Text style={[styles.button, {opacity: insertUpdateDisabled ? 0.5 : 1}]}>
  254. {this._linkIsNew() ? linkOption.insertText : linkOption.updateText}
  255. </Text>
  256. </TouchableOpacity>
  257. </View>
  258. );
  259. }
  260. _linkIsNew() {
  261. return !this.state.linkInitialUrl;
  262. }
  263. render() {
  264. //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
  265. const pageSource = PlatformIOS ? require('./editor.html') : { uri: 'file:///android_asset/editor.html' };
  266. return (
  267. <View style={{flex: 1}}>
  268. <WebView
  269. {...this.props}
  270. hideKeyboardAccessoryView={true}
  271. keyboardDisplayRequiresUserAction={false}
  272. ref={(r) => {this.webview = r}}
  273. onMessage={(message) => this.onMessage(message)}
  274. source={pageSource}
  275. onLoad={() => this.init()}
  276. />
  277. {this._renderLinkModal()}
  278. </View>
  279. );
  280. }
  281. _sendAction(action, data) {
  282. let jsToBeExecutedOnPage = MessageConverter({ type: action, data });
  283. console.log(jsToBeExecutedOnPage + ';true;')
  284. this.webview.injectJavaScript(jsToBeExecutedOnPage + ';true;');
  285. }
  286. //-------------------------------------------------------------------------------
  287. //--------------- Public API
  288. showLinkDialog(optionalTitle = '', optionalUrl = '') {
  289. this.prepareInsert();
  290. this.setState({
  291. linkInitialUrl: optionalUrl,
  292. linkTitle: optionalTitle,
  293. linkUrl: optionalUrl,
  294. isShowLinkDialog: true
  295. });
  296. }
  297. focusTitle() {
  298. this._sendAction(actions.focusTitle);
  299. }
  300. focusContent() {
  301. this._sendAction(actions.focusContent);
  302. }
  303. registerToolbar(listener) {
  304. this.setState({
  305. selectionChangeListeners: [...this.state.selectionChangeListeners, listener]
  306. });
  307. }
  308. enableOnChange() {
  309. this._sendAction(actions.enableOnChange);
  310. }
  311. registerContentChangeListener(listener) {
  312. this.setState({
  313. onChange: [...this.state.onChange, listener]
  314. });
  315. }
  316. setTitleHTML(html) {
  317. this._sendAction(actions.setTitleHtml, html);
  318. }
  319. hideTitle() {
  320. this._sendAction(actions.hideTitle);
  321. }
  322. showTitle() {
  323. this._sendAction(actions.showTitle);
  324. }
  325. toggleTitle() {
  326. this._sendAction(actions.toggleTitle);
  327. }
  328. setContentHTML(html) {
  329. this._sendAction(actions.setContentHtml, html);
  330. }
  331. blurTitleEditor() {
  332. this._sendAction(actions.blurTitleEditor);
  333. }
  334. blurContentEditor() {
  335. this._sendAction(actions.blurContentEditor);
  336. }
  337. setBold() {
  338. this._sendAction(actions.setBold);
  339. }
  340. setItalic() {
  341. this._sendAction(actions.setItalic);
  342. }
  343. setUnderline() {
  344. this._sendAction(actions.setUnderline);
  345. }
  346. heading1() {
  347. this._sendAction(actions.heading1);
  348. }
  349. heading2() {
  350. this._sendAction(actions.heading2);
  351. }
  352. heading3() {
  353. this._sendAction(actions.heading3);
  354. }
  355. heading4() {
  356. this._sendAction(actions.heading4);
  357. }
  358. heading5() {
  359. this._sendAction(actions.heading5);
  360. }
  361. heading6() {
  362. this._sendAction(actions.heading6);
  363. }
  364. setParagraph() {
  365. this._sendAction(actions.setParagraph);
  366. }
  367. removeFormat() {
  368. this._sendAction(actions.removeFormat);
  369. }
  370. alignLeft() {
  371. this._sendAction(actions.alignLeft);
  372. }
  373. alignCenter() {
  374. this._sendAction(actions.alignCenter);
  375. }
  376. alignRight() {
  377. this._sendAction(actions.alignRight);
  378. }
  379. alignFull() {
  380. this._sendAction(actions.alignFull);
  381. }
  382. insertBulletsList() {
  383. this._sendAction(actions.insertBulletsList);
  384. }
  385. insertOrderedList() {
  386. this._sendAction(actions.insertOrderedList);
  387. }
  388. insertLink(url, title) {
  389. this._sendAction(actions.insertLink, {url, title});
  390. }
  391. updateLink(url, title) {
  392. this._sendAction(actions.updateLink, {url, title});
  393. }
  394. insertImage(url) {
  395. this._sendAction(actions.insertImage, url);
  396. }
  397. insertEmoji(url) {
  398. this._sendAction(actions.insertEmoji, url);
  399. }
  400. setSubscript() {
  401. this._sendAction(actions.setSubscript);
  402. }
  403. setSuperscript() {
  404. this._sendAction(actions.setSuperscript);
  405. }
  406. setStrikethrough() {
  407. this._sendAction(actions.setStrikethrough);
  408. }
  409. setHR() {
  410. this._sendAction(actions.setHR);
  411. }
  412. setIndent() {
  413. this._sendAction(actions.setIndent);
  414. }
  415. setOutdent() {
  416. this._sendAction(actions.setOutdent);
  417. }
  418. setBackgroundColor(color) {
  419. this._sendAction(actions.setBackgroundColor, color);
  420. }
  421. setTextColor(color) {
  422. this._sendAction(actions.setTextColor, color);
  423. }
  424. setTitlePlaceholder(placeholder) {
  425. this._sendAction(actions.setTitlePlaceholder, placeholder);
  426. }
  427. setContentPlaceholder(placeholder) {
  428. this._sendAction(actions.setContentPlaceholder, placeholder);
  429. }
  430. setCustomCSS(css) {
  431. this._sendAction(actions.setCustomCSS, css);
  432. }
  433. prepareInsert() {
  434. this._sendAction(actions.prepareInsert);
  435. }
  436. restoreSelection() {
  437. this._sendAction(actions.restoreSelection);
  438. }
  439. init() {
  440. this._sendAction(actions.init);
  441. this.setPlatform();
  442. if (this.props.footerHeight) {
  443. this.setFooterHeight();
  444. }
  445. }
  446. setEditorHeight(height) {
  447. this._sendAction(actions.setEditorHeight, height);
  448. }
  449. setFooterHeight() {
  450. this._sendAction(actions.setFooterHeight, this.props.footerHeight);
  451. }
  452. setPlatform() {
  453. this._sendAction(actions.setPlatform, Platform.OS);
  454. }
  455. async getTitleHtml() {
  456. return new Promise((resolve, reject) => {
  457. this.titleResolve = resolve;
  458. this.titleReject = reject;
  459. this._sendAction(actions.getTitleHtml);
  460. this.pendingTitleHtml = setTimeout(() => {
  461. if (this.titleReject) {
  462. this.titleReject('timeout')
  463. }
  464. }, 5000);
  465. });
  466. }
  467. async getTitleText() {
  468. return new Promise((resolve, reject) => {
  469. this.titleTextResolve = resolve;
  470. this.titleTextReject = reject;
  471. this._sendAction(actions.getTitleText);
  472. this.pendingTitleText = setTimeout(() => {
  473. if (this.titleTextReject) {
  474. this.titleTextReject('timeout');
  475. }
  476. }, 5000);
  477. });
  478. }
  479. async getContentHtml() {
  480. return new Promise((resolve, reject) => {
  481. this.contentResolve = resolve;
  482. this.contentReject = reject;
  483. this._sendAction(actions.getContentHtml);
  484. this.pendingContentHtml = setTimeout(() => {
  485. if (this.contentReject) {
  486. this.contentReject('timeout')
  487. }
  488. }, 5000);
  489. });
  490. }
  491. async getSelectedText() {
  492. return new Promise((resolve, reject) => {
  493. this.selectedTextResolve = resolve;
  494. this.selectedTextReject = reject;
  495. this._sendAction(actions.getSelectedText);
  496. this.pendingSelectedText = setTimeout(() => {
  497. if (this.selectedTextReject) {
  498. this.selectedTextReject('timeout')
  499. }
  500. }, 5000);
  501. });
  502. }
  503. setTitleFocusHandler(callbackHandler) {
  504. this.titleFocusHandler = callbackHandler;
  505. this._sendAction(actions.setTitleFocusHandler);
  506. }
  507. setContentFocusHandler(callbackHandler) {
  508. this.contentFocusHandler = callbackHandler;
  509. this._sendAction(actions.setContentFocusHandler);
  510. }
  511. addSelectedTextChangeListener(listener) {
  512. this._selectedTextChangeListeners.push(listener);
  513. }
  514. }
  515. const styles = StyleSheet.create({
  516. modal: {
  517. flex: 1,
  518. justifyContent: 'center',
  519. alignItems: 'center',
  520. backgroundColor: 'rgba(0, 0, 0, 0.5)'
  521. },
  522. innerModal: {
  523. backgroundColor: 'rgba(255, 255, 255, 0.9)',
  524. paddingTop: 20,
  525. paddingBottom: PlatformIOS ? 0 : 20,
  526. paddingLeft: 20,
  527. paddingRight: 20,
  528. alignSelf: 'stretch',
  529. margin: 40,
  530. borderRadius: PlatformIOS ? 8 : 2
  531. },
  532. button: {
  533. fontSize: 16,
  534. color: '#4a4a4a',
  535. textAlign: 'center'
  536. },
  537. inputWrapper: {
  538. marginTop: 5,
  539. marginBottom: 10,
  540. borderBottomColor: '#4a4a4a',
  541. borderBottomWidth: PlatformIOS ? 1 / PixelRatio.get() : 0
  542. },
  543. inputTitle: {
  544. color: '#4a4a4a'
  545. },
  546. input: {
  547. height: PlatformIOS ? 20 : 40,
  548. paddingTop: 0
  549. },
  550. lineSeparator: {
  551. height: 1 / PixelRatio.get(),
  552. backgroundColor: '#d5d5d5',
  553. marginLeft: -20,
  554. marginRight: -20,
  555. marginTop: 20
  556. }
  557. });