2018-07-15 05:26:26 +00:00
|
|
|
|
// @flow
|
2020-06-20 20:59:15 +00:00
|
|
|
|
import { lighten } from "polished";
|
2020-08-09 05:53:59 +00:00
|
|
|
|
import * as React from "react";
|
2020-11-30 04:04:58 +00:00
|
|
|
|
import { useTranslation } from "react-i18next";
|
2020-08-09 05:53:59 +00:00
|
|
|
|
import { withRouter, type RouterHistory } from "react-router-dom";
|
2021-09-11 05:46:57 +00:00
|
|
|
|
import { Extension } from "rich-markdown-editor";
|
2020-08-09 05:53:59 +00:00
|
|
|
|
import styled, { withTheme } from "styled-components";
|
2021-09-11 05:46:57 +00:00
|
|
|
|
import embeds from "shared/embeds";
|
2021-07-21 17:34:55 +00:00
|
|
|
|
import { light } from "shared/theme";
|
2020-06-20 20:59:15 +00:00
|
|
|
|
import UiStore from "stores/UiStore";
|
2020-08-15 00:23:58 +00:00
|
|
|
|
import ErrorBoundary from "components/ErrorBoundary";
|
2020-08-09 05:53:59 +00:00
|
|
|
|
import Tooltip from "components/Tooltip";
|
2021-05-13 03:00:10 +00:00
|
|
|
|
import useMediaQuery from "hooks/useMediaQuery";
|
2021-07-20 09:06:10 +00:00
|
|
|
|
import useToasts from "hooks/useToasts";
|
2021-05-13 03:00:10 +00:00
|
|
|
|
import { type Theme } from "types";
|
2021-01-21 15:28:10 +00:00
|
|
|
|
import { isModKey } from "utils/keyboard";
|
2020-08-09 05:53:59 +00:00
|
|
|
|
import { uploadFile } from "utils/uploadFile";
|
2021-01-16 19:12:10 +00:00
|
|
|
|
import { isInternalUrl } from "utils/urls";
|
2018-07-15 05:26:26 +00:00
|
|
|
|
|
2021-06-04 05:01:23 +00:00
|
|
|
|
const RichMarkdownEditor = React.lazy(() =>
|
|
|
|
|
import(/* webpackChunkName: "rich-markdown-editor" */ "rich-markdown-editor")
|
|
|
|
|
);
|
2020-08-15 00:23:58 +00:00
|
|
|
|
|
2020-05-20 03:39:34 +00:00
|
|
|
|
const EMPTY_ARRAY = [];
|
|
|
|
|
|
2021-01-30 05:36:09 +00:00
|
|
|
|
export type Props = {|
|
2020-08-09 16:48:04 +00:00
|
|
|
|
id?: string,
|
2021-01-30 05:36:09 +00:00
|
|
|
|
value?: string,
|
2018-08-26 22:27:32 +00:00
|
|
|
|
defaultValue?: string,
|
2018-11-18 19:14:26 +00:00
|
|
|
|
readOnly?: boolean,
|
2019-12-19 05:00:36 +00:00
|
|
|
|
grow?: boolean,
|
2018-12-15 22:06:29 +00:00
|
|
|
|
disableEmbeds?: boolean,
|
2020-08-09 16:48:04 +00:00
|
|
|
|
ui?: UiStore,
|
2021-09-11 05:46:57 +00:00
|
|
|
|
style?: Object,
|
|
|
|
|
extensions?: Extension[],
|
2021-05-23 02:34:05 +00:00
|
|
|
|
shareId?: ?string,
|
2021-01-30 05:36:09 +00:00
|
|
|
|
autoFocus?: boolean,
|
|
|
|
|
template?: boolean,
|
|
|
|
|
placeholder?: string,
|
2021-02-13 00:20:49 +00:00
|
|
|
|
maxLength?: number,
|
2021-01-30 05:36:09 +00:00
|
|
|
|
scrollTo?: string,
|
2021-05-13 03:00:10 +00:00
|
|
|
|
theme?: Theme,
|
2021-02-13 00:20:49 +00:00
|
|
|
|
handleDOMEvents?: Object,
|
2021-01-30 05:36:09 +00:00
|
|
|
|
readOnlyWriteCheckboxes?: boolean,
|
|
|
|
|
onBlur?: (event: SyntheticEvent<>) => any,
|
|
|
|
|
onFocus?: (event: SyntheticEvent<>) => any,
|
|
|
|
|
onPublish?: (event: SyntheticEvent<>) => any,
|
|
|
|
|
onSave?: ({ done?: boolean, autosave?: boolean, publish?: boolean }) => any,
|
|
|
|
|
onCancel?: () => any,
|
2021-02-13 00:20:49 +00:00
|
|
|
|
onDoubleClick?: () => any,
|
2021-01-30 05:36:09 +00:00
|
|
|
|
onChange?: (getValue: () => string) => any,
|
|
|
|
|
onSearchLink?: (title: string) => any,
|
|
|
|
|
onHoverLink?: (event: MouseEvent) => any,
|
|
|
|
|
onCreateLink?: (title: string) => Promise<string>,
|
|
|
|
|
onImageUploadStart?: () => any,
|
|
|
|
|
onImageUploadStop?: () => any,
|
|
|
|
|
|};
|
2020-08-09 16:48:04 +00:00
|
|
|
|
|
|
|
|
|
type PropsWithRef = Props & {
|
2020-08-15 00:23:58 +00:00
|
|
|
|
forwardedRef: React.Ref<any>,
|
2020-08-09 16:48:04 +00:00
|
|
|
|
history: RouterHistory,
|
2018-07-15 05:26:26 +00:00
|
|
|
|
};
|
|
|
|
|
|
2020-11-30 04:04:58 +00:00
|
|
|
|
function Editor(props: PropsWithRef) {
|
2021-07-20 09:06:10 +00:00
|
|
|
|
const { id, shareId, history } = props;
|
2020-11-30 04:04:58 +00:00
|
|
|
|
const { t } = useTranslation();
|
2021-07-20 09:06:10 +00:00
|
|
|
|
const { showToast } = useToasts();
|
2021-05-13 03:00:10 +00:00
|
|
|
|
const isPrinting = useMediaQuery("print");
|
2020-11-30 04:04:58 +00:00
|
|
|
|
|
|
|
|
|
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;
|
2018-11-18 19:14:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-01-21 15:28:10 +00:00
|
|
|
|
if (isInternalUrl(href) && !isModKey(event) && !event.shiftKey) {
|
2020-11-30 04:04:58 +00:00
|
|
|
|
// relative
|
|
|
|
|
let navigateTo = href;
|
|
|
|
|
|
|
|
|
|
// probably absolute
|
|
|
|
|
if (href[0] !== "/") {
|
|
|
|
|
try {
|
|
|
|
|
const url = new URL(href);
|
|
|
|
|
navigateTo = url.pathname + url.hash;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
navigateTo = href;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-23 02:34:05 +00:00
|
|
|
|
if (shareId) {
|
|
|
|
|
navigateTo = `/share/${shareId}${navigateTo}`;
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-30 04:04:58 +00:00
|
|
|
|
history.push(navigateTo);
|
|
|
|
|
} else if (href) {
|
|
|
|
|
window.open(href, "_blank");
|
|
|
|
|
}
|
|
|
|
|
},
|
2021-05-23 02:34:05 +00:00
|
|
|
|
[history, shareId]
|
2020-11-30 04:04:58 +00:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const onShowToast = React.useCallback(
|
|
|
|
|
(message: string) => {
|
2021-07-20 09:06:10 +00:00
|
|
|
|
showToast(message);
|
2020-11-30 04:04:58 +00:00
|
|
|
|
},
|
2021-07-20 09:06:10 +00:00
|
|
|
|
[showToast]
|
2020-11-30 04:04:58 +00:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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"),
|
2021-04-28 00:21:45 +00:00
|
|
|
|
deleteImage: t("Delete image"),
|
2021-05-23 00:17:46 +00:00
|
|
|
|
downloadImage: t("Download image"),
|
2021-04-28 00:21:45 +00:00
|
|
|
|
alignImageLeft: t("Float left"),
|
|
|
|
|
alignImageRight: t("Float right"),
|
|
|
|
|
alignImageDefault: t("Center large"),
|
2020-11-30 04:04:58 +00:00
|
|
|
|
em: t("Italic"),
|
|
|
|
|
embedInvalidLink: t("Sorry, that link won’t work for this embed type"),
|
2020-12-15 05:16:02 +00:00
|
|
|
|
findOrCreateDoc: `${t("Find or create a doc")}…`,
|
2020-11-30 04:04:58 +00:00
|
|
|
|
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"),
|
2021-07-28 22:01:01 +00:00
|
|
|
|
imageCaptionPlaceholder: t("Write a caption"),
|
2020-11-30 04:04:58 +00:00
|
|
|
|
info: t("Info"),
|
|
|
|
|
infoNotice: t("Info notice"),
|
|
|
|
|
link: t("Link"),
|
|
|
|
|
linkCopied: t("Link copied to clipboard"),
|
|
|
|
|
mark: t("Highlight"),
|
2020-12-15 05:16:02 +00:00
|
|
|
|
newLineEmpty: `${t("Type '/' to insert")}…`,
|
|
|
|
|
newLineWithSlash: `${t("Keep typing to filter")}…`,
|
2020-11-30 04:04:58 +00:00
|
|
|
|
noResults: t("No results"),
|
|
|
|
|
openLink: t("Open link"),
|
|
|
|
|
orderedList: t("Ordered list"),
|
2021-04-28 00:21:45 +00:00
|
|
|
|
pageBreak: t("Page break"),
|
2020-12-15 05:16:02 +00:00
|
|
|
|
pasteLink: `${t("Paste a link")}…`,
|
2020-11-30 04:04:58 +00:00
|
|
|
|
pasteLinkWithTitle: (service: string) =>
|
|
|
|
|
t("Paste a {{service}} link…", { service }),
|
|
|
|
|
placeholder: t("Placeholder"),
|
|
|
|
|
quote: t("Quote"),
|
|
|
|
|
removeLink: t("Remove link"),
|
2020-12-15 05:16:02 +00:00
|
|
|
|
searchOrPasteLink: `${t("Search or paste a link")}…`,
|
2020-11-30 04:04:58 +00:00
|
|
|
|
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}
|
2021-05-13 03:00:10 +00:00
|
|
|
|
theme={isPrinting ? light : props.theme}
|
2020-11-30 04:04:58 +00:00
|
|
|
|
/>
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
);
|
2018-07-15 05:26:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-07-08 02:25:45 +00:00
|
|
|
|
const StyledEditor = styled(RichMarkdownEditor)`
|
2020-08-09 01:53:11 +00:00
|
|
|
|
flex-grow: ${(props) => (props.grow ? 1 : 0)};
|
2019-07-08 02:25:45 +00:00
|
|
|
|
justify-content: start;
|
|
|
|
|
|
|
|
|
|
> div {
|
2021-02-13 00:20:49 +00:00
|
|
|
|
background: transparent;
|
2019-07-08 02:25:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-11-22 00:16:38 +00:00
|
|
|
|
& * {
|
|
|
|
|
box-sizing: content-box;
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-21 02:03:14 +00:00
|
|
|
|
.notice-block.tip,
|
|
|
|
|
.notice-block.warning {
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-15 03:50:10 +00:00
|
|
|
|
.heading-anchor {
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-19 14:40:12 +00:00
|
|
|
|
.heading-name {
|
2020-11-13 06:02:19 +00:00
|
|
|
|
pointer-events: none;
|
2021-01-15 16:50:19 +00:00
|
|
|
|
display: block;
|
|
|
|
|
position: relative;
|
|
|
|
|
top: -60px;
|
|
|
|
|
visibility: hidden;
|
2020-10-20 06:07:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-11-13 05:51:20 +00:00
|
|
|
|
.heading-name:first-child {
|
|
|
|
|
& + h1,
|
|
|
|
|
& + h2,
|
|
|
|
|
& + h3,
|
|
|
|
|
& + h4 {
|
|
|
|
|
margin-top: 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-08 02:25:45 +00:00
|
|
|
|
p {
|
|
|
|
|
a {
|
2020-08-09 01:53:11 +00:00
|
|
|
|
color: ${(props) => props.theme.text};
|
|
|
|
|
border-bottom: 1px solid ${(props) => lighten(0.5, props.theme.text)};
|
2020-07-21 02:03:14 +00:00
|
|
|
|
text-decoration: none !important;
|
2019-07-08 02:25:45 +00:00
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
2020-08-09 01:53:11 +00:00
|
|
|
|
border-bottom: 1px solid ${(props) => props.theme.text};
|
2019-07-08 02:25:45 +00:00
|
|
|
|
text-decoration: none;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-09-11 05:46:57 +00:00
|
|
|
|
|
|
|
|
|
.ProseMirror {
|
|
|
|
|
.ProseMirror-yjs-cursor {
|
|
|
|
|
position: relative;
|
|
|
|
|
margin-left: -1px;
|
|
|
|
|
margin-right: -1px;
|
|
|
|
|
border-left: 1px solid black;
|
|
|
|
|
border-right: 1px solid black;
|
|
|
|
|
height: 1em;
|
|
|
|
|
word-break: normal;
|
|
|
|
|
&:after {
|
|
|
|
|
content: "";
|
|
|
|
|
display: block;
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: -8px;
|
|
|
|
|
right: -8px;
|
|
|
|
|
top: 0;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
}
|
|
|
|
|
> div {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: -1.8em;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
background-color: rgb(250, 129, 0);
|
|
|
|
|
font-style: normal;
|
|
|
|
|
line-height: normal;
|
|
|
|
|
user-select: none;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
color: white;
|
|
|
|
|
padding: 2px 6px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
left: -1px;
|
|
|
|
|
}
|
|
|
|
|
&:hover {
|
|
|
|
|
> div {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
transition: opacity 100ms ease-in-out;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-07-10 04:17:25 +00:00
|
|
|
|
`;
|
|
|
|
|
|
2019-08-30 07:27:40 +00:00
|
|
|
|
const EditorTooltip = ({ children, ...props }) => (
|
|
|
|
|
<Tooltip offset="0, 16" delay={150} {...props}>
|
2020-07-21 02:37:50 +00:00
|
|
|
|
<Span>{children}</Span>
|
2019-08-30 07:27:40 +00:00
|
|
|
|
</Tooltip>
|
2019-07-10 04:17:25 +00:00
|
|
|
|
);
|
2019-07-04 04:32:21 +00:00
|
|
|
|
|
2020-07-21 02:37:50 +00:00
|
|
|
|
const Span = styled.span`
|
|
|
|
|
outline: none;
|
|
|
|
|
`;
|
|
|
|
|
|
2020-02-27 06:29:22 +00:00
|
|
|
|
const EditorWithRouterAndTheme = withRouter(withTheme(Editor));
|
|
|
|
|
|
2020-08-09 16:48:04 +00:00
|
|
|
|
export default React.forwardRef<Props, typeof Editor>((props, ref) => (
|
2020-02-27 06:29:22 +00:00
|
|
|
|
<EditorWithRouterAndTheme {...props} forwardedRef={ref} />
|
|
|
|
|
));
|