Draft.jsx 6.5 KB

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