Geen omschrijving

RichTextEditor.js 19KB

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