LinkEditModal.jsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import {
  4. Modal,
  5. ModalHeader,
  6. ModalBody,
  7. ModalFooter,
  8. } from 'reactstrap';
  9. import Preview from './Preview';
  10. import AppContainer from '../../services/AppContainer';
  11. import PageContainer from '../../services/PageContainer';
  12. import SearchTypeahead from '../SearchTypeahead';
  13. import Linker from '../../models/Linker';
  14. import { withUnstatedContainers } from '../UnstatedUtils';
  15. class LinkEditModal extends React.PureComponent {
  16. constructor(props) {
  17. super(props);
  18. this.state = {
  19. show: false,
  20. isUseRelativePath: false,
  21. isUsePermanentLink: false,
  22. linkInputValue: '',
  23. labelInputValue: '',
  24. linkerType: Linker.types.markdownLink,
  25. markdown: '',
  26. permalink: '',
  27. isEnablePermanentLink: false,
  28. };
  29. this.isApplyPukiwikiLikeLinkerPlugin = window.growiRenderer.preProcessors.some(process => process.constructor.name === 'PukiwikiLikeLinker');
  30. this.show = this.show.bind(this);
  31. this.hide = this.hide.bind(this);
  32. this.cancel = this.cancel.bind(this);
  33. this.handleChangeTypeahead = this.handleChangeTypeahead.bind(this);
  34. this.handleChangeLabelInput = this.handleChangeLabelInput.bind(this);
  35. this.handleChangeLinkInput = this.handleChangeLinkInput.bind(this);
  36. this.handleSelecteLinkerType = this.handleSelecteLinkerType.bind(this);
  37. this.toggleIsUseRelativePath = this.toggleIsUseRelativePath.bind(this);
  38. this.toggleIsUsePamanentLink = this.toggleIsUsePamanentLink.bind(this);
  39. this.save = this.save.bind(this);
  40. this.generateLink = this.generateLink.bind(this);
  41. this.getPreviewWithLinkInputValue = this.getPreviewWithLinkInputValue.bind(this);
  42. this.renderPreview = this.renderPreview.bind(this);
  43. }
  44. componentDidUpdate(prevState) {
  45. const { linkInputValue: prevLinkInputValue } = prevState;
  46. const { linkInputValue } = this.state;
  47. if (linkInputValue !== prevLinkInputValue) {
  48. this.getPreviewWithLinkInputValue(linkInputValue);
  49. }
  50. }
  51. // defaultMarkdownLink is an instance of Linker
  52. show(defaultMarkdownLink = null) {
  53. // if defaultMarkdownLink is null, set default value in inputs.
  54. const { label = '', link = '' } = defaultMarkdownLink;
  55. let { type = Linker.types.markdownLink } = defaultMarkdownLink;
  56. // if type of defaultMarkdownLink is pukiwikiLink when pukiwikiLikeLinker plugin is disable, change type(not change label and link)
  57. if (type === Linker.types.pukiwikiLink && !this.isApplyPukiwikiLikeLinkerPlugin) {
  58. type = Linker.types.markdownLink;
  59. }
  60. this.setState({
  61. show: true,
  62. labelInputValue: label,
  63. linkInputValue: link,
  64. linkerType: type,
  65. });
  66. }
  67. cancel() {
  68. this.hide();
  69. }
  70. hide() {
  71. this.setState({
  72. show: false,
  73. });
  74. }
  75. toggleIsUseRelativePath() {
  76. if (this.state.linkerType === Linker.types.growiLink) {
  77. return;
  78. }
  79. // User can't use both relativePath and permalink at the same time
  80. this.setState({ isUseRelativePath: !this.state.isUseRelativePath, isUsePermanentLink: false });
  81. }
  82. toggleIsUsePamanentLink() {
  83. if (!this.state.isEnablePermanentLink) {
  84. return;
  85. }
  86. // User can't use both relativePath and permalink at the same time
  87. this.setState({ isUsePermanentLink: !this.state.isUsePermanentLink, isUseRelativePath: false });
  88. }
  89. renderPreview() {
  90. return (
  91. <div className="linkedit-preview">
  92. <Preview
  93. markdown={this.state.markdown}
  94. />
  95. </div>
  96. );
  97. }
  98. async setMarkdown(path) {
  99. let markdown = '';
  100. try {
  101. await this.props.appContainer.apiGet('/pages.get', { path }).then((res) => {
  102. markdown = res.page.revision.body;
  103. });
  104. }
  105. catch (err) {
  106. markdown = `<div class="alert alert-warning" role="alert"><strong>${err.message}</strong></div>`;
  107. }
  108. this.setState({ markdown });
  109. }
  110. handleChangeTypeahead(selected) {
  111. const page = selected[0];
  112. if (page != null) {
  113. this.setState({ linkInputValue: page.path });
  114. }
  115. }
  116. handleChangeLabelInput(label) {
  117. this.setState({ labelInputValue: label });
  118. }
  119. handleChangeLinkInput(link) {
  120. this.setState({ linkInputValue: link });
  121. }
  122. handleSelecteLinkerType(linkerType) {
  123. if (this.state.isUseRelativePath && linkerType === Linker.types.growiLink) {
  124. this.toggleIsUseRelativePath();
  125. }
  126. this.setState({ linkerType });
  127. }
  128. save() {
  129. const output = this.generateLink();
  130. if (this.props.onSave != null) {
  131. this.props.onSave(output);
  132. }
  133. this.hide();
  134. }
  135. async getPreviewWithLinkInputValue(path) {
  136. let markdown = '';
  137. let permalink = '';
  138. let isEnablePermanentLink = false;
  139. try {
  140. const res = await this.props.appContainer.apiGet('/pages.get', { path });
  141. markdown = res.page.revision.body;
  142. permalink = `${window.location.origin}/${res.page.id}`;
  143. isEnablePermanentLink = true;
  144. }
  145. catch (err) {
  146. markdown = `<div class="alert alert-warning" role="alert"><strong>${err.message}</strong></div>`;
  147. }
  148. this.setState({ markdown, permalink, isEnablePermanentLink });
  149. }
  150. generateLink() {
  151. const { pageContainer } = this.props;
  152. const {
  153. linkInputValue,
  154. labelInputValue,
  155. linkerType,
  156. isUseRelativePath,
  157. isUsePermanentLink,
  158. permalink,
  159. } = this.state;
  160. return new Linker(
  161. linkerType,
  162. labelInputValue,
  163. linkInputValue,
  164. isUseRelativePath,
  165. isUsePermanentLink,
  166. permalink,
  167. pageContainer.state.path,
  168. );
  169. }
  170. render() {
  171. return (
  172. <Modal isOpen={this.state.show} toggle={this.cancel} size="lg">
  173. <ModalHeader tag="h4" toggle={this.cancel} className="bg-primary text-light">
  174. Edit Links
  175. </ModalHeader>
  176. <ModalBody className="container">
  177. <div className="row">
  178. <div className="col-12 col-lg-6">
  179. <form className="form-group">
  180. <div className="form-gorup my-3">
  181. <label htmlFor="linkInput">Link</label>
  182. <div className="input-group">
  183. <SearchTypeahead
  184. onChange={this.handleChangeTypeahead}
  185. onInputChange={this.handleChangeLinkInput}
  186. inputName="link"
  187. placeholder="Input page path or URL"
  188. keywordOnInit={this.state.linkInputValue}
  189. />
  190. </div>
  191. </div>
  192. </form>
  193. <div className="d-block d-lg-none mb-3 overflow-auto">
  194. {this.renderPreview()}
  195. </div>
  196. <div className="card">
  197. <div className="card-body">
  198. <form className="form-group">
  199. <div className="form-group btn-group d-flex" role="group" aria-label="type">
  200. <button
  201. type="button"
  202. name={Linker.types.markdownLink}
  203. className={`btn btn-outline-secondary w-100 ${this.state.linkerType === Linker.types.markdownLink && 'active'}`}
  204. onClick={e => this.handleSelecteLinkerType(e.target.name)}
  205. >
  206. Markdown
  207. </button>
  208. <button
  209. type="button"
  210. name={Linker.types.growiLink}
  211. className={`btn btn-outline-secondary w-100 ${this.state.linkerType === Linker.types.growiLink && 'active'}`}
  212. onClick={e => this.handleSelecteLinkerType(e.target.name)}
  213. >
  214. Growi Original
  215. </button>
  216. {this.isApplyPukiwikiLikeLinkerPlugin && (
  217. <button
  218. type="button"
  219. name={Linker.types.pukiwikiLink}
  220. className={`btn btn-outline-secondary w-100 ${this.state.linkerType === Linker.types.pukiwikiLink && 'active'}`}
  221. onClick={e => this.handleSelecteLinkerType(e.target.name)}
  222. >
  223. Pukiwiki
  224. </button>
  225. )}
  226. </div>
  227. <div className="form-group">
  228. <label htmlFor="label">Label</label>
  229. <input
  230. type="text"
  231. className="form-control"
  232. id="label"
  233. value={this.state.labelInputValue}
  234. onChange={e => this.handleChangeLabelInput(e.target.value)}
  235. disabled={this.state.linkerType === Linker.types.growiLink}
  236. />
  237. </div>
  238. <div className="form-inline">
  239. <div className="custom-control custom-checkbox custom-checkbox-info">
  240. <input
  241. className="custom-control-input"
  242. id="relativePath"
  243. type="checkbox"
  244. checked={this.state.isUseRelativePath}
  245. disabled={this.state.linkerType === Linker.types.growiLink}
  246. />
  247. <label className="custom-control-label" htmlFor="relativePath" onClick={this.toggleIsUseRelativePath}>
  248. Use relative path
  249. </label>
  250. </div>
  251. </div>
  252. <div className="form-inline">
  253. <div className="custom-control custom-checkbox custom-checkbox-info">
  254. <input
  255. className="custom-control-input"
  256. id="permanentLink"
  257. type="checkbox"
  258. checked={this.state.isUsePermanentLink}
  259. disabled={!this.state.isEnablePermanentLink}
  260. />
  261. <label className="custom-control-label" htmlFor="permanentLink" onClick={this.toggleIsUsePamanentLink}>
  262. Use permanent link
  263. </label>
  264. </div>
  265. </div>
  266. </form>
  267. </div>
  268. </div>
  269. </div>
  270. <div className="col d-none d-lg-block pr-0 mr-3 overflow-auto">
  271. {this.renderPreview()}
  272. </div>
  273. </div>
  274. </ModalBody>
  275. <ModalFooter>
  276. <button type="button" className="btn btn-sm btn-outline-secondary" onClick={this.hide}>
  277. Cancel
  278. </button>
  279. <button type="submit" className="btn btn-sm btn-primary" onClick={this.save}>
  280. Done
  281. </button>
  282. </ModalFooter>
  283. </Modal>
  284. );
  285. }
  286. }
  287. LinkEditModal.propTypes = {
  288. appContainer: PropTypes.instanceOf(AppContainer).isRequired,
  289. pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
  290. onSave: PropTypes.func,
  291. };
  292. /**
  293. * Wrapper component for using unstated
  294. */
  295. const LinkEditModalWrapper = withUnstatedContainers(LinkEditModal, [AppContainer, PageContainer]);
  296. export default LinkEditModalWrapper;