227 lines
5.9 KiB
JavaScript
227 lines
5.9 KiB
JavaScript
// @flow
|
||
import { lighten } from "polished";
|
||
import * as React from "react";
|
||
import { useTranslation } from "react-i18next";
|
||
import { withRouter, type RouterHistory } from "react-router-dom";
|
||
import styled, { withTheme } from "styled-components";
|
||
import UiStore from "stores/UiStore";
|
||
import ErrorBoundary from "components/ErrorBoundary";
|
||
import Tooltip from "components/Tooltip";
|
||
import embeds from "../embeds";
|
||
import isInternalUrl from "utils/isInternalUrl";
|
||
import { isMetaKey } from "utils/keyboard";
|
||
import { uploadFile } from "utils/uploadFile";
|
||
|
||
const RichMarkdownEditor = React.lazy(() => import("rich-markdown-editor"));
|
||
|
||
const EMPTY_ARRAY = [];
|
||
|
||
type Props = {
|
||
id?: string,
|
||
defaultValue?: string,
|
||
readOnly?: boolean,
|
||
grow?: boolean,
|
||
disableEmbeds?: boolean,
|
||
ui?: UiStore,
|
||
};
|
||
|
||
type PropsWithRef = Props & {
|
||
forwardedRef: React.Ref<any>,
|
||
history: RouterHistory,
|
||
};
|
||
|
||
function Editor(props: PropsWithRef) {
|
||
const { id, ui, history } = props;
|
||
const { t } = useTranslation();
|
||
|
||
const onUploadImage = React.useCallback(
|
||
async (file: File) => {
|
||
const result = await uploadFile(file, { documentId: id });
|
||
return result.url;
|
||
},
|
||
[id]
|
||
);
|
||
|
||
const onClickLink = React.useCallback(
|
||
(href: string, event: MouseEvent) => {
|
||
// on page hash
|
||
if (href[0] === "#") {
|
||
window.location.href = href;
|
||
return;
|
||
}
|
||
|
||
if (isInternalUrl(href) && !isMetaKey(event) && !event.shiftKey) {
|
||
// relative
|
||
let navigateTo = href;
|
||
|
||
// probably absolute
|
||
if (href[0] !== "/") {
|
||
try {
|
||
const url = new URL(href);
|
||
navigateTo = url.pathname + url.hash;
|
||
} catch (err) {
|
||
navigateTo = href;
|
||
}
|
||
}
|
||
|
||
history.push(navigateTo);
|
||
} else if (href) {
|
||
window.open(href, "_blank");
|
||
}
|
||
},
|
||
[history]
|
||
);
|
||
|
||
const onShowToast = React.useCallback(
|
||
(message: string) => {
|
||
if (ui) {
|
||
ui.showToast(message);
|
||
}
|
||
},
|
||
[ui]
|
||
);
|
||
|
||
const dictionary = React.useMemo(() => {
|
||
return {
|
||
addColumnAfter: t("Insert column after"),
|
||
addColumnBefore: t("Insert column before"),
|
||
addRowAfter: t("Insert row after"),
|
||
addRowBefore: t("Insert row before"),
|
||
alignCenter: t("Align center"),
|
||
alignLeft: t("Align left"),
|
||
alignRight: t("Align right"),
|
||
bulletList: t("Bulleted list"),
|
||
checkboxList: t("Todo list"),
|
||
codeBlock: t("Code block"),
|
||
codeCopied: t("Copied to clipboard"),
|
||
codeInline: t("Code"),
|
||
createLink: t("Create link"),
|
||
createLinkError: t("Sorry, an error occurred creating the link"),
|
||
createNewDoc: t("Create a new doc"),
|
||
deleteColumn: t("Delete column"),
|
||
deleteRow: t("Delete row"),
|
||
deleteTable: t("Delete table"),
|
||
em: t("Italic"),
|
||
embedInvalidLink: t("Sorry, that link won’t work for this embed type"),
|
||
findOrCreateDoc: `${t("Find or create a doc")}…`,
|
||
h1: t("Big heading"),
|
||
h2: t("Medium heading"),
|
||
h3: t("Small heading"),
|
||
heading: t("Heading"),
|
||
hr: t("Divider"),
|
||
image: t("Image"),
|
||
imageUploadError: t("Sorry, an error occurred uploading the image"),
|
||
info: t("Info"),
|
||
infoNotice: t("Info notice"),
|
||
link: t("Link"),
|
||
linkCopied: t("Link copied to clipboard"),
|
||
mark: t("Highlight"),
|
||
newLineEmpty: `${t("Type '/' to insert")}…`,
|
||
newLineWithSlash: `${t("Keep typing to filter")}…`,
|
||
noResults: t("No results"),
|
||
openLink: t("Open link"),
|
||
orderedList: t("Ordered list"),
|
||
pasteLink: `${t("Paste a link")}…`,
|
||
pasteLinkWithTitle: (service: string) =>
|
||
t("Paste a {{service}} link…", { service }),
|
||
placeholder: t("Placeholder"),
|
||
quote: t("Quote"),
|
||
removeLink: t("Remove link"),
|
||
searchOrPasteLink: `${t("Search or paste a link")}…`,
|
||
strikethrough: t("Strikethrough"),
|
||
strong: t("Bold"),
|
||
subheading: t("Subheading"),
|
||
table: t("Table"),
|
||
tip: t("Tip"),
|
||
tipNotice: t("Tip notice"),
|
||
warning: t("Warning"),
|
||
warningNotice: t("Warning notice"),
|
||
};
|
||
}, [t]);
|
||
|
||
return (
|
||
<ErrorBoundary reloadOnChunkMissing>
|
||
<StyledEditor
|
||
ref={props.forwardedRef}
|
||
uploadImage={onUploadImage}
|
||
onClickLink={onClickLink}
|
||
onShowToast={onShowToast}
|
||
embeds={props.disableEmbeds ? EMPTY_ARRAY : embeds}
|
||
tooltip={EditorTooltip}
|
||
dictionary={dictionary}
|
||
{...props}
|
||
/>
|
||
</ErrorBoundary>
|
||
);
|
||
}
|
||
|
||
const StyledEditor = styled(RichMarkdownEditor)`
|
||
flex-grow: ${(props) => (props.grow ? 1 : 0)};
|
||
justify-content: start;
|
||
|
||
> div {
|
||
transition: ${(props) => props.theme.backgroundTransition};
|
||
}
|
||
|
||
& * {
|
||
box-sizing: content-box;
|
||
}
|
||
|
||
.notice-block.tip,
|
||
.notice-block.warning {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.heading-name {
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* pseudo element allows us to add spacing for fixed header */
|
||
/* ref: https://stackoverflow.com/a/28824157 */
|
||
.heading-name::before {
|
||
content: "";
|
||
display: ${(props) => (props.readOnly ? "block" : "none")};
|
||
height: 72px;
|
||
margin: -72px 0 0;
|
||
}
|
||
|
||
.heading-name:first-child {
|
||
& + h1,
|
||
& + h2,
|
||
& + h3,
|
||
& + h4 {
|
||
margin-top: 0;
|
||
}
|
||
}
|
||
|
||
p {
|
||
a {
|
||
color: ${(props) => props.theme.text};
|
||
border-bottom: 1px solid ${(props) => lighten(0.5, props.theme.text)};
|
||
text-decoration: none !important;
|
||
font-weight: 500;
|
||
|
||
&:hover {
|
||
border-bottom: 1px solid ${(props) => props.theme.text};
|
||
text-decoration: none;
|
||
}
|
||
}
|
||
}
|
||
`;
|
||
|
||
const EditorTooltip = ({ children, ...props }) => (
|
||
<Tooltip offset="0, 16" delay={150} {...props}>
|
||
<Span>{children}</Span>
|
||
</Tooltip>
|
||
);
|
||
|
||
const Span = styled.span`
|
||
outline: none;
|
||
`;
|
||
|
||
const EditorWithRouterAndTheme = withRouter(withTheme(Editor));
|
||
|
||
export default React.forwardRef<Props, typeof Editor>((props, ref) => (
|
||
<EditorWithRouterAndTheme {...props} forwardedRef={ref} />
|
||
));
|