PageEditor.js 5.6 KB

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