Draft.jsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import { CopyToClipboard } from 'react-copy-to-clipboard';
  4. import { useTranslation } from 'react-i18next';
  5. import {
  6. Collapse,
  7. UncontrolledTooltip,
  8. } from 'reactstrap';
  9. import AppContainer from '~/client/services/AppContainer';
  10. import RevisionBody from '../Page/RevisionBody';
  11. import { withUnstatedContainers } from '../UnstatedUtils';
  12. class Draft extends React.Component {
  13. constructor(props) {
  14. super(props);
  15. this.state = {
  16. html: '',
  17. isRendered: false,
  18. isPanelExpanded: false,
  19. showCopiedMessage: false,
  20. };
  21. this.growiRenderer = this.props.appContainer.getRenderer('draft');
  22. this.changeToolTipLabel = this.changeToolTipLabel.bind(this);
  23. this.expandPanelHandler = this.expandPanelHandler.bind(this);
  24. this.collapsePanelHandler = this.collapsePanelHandler.bind(this);
  25. this.renderHtml = this.renderHtml.bind(this);
  26. this.renderAccordionTitle = this.renderAccordionTitle.bind(this);
  27. }
  28. changeToolTipLabel() {
  29. this.setState({ showCopiedMessage: true });
  30. setTimeout(() => {
  31. this.setState({ showCopiedMessage: false });
  32. }, 1000);
  33. }
  34. expandPanelHandler() {
  35. this.setState({ isPanelExpanded: true });
  36. if (!this.state.isRendered) {
  37. this.renderHtml();
  38. }
  39. }
  40. collapsePanelHandler() {
  41. this.setState({ isPanelExpanded: false });
  42. }
  43. async renderHtml() {
  44. const context = {
  45. markdown: this.props.markdown,
  46. };
  47. const growiRenderer = this.growiRenderer;
  48. const interceptorManager = this.props.appContainer.interceptorManager;
  49. await interceptorManager.process('prePreProcess', context)
  50. .then(() => {
  51. context.markdown = growiRenderer.preProcess(context.markdown, context);
  52. })
  53. .then(() => { return interceptorManager.process('postPreProcess', context) })
  54. .then(() => {
  55. const parsedHTML = growiRenderer.process(context.markdown, context);
  56. context.parsedHTML = parsedHTML;
  57. })
  58. .then(() => { return interceptorManager.process('prePostProcess', context) })
  59. .then(() => {
  60. context.parsedHTML = growiRenderer.postProcess(context.parsedHTML, context);
  61. })
  62. .then(() => { return interceptorManager.process('postPostProcess', context) })
  63. .then(() => {
  64. this.setState({ html: context.parsedHTML, isRendered: true });
  65. });
  66. }
  67. renderAccordionTitle(isExist) {
  68. const { isPanelExpanded } = this.state;
  69. const { t } = this.props;
  70. const iconClass = isPanelExpanded ? 'fa-rotate-90' : '';
  71. return (
  72. <span>
  73. <span className="mr-2 draft-path" onClick={() => this.setState({ isPanelExpanded: !isPanelExpanded })}>
  74. <i className={`fa fa-fw fa-angle-right mr-2 ${iconClass}`}></i>
  75. {this.props.path}
  76. </span>
  77. { isExist && (
  78. <span className="badge badge-warning">{t('page exists')}</span>
  79. ) }
  80. { !isExist && (
  81. <span className="badge badge-info">draft</span>
  82. ) }
  83. <a className="ml-2" href={this.props.path}><i className="icon icon-login"></i></a>
  84. </span>
  85. );
  86. }
  87. renderControls() {
  88. const { t, path, index } = this.props;
  89. const tooltipTargetId = `draft-copied-tooltip_${index}`;
  90. return (
  91. <div className="icon-container">
  92. {this.props.isExist
  93. ? null
  94. : (
  95. <a
  96. href={`${path}#edit`}
  97. target="_blank"
  98. rel="noopener noreferrer"
  99. data-toggle="tooltip"
  100. title={this.props.t('Edit')}
  101. >
  102. <i className="mx-2 icon-note" />
  103. </a>
  104. )
  105. }
  106. <span id={tooltipTargetId}>
  107. <CopyToClipboard text={this.props.markdown} onCopy={this.changeToolTipLabel}>
  108. <a
  109. className="text-center draft-copy"
  110. >
  111. <i className="mx-2 ti-clipboard" />
  112. </a>
  113. </CopyToClipboard>
  114. </span>
  115. <UncontrolledTooltip placement="top" target={tooltipTargetId} fade={false} trigger="hover">
  116. { this.state.showCopiedMessage && (
  117. <strong>copied!</strong>
  118. ) }
  119. { !this.state.showCopiedMessage && (
  120. <span>{this.props.t('Copy')}</span>
  121. ) }
  122. </UncontrolledTooltip>
  123. <a
  124. className="text-danger text-center"
  125. data-toggle="tooltip"
  126. data-placement="top"
  127. title={t('Delete')}
  128. onClick={() => { return this.props.clearDraft(this.props.path) }}
  129. >
  130. <i className="mx-2 icon-trash" />
  131. </a>
  132. </div>
  133. );
  134. }
  135. render() {
  136. const { isPanelExpanded } = this.state;
  137. return (
  138. <div className="accordion draft-list-item" role="tablist">
  139. <div className="card">
  140. <div className="card-header d-flex" role="tab">
  141. {this.renderAccordionTitle(this.props.isExist)}
  142. <div className="flex-grow-1"></div>
  143. {this.renderControls()}
  144. </div>
  145. <Collapse isOpen={isPanelExpanded} onEntering={this.expandPanelHandler} onExiting={this.collapsePanelHandler}>
  146. <div className="card-body">
  147. {/* loading spinner */}
  148. { this.state.isPanelExpanded && !this.state.isRendered && (
  149. <div className="text-center">
  150. <i className="fa fa-lg fa-spinner fa-pulse mx-auto text-muted"></i>
  151. </div>
  152. ) }
  153. {/* contents */}
  154. { this.state.isPanelExpanded && this.state.isRendered && (
  155. <RevisionBody html={this.state.html} />
  156. ) }
  157. </div>
  158. </Collapse>
  159. </div>
  160. </div>
  161. );
  162. }
  163. }
  164. Draft.propTypes = {
  165. t: PropTypes.func.isRequired,
  166. appContainer: PropTypes.instanceOf(AppContainer).isRequired,
  167. index: PropTypes.number.isRequired,
  168. path: PropTypes.string.isRequired,
  169. markdown: PropTypes.string.isRequired,
  170. isExist: PropTypes.bool.isRequired,
  171. clearDraft: PropTypes.func.isRequired,
  172. };
  173. const DraftWrapperFC = (props) => {
  174. const { t } = useTranslation();
  175. return <Draft t={t} {...props} />;
  176. };
  177. /**
  178. * Wrapper component for using unstated
  179. */
  180. const DraftWrapper = withUnstatedContainers(DraftWrapperFC, [AppContainer]);
  181. export default DraftWrapper;