import React from 'react'; import path from 'path'; import { useTranslation } from 'next-i18next'; import PropTypes from 'prop-types'; import { Modal, ModalHeader, ModalBody, ModalFooter, Popover, PopoverBody, } from 'reactstrap'; import validator from 'validator'; import Linker from '~/client/models/Linker'; import { apiv3Get } from '~/client/util/apiv3-client'; import { useCurrentPagePath } from '~/stores/context'; import PagePreviewIcon from '../Icons/PagePreviewIcon'; import SearchTypeahead from '../SearchTypeahead'; import Preview from './Preview'; import styles from './LinkEditPreview.module.scss'; class LinkEditModal extends React.PureComponent { constructor(props) { super(props); this.state = { show: false, isUseRelativePath: false, isUsePermanentLink: false, linkInputValue: '', labelInputValue: '', linkerType: Linker.types.markdownLink, markdown: null, pagePath: null, previewError: '', permalink: '', isPreviewOpen: false, }; // this.isApplyPukiwikiLikeLinkerPlugin = window.growiRenderer.preProcessors.some(process => process.constructor.name === 'PukiwikiLikeLinker'); this.show = this.show.bind(this); this.hide = this.hide.bind(this); this.cancel = this.cancel.bind(this); this.handleChangeTypeahead = this.handleChangeTypeahead.bind(this); this.handleChangeLabelInput = this.handleChangeLabelInput.bind(this); this.handleChangeLinkInput = this.handleChangeLinkInput.bind(this); this.handleSelecteLinkerType = this.handleSelecteLinkerType.bind(this); this.toggleIsUseRelativePath = this.toggleIsUseRelativePath.bind(this); this.toggleIsUsePamanentLink = this.toggleIsUsePamanentLink.bind(this); this.save = this.save.bind(this); this.generateLink = this.generateLink.bind(this); this.getRootPath = this.getRootPath.bind(this); this.toggleIsPreviewOpen = this.toggleIsPreviewOpen.bind(this); this.setMarkdown = this.setMarkdown.bind(this); } // defaultMarkdownLink is an instance of Linker show(defaultMarkdownLink = null) { // if defaultMarkdownLink is null, set default value in inputs. const { label = '', link = '' } = defaultMarkdownLink; let { type = Linker.types.markdownLink } = defaultMarkdownLink; // if type of defaultMarkdownLink is pukiwikiLink when pukiwikiLikeLinker plugin is disable, change type(not change label and link) if (type === Linker.types.pukiwikiLink && !this.isApplyPukiwikiLikeLinkerPlugin) { type = Linker.types.markdownLink; } this.parseLinkAndSetState(link, type); this.setState({ show: true, labelInputValue: label, isUsePermanentLink: false, permalink: '', linkerType: type, }); } // parse link, link is ... // case-1. url of this growi's page (ex. 'http://localhost:3000/hoge/fuga') // case-2. absolute path of this growi's page (ex. '/hoge/fuga') // case-3. relative path of this growi's page (ex. '../fuga', 'hoge') // case-4. external link (ex. 'https://growi.org') // case-5. the others (ex. '') parseLinkAndSetState(link, type) { // create url from link, add dummy origin if link is not valid url. // ex-1. link = 'https://growi.org/' -> url = 'https://growi.org/' (case-1,4) // ex-2. link = 'hoge' -> url = 'http://example.com/hoge' (case-2,3,5) const url = new URL(link, 'http://example.com'); const isUrl = url.origin !== 'http://example.com'; let isUseRelativePath = false; let reshapedLink = link; // if case-1, reshapedLink becomes page path reshapedLink = this.convertUrlToPathIfPageUrl(reshapedLink, url); // case-3 if (!isUrl && !reshapedLink.startsWith('/') && reshapedLink !== '') { isUseRelativePath = true; const rootPath = this.getRootPath(type); reshapedLink = path.resolve(rootPath, reshapedLink); } this.setState({ linkInputValue: reshapedLink, isUseRelativePath, }); } // return path name of link if link is this growi page url, else return original link. convertUrlToPathIfPageUrl(link, url) { // when link is this growi's page url, url.origin === window.location.origin and return path name return url.origin === window.location.origin ? decodeURI(url.pathname) : link; } cancel() { this.hide(); } hide() { this.setState({ show: false, }); } toggleIsUseRelativePath() { if (!this.state.linkInputValue.startsWith('/') || this.state.linkerType === Linker.types.growiLink) { return; } // User can't use both relativePath and permalink at the same time this.setState({ isUseRelativePath: !this.state.isUseRelativePath, isUsePermanentLink: false }); } toggleIsUsePamanentLink() { if (this.state.permalink === '' || this.state.linkerType === Linker.types.growiLink) { return; } // User can't use both relativePath and permalink at the same time this.setState({ isUsePermanentLink: !this.state.isUsePermanentLink, isUseRelativePath: false }); } async setMarkdown() { const { t } = this.props; const path = this.state.linkInputValue; let markdown = null; let pagePath = null; let permalink = ''; let previewError = ''; if (path.startsWith('/')) { const pathWithoutFragment = new URL(path, 'http://dummy').pathname; const isPermanentLink = validator.isMongoId(pathWithoutFragment.slice(1)); const pageId = isPermanentLink ? pathWithoutFragment.slice(1) : null; try { const { data } = await apiv3Get('/page', { path: pathWithoutFragment, page_id: pageId }); const { page } = data; markdown = page.revision.body; pagePath = page.path; permalink = page.id; } catch (err) { previewError = err.message; } } else { previewError = t('link_edit.page_not_found_in_preview', { path }); } this.setState({ markdown, pagePath, previewError, permalink, }); } renderLinkPreview() { const linker = this.generateLink(); return (
); } handleChangeTypeahead(selected) { const pageWithMeta = selected[0]; if (pageWithMeta != null) { const page = pageWithMeta.data; const permalink = `${window.location.origin}/${page.id}`; this.setState({ linkInputValue: page.path, permalink }); } } handleChangeLabelInput(label) { this.setState({ labelInputValue: label }); } handleChangeLinkInput(link) { let isUseRelativePath = this.state.isUseRelativePath; if (!this.state.linkInputValue.startsWith('/') || this.state.linkerType === Linker.types.growiLink) { isUseRelativePath = false; } this.setState({ linkInputValue: link, isUseRelativePath, isUsePermanentLink: false, permalink: '', }); } handleSelecteLinkerType(linkerType) { let { isUseRelativePath, isUsePermanentLink } = this.state; if (linkerType === Linker.types.growiLink) { isUseRelativePath = false; isUsePermanentLink = false; } this.setState({ linkerType, isUseRelativePath, isUsePermanentLink }); } save() { const linker = this.generateLink(); if (this.props.onSave != null) { this.props.onSave(linker.generateMarkdownText()); } this.hide(); } generateLink() { const { linkInputValue, labelInputValue, linkerType, isUseRelativePath, isUsePermanentLink, permalink, } = this.state; let reshapedLink = linkInputValue; if (isUseRelativePath) { const rootPath = this.getRootPath(linkerType); reshapedLink = rootPath === linkInputValue ? '.' : path.relative(rootPath, linkInputValue); } if (isUsePermanentLink && permalink != null) { reshapedLink = permalink; } return new Linker(linkerType, labelInputValue, reshapedLink); } getRootPath(type) { const { pagePath } = this.props; // rootPaths of md link and pukiwiki link are different return type === Linker.types.markdownLink ? path.dirname(pagePath) : pagePath; } async toggleIsPreviewOpen() { // open popover if (this.state.isPreviewOpen === false) { this.setMarkdown(); } this.setState({ isPreviewOpen: !this.state.isPreviewOpen }); } renderLinkAndLabelForm() { const { t } = this.props; const { pagePath } = this.state; return ( <>