説明なし

RichTextEditor.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. import React, {Component, PropTypes} from 'react';
  2. import WebViewBridge from 'react-native-webview-bridge-updated';
  3. import {InjectedMessageHandler} from './WebviewMessageHandler';
  4. import {actions, messages} from './const';
  5. import {Modal, View, Text, StyleSheet, TextInput, TouchableOpacity, Platform, PixelRatio, Keyboard} from 'react-native';
  6. const injectScript = `
  7. (function () {
  8. ${InjectedMessageHandler}
  9. }());
  10. `;
  11. const PlatfomIOS = Platform.OS === 'ios';
  12. export default class RichTextEditor extends Component {
  13. static propTypes = {
  14. initialTitleHTML: PropTypes.string,
  15. initialContentHTML: PropTypes.string,
  16. titlePlaceholder: PropTypes.string,
  17. contentPlaceholder: PropTypes.string,
  18. editorInitializedCallback: PropTypes.func,
  19. customCSS: PropTypes.string
  20. };
  21. constructor(props) {
  22. super(props);
  23. this._sendAction = this._sendAction.bind(this);
  24. this.registerToolbar = this.registerToolbar.bind(this);
  25. this.onBridgeMessage = this.onBridgeMessage.bind(this);
  26. this._onKeyboardWillShow = this._onKeyboardWillShow.bind(this);
  27. this._onKeyboardWillHide = this._onKeyboardWillHide.bind(this);
  28. this.state = {
  29. listeners: [],
  30. showLinkDialog: false,
  31. linkTitle: '',
  32. linkUrl: '',
  33. keyboardHeight: 0
  34. };
  35. }
  36. componentWillMount() {
  37. if(PlatfomIOS) {
  38. this.keyboardEventListeners = [
  39. Keyboard.addListener('keyboardWillShow', this._onKeyboardWillShow),
  40. Keyboard.addListener('keyboardWillHide', this._onKeyboardWillHide)
  41. ];
  42. } else {
  43. this.keyboardEventListeners = [
  44. Keyboard.addListener('keyboardDidShow', this._onKeyboardWillShow),
  45. Keyboard.addListener('keyboardDidHide', this._onKeyboardWillHide)
  46. ];
  47. }
  48. }
  49. componentWillUnmount() {
  50. this.keyboardEventListeners.forEach((eventListener) => eventListener.remove());
  51. }
  52. _onKeyboardWillShow(event) {
  53. console.log('!!!!', event);
  54. const newKeyboardHeight = event.endCoordinates.height;
  55. if (this.state.keyboardHeight === newKeyboardHeight) {
  56. return;
  57. }
  58. this.setState({keyboardHeight: newKeyboardHeight});
  59. }
  60. _onKeyboardWillHide(event) {
  61. this.setState({keyboardHeight: 0});
  62. }
  63. onBridgeMessage(str){
  64. try {
  65. const message = JSON.parse(str);
  66. switch (message.type) {
  67. case messages.TITLE_HTML_RESPONSE:
  68. if (this.titleResolve) {
  69. this.titleResolve(message.data);
  70. this.titleResolve = undefined;
  71. this.titleReject = undefined;
  72. if (this.pendingTitleHtml) {
  73. clearTimeout(this.pendingTitleHtml);
  74. this.pendingTitleHtml = undefined;
  75. }
  76. }
  77. break;
  78. case messages.TITLE_TEXT_RESPONSE:
  79. if (this.titleTextResolve) {
  80. this.titleTextResolve(message.data);
  81. this.titleTextResolve = undefined;
  82. this.titleTextReject = undefined;
  83. if (this.pendingTitleText) {
  84. clearTimeout(this.pendingTitleText);
  85. this.pendingTitleText = undefined;
  86. }
  87. }
  88. break;
  89. case messages.CONTENT_HTML_RESPONSE:
  90. if (this.contentResolve) {
  91. this.contentResolve(message.data);
  92. this.contentResolve = undefined;
  93. this.contentReject = undefined;
  94. if (this.pendingContentHtml) {
  95. clearTimeout(this.pendingContentHtml);
  96. this.pendingContentHtml = undefined;
  97. }
  98. }
  99. break;
  100. case messages.ZSS_INITIALIZED:
  101. if (this.props.customCSS) {
  102. this.setCustomCSS(this.props.customCSS);
  103. }
  104. this.setTitlePlaceholder(this.props.titlePlaceholder);
  105. this.setContentPlaceholder(this.props.contentPlaceholder);
  106. this.setTitleHTML(this.htmlEcodeString(this.props.initialTitleHTML));
  107. this.setContentHTML(this.htmlEcodeString(this.props.initialContentHTML));
  108. this.props.editorInitializedCallback && this.props.editorInitializedCallback();
  109. break;
  110. case messages.LOG:
  111. console.log('FROM ZSS', message.data);
  112. break;
  113. case messages.SCROLL:
  114. this.webviewBridge.setNativeProps({contentOffset: {y: message.data}});
  115. break;
  116. case messages.TITLE_FOCUSED:
  117. this.titleFocusHandler && this.titleFocusHandler();
  118. break;
  119. case messages.CONTENT_FOCUSED:
  120. this.contentFocusHandler && this.contentFocusHandler();
  121. break;
  122. case messages.SELECTION_CHANGE:
  123. const items = message.data.items;
  124. this.state.listeners.map((listener) => listener(items));
  125. break
  126. }
  127. } catch(e) {
  128. //alert('NON JSON MESSAGE');
  129. }
  130. }
  131. _renderLinkModal() {
  132. return (
  133. <Modal
  134. animationType={"fade"}
  135. transparent
  136. visible={this.state.showLinkDialog}
  137. onRequestClose={() => this.setState({showLinkDialog: false})}
  138. >
  139. <View style={styles.modal}>
  140. <View style={[styles.innerModal, {marginBottom: this.state.keyboardHeight}]}>
  141. <Text style={styles.inputTitle}>Title</Text>
  142. <View style={styles.inputWrapper}>
  143. <TextInput
  144. style={{height: 20}}
  145. onChangeText={(text) => this.setState({linkTitle: text})}
  146. value={this.state.linkTitle}
  147. />
  148. </View>
  149. <Text style={[styles.inputTitle ,{marginTop: 10}]}>URL</Text>
  150. <View style={styles.inputWrapper}>
  151. <TextInput
  152. style={{height: 20}}
  153. onChangeText={(text) => this.setState({linkUrl: text})}
  154. value={this.state.linkUrl}
  155. keyboardType="url"
  156. autoCapitalize="none"
  157. autoCorrect={false}
  158. />
  159. </View>
  160. {PlatfomIOS && <View style={styles.lineSeparator}/>}
  161. {this._renderModalButtons()}
  162. </View>
  163. </View>
  164. </Modal>
  165. );
  166. }
  167. _hideModal() {
  168. this.setState({
  169. showLinkDialog: false,
  170. linkTitle: '',
  171. linkUrl: ''
  172. })
  173. }
  174. _renderModalButtons() {
  175. const insertDisabled = this.state.linkTitle.trim().length <= 0 || this.state.linkUrl.trim().length <= 0;
  176. const containerPlatformStyle = PlatfomIOS ? {justifyContent: 'space-between'} : {paddingTop: 15};
  177. const buttonPlatformStyle = PlatfomIOS ? {flex: 1, height: 45, justifyContent: 'center'} : {};
  178. return (
  179. <View style={[{alignSelf: 'stretch', flexDirection: 'row'}, containerPlatformStyle]}>
  180. {!PlatfomIOS && <View style={{flex: 1}}/>}
  181. <TouchableOpacity
  182. onPress={() => this._hideModal()}
  183. style={buttonPlatformStyle}
  184. >
  185. <Text style={[styles.button, {paddingRight: 10}]}>
  186. {PlatfomIOS ? 'Cancel' : 'CANCEL'}
  187. </Text>
  188. </TouchableOpacity>
  189. <TouchableOpacity
  190. onPress={() => {
  191. this.insertLink(this.state.linkUrl, this.state.linkTitle);
  192. this._hideModal();
  193. }}
  194. disabled={insertDisabled}
  195. style={buttonPlatformStyle}
  196. >
  197. <Text style={[styles.button, {opacity: insertDisabled ? 0.5 : 1}]}>
  198. {PlatfomIOS ? 'Insert' : 'INSERT'}
  199. </Text>
  200. </TouchableOpacity>
  201. </View>
  202. );
  203. }
  204. render() {
  205. //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
  206. const pageSource = PlatfomIOS ? require('./editor.html') : { uri: 'file:///android_asset/editor.html' };
  207. return (
  208. <View style={{flex: 1}}>
  209. <WebViewBridge
  210. style={{flex: 1}}
  211. {...this.props}
  212. hideKeyboardAccessoryView={true}
  213. ref={(r) => {this.webviewBridge = r}}
  214. onBridgeMessage={(message) => this.onBridgeMessage(message)}
  215. injectedJavaScript={injectScript}
  216. source={pageSource}
  217. onLoad={() => this.init()}
  218. onShouldStartLoadWithRequest={(event) => {return this._onShouldStartLoadWithRequest(event)}}
  219. />
  220. {this._renderLinkModal()}
  221. </View>
  222. );
  223. }
  224. _onShouldStartLoadWithRequest(event) {
  225. return (event.url.indexOf('editor.html') != -1);
  226. }
  227. escapeJSONString = function(string) {
  228. return string
  229. .replace(/[\\]/g, '\\\\')
  230. .replace(/[\"]/g, '\\\"')
  231. .replace(/[\/]/g, '\\/')
  232. .replace(/[\b]/g, '\\b')
  233. .replace(/[\f]/g, '\\f')
  234. .replace(/[\n]/g, '\\n')
  235. .replace(/[\r]/g, '\\r')
  236. .replace(/[\t]/g, '\\t');
  237. };
  238. htmlEcodeString = function (string) {
  239. //for some reason there's an issue only with apostrophes
  240. return string
  241. .replace(/'/g, '&apos;');
  242. }
  243. _sendAction(action, data) {
  244. let jsonString = JSON.stringify({type: action, data});
  245. jsonString = this.escapeJSONString(jsonString);
  246. this.webviewBridge.sendToBridge(jsonString);
  247. }
  248. //-------------------------------------------------------------------------------
  249. //--------------- Public API
  250. showLinkDialog() {
  251. this.setState({
  252. showLinkDialog: true
  253. });
  254. }
  255. focusTitle() {
  256. this._sendAction(actions.focusTitle);
  257. }
  258. focusContent() {
  259. this._sendAction(actions.focusContent);
  260. }
  261. registerToolbar(listener) {
  262. this.setState({
  263. listeners: [...this.state.listeners, listener]
  264. });
  265. }
  266. setTitleHTML(html) {
  267. this._sendAction(actions.setTitleHtml, html);
  268. }
  269. setContentHTML(html) {
  270. this._sendAction(actions.setContentHtml, html);
  271. }
  272. blurTitleEditor() {
  273. this._sendAction(actions.blurTitleEditor);
  274. }
  275. blurContentEditor() {
  276. this._sendAction(actions.blurContentEditor);
  277. }
  278. setBold() {
  279. this._sendAction(actions.setBold);
  280. }
  281. setItalic() {
  282. this._sendAction(actions.setItalic);
  283. }
  284. setUnderline() {
  285. this._sendAction(actions.setUnderline);
  286. }
  287. heading1() {
  288. this._sendAction(actions.heading1);
  289. }
  290. heading2() {
  291. this._sendAction(actions.heading2);
  292. }
  293. heading3() {
  294. this._sendAction(actions.heading3);
  295. }
  296. heading4() {
  297. this._sendAction(actions.heading4);
  298. }
  299. heading5() {
  300. this._sendAction(actions.heading5);
  301. }
  302. heading6() {
  303. this._sendAction(actions.heading6);
  304. }
  305. setParagraph() {
  306. this._sendAction(actions.setParagraph);
  307. }
  308. removeFormat() {
  309. this._sendAction(actions.removeFormat);
  310. }
  311. alignLeft() {
  312. this._sendAction(actions.alignLeft);
  313. }
  314. alignCenter() {
  315. this._sendAction(actions.alignCenter);
  316. }
  317. alignRight() {
  318. this._sendAction(actions.alignRight);
  319. }
  320. alignFull() {
  321. this._sendAction(actions.alignFull);
  322. }
  323. insertBulletsList() {
  324. this._sendAction(actions.insertBulletsList);
  325. }
  326. insertOrderedList() {
  327. this._sendAction(actions.insertOrderedList);
  328. }
  329. insertLink(url, title) {
  330. this._sendAction(actions.insertLink, {url, title: this.htmlEcodeString(title)});
  331. }
  332. insertImage(url, alt) {
  333. this._sendAction(actions.insertImage, {url, alt});
  334. this.prepareInsert(); //This must be called BEFORE insertImage. But WebViewBridge uses a stack :/
  335. }
  336. setSubscript() {
  337. this._sendAction(actions.setSubscript);
  338. }
  339. setSuperscript() {
  340. this._sendAction(actions.setSuperscript);
  341. }
  342. setStrikethrough() {
  343. this._sendAction(actions.setStrikethrough);
  344. }
  345. setHR() {
  346. this._sendAction(actions.setHR);
  347. }
  348. setIndent() {
  349. this._sendAction(actions.setIndent);
  350. }
  351. setOutdent() {
  352. this._sendAction(actions.setOutdent);
  353. }
  354. setBackgroundColor(color) {
  355. this._sendAction(actions.setBackgroundColor, color);
  356. }
  357. setTextColor(color) {
  358. this._sendAction(actions.setTextColor, color);
  359. }
  360. setTitlePlaceholder(placeholder) {
  361. this._sendAction(actions.setTitlePlaceholder, placeholder);
  362. }
  363. setContentPlaceholder(placeholder) {
  364. this._sendAction(actions.setContentPlaceholder, placeholder);
  365. }
  366. setCustomCSS(css) {
  367. this._sendAction(actions.setCustomCSS, css);
  368. }
  369. prepareInsert() {
  370. this._sendAction(actions.prepareInsert);
  371. }
  372. restoreSelection() {
  373. this._sendAction(actions.restoreSelection);
  374. }
  375. init() {
  376. this._sendAction(actions.init);
  377. }
  378. async getTitleHtml() {
  379. return new Promise((resolve, reject) => {
  380. this.titleResolve = resolve;
  381. this.titleReject = reject;
  382. this._sendAction(actions.getTitleHtml);
  383. this.pendingTitleHtml = setTimeout(() => {
  384. if (this.titleReject) {
  385. this.titleReject('timeout')
  386. }
  387. }, 5000);
  388. });
  389. }
  390. async getTitleText() {
  391. return new Promise((resolve, reject) => {
  392. this.titleTextResolve = resolve;
  393. this.titleTextReject = reject;
  394. this._sendAction(actions.getTitleText);
  395. this.pendingTitleText = setTimeout(() => {
  396. if (this.titleTextReject) {
  397. this.titleTextReject('timeout');
  398. }
  399. }, 5000);
  400. });
  401. }
  402. async getContentHtml() {
  403. return new Promise((resolve, reject) => {
  404. this.contentResolve = resolve;
  405. this.contentReject = reject;
  406. this._sendAction(actions.getContentHtml);
  407. this.pendingContentHtml = setTimeout(() => {
  408. if (this.contentReject) {
  409. this.contentReject('timeout')
  410. }
  411. }, 5000);
  412. });
  413. }
  414. setTitleFocusHandler(callbackHandler) {
  415. this.titleFocusHandler = callbackHandler;
  416. this._sendAction(actions.setTitleFocusHandler);
  417. }
  418. setContentFocusHandler(callbackHandler) {
  419. this.contentFocusHandler = callbackHandler;
  420. this._sendAction(actions.setContentFocusHandler);
  421. }
  422. }
  423. const styles = StyleSheet.create({
  424. modal: {
  425. flex: 1,
  426. justifyContent: 'center',
  427. alignItems: 'center',
  428. backgroundColor: 'rgba(0, 0, 0, 0.5)'
  429. },
  430. innerModal: {
  431. backgroundColor: 'rgba(255, 255, 255, 0.9)',
  432. paddingTop: 20,
  433. paddingBottom: PlatfomIOS ? 0 : 20,
  434. paddingLeft: 20,
  435. paddingRight: 20,
  436. alignSelf: 'stretch',
  437. margin: 40,
  438. borderRadius: PlatfomIOS ? 8 : 2
  439. },
  440. button: {
  441. fontSize: 16,
  442. color: '#4a4a4a',
  443. textAlign: 'center'
  444. },
  445. inputWrapper: {
  446. marginTop: 5,
  447. marginBottom: 10,
  448. borderBottomColor: '#4a4a4a',
  449. borderBottomWidth: PlatfomIOS ? 1 / PixelRatio.get() : 0
  450. },
  451. inputTitle: {
  452. color: '#4a4a4a'
  453. },
  454. lineSeparator: {
  455. height: 1 / PixelRatio.get(),
  456. backgroundColor: '#d5d5d5',
  457. marginLeft: -20,
  458. marginRight: -20,
  459. marginTop: 20
  460. }
  461. });