PageEditor.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import * as toastr from 'toastr';
  4. import {debounce} from 'throttle-debounce';
  5. import Editor from './PageEditor/Editor';
  6. import Preview from './PageEditor/Preview';
  7. export default class PageEditor extends React.Component {
  8. constructor(props) {
  9. super(props);
  10. this.state = {
  11. revisionId: this.props.revisionId,
  12. markdown: this.props.markdown,
  13. };
  14. this.setCaretLine = this.setCaretLine.bind(this);
  15. this.focusToEditor = this.focusToEditor.bind(this);
  16. this.onMarkdownChanged = this.onMarkdownChanged.bind(this);
  17. this.onSave = this.onSave.bind(this);
  18. this.onEditorScroll = this.onEditorScroll.bind(this);
  19. this.getMaxScrollTop = this.getMaxScrollTop.bind(this);
  20. this.getScrollTop = this.getScrollTop.bind(this);
  21. this.saveDraft = this.saveDraft.bind(this);
  22. this.clearDraft = this.clearDraft.bind(this);
  23. // create debounced function
  24. this.saveDraftWithDebounce = debounce(300, this.saveDraft);
  25. }
  26. componentWillMount() {
  27. // restore draft
  28. this.restoreDraft();
  29. // initial preview
  30. this.renderPreview();
  31. }
  32. focusToEditor() {
  33. this.refs.editor.forceToFocus();
  34. }
  35. /**
  36. * set caret position of editor
  37. * @param {number} line
  38. */
  39. setCaretLine(line) {
  40. this.refs.editor.setCaretLine(line);
  41. }
  42. /**
  43. * the change event handler for `markdown` state
  44. * @param {string} value
  45. */
  46. onMarkdownChanged(value) {
  47. this.setState({
  48. markdown: value,
  49. });
  50. this.renderPreview();
  51. this.saveDraftWithDebounce()
  52. }
  53. /**
  54. * the save event handler
  55. */
  56. onSave() {
  57. let endpoint;
  58. let data;
  59. // update
  60. if (this.props.pageId != null) {
  61. endpoint = '/pages.update';
  62. data = {
  63. page_id: this.props.pageId,
  64. revision_id: this.state.revisionId,
  65. body: this.state.markdown,
  66. };
  67. }
  68. // create
  69. else {
  70. endpoint = '/pages.create';
  71. data = {
  72. path: this.props.pagePath,
  73. body: this.state.markdown,
  74. };
  75. }
  76. this.props.crowi.apiPost(endpoint, data)
  77. .then((res) => {
  78. const page = res.page;
  79. toastr.success(undefined, 'Saved successfully', {
  80. closeButton: true,
  81. progressBar: true,
  82. newestOnTop: false,
  83. showDuration: "100",
  84. hideDuration: "100",
  85. timeOut: "1200",
  86. extendedTimeOut: "150",
  87. });
  88. // update states
  89. this.setState({
  90. revisionId: page.revision._id,
  91. markdown: page.revision.body
  92. })
  93. // clear draft
  94. this.clearDraft();
  95. })
  96. .catch((error) => {
  97. console.error(error);
  98. toastr.error(error.message, 'Error occured on saveing', {
  99. closeButton: true,
  100. progressBar: true,
  101. newestOnTop: false,
  102. showDuration: "100",
  103. hideDuration: "100",
  104. timeOut: "3000",
  105. });
  106. });
  107. }
  108. /**
  109. * the scroll event handler from codemirror
  110. * @param {any} data {left, top, width, height, clientWidth, clientHeight} object that represents the current scroll position, the size of the scrollable area, and the size of the visible area (minus scrollbars).
  111. * see https://codemirror.net/doc/manual.html#events
  112. */
  113. onEditorScroll(data) {
  114. const rate = data.top / (data.height - data.clientHeight)
  115. const top = this.getScrollTop(this.previewElement, rate);
  116. this.previewElement.scrollTop = top;
  117. }
  118. /**
  119. * transplanted from crowi-form.js -- 2018.01.21 Yuki Takei
  120. * @param {*} dom
  121. */
  122. getMaxScrollTop(dom) {
  123. var rect = dom.getBoundingClientRect();
  124. return dom.scrollHeight - rect.height;
  125. };
  126. /**
  127. * transplanted from crowi-form.js -- 2018.01.21 Yuki Takei
  128. * @param {*} dom
  129. */
  130. getScrollTop(dom, rate) {
  131. var maxScrollTop = this.getMaxScrollTop(dom);
  132. var top = maxScrollTop * rate;
  133. return top;
  134. };
  135. restoreDraft() {
  136. // restore draft when the first time to edit
  137. const draft = this.props.crowi.findDraft(this.props.pagePath);
  138. if (!this.props.revisionId && draft != null) {
  139. this.setState({markdown: draft});
  140. }
  141. }
  142. saveDraft() {
  143. // only when the first time to edit
  144. if (!this.state.revisionId) {
  145. this.props.crowi.saveDraft(this.props.pagePath, this.state.markdown);
  146. }
  147. }
  148. clearDraft() {
  149. this.props.crowi.clearDraft(this.props.pagePath);
  150. }
  151. renderPreview() {
  152. const config = this.props.crowi.config;
  153. // generate options obj
  154. const rendererOptions = {
  155. // see: https://www.npmjs.com/package/marked
  156. marked: {
  157. breaks: config.isEnabledLineBreaks,
  158. }
  159. };
  160. // render html
  161. var context = {
  162. markdown: this.state.markdown,
  163. dom: this.previewElement,
  164. currentPagePath: decodeURIComponent(location.pathname)
  165. };
  166. this.props.crowi.interceptorManager.process('preRenderPreview', context)
  167. .then(() => crowi.interceptorManager.process('prePreProcess', context))
  168. .then(() => {
  169. context.markdown = crowiRenderer.preProcess(context.markdown, context.dom);
  170. })
  171. .then(() => crowi.interceptorManager.process('postPreProcess', context))
  172. .then(() => {
  173. var parsedHTML = crowiRenderer.render(context.markdown, context.dom, rendererOptions);
  174. context['parsedHTML'] = parsedHTML;
  175. })
  176. .then(() => crowi.interceptorManager.process('postRenderPreview', context))
  177. .then(() => crowi.interceptorManager.process('preRenderPreviewHtml', context))
  178. .then(() => {
  179. this.setState({html: context.parsedHTML});
  180. // set html to the hidden input (for submitting to save)
  181. $('#form-body').val(this.state.markdown);
  182. })
  183. // process interceptors for post rendering
  184. .then(() => crowi.interceptorManager.process('postRenderPreviewHtml', context));
  185. }
  186. render() {
  187. return (
  188. <div className="row">
  189. <div className="col-md-6 col-sm-12 page-editor-editor-container">
  190. <Editor ref="editor" value={this.state.markdown}
  191. onScroll={this.onEditorScroll}
  192. onChange={this.onMarkdownChanged}
  193. onSave={this.onSave}
  194. />
  195. </div>
  196. <div className="col-md-6 hidden-sm hidden-xs page-editor-preview-container">
  197. <Preview html={this.state.html} inputRef={el => this.previewElement = el} />
  198. </div>
  199. </div>
  200. )
  201. }
  202. }
  203. PageEditor.propTypes = {
  204. crowi: PropTypes.object.isRequired,
  205. markdown: PropTypes.string.isRequired,
  206. pageId: PropTypes.string,
  207. revisionId: PropTypes.string,
  208. pagePath: PropTypes.string,
  209. };