PageEditor.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  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. // dispatch onSaveSuccess
  96. this.dispatchSaveSuccess(page);
  97. })
  98. .catch((error) => {
  99. console.error(error);
  100. toastr.error(error.message, 'Error occured on saveing', {
  101. closeButton: true,
  102. progressBar: true,
  103. newestOnTop: false,
  104. showDuration: "100",
  105. hideDuration: "100",
  106. timeOut: "3000",
  107. });
  108. });
  109. }
  110. /**
  111. * the scroll event handler from codemirror
  112. * @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).
  113. * see https://codemirror.net/doc/manual.html#events
  114. */
  115. onEditorScroll(data) {
  116. const rate = data.top / (data.height - data.clientHeight)
  117. const top = this.getScrollTop(this.previewElement, rate);
  118. this.previewElement.scrollTop = top;
  119. }
  120. /**
  121. * transplanted from crowi-form.js -- 2018.01.21 Yuki Takei
  122. * @param {*} dom
  123. */
  124. getMaxScrollTop(dom) {
  125. var rect = dom.getBoundingClientRect();
  126. return dom.scrollHeight - rect.height;
  127. };
  128. /**
  129. * transplanted from crowi-form.js -- 2018.01.21 Yuki Takei
  130. * @param {*} dom
  131. */
  132. getScrollTop(dom, rate) {
  133. var maxScrollTop = this.getMaxScrollTop(dom);
  134. var top = maxScrollTop * rate;
  135. return top;
  136. };
  137. restoreDraft() {
  138. // restore draft when the first time to edit
  139. const draft = this.props.crowi.findDraft(this.props.pagePath);
  140. if (!this.props.revisionId && draft != null) {
  141. this.setState({markdown: draft});
  142. }
  143. }
  144. saveDraft() {
  145. // only when the first time to edit
  146. if (!this.state.revisionId) {
  147. this.props.crowi.saveDraft(this.props.pagePath, this.state.markdown);
  148. }
  149. }
  150. clearDraft() {
  151. this.props.crowi.clearDraft(this.props.pagePath);
  152. }
  153. /**
  154. * dispatch onSaveSuccess event
  155. */
  156. dispatchSaveSuccess(page) {
  157. if (this.props.onSaveSuccess != null) {
  158. this.props.onSaveSuccess(page);
  159. }
  160. }
  161. renderPreview() {
  162. const config = this.props.crowi.config;
  163. // generate options obj
  164. const rendererOptions = {
  165. // see: https://www.npmjs.com/package/marked
  166. marked: {
  167. breaks: config.isEnabledLineBreaks,
  168. }
  169. };
  170. // render html
  171. var context = {
  172. markdown: this.state.markdown,
  173. dom: this.previewElement,
  174. currentPagePath: decodeURIComponent(location.pathname)
  175. };
  176. this.props.crowi.interceptorManager.process('preRenderPreview', context)
  177. .then(() => crowi.interceptorManager.process('prePreProcess', context))
  178. .then(() => {
  179. context.markdown = crowiRenderer.preProcess(context.markdown, context.dom);
  180. })
  181. .then(() => crowi.interceptorManager.process('postPreProcess', context))
  182. .then(() => {
  183. var parsedHTML = crowiRenderer.render(context.markdown, context.dom, rendererOptions);
  184. context['parsedHTML'] = parsedHTML;
  185. })
  186. .then(() => crowi.interceptorManager.process('postRenderPreview', context))
  187. .then(() => crowi.interceptorManager.process('preRenderPreviewHtml', context))
  188. .then(() => {
  189. this.setState({html: context.parsedHTML});
  190. // set html to the hidden input (for submitting to save)
  191. $('#form-body').val(this.state.markdown);
  192. })
  193. // process interceptors for post rendering
  194. .then(() => crowi.interceptorManager.process('postRenderPreviewHtml', context));
  195. }
  196. render() {
  197. return (
  198. <div className="row">
  199. <div className="col-md-6 col-sm-12 page-editor-editor-container">
  200. <Editor ref="editor" value={this.state.markdown}
  201. onScroll={this.onEditorScroll}
  202. onChange={this.onMarkdownChanged}
  203. onSave={this.onSave}
  204. />
  205. </div>
  206. <div className="col-md-6 hidden-sm hidden-xs page-editor-preview-container">
  207. <Preview html={this.state.html} inputRef={el => this.previewElement = el} />
  208. </div>
  209. </div>
  210. )
  211. }
  212. }
  213. PageEditor.propTypes = {
  214. crowi: PropTypes.object.isRequired,
  215. markdown: PropTypes.string.isRequired,
  216. pageId: PropTypes.string,
  217. revisionId: PropTypes.string,
  218. pagePath: PropTypes.string,
  219. onSaveSuccess: PropTypes.func,
  220. };