2018-07-15 05:26:26 +00:00
|
|
|
// @flow
|
|
|
|
import * as React from 'react';
|
2020-02-17 01:28:24 +00:00
|
|
|
import { withRouter, type RouterHistory } from 'react-router-dom';
|
2019-01-19 08:23:39 +00:00
|
|
|
import { observable } from 'mobx';
|
|
|
|
import { observer } from 'mobx-react';
|
2019-07-08 02:25:45 +00:00
|
|
|
import { lighten } from 'polished';
|
2019-07-10 04:17:25 +00:00
|
|
|
import styled, { withTheme, createGlobalStyle } from 'styled-components';
|
2018-11-19 02:28:22 +00:00
|
|
|
import RichMarkdownEditor from 'rich-markdown-editor';
|
2019-07-08 02:25:45 +00:00
|
|
|
import Placeholder from 'rich-markdown-editor/lib/components/Placeholder';
|
2018-11-18 19:14:26 +00:00
|
|
|
import { uploadFile } from 'utils/uploadFile';
|
|
|
|
import isInternalUrl from 'utils/isInternalUrl';
|
2019-07-04 04:32:21 +00:00
|
|
|
import Tooltip from 'components/Tooltip';
|
2019-08-09 06:09:09 +00:00
|
|
|
import UiStore from 'stores/UiStore';
|
2018-12-15 22:06:29 +00:00
|
|
|
import Embed from './Embed';
|
|
|
|
import embeds from '../../embeds';
|
2018-07-15 05:26:26 +00:00
|
|
|
|
|
|
|
type Props = {
|
2020-01-16 17:42:42 +00:00
|
|
|
id: 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-02-17 01:28:24 +00:00
|
|
|
history: RouterHistory,
|
2019-08-09 06:09:09 +00:00
|
|
|
forwardedRef: React.Ref<RichMarkdownEditor>,
|
|
|
|
ui: UiStore,
|
2018-07-15 05:26:26 +00:00
|
|
|
};
|
|
|
|
|
2019-01-19 08:23:39 +00:00
|
|
|
@observer
|
2018-07-15 05:26:26 +00:00
|
|
|
class Editor extends React.Component<Props> {
|
2019-01-19 08:23:39 +00:00
|
|
|
@observable redirectTo: ?string;
|
|
|
|
|
2018-11-18 19:14:26 +00:00
|
|
|
onUploadImage = async (file: File) => {
|
2020-01-16 17:42:42 +00:00
|
|
|
const result = await uploadFile(file, { documentId: this.props.id });
|
2018-11-18 19:14:26 +00:00
|
|
|
return result.url;
|
|
|
|
};
|
|
|
|
|
|
|
|
onClickLink = (href: string) => {
|
|
|
|
// on page hash
|
|
|
|
if (href[0] === '#') {
|
|
|
|
window.location.href = href;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isInternalUrl(href)) {
|
|
|
|
// relative
|
|
|
|
let navigateTo = href;
|
|
|
|
|
|
|
|
// probably absolute
|
|
|
|
if (href[0] !== '/') {
|
|
|
|
try {
|
|
|
|
const url = new URL(href);
|
|
|
|
navigateTo = url.pathname + url.hash;
|
|
|
|
} catch (err) {
|
|
|
|
navigateTo = href;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-17 01:28:24 +00:00
|
|
|
this.props.history.push(navigateTo);
|
2018-11-18 19:14:26 +00:00
|
|
|
} else {
|
|
|
|
window.open(href, '_blank');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
onShowToast = (message: string) => {
|
2019-04-18 02:11:23 +00:00
|
|
|
this.props.ui.showToast(message);
|
2018-11-18 19:14:26 +00:00
|
|
|
};
|
|
|
|
|
2018-12-15 22:06:29 +00:00
|
|
|
getLinkComponent = node => {
|
|
|
|
if (this.props.disableEmbeds) return;
|
|
|
|
|
|
|
|
const url = node.data.get('href');
|
|
|
|
const keys = Object.keys(embeds);
|
|
|
|
|
|
|
|
for (const key of keys) {
|
|
|
|
const component = embeds[key];
|
|
|
|
|
|
|
|
for (const host of component.ENABLED) {
|
|
|
|
const matches = url.match(host);
|
|
|
|
if (matches) return Embed;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-07-15 05:26:26 +00:00
|
|
|
render() {
|
|
|
|
return (
|
2019-07-10 04:17:25 +00:00
|
|
|
<React.Fragment>
|
|
|
|
<PrismStyles />
|
|
|
|
<StyledEditor
|
|
|
|
ref={this.props.forwardedRef}
|
|
|
|
uploadImage={this.onUploadImage}
|
|
|
|
onClickLink={this.onClickLink}
|
|
|
|
onShowToast={this.onShowToast}
|
|
|
|
getLinkComponent={this.getLinkComponent}
|
|
|
|
tooltip={EditorTooltip}
|
2020-04-05 19:22:26 +00:00
|
|
|
toc={false}
|
2019-07-10 04:17:25 +00:00
|
|
|
{...this.props}
|
|
|
|
/>
|
|
|
|
</React.Fragment>
|
2018-07-15 05:26:26 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-08 02:25:45 +00:00
|
|
|
const StyledEditor = styled(RichMarkdownEditor)`
|
2020-01-03 06:49:40 +00:00
|
|
|
flex-grow: ${props => (props.grow ? 1 : 0)};
|
2019-07-08 02:25:45 +00:00
|
|
|
justify-content: start;
|
|
|
|
|
|
|
|
> div {
|
|
|
|
transition: ${props => props.theme.backgroundTransition};
|
|
|
|
}
|
|
|
|
|
|
|
|
p {
|
|
|
|
${Placeholder} {
|
|
|
|
visibility: hidden;
|
|
|
|
}
|
|
|
|
}
|
2020-04-05 22:07:34 +00:00
|
|
|
p:nth-child(1):last-child {
|
2019-07-08 02:25:45 +00:00
|
|
|
${Placeholder} {
|
|
|
|
visibility: visible;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
p {
|
|
|
|
a {
|
|
|
|
color: ${props => props.theme.link};
|
|
|
|
border-bottom: 1px solid ${props => lighten(0.5, props.theme.link)};
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
border-bottom: 1px solid ${props => props.theme.link};
|
|
|
|
text-decoration: none;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-04-05 22:07:34 +00:00
|
|
|
|
|
|
|
h1:first-child,
|
|
|
|
h2:first-child,
|
|
|
|
h3:first-child,
|
|
|
|
h4:first-child,
|
|
|
|
h5:first-child,
|
|
|
|
h6:first-child {
|
|
|
|
margin-top: 0;
|
|
|
|
}
|
2019-07-08 02:25:45 +00:00
|
|
|
`;
|
|
|
|
|
2019-07-10 04:17:25 +00:00
|
|
|
/*
|
|
|
|
Based on Prism template by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/prism/)
|
|
|
|
Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
|
|
|
|
*/
|
|
|
|
const PrismStyles = createGlobalStyle`
|
|
|
|
code[class*="language-"],
|
|
|
|
pre[class*="language-"] {
|
|
|
|
-webkit-font-smoothing: initial;
|
|
|
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
|
|
|
font-size: 13px;
|
|
|
|
line-height: 1.375;
|
|
|
|
direction: ltr;
|
|
|
|
text-align: left;
|
|
|
|
white-space: pre;
|
|
|
|
word-spacing: normal;
|
|
|
|
word-break: normal;
|
|
|
|
-moz-tab-size: 4;
|
|
|
|
-o-tab-size: 4;
|
|
|
|
tab-size: 4;
|
|
|
|
-webkit-hyphens: none;
|
|
|
|
-moz-hyphens: none;
|
|
|
|
-ms-hyphens: none;
|
|
|
|
hyphens: none;
|
|
|
|
color: #24292e;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Code blocks */
|
|
|
|
pre[class*="language-"] {
|
|
|
|
padding: 1em;
|
|
|
|
margin: .5em 0;
|
|
|
|
overflow: auto;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Inline code */
|
|
|
|
:not(pre) > code[class*="language-"] {
|
|
|
|
padding: .1em;
|
|
|
|
border-radius: .3em;
|
|
|
|
}
|
|
|
|
|
|
|
|
.token.comment,
|
|
|
|
.token.prolog,
|
|
|
|
.token.doctype,
|
|
|
|
.token.cdata {
|
|
|
|
color: #6a737d;
|
|
|
|
}
|
|
|
|
|
|
|
|
.token.punctuation {
|
|
|
|
color: #5e6687;
|
|
|
|
}
|
|
|
|
|
|
|
|
.token.namespace {
|
|
|
|
opacity: .7;
|
|
|
|
}
|
|
|
|
|
|
|
|
.token.operator,
|
|
|
|
.token.boolean,
|
|
|
|
.token.number {
|
|
|
|
color: #d73a49;
|
|
|
|
}
|
|
|
|
|
|
|
|
.token.property {
|
|
|
|
color: #c08b30;
|
|
|
|
}
|
|
|
|
|
|
|
|
.token.tag {
|
|
|
|
color: #3d8fd1;
|
|
|
|
}
|
|
|
|
|
|
|
|
.token.string {
|
|
|
|
color: #032f62;
|
|
|
|
}
|
|
|
|
|
|
|
|
.token.selector {
|
|
|
|
color: #6679cc;
|
|
|
|
}
|
|
|
|
|
|
|
|
.token.attr-name {
|
|
|
|
color: #c76b29;
|
|
|
|
}
|
|
|
|
|
|
|
|
.token.entity,
|
|
|
|
.token.url,
|
|
|
|
.language-css .token.string,
|
|
|
|
.style .token.string {
|
|
|
|
color: #22a2c9;
|
|
|
|
}
|
|
|
|
|
|
|
|
.token.attr-value,
|
|
|
|
.token.keyword,
|
|
|
|
.token.control,
|
|
|
|
.token.directive,
|
|
|
|
.token.unit {
|
|
|
|
color: #d73a49;
|
|
|
|
}
|
|
|
|
|
|
|
|
.token.function {
|
|
|
|
color: #6f42c1;
|
|
|
|
}
|
|
|
|
|
|
|
|
.token.statement,
|
|
|
|
.token.regex,
|
|
|
|
.token.atrule {
|
|
|
|
color: #22a2c9;
|
|
|
|
}
|
|
|
|
|
|
|
|
.token.placeholder,
|
|
|
|
.token.variable {
|
|
|
|
color: #3d8fd1;
|
|
|
|
}
|
|
|
|
|
|
|
|
.token.deleted {
|
|
|
|
text-decoration: line-through;
|
|
|
|
}
|
|
|
|
|
|
|
|
.token.inserted {
|
|
|
|
border-bottom: 1px dotted #202746;
|
|
|
|
text-decoration: none;
|
|
|
|
}
|
|
|
|
|
|
|
|
.token.italic {
|
|
|
|
font-style: italic;
|
|
|
|
}
|
|
|
|
|
|
|
|
.token.important,
|
|
|
|
.token.bold {
|
|
|
|
font-weight: bold;
|
|
|
|
}
|
|
|
|
|
|
|
|
.token.important {
|
|
|
|
color: #c94922;
|
|
|
|
}
|
|
|
|
|
|
|
|
.token.entity {
|
|
|
|
cursor: help;
|
|
|
|
}
|
|
|
|
|
|
|
|
pre > code.highlight {
|
|
|
|
outline: 0.4em solid #c94922;
|
|
|
|
outline-offset: .4em;
|
|
|
|
}
|
|
|
|
`;
|
|
|
|
|
2019-08-30 07:27:40 +00:00
|
|
|
const EditorTooltip = ({ children, ...props }) => (
|
|
|
|
<Tooltip offset="0, 16" delay={150} {...props}>
|
|
|
|
<span>{children}</span>
|
|
|
|
</Tooltip>
|
2019-07-10 04:17:25 +00:00
|
|
|
);
|
2019-07-04 04:32:21 +00:00
|
|
|
|
2020-02-27 06:29:22 +00:00
|
|
|
const EditorWithRouterAndTheme = withRouter(withTheme(Editor));
|
|
|
|
|
|
|
|
// $FlowIssue - https://github.com/facebook/flow/issues/6103
|
|
|
|
export default React.forwardRef((props, ref) => (
|
|
|
|
<EditorWithRouterAndTheme {...props} forwardedRef={ref} />
|
|
|
|
));
|