import React, { useEffect, useState, useCallback } from 'react'; import path from 'path'; import Linker from '@growi/editor/src/services/link-util/Linker'; import { useLinkEditModal } from '@growi/editor/src/stores/use-link-edit-modal'; import { useTranslation } from 'next-i18next'; import { Modal, ModalHeader, ModalBody, ModalFooter, Popover, PopoverBody, } from 'reactstrap'; import validator from 'validator'; import { apiv3Get } from '~/client/util/apiv3-client'; import { useCurrentPagePath } from '~/stores/page'; import { usePreviewOptions } from '~/stores/renderer'; import loggerFactory from '~/utils/logger'; import SearchTypeahead from '../SearchTypeahead'; import Preview from './Preview'; import styles from './LinkEditPreview.module.scss'; const logger = loggerFactory('growi:components:LinkEditModal'); export const LinkEditModal = (): JSX.Element => { const { t } = useTranslation(); const { data: currentPath } = useCurrentPagePath(); const { data: rendererOptions } = usePreviewOptions(); const { data: linkEditModalStatus, close } = useLinkEditModal(); const [isUseRelativePath, setIsUseRelativePath] = useState(false); const [isUsePermanentLink, setIsUsePermanentLink] = useState(false); const [linkInputValue, setLinkInputValue] = useState(''); const [labelInputValue, setLabelInputValue] = useState(''); const [linkerType, setLinkerType] = useState(''); const [markdown, setMarkdown] = useState(''); const [pagePath, setPagePath] = useState(''); const [previewError, setPreviewError] = useState(); const [permalink, setPermalink] = useState(''); const [isPreviewOpen, setIsPreviewOpen] = useState(false); const getRootPath = useCallback((type: string) => { // rootPaths of md link and pukiwiki link are different if (currentPath == null) return ''; return type === Linker.types.markdownLink ? path.dirname(currentPath) : currentPath; }, [currentPath]); // 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. '') const parseLinkAndSetState = useCallback((link: string, type: string) => { // 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) let isFqcn = false; let isUseRelativePath = false; let url; try { const url = new URL(link, 'http://example.com'); isFqcn = url.origin !== 'http://example.com'; } catch (err) { logger.debug(err); } // case-1: when link is this growi's page url, return pathname only let reshapedLink = url != null && url.origin === window.location.origin ? decodeURIComponent(url.pathname) : link; // case-3 if (!isFqcn && !reshapedLink.startsWith('/') && reshapedLink !== '') { isUseRelativePath = true; const rootPath = getRootPath(type); reshapedLink = path.resolve(rootPath, reshapedLink); } setLinkInputValue(reshapedLink); setIsUseRelativePath(isUseRelativePath); }, [getRootPath]); useEffect(() => { if (linkEditModalStatus == null) { return } const { label = '', link = '' } = linkEditModalStatus.defaultMarkdownLink ?? {}; const { type = Linker.types.markdownLink } = linkEditModalStatus.defaultMarkdownLink ?? {}; parseLinkAndSetState(link, type); setLabelInputValue(label); setIsUsePermanentLink(false); setPermalink(''); setLinkerType(type); }, [linkEditModalStatus, parseLinkAndSetState]); const toggleIsUseRelativePath = () => { if (!linkInputValue.startsWith('/') || linkerType === Linker.types.growiLink) { return; } // User can't use both relativePath and permalink at the same time setIsUseRelativePath(!isUseRelativePath); setIsUsePermanentLink(false); }; const toggleIsUsePamanentLink = () => { if (permalink === '' || linkerType === Linker.types.growiLink) { return; } // User can't use both relativePath and permalink at the same time setIsUsePermanentLink(!isUsePermanentLink); setIsUseRelativePath(false); }; const setMarkdownHandler = async() => { const path = linkInputValue; let markdown = ''; let pagePath = ''; let permalink = ''; if (path.startsWith('/')) { try { const pathWithoutFragment = new URL(path, 'http://dummy').pathname; const isPermanentLink = validator.isMongoId(pathWithoutFragment.slice(1)); const pageId = isPermanentLink ? pathWithoutFragment.slice(1) : null; 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) { setPreviewError(err.message); } } else { setPreviewError(t('link_edit.page_not_found_in_preview', { path })); } setMarkdown(markdown); setPagePath(pagePath); setPermalink(permalink); }; const generateLink = () => { let reshapedLink = linkInputValue; if (isUseRelativePath) { const rootPath = getRootPath(linkerType); reshapedLink = rootPath === linkInputValue ? '.' : path.relative(rootPath, linkInputValue); } if (isUsePermanentLink && permalink != null) { reshapedLink = permalink; } return new Linker(linkerType, labelInputValue, reshapedLink); }; const renderLinkPreview = (): JSX.Element => { const linker = generateLink(); return (

Markdown

{linker.generateMarkdownText()}

arrow_right arrow_drop_down
); }; const handleChangeTypeahead = (selected) => { const pageWithMeta = selected[0]; if (pageWithMeta != null) { const page = pageWithMeta.data; const permalink = `${window.location.origin}/${page.id}`; setLinkInputValue(page.path); setPermalink(permalink); } }; const handleChangeLabelInput = (label: string) => { setLabelInputValue(label); }; const handleChangeLinkInput = (link) => { let useRelativePath = isUseRelativePath; if (!linkInputValue.startsWith('/') || linkerType === Linker.types.growiLink) { useRelativePath = false; } setLinkInputValue(link); setIsUseRelativePath(useRelativePath); setIsUsePermanentLink(false); setPermalink(''); }; const save = () => { const linker = generateLink(); if (linkEditModalStatus?.onSave != null) { linkEditModalStatus.onSave(linker.generateMarkdownText() ?? ''); } close(); }; const toggleIsPreviewOpen = async() => { // open popover if (!isPreviewOpen) { setMarkdownHandler(); } setIsPreviewOpen(!isPreviewOpen); }; const renderLinkAndLabelForm = (): JSX.Element => { return ( <>

{t('link_edit.set_link_and_label')}

{t('link_edit.link')}
{markdown != null && pagePath != null && rendererOptions != null && (
) }
{t('link_edit.label')}
handleChangeLabelInput(e.target.value)} disabled={linkerType === Linker.types.growiLink} placeholder={linkInputValue} />
); }; const renderPathFormatForm = (): JSX.Element => { return (
); }; if (linkEditModalStatus == null) { return <>; } return ( {t('link_edit.edit_link')}
{renderLinkAndLabelForm()} {renderPathFormatForm()}

{t('link_edit.preview')}

{renderLinkPreview()}
{ previewError && {previewError}}
); }; LinkEditModal.displayName = 'LinkEditModal';