This repository has been archived on 2022-08-14. You can view files and clone it, but cannot push or open issues or pull requests.
outline/app/components/Editor.js

226 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @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 { 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) && !event.metaKey && !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 wont 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} />
));