Merge pull request #639 from outline/refactor-editor
Extract Markdown Editor
This commit is contained in:
@@ -35,7 +35,6 @@ module.file_ext=.json
|
||||
esproposal.decorators=ignore
|
||||
esproposal.class_static_fields=enable
|
||||
esproposal.class_instance_fields=enable
|
||||
unsafe.enable_getters_and_setters=true
|
||||
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'shared/styles/constants';
|
||||
|
||||
type Props = {
|
||||
children: React.Element<*>,
|
||||
children: React.Node,
|
||||
type?: 'info' | 'success' | 'warning' | 'danger' | 'offline',
|
||||
};
|
||||
|
||||
@observer
|
||||
class Alert extends React.Component {
|
||||
props: Props;
|
||||
class Alert extends React.Component<Props> {
|
||||
defaultProps = {
|
||||
type: 'info',
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { Provider } from 'mobx-react';
|
||||
import stores from 'stores';
|
||||
import ApiKeysStore from 'stores/ApiKeysStore';
|
||||
@@ -10,7 +10,7 @@ import IntegrationsStore from 'stores/IntegrationsStore';
|
||||
import CacheStore from 'stores/CacheStore';
|
||||
|
||||
type Props = {
|
||||
children?: React.Element<any>,
|
||||
children?: React.Node,
|
||||
};
|
||||
|
||||
let authenticatedStores;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
@@ -7,7 +7,7 @@ import { color } from 'shared/styles/constants';
|
||||
import placeholder from './placeholder.png';
|
||||
|
||||
@observer
|
||||
class Avatar extends Component {
|
||||
class Avatar extends React.Component<*> {
|
||||
@observable error: boolean;
|
||||
|
||||
handleError = () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'shared/styles/constants';
|
||||
import { darken, lighten } from 'polished';
|
||||
@@ -88,9 +88,9 @@ const Inner = styled.span`
|
||||
export type Props = {
|
||||
type?: string,
|
||||
value?: string,
|
||||
icon?: React$Element<any>,
|
||||
icon?: React.Node,
|
||||
className?: string,
|
||||
children?: React$Element<any>,
|
||||
children?: React.Node,
|
||||
};
|
||||
|
||||
export default function Button({
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import breakpoint from 'styled-components-breakpoint';
|
||||
|
||||
type Props = {
|
||||
children?: React.Element<any>,
|
||||
children?: React.Node,
|
||||
};
|
||||
|
||||
const Container = styled.div`
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
|
||||
import styled from 'styled-components';
|
||||
import Flex from 'shared/components/Flex';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { observable, computed, action } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import styled from 'styled-components';
|
||||
@@ -26,9 +26,7 @@ type Props = {
|
||||
};
|
||||
|
||||
@observer
|
||||
class ColorPicker extends React.Component {
|
||||
props: Props;
|
||||
|
||||
class ColorPicker extends React.Component<Props> {
|
||||
@observable selectedColor: string = colors[0];
|
||||
@observable customColorValue: string = '';
|
||||
@observable customColorSelected: boolean;
|
||||
@@ -69,14 +67,14 @@ class ColorPicker extends React.Component {
|
||||
};
|
||||
|
||||
@action
|
||||
focusOnCustomColor = (event: SyntheticEvent) => {
|
||||
focusOnCustomColor = (event: SyntheticEvent<*>) => {
|
||||
this.selectedColor = '';
|
||||
this.customColorSelected = true;
|
||||
this.fireCallback();
|
||||
};
|
||||
|
||||
@action
|
||||
setCustomColor = (event: SyntheticEvent) => {
|
||||
setCustomColor = (event: SyntheticEvent<*>) => {
|
||||
let target = event.target;
|
||||
if (target instanceof HTMLInputElement) {
|
||||
const color = target.value;
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
// @flow
|
||||
import React, { PureComponent } from 'react';
|
||||
import * as React from 'react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
|
||||
type Props = {
|
||||
text: string,
|
||||
children?: React.Element<any>,
|
||||
children?: React.Node,
|
||||
onClick?: () => void,
|
||||
onCopy: () => void,
|
||||
};
|
||||
|
||||
class CopyToClipboard extends PureComponent {
|
||||
props: Props;
|
||||
|
||||
onClick = (ev: SyntheticEvent) => {
|
||||
class CopyToClipboard extends React.PureComponent<Props> {
|
||||
onClick = (ev: SyntheticEvent<*>) => {
|
||||
const { text, onCopy, children } = this.props;
|
||||
const elem = React.Children.only(children);
|
||||
copy(text, {
|
||||
debug: __DEV__,
|
||||
debug: !!__DEV__,
|
||||
});
|
||||
|
||||
if (onCopy) onCopy();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import Flex from 'shared/components/Flex';
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import Document from 'models/Document';
|
||||
import DocumentPreview from 'components/DocumentPreview';
|
||||
import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
|
||||
|
||||
class DocumentList extends React.Component {
|
||||
props: {
|
||||
documents: Document[],
|
||||
showCollection?: boolean,
|
||||
limit?: number,
|
||||
};
|
||||
type Props = {
|
||||
documents: Document[],
|
||||
showCollection?: boolean,
|
||||
limit?: number,
|
||||
};
|
||||
|
||||
class DocumentList extends React.Component<Props> {
|
||||
render() {
|
||||
const { limit, showCollection } = this.props;
|
||||
const documents = limit
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Document from 'models/Document';
|
||||
@@ -7,7 +7,7 @@ import styled from 'styled-components';
|
||||
import { color } from 'shared/styles/constants';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import Highlight from 'components/Highlight';
|
||||
import StarredIcon from 'components/Icon/StarredIcon';
|
||||
import { StarredIcon } from 'outline-icons';
|
||||
import PublishingInfo from './components/PublishingInfo';
|
||||
import DocumentMenu from 'menus/DocumentMenu';
|
||||
|
||||
@@ -89,16 +89,14 @@ const Actions = styled(Flex)`
|
||||
`;
|
||||
|
||||
@observer
|
||||
class DocumentPreview extends Component {
|
||||
props: Props;
|
||||
|
||||
star = (ev: SyntheticEvent) => {
|
||||
class DocumentPreview extends React.Component<Props> {
|
||||
star = (ev: SyntheticEvent<*>) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.document.star();
|
||||
};
|
||||
|
||||
unstar = (ev: SyntheticEvent) => {
|
||||
unstar = (ev: SyntheticEvent<*>) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.document.unstar();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import * as React from 'react';
|
||||
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'shared/styles/constants';
|
||||
@@ -17,13 +17,13 @@ const Modified = styled.span`
|
||||
font-weight: ${props => (props.highlight ? '600' : '400')};
|
||||
`;
|
||||
|
||||
class PublishingInfo extends Component {
|
||||
props: {
|
||||
collection?: Collection,
|
||||
document: Document,
|
||||
views?: number,
|
||||
};
|
||||
type Props = {
|
||||
collection?: Collection,
|
||||
document: Document,
|
||||
views?: number,
|
||||
};
|
||||
|
||||
class PublishingInfo extends React.Component<Props> {
|
||||
render() {
|
||||
const { collection, document } = this.props;
|
||||
const {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import * as React from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import Popover from 'components/Popover';
|
||||
@@ -27,11 +27,10 @@ type Props = {
|
||||
};
|
||||
|
||||
@observer
|
||||
class DocumentViews extends Component {
|
||||
class DocumentViews extends React.Component<Props> {
|
||||
@observable opened: boolean = false;
|
||||
anchor: HTMLElement;
|
||||
anchor: ?HTMLElement;
|
||||
store: DocumentViewersStore;
|
||||
props: Props;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
@@ -46,7 +45,7 @@ class DocumentViews extends Component {
|
||||
this.opened = false;
|
||||
};
|
||||
|
||||
setRef = (ref: HTMLElement) => {
|
||||
setRef = (ref: ?HTMLElement) => {
|
||||
this.anchor = ref;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import * as React from 'react';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import styled from 'styled-components';
|
||||
import map from 'lodash/map';
|
||||
@@ -26,9 +26,7 @@ const UserName = styled.span`
|
||||
padding-left: 8px;
|
||||
`;
|
||||
|
||||
class DocumentViewers extends Component {
|
||||
props: Props;
|
||||
|
||||
class DocumentViewers extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.onMount();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import * as React from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import { injectGlobal } from 'styled-components';
|
||||
@@ -12,7 +12,7 @@ import DocumentsStore from 'stores/DocumentsStore';
|
||||
import LoadingIndicator from 'components/LoadingIndicator';
|
||||
|
||||
type Props = {
|
||||
children?: React$Element<any>,
|
||||
children?: React.Node,
|
||||
collectionId: string,
|
||||
documentId?: string,
|
||||
activeClassName?: string,
|
||||
@@ -35,9 +35,8 @@ injectGlobal`
|
||||
`;
|
||||
|
||||
@observer
|
||||
class DropToImport extends Component {
|
||||
class DropToImport extends React.Component<Props> {
|
||||
@observable isImporting: boolean = false;
|
||||
props: Props;
|
||||
|
||||
onDropAccepted = async (files = []) => {
|
||||
this.isImporting = true;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import * as React from 'react';
|
||||
import invariant from 'invariant';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
@@ -10,22 +10,21 @@ import { color } from 'shared/styles/constants';
|
||||
import { fadeAndScaleIn } from 'shared/styles/animations';
|
||||
|
||||
type Props = {
|
||||
label: React.Element<*>,
|
||||
label: React.Node,
|
||||
onOpen?: () => void,
|
||||
onClose?: () => void,
|
||||
children?: React.Element<*>,
|
||||
children?: React.Node,
|
||||
className?: string,
|
||||
style?: Object,
|
||||
};
|
||||
|
||||
@observer
|
||||
class DropdownMenu extends Component {
|
||||
props: Props;
|
||||
class DropdownMenu extends React.Component<Props> {
|
||||
@observable top: number;
|
||||
@observable right: number;
|
||||
|
||||
handleOpen = (openPortal: SyntheticEvent => *) => {
|
||||
return (ev: SyntheticMouseEvent) => {
|
||||
handleOpen = (openPortal: (SyntheticEvent<*>) => *) => {
|
||||
return (ev: SyntheticMouseEvent<*>) => {
|
||||
ev.preventDefault();
|
||||
const currentTarget = ev.currentTarget;
|
||||
invariant(document.body, 'why you not here');
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'shared/styles/constants';
|
||||
|
||||
type Props = {
|
||||
onClick?: SyntheticEvent => void,
|
||||
children?: React.Element<any>,
|
||||
onClick?: (SyntheticEvent<*>) => *,
|
||||
children?: React.Node,
|
||||
};
|
||||
|
||||
const DropdownMenuItem = ({ onClick, children, ...rest }: Props) => {
|
||||
|
||||
@@ -1,351 +0,0 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Value, Change } from 'slate';
|
||||
import { Editor } from 'slate-react';
|
||||
import styled from 'styled-components';
|
||||
import breakpoint from 'styled-components-breakpoint';
|
||||
import keydown from 'react-keydown';
|
||||
import type { SlateNodeProps, Plugin } from './types';
|
||||
import getDataTransferFiles from 'utils/getDataTransferFiles';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import ClickablePadding from './components/ClickablePadding';
|
||||
import Toolbar from './components/Toolbar';
|
||||
import BlockInsert from './components/BlockInsert';
|
||||
import Placeholder from './components/Placeholder';
|
||||
import Contents from './components/Contents';
|
||||
import Markdown from './serializer';
|
||||
import createPlugins from './plugins';
|
||||
import { insertImageFile } from './changes';
|
||||
import renderMark from './marks';
|
||||
import createRenderNode from './nodes';
|
||||
import schema from './schema';
|
||||
import { isModKey } from './utils';
|
||||
|
||||
type Props = {
|
||||
text: string,
|
||||
onChange: Change => *,
|
||||
onSave: ({ redirect?: boolean, publish?: boolean }) => *,
|
||||
onCancel: () => void,
|
||||
onImageUploadStart: () => void,
|
||||
onImageUploadStop: () => void,
|
||||
emoji?: string,
|
||||
readOnly: boolean,
|
||||
};
|
||||
|
||||
@observer
|
||||
class MarkdownEditor extends Component {
|
||||
props: Props;
|
||||
editor: Editor;
|
||||
renderNode: SlateNodeProps => *;
|
||||
plugins: Plugin[];
|
||||
@observable editorValue: Value;
|
||||
@observable editorLoaded: boolean = false;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.renderNode = createRenderNode({
|
||||
onInsertImage: this.insertImageFile,
|
||||
});
|
||||
this.plugins = createPlugins({
|
||||
onImageUploadStart: props.onImageUploadStart,
|
||||
onImageUploadStop: props.onImageUploadStop,
|
||||
});
|
||||
|
||||
this.editorValue = Markdown.deserialize(props.text);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.readOnly) return;
|
||||
if (this.props.text) {
|
||||
this.focusAtEnd();
|
||||
} else {
|
||||
this.focusAtStart();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (prevProps.readOnly && !this.props.readOnly) {
|
||||
this.focusAtEnd();
|
||||
}
|
||||
}
|
||||
|
||||
setEditorRef = (ref: Editor) => {
|
||||
this.editor = ref;
|
||||
// Force re-render to show ToC (<Content />)
|
||||
this.editorLoaded = true;
|
||||
};
|
||||
|
||||
onChange = (change: Change) => {
|
||||
if (this.editorValue !== change.value) {
|
||||
this.props.onChange(Markdown.serialize(change.value));
|
||||
this.editorValue = change.value;
|
||||
}
|
||||
};
|
||||
|
||||
handleDrop = async (ev: SyntheticEvent) => {
|
||||
if (this.props.readOnly) return;
|
||||
// check if this event was already handled by the Editor
|
||||
if (ev.isDefaultPrevented()) return;
|
||||
|
||||
// otherwise we'll handle this
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
const files = getDataTransferFiles(ev);
|
||||
for (const file of files) {
|
||||
if (file.type.startsWith('image/')) {
|
||||
await this.insertImageFile(file);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
insertImageFile = (file: window.File) => {
|
||||
this.editor.change(change =>
|
||||
change.call(
|
||||
insertImageFile,
|
||||
file,
|
||||
this.editor,
|
||||
this.props.onImageUploadStart,
|
||||
this.props.onImageUploadStop
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
cancelEvent = (ev: SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
};
|
||||
|
||||
// Handling of keyboard shortcuts outside of editor focus
|
||||
@keydown('meta+s')
|
||||
onSave(ev: SyntheticKeyboardEvent) {
|
||||
if (this.props.readOnly) return;
|
||||
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.onSave({ redirect: false });
|
||||
}
|
||||
|
||||
@keydown('meta+enter')
|
||||
onSaveAndExit(ev: SyntheticKeyboardEvent) {
|
||||
if (this.props.readOnly) return;
|
||||
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.onSave({ redirect: true });
|
||||
}
|
||||
|
||||
@keydown('esc')
|
||||
onCancel() {
|
||||
if (this.props.readOnly) return;
|
||||
this.props.onCancel();
|
||||
}
|
||||
|
||||
// Handling of keyboard shortcuts within editor focus
|
||||
onKeyDown = (ev: SyntheticKeyboardEvent, change: Change) => {
|
||||
if (!isModKey(ev)) return;
|
||||
|
||||
switch (ev.key) {
|
||||
case 's':
|
||||
this.onSave(ev);
|
||||
return change;
|
||||
case 'Enter':
|
||||
this.onSaveAndExit(ev);
|
||||
return change;
|
||||
case 'Escape':
|
||||
this.onCancel();
|
||||
return change;
|
||||
default:
|
||||
}
|
||||
};
|
||||
|
||||
focusAtStart = () => {
|
||||
this.editor.change(change =>
|
||||
change.collapseToStartOf(change.value.document).focus()
|
||||
);
|
||||
};
|
||||
|
||||
focusAtEnd = () => {
|
||||
this.editor.change(change =>
|
||||
change.collapseToEndOf(change.value.document).focus()
|
||||
);
|
||||
};
|
||||
|
||||
render = () => {
|
||||
const { readOnly, emoji, onSave } = this.props;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
onDrop={this.handleDrop}
|
||||
onDragOver={this.cancelEvent}
|
||||
onDragEnter={this.cancelEvent}
|
||||
align="flex-start"
|
||||
justify="center"
|
||||
auto
|
||||
>
|
||||
<MaxWidth column auto>
|
||||
<Header onClick={this.focusAtStart} readOnly={readOnly} />
|
||||
{readOnly &&
|
||||
this.editorLoaded &&
|
||||
this.editor && <Contents editor={this.editor} />}
|
||||
{!readOnly &&
|
||||
this.editor && (
|
||||
<Toolbar value={this.editorValue} editor={this.editor} />
|
||||
)}
|
||||
{!readOnly &&
|
||||
this.editor && (
|
||||
<BlockInsert
|
||||
editor={this.editor}
|
||||
onInsertImage={this.insertImageFile}
|
||||
/>
|
||||
)}
|
||||
<StyledEditor
|
||||
innerRef={this.setEditorRef}
|
||||
placeholder="Start with a title…"
|
||||
bodyPlaceholder="…the rest is your canvas"
|
||||
plugins={this.plugins}
|
||||
emoji={emoji}
|
||||
value={this.editorValue}
|
||||
renderNode={this.renderNode}
|
||||
renderMark={renderMark}
|
||||
schema={schema}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onChange={this.onChange}
|
||||
onSave={onSave}
|
||||
readOnly={readOnly}
|
||||
spellCheck={!readOnly}
|
||||
/>
|
||||
<ClickablePadding
|
||||
onClick={!readOnly ? this.focusAtEnd : undefined}
|
||||
grow
|
||||
/>
|
||||
</MaxWidth>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const MaxWidth = styled(Flex)`
|
||||
padding: 0 20px;
|
||||
max-width: 100vw;
|
||||
height: 100%;
|
||||
|
||||
${breakpoint('tablet')`
|
||||
padding: 0;
|
||||
margin: 0 60px;
|
||||
max-width: 46em;
|
||||
`};
|
||||
`;
|
||||
|
||||
const Header = styled(Flex)`
|
||||
height: 60px;
|
||||
flex-shrink: 0;
|
||||
align-items: flex-end;
|
||||
${({ readOnly }) => !readOnly && 'cursor: text;'};
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledEditor = styled(Editor)`
|
||||
font-weight: 400;
|
||||
font-size: 1em;
|
||||
line-height: 1.7em;
|
||||
width: 100%;
|
||||
color: #1b2830;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
h1:first-of-type {
|
||||
${Placeholder} {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
p:nth-child(2) {
|
||||
${Placeholder} {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: 0 0.1em;
|
||||
padding-left: 1em;
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: 0.1em;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: ${({ readOnly }) => (readOnly ? 'underline' : 'none')};
|
||||
}
|
||||
|
||||
li p {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.todoList {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
|
||||
.todoList {
|
||||
padding-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.todo {
|
||||
span:last-child:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 3px solid #efefef;
|
||||
margin: 0;
|
||||
padding-left: 10px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 5px 20px 5px 0;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
`;
|
||||
|
||||
export default MarkdownEditor;
|
||||
@@ -1,93 +0,0 @@
|
||||
// @flow
|
||||
import { Change } from 'slate';
|
||||
import { Editor } from 'slate-react';
|
||||
import uuid from 'uuid';
|
||||
import EditList from './plugins/EditList';
|
||||
import { uploadFile } from 'utils/uploadFile';
|
||||
|
||||
const { changes } = EditList;
|
||||
|
||||
type Options = {
|
||||
type: string | Object,
|
||||
wrapper?: string | Object,
|
||||
};
|
||||
|
||||
export function splitAndInsertBlock(change: Change, options: Options) {
|
||||
const { type, wrapper } = options;
|
||||
const parent = change.value.document.getParent(change.value.startBlock.key);
|
||||
|
||||
// lists get some special treatment
|
||||
if (parent && parent.type === 'list-item') {
|
||||
change
|
||||
.collapseToStart()
|
||||
.call(changes.splitListItem)
|
||||
.collapseToEndOfPreviousBlock()
|
||||
.call(changes.unwrapList);
|
||||
}
|
||||
|
||||
if (wrapper) change.collapseToStartOfNextBlock();
|
||||
|
||||
// this is a hack as insertBlock with normalize: false does not appear to work
|
||||
change.insertBlock('paragraph').setBlock(type, { normalize: false });
|
||||
|
||||
if (wrapper) change.wrapBlock(wrapper);
|
||||
return change;
|
||||
}
|
||||
|
||||
export async function insertImageFile(
|
||||
change: Change,
|
||||
file: window.File,
|
||||
editor: Editor,
|
||||
onImageUploadStart: () => void,
|
||||
onImageUploadStop: () => void
|
||||
) {
|
||||
onImageUploadStart();
|
||||
try {
|
||||
// load the file as a data URL
|
||||
const id = uuid.v4();
|
||||
const alt = '';
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', () => {
|
||||
const src = reader.result;
|
||||
const node = {
|
||||
type: 'image',
|
||||
isVoid: true,
|
||||
data: { src, id, alt, loading: true },
|
||||
};
|
||||
|
||||
// insert / replace into document as uploading placeholder replacing
|
||||
// empty paragraphs if available.
|
||||
if (
|
||||
!change.value.startBlock.text &&
|
||||
change.value.startBlock.type === 'paragraph'
|
||||
) {
|
||||
change.setBlock(node);
|
||||
} else {
|
||||
change.insertBlock(node);
|
||||
}
|
||||
|
||||
editor.onChange(change);
|
||||
});
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
// now we have a placeholder, start the upload
|
||||
const asset = await uploadFile(file);
|
||||
const src = asset.url;
|
||||
|
||||
// we dont use the original change provided to the callback here
|
||||
// as the state may have changed significantly in the time it took to
|
||||
// upload the file.
|
||||
const placeholder = editor.value.document.findDescendant(
|
||||
node => node.data && node.data.get('id') === id
|
||||
);
|
||||
|
||||
change.setNodeByKey(placeholder.key, {
|
||||
data: { src, alt, loading: false },
|
||||
});
|
||||
editor.onChange(change);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
} finally {
|
||||
onImageUploadStop();
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { Portal } from 'react-portal';
|
||||
import { Node } from 'slate';
|
||||
import { Editor, findDOMNode } from 'slate-react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'shared/styles/constants';
|
||||
import PlusIcon from 'components/Icon/PlusIcon';
|
||||
|
||||
type Props = {
|
||||
editor: Editor,
|
||||
};
|
||||
|
||||
function findClosestRootNode(value, ev) {
|
||||
let previous;
|
||||
|
||||
for (const node of value.document.nodes) {
|
||||
const element = findDOMNode(node);
|
||||
const bounds = element.getBoundingClientRect();
|
||||
if (bounds.top > ev.clientY) return previous;
|
||||
previous = { node, element, bounds };
|
||||
}
|
||||
|
||||
return previous;
|
||||
}
|
||||
|
||||
@observer
|
||||
export default class BlockInsert extends Component {
|
||||
props: Props;
|
||||
mouseMoveTimeout: number;
|
||||
mouseMovementSinceClick: number = 0;
|
||||
lastClientX: number = 0;
|
||||
lastClientY: number = 0;
|
||||
|
||||
@observable closestRootNode: Node;
|
||||
@observable active: boolean = false;
|
||||
@observable top: number;
|
||||
@observable left: number;
|
||||
|
||||
componentDidMount = () => {
|
||||
window.addEventListener('mousemove', this.handleMouseMove);
|
||||
};
|
||||
|
||||
componentWillUnmount = () => {
|
||||
window.removeEventListener('mousemove', this.handleMouseMove);
|
||||
};
|
||||
|
||||
setInactive = () => {
|
||||
this.active = false;
|
||||
};
|
||||
|
||||
handleMouseMove = (ev: SyntheticMouseEvent) => {
|
||||
const windowWidth = window.innerWidth / 2.5;
|
||||
const result = findClosestRootNode(this.props.editor.value, ev);
|
||||
const movementThreshold = 200;
|
||||
|
||||
this.mouseMovementSinceClick +=
|
||||
Math.abs(this.lastClientX - ev.clientX) +
|
||||
Math.abs(this.lastClientY - ev.clientY);
|
||||
this.lastClientX = ev.clientX;
|
||||
this.lastClientY = ev.clientY;
|
||||
|
||||
this.active =
|
||||
ev.clientX < windowWidth &&
|
||||
this.mouseMovementSinceClick > movementThreshold;
|
||||
|
||||
if (result) {
|
||||
this.closestRootNode = result.node;
|
||||
|
||||
// do not show block menu on title heading or editor
|
||||
const firstNode = this.props.editor.value.document.nodes.first();
|
||||
if (
|
||||
result.node === firstNode ||
|
||||
result.node.type === 'block-toolbar' ||
|
||||
!!result.node.text.trim()
|
||||
) {
|
||||
this.left = -1000;
|
||||
} else {
|
||||
this.left = Math.round(result.bounds.left - 20);
|
||||
this.top = Math.round(result.bounds.top + window.scrollY);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.active) {
|
||||
clearTimeout(this.mouseMoveTimeout);
|
||||
this.mouseMoveTimeout = setTimeout(this.setInactive, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
handleClick = (ev: SyntheticMouseEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
this.mouseMovementSinceClick = 0;
|
||||
this.active = false;
|
||||
|
||||
const { editor } = this.props;
|
||||
|
||||
editor.change(change => {
|
||||
// remove any existing toolbars in the document as a fail safe
|
||||
editor.value.document.nodes.forEach(node => {
|
||||
if (node.type === 'block-toolbar') {
|
||||
change.removeNodeByKey(node.key);
|
||||
}
|
||||
});
|
||||
|
||||
change.collapseToStartOf(this.closestRootNode);
|
||||
|
||||
// if we're on an empty paragraph then just replace it with the block
|
||||
// toolbar. Otherwise insert the toolbar as an extra Node.
|
||||
if (
|
||||
!this.closestRootNode.text.trim() &&
|
||||
this.closestRootNode.type === 'paragraph'
|
||||
) {
|
||||
change.setNodeByKey(this.closestRootNode.key, {
|
||||
type: 'block-toolbar',
|
||||
isVoid: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const style = { top: `${this.top}px`, left: `${this.left}px` };
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Trigger active={this.active} style={style}>
|
||||
<PlusIcon onClick={this.handleClick} color={color.slate} />
|
||||
</Trigger>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Trigger = styled.div`
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
background-color: ${color.white};
|
||||
transition: opacity 150ms cubic-bezier(0.175, 0.885, 0.32, 1.275),
|
||||
transform 150ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
line-height: 0;
|
||||
margin-left: -10px;
|
||||
box-shadow: inset 0 0 0 2px ${color.slate};
|
||||
border-radius: 100%;
|
||||
transform: scale(0.9);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: ${color.slate};
|
||||
|
||||
svg {
|
||||
fill: ${color.white};
|
||||
}
|
||||
}
|
||||
|
||||
${({ active }) =>
|
||||
active &&
|
||||
`
|
||||
transform: scale(1);
|
||||
opacity: .9;
|
||||
`};
|
||||
`;
|
||||
@@ -1,22 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
type Props = {
|
||||
onClick?: ?Function,
|
||||
grow?: boolean,
|
||||
};
|
||||
|
||||
const ClickablePadding = (props: Props) => {
|
||||
return <Container grow={props.grow} onClick={props.onClick} />;
|
||||
};
|
||||
|
||||
const Container = styled.div`
|
||||
min-height: 50vh;
|
||||
padding-top: 50px;
|
||||
cursor: ${({ onClick }) => (onClick ? 'text' : 'default')};
|
||||
|
||||
${({ grow }) => grow && `flex-grow: 1;`};
|
||||
`;
|
||||
|
||||
export default ClickablePadding;
|
||||
@@ -1,52 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import type { SlateNodeProps } from '../types';
|
||||
import CopyButton from './CopyButton';
|
||||
import { color } from 'shared/styles/constants';
|
||||
|
||||
function getCopyText(node) {
|
||||
return node.nodes.reduce((memo, line) => `${memo}${line.text}\n`, '');
|
||||
}
|
||||
|
||||
export default function Code({
|
||||
children,
|
||||
node,
|
||||
readOnly,
|
||||
attributes,
|
||||
}: SlateNodeProps) {
|
||||
// TODO: There is a currently a bug in slate-prism that prevents code elements
|
||||
// with a language class name from formatting correctly on first load.
|
||||
// const language = node.data.get('language') || 'javascript';
|
||||
|
||||
return (
|
||||
<Container {...attributes} spellCheck={false}>
|
||||
{readOnly && <CopyButton text={getCopyText(node)} />}
|
||||
<code>{children}</code>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
background: ${color.smokeLight};
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${color.smokeDark};
|
||||
|
||||
code {
|
||||
display: block;
|
||||
overflow-x: scroll;
|
||||
padding: 0.5em 1em;
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
> span {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,163 +0,0 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import breakpoint from 'styled-components-breakpoint';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Editor } from 'slate-react';
|
||||
import { Block } from 'slate';
|
||||
import { List } from 'immutable';
|
||||
import { color } from 'shared/styles/constants';
|
||||
import headingToSlug from '../headingToSlug';
|
||||
|
||||
type Props = {
|
||||
editor: Editor,
|
||||
};
|
||||
|
||||
@observer
|
||||
class Contents extends Component {
|
||||
props: Props;
|
||||
@observable activeHeading: ?string;
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('scroll', this.updateActiveHeading);
|
||||
this.updateActiveHeading();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('scroll', this.updateActiveHeading);
|
||||
}
|
||||
|
||||
updateActiveHeading = () => {
|
||||
const elements = this.headingElements;
|
||||
if (!elements.length) return;
|
||||
|
||||
let activeHeading = elements[0].id;
|
||||
|
||||
for (const element of elements) {
|
||||
const bounds = element.getBoundingClientRect();
|
||||
if (bounds.top <= 0) activeHeading = element.id;
|
||||
}
|
||||
|
||||
this.activeHeading = activeHeading;
|
||||
};
|
||||
|
||||
get headingElements(): HTMLElement[] {
|
||||
const elements = [];
|
||||
const tagNames = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
|
||||
|
||||
for (const tagName of tagNames) {
|
||||
for (const ele of document.getElementsByTagName(tagName)) {
|
||||
elements.push(ele);
|
||||
}
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
get headings(): List<Block> {
|
||||
const { editor } = this.props;
|
||||
|
||||
return editor.value.document.nodes.filter((node: Block) => {
|
||||
if (!node.text) return false;
|
||||
return node.type.match(/^heading/);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { editor } = this.props;
|
||||
|
||||
// If there are one or less headings in the document no need for a minimap
|
||||
if (this.headings.size <= 1) return null;
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Sections>
|
||||
{this.headings.map(heading => {
|
||||
const slug = headingToSlug(editor.value.document, heading);
|
||||
const active = this.activeHeading === slug;
|
||||
|
||||
return (
|
||||
<ListItem type={heading.type} active={active} key={slug}>
|
||||
<Anchor href={`#${slug}`} active={active}>
|
||||
{heading.text}
|
||||
</Anchor>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</Sections>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: none;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 150px;
|
||||
z-index: 100;
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
|
||||
${breakpoint('tablet')`
|
||||
display: block;
|
||||
`};
|
||||
`;
|
||||
|
||||
const Anchor = styled.a`
|
||||
color: ${props => (props.active ? color.slateDark : color.slate)};
|
||||
font-weight: ${props => (props.active ? 500 : 400)};
|
||||
opacity: 0;
|
||||
transition: all 100ms ease-in-out;
|
||||
margin-right: -5px;
|
||||
padding: 2px 0;
|
||||
pointer-events: none;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:hover {
|
||||
color: ${color.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
const ListItem = styled.li`
|
||||
position: relative;
|
||||
margin-left: ${props => (props.type.match(/heading[12]/) ? '8px' : '16px')};
|
||||
text-align: right;
|
||||
color: ${color.slate};
|
||||
padding-right: 16px;
|
||||
white-space: nowrap;
|
||||
|
||||
&:after {
|
||||
color: ${props => (props.active ? color.slateDark : color.slate)};
|
||||
content: "${props => (props.type.match(/heading[12]/) ? '—' : '–')}";
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const Sections = styled.ol`
|
||||
margin: 0 0 0 -8px;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
font-size: 13px;
|
||||
width: 100px;
|
||||
transition-delay: 1s;
|
||||
transition: width 100ms ease-in-out;
|
||||
|
||||
&:hover {
|
||||
width: 300px;
|
||||
transition-delay: 0s;
|
||||
|
||||
${Anchor} {
|
||||
opacity: 1;
|
||||
margin-right: 0;
|
||||
background: ${color.white};
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Contents;
|
||||
@@ -1,51 +0,0 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import { color } from 'shared/styles/constants';
|
||||
import styled from 'styled-components';
|
||||
import CopyToClipboard from 'components/CopyToClipboard';
|
||||
|
||||
@observer
|
||||
class CopyButton extends Component {
|
||||
@observable copied: boolean = false;
|
||||
copiedTimeout: ?number;
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.copiedTimeout);
|
||||
}
|
||||
|
||||
handleCopy = () => {
|
||||
this.copied = true;
|
||||
this.copiedTimeout = setTimeout(() => (this.copied = false), 3000);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<StyledCopyToClipboard onCopy={this.handleCopy} {...this.props}>
|
||||
<span>{this.copied ? 'Copied!' : 'Copy'}</span>
|
||||
</StyledCopyToClipboard>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const StyledCopyToClipboard = styled(CopyToClipboard)`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
opacity: 0;
|
||||
transition: opacity 50ms ease-in-out;
|
||||
z-index: 1;
|
||||
font-size: 12px;
|
||||
background: ${color.smoke};
|
||||
border-radius: 0 2px 0 2px;
|
||||
padding: 1px 6px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: ${color.smokeDark};
|
||||
}
|
||||
`;
|
||||
|
||||
export default CopyButton;
|
||||
@@ -1,97 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { Document } from 'slate';
|
||||
import type { SlateNodeProps } from '../types';
|
||||
import styled from 'styled-components';
|
||||
import headingToSlug from '../headingToSlug';
|
||||
import Placeholder from './Placeholder';
|
||||
|
||||
type Props = SlateNodeProps & {
|
||||
component: string,
|
||||
className: string,
|
||||
placeholder: string,
|
||||
};
|
||||
|
||||
function Heading(props: Props) {
|
||||
const {
|
||||
parent,
|
||||
placeholder,
|
||||
node,
|
||||
editor,
|
||||
readOnly,
|
||||
children,
|
||||
component = 'h1',
|
||||
className,
|
||||
attributes,
|
||||
} = props;
|
||||
const parentIsDocument = parent instanceof Document;
|
||||
const firstHeading = parentIsDocument && parent.nodes.first() === node;
|
||||
const showPlaceholder = placeholder && firstHeading && !node.text;
|
||||
const slugish = headingToSlug(editor.value.document, node);
|
||||
const showHash = readOnly && !!slugish;
|
||||
const Component = component;
|
||||
const emoji = editor.props.emoji || '';
|
||||
const title = node.text.trim();
|
||||
const startsWithEmojiAndSpace =
|
||||
emoji && title.match(new RegExp(`^${emoji}\\s`));
|
||||
|
||||
return (
|
||||
<Component {...attributes} id={slugish} className={className}>
|
||||
<Wrapper hasEmoji={startsWithEmojiAndSpace}>{children}</Wrapper>
|
||||
{showPlaceholder && (
|
||||
<Placeholder contentEditable={false}>
|
||||
{editor.props.placeholder}
|
||||
</Placeholder>
|
||||
)}
|
||||
{showHash && (
|
||||
<Anchor name={slugish} href={`#${slugish}`}>
|
||||
#
|
||||
</Anchor>
|
||||
)}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: inline;
|
||||
margin-left: ${(props: SlateNodeProps) => (props.hasEmoji ? '-1.2em' : 0)};
|
||||
`;
|
||||
|
||||
const Anchor = styled.a`
|
||||
visibility: hidden;
|
||||
padding-left: 0.25em;
|
||||
color: #dedede;
|
||||
|
||||
&:hover {
|
||||
color: #cdcdcd;
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledHeading = styled(Heading)`
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
${Anchor} {
|
||||
visibility: visible;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const Heading1 = (props: SlateNodeProps) => (
|
||||
<StyledHeading component="h1" {...props} />
|
||||
);
|
||||
export const Heading2 = (props: SlateNodeProps) => (
|
||||
<StyledHeading component="h2" {...props} />
|
||||
);
|
||||
export const Heading3 = (props: SlateNodeProps) => (
|
||||
<StyledHeading component="h3" {...props} />
|
||||
);
|
||||
export const Heading4 = (props: SlateNodeProps) => (
|
||||
<StyledHeading component="h4" {...props} />
|
||||
);
|
||||
export const Heading5 = (props: SlateNodeProps) => (
|
||||
<StyledHeading component="h5" {...props} />
|
||||
);
|
||||
export const Heading6 = (props: SlateNodeProps) => (
|
||||
<StyledHeading component="h6" {...props} />
|
||||
);
|
||||
@@ -1,22 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import type { SlateNodeProps } from '../types';
|
||||
import { color } from 'shared/styles/constants';
|
||||
|
||||
function HorizontalRule(props: SlateNodeProps) {
|
||||
const { editor, node, attributes } = props;
|
||||
const active =
|
||||
editor.value.isFocused && editor.value.selection.hasEdgeIn(node);
|
||||
return <StyledHr active={active} {...attributes} />;
|
||||
}
|
||||
|
||||
const StyledHr = styled.hr`
|
||||
padding-top: 0.75em;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
border-bottom: 1px solid
|
||||
${props => (props.active ? color.slate : color.slateLight)};
|
||||
`;
|
||||
|
||||
export default HorizontalRule;
|
||||
@@ -1,104 +0,0 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import ImageZoom from 'react-medium-image-zoom';
|
||||
import styled from 'styled-components';
|
||||
import type { SlateNodeProps } from '../types';
|
||||
import { color } from 'shared/styles/constants';
|
||||
|
||||
class Image extends Component {
|
||||
props: SlateNodeProps;
|
||||
|
||||
handleChange = (ev: SyntheticInputEvent) => {
|
||||
const alt = ev.target.value;
|
||||
const { editor, node } = this.props;
|
||||
const data = node.data.toObject();
|
||||
|
||||
editor.change(change =>
|
||||
change.setNodeByKey(node.key, { data: { ...data, alt } })
|
||||
);
|
||||
};
|
||||
|
||||
handleClick = (ev: SyntheticInputEvent) => {
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { attributes, editor, node, readOnly } = this.props;
|
||||
const loading = node.data.get('loading');
|
||||
const caption = node.data.get('alt');
|
||||
const src = node.data.get('src');
|
||||
const active =
|
||||
editor.value.isFocused && editor.value.selection.hasEdgeIn(node);
|
||||
const showCaption = !readOnly || caption;
|
||||
|
||||
return (
|
||||
<CenteredImage>
|
||||
{!readOnly ? (
|
||||
<StyledImg
|
||||
{...attributes}
|
||||
src={src}
|
||||
alt={caption}
|
||||
active={active}
|
||||
loading={loading}
|
||||
/>
|
||||
) : (
|
||||
<ImageZoom
|
||||
image={{
|
||||
src,
|
||||
alt: caption,
|
||||
style: {
|
||||
maxWidth: '100%',
|
||||
},
|
||||
...attributes,
|
||||
}}
|
||||
shouldRespectMaxDimension
|
||||
/>
|
||||
)}
|
||||
{showCaption && (
|
||||
<Caption
|
||||
type="text"
|
||||
placeholder="Write a caption"
|
||||
onChange={this.handleChange}
|
||||
onClick={this.handleClick}
|
||||
defaultValue={caption}
|
||||
contentEditable={false}
|
||||
disabled={readOnly}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
)}
|
||||
</CenteredImage>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const StyledImg = styled.img`
|
||||
max-width: 100%;
|
||||
box-shadow: ${props => (props.active ? `0 0 0 2px ${color.slate}` : '0')};
|
||||
border-radius: ${props => (props.active ? `2px` : '0')};
|
||||
opacity: ${props => (props.loading ? 0.5 : 1)};
|
||||
`;
|
||||
|
||||
const CenteredImage = styled.span`
|
||||
display: block;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const Caption = styled.input`
|
||||
border: 0;
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
color: ${color.slate};
|
||||
padding: 2px 0;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
background: none;
|
||||
|
||||
&::placeholder {
|
||||
color: ${color.slate};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Image;
|
||||
@@ -1,14 +0,0 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'shared/styles/constants';
|
||||
|
||||
const InlineCode = styled.code.attrs({
|
||||
spellCheck: false,
|
||||
})`
|
||||
padding: 0.25em;
|
||||
background: ${color.smoke};
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${color.smokeDark};
|
||||
`;
|
||||
|
||||
export default InlineCode;
|
||||
@@ -1,51 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { Link as InternalLink } from 'react-router-dom';
|
||||
import type { SlateNodeProps } from '../types';
|
||||
|
||||
function getPathFromUrl(href: string) {
|
||||
if (href[0] === '/') return href;
|
||||
|
||||
try {
|
||||
const parsed = new URL(href);
|
||||
return parsed.pathname;
|
||||
} catch (err) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function isInternalUrl(href: string) {
|
||||
if (href[0] === '/') return true;
|
||||
|
||||
try {
|
||||
const outline = new URL(BASE_URL);
|
||||
const parsed = new URL(href);
|
||||
return parsed.hostname === outline.hostname;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default function Link({
|
||||
attributes,
|
||||
node,
|
||||
children,
|
||||
readOnly,
|
||||
}: SlateNodeProps) {
|
||||
const href = node.data.get('href');
|
||||
const path = getPathFromUrl(href);
|
||||
|
||||
if (isInternalUrl(href) && readOnly) {
|
||||
return (
|
||||
<InternalLink {...attributes} to={path}>
|
||||
{children}
|
||||
</InternalLink>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<a {...attributes} href={readOnly ? href : undefined} target="_blank">
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import type { SlateNodeProps } from '../types';
|
||||
import TodoItem from './TodoItem';
|
||||
|
||||
export default function ListItem({
|
||||
children,
|
||||
node,
|
||||
attributes,
|
||||
...props
|
||||
}: SlateNodeProps) {
|
||||
const checked = node.data.get('checked');
|
||||
|
||||
if (checked !== undefined) {
|
||||
return (
|
||||
<TodoItem node={node} attributes={attributes} {...props}>
|
||||
{children}
|
||||
</TodoItem>
|
||||
);
|
||||
}
|
||||
return <li {...attributes}>{children}</li>;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { Document } from 'slate';
|
||||
import type { SlateNodeProps } from '../types';
|
||||
import Placeholder from './Placeholder';
|
||||
|
||||
export default function Link({
|
||||
attributes,
|
||||
editor,
|
||||
node,
|
||||
parent,
|
||||
children,
|
||||
readOnly,
|
||||
}: SlateNodeProps) {
|
||||
const parentIsDocument = parent instanceof Document;
|
||||
const firstParagraph = parent && parent.nodes.get(1) === node;
|
||||
const lastParagraph = parent && parent.nodes.last() === node;
|
||||
const showPlaceholder =
|
||||
!readOnly &&
|
||||
parentIsDocument &&
|
||||
firstParagraph &&
|
||||
lastParagraph &&
|
||||
!node.text;
|
||||
|
||||
return (
|
||||
<p {...attributes}>
|
||||
{children}
|
||||
{showPlaceholder && (
|
||||
<Placeholder contentEditable={false}>
|
||||
{editor.props.bodyPlaceholder}
|
||||
</Placeholder>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
|
||||
export default styled.span`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
color: #b1becc;
|
||||
`;
|
||||
@@ -1,51 +0,0 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'shared/styles/constants';
|
||||
import type { SlateNodeProps } from '../types';
|
||||
|
||||
export default class TodoItem extends Component {
|
||||
props: SlateNodeProps;
|
||||
|
||||
handleChange = (ev: SyntheticInputEvent) => {
|
||||
const checked = ev.target.checked;
|
||||
const { editor, node } = this.props;
|
||||
editor.change(change =>
|
||||
change.setNodeByKey(node.key, { data: { checked } })
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { children, node, attributes, readOnly } = this.props;
|
||||
const checked = node.data.get('checked');
|
||||
|
||||
return (
|
||||
<ListItem checked={checked} {...attributes}>
|
||||
<Input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={this.handleChange}
|
||||
disabled={readOnly}
|
||||
contentEditable={false}
|
||||
/>
|
||||
{children}
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ListItem = styled.li`
|
||||
padding-left: 1.4em;
|
||||
position: relative;
|
||||
|
||||
> p > span {
|
||||
color: ${props => (props.checked ? color.slateDark : 'inherit')};
|
||||
text-decoration: ${props => (props.checked ? 'line-through' : 'none')};
|
||||
}
|
||||
`;
|
||||
|
||||
const Input = styled.input`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0.4em;
|
||||
`;
|
||||
@@ -1,13 +0,0 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
|
||||
const TodoList = styled.ul`
|
||||
list-style: none;
|
||||
padding: 0 !important;
|
||||
|
||||
ul {
|
||||
padding-left: 1em;
|
||||
}
|
||||
`;
|
||||
|
||||
export default TodoList;
|
||||
@@ -1,220 +0,0 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import keydown from 'react-keydown';
|
||||
import styled from 'styled-components';
|
||||
import getDataTransferFiles from 'utils/getDataTransferFiles';
|
||||
import Heading1Icon from 'components/Icon/Heading1Icon';
|
||||
import Heading2Icon from 'components/Icon/Heading2Icon';
|
||||
import BlockQuoteIcon from 'components/Icon/BlockQuoteIcon';
|
||||
import ImageIcon from 'components/Icon/ImageIcon';
|
||||
import CodeIcon from 'components/Icon/CodeIcon';
|
||||
import BulletedListIcon from 'components/Icon/BulletedListIcon';
|
||||
import OrderedListIcon from 'components/Icon/OrderedListIcon';
|
||||
import HorizontalRuleIcon from 'components/Icon/HorizontalRuleIcon';
|
||||
import TodoListIcon from 'components/Icon/TodoListIcon';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import ToolbarButton from './components/ToolbarButton';
|
||||
import type { SlateNodeProps } from '../../types';
|
||||
import { color } from 'shared/styles/constants';
|
||||
import { fadeIn } from 'shared/styles/animations';
|
||||
import { splitAndInsertBlock } from '../../changes';
|
||||
|
||||
type Props = SlateNodeProps & {
|
||||
onInsertImage: *,
|
||||
};
|
||||
|
||||
type Options = {
|
||||
type: string | Object,
|
||||
wrapper?: string | Object,
|
||||
};
|
||||
|
||||
class BlockToolbar extends Component {
|
||||
props: Props;
|
||||
bar: HTMLDivElement;
|
||||
file: HTMLInputElement;
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('click', this.handleOutsideMouseClick);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('click', this.handleOutsideMouseClick);
|
||||
}
|
||||
|
||||
handleOutsideMouseClick = (ev: SyntheticMouseEvent) => {
|
||||
const element = findDOMNode(this.bar);
|
||||
|
||||
if (
|
||||
!element ||
|
||||
(ev.target instanceof Node && element.contains(ev.target)) ||
|
||||
(ev.button && ev.button !== 0)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.removeSelf(ev);
|
||||
};
|
||||
|
||||
@keydown('esc')
|
||||
removeSelf(ev: SyntheticEvent) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
this.props.editor.change(change =>
|
||||
change.setNodeByKey(this.props.node.key, {
|
||||
type: 'paragraph',
|
||||
text: '',
|
||||
isVoid: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
insertBlock = (
|
||||
options: Options,
|
||||
cursorPosition: 'before' | 'on' | 'after' = 'on'
|
||||
) => {
|
||||
const { editor } = this.props;
|
||||
|
||||
editor.change(change => {
|
||||
change
|
||||
.collapseToEndOf(this.props.node)
|
||||
.call(splitAndInsertBlock, options)
|
||||
.removeNodeByKey(this.props.node.key)
|
||||
.collapseToEnd();
|
||||
|
||||
if (cursorPosition === 'before') change.collapseToStartOfPreviousBlock();
|
||||
if (cursorPosition === 'after') change.collapseToStartOfNextBlock();
|
||||
return change.focus();
|
||||
});
|
||||
};
|
||||
|
||||
handleClickBlock = (ev: SyntheticEvent, type: string) => {
|
||||
ev.preventDefault();
|
||||
|
||||
switch (type) {
|
||||
case 'heading1':
|
||||
case 'heading2':
|
||||
case 'block-quote':
|
||||
case 'code':
|
||||
return this.insertBlock({ type });
|
||||
case 'horizontal-rule':
|
||||
return this.insertBlock(
|
||||
{
|
||||
type: { type: 'horizontal-rule', isVoid: true },
|
||||
},
|
||||
'after'
|
||||
);
|
||||
case 'bulleted-list':
|
||||
return this.insertBlock({
|
||||
type: 'list-item',
|
||||
wrapper: 'bulleted-list',
|
||||
});
|
||||
case 'ordered-list':
|
||||
return this.insertBlock({
|
||||
type: 'list-item',
|
||||
wrapper: 'ordered-list',
|
||||
});
|
||||
case 'todo-list':
|
||||
return this.insertBlock({
|
||||
type: { type: 'list-item', data: { checked: false } },
|
||||
wrapper: 'todo-list',
|
||||
});
|
||||
case 'image':
|
||||
return this.onPickImage();
|
||||
default:
|
||||
}
|
||||
};
|
||||
|
||||
onPickImage = () => {
|
||||
// simulate a click on the file upload input element
|
||||
this.file.click();
|
||||
};
|
||||
|
||||
onImagePicked = async (ev: SyntheticEvent) => {
|
||||
const files = getDataTransferFiles(ev);
|
||||
for (const file of files) {
|
||||
await this.props.onInsertImage(file);
|
||||
}
|
||||
};
|
||||
|
||||
renderBlockButton = (type: string, IconClass: Function) => {
|
||||
return (
|
||||
<ToolbarButton onMouseDown={ev => this.handleClickBlock(ev, type)}>
|
||||
<IconClass color={color.text} />
|
||||
</ToolbarButton>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { editor, attributes, node } = this.props;
|
||||
const active =
|
||||
editor.value.isFocused && editor.value.selection.hasEdgeIn(node);
|
||||
|
||||
return (
|
||||
<Bar active={active} {...attributes} ref={ref => (this.bar = ref)}>
|
||||
<HiddenInput
|
||||
type="file"
|
||||
innerRef={ref => (this.file = ref)}
|
||||
onChange={this.onImagePicked}
|
||||
accept="image/*"
|
||||
/>
|
||||
{this.renderBlockButton('heading1', Heading1Icon)}
|
||||
{this.renderBlockButton('heading2', Heading2Icon)}
|
||||
<Separator />
|
||||
{this.renderBlockButton('bulleted-list', BulletedListIcon)}
|
||||
{this.renderBlockButton('ordered-list', OrderedListIcon)}
|
||||
{this.renderBlockButton('todo-list', TodoListIcon)}
|
||||
<Separator />
|
||||
{this.renderBlockButton('block-quote', BlockQuoteIcon)}
|
||||
{this.renderBlockButton('code', CodeIcon)}
|
||||
{this.renderBlockButton('horizontal-rule', HorizontalRuleIcon)}
|
||||
{this.renderBlockButton('image', ImageIcon)}
|
||||
</Bar>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Separator = styled.div`
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
background: ${color.smokeDark};
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
`;
|
||||
|
||||
const Bar = styled(Flex)`
|
||||
z-index: 100;
|
||||
animation: ${fadeIn} 150ms ease-in-out;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
background: ${color.smoke};
|
||||
height: 44px;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
background: ${color.smoke};
|
||||
}
|
||||
|
||||
&:after {
|
||||
left: auto;
|
||||
right: -100%;
|
||||
}
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const HiddenInput = styled.input`
|
||||
position: absolute;
|
||||
top: -100px;
|
||||
left: -100px;
|
||||
visibility: hidden;
|
||||
`;
|
||||
|
||||
export default BlockToolbar;
|
||||
@@ -1,186 +0,0 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Portal } from 'react-portal';
|
||||
import { Editor, findDOMNode } from 'slate-react';
|
||||
import { Node, Value } from 'slate';
|
||||
import styled from 'styled-components';
|
||||
import FormattingToolbar from './components/FormattingToolbar';
|
||||
import LinkToolbar from './components/LinkToolbar';
|
||||
|
||||
function getLinkInSelection(value): any {
|
||||
try {
|
||||
const selectedLinks = value.document
|
||||
.getInlinesAtRange(value.selection)
|
||||
.filter(node => node.type === 'link');
|
||||
|
||||
if (selectedLinks.size) {
|
||||
const link = selectedLinks.first();
|
||||
if (value.selection.hasEdgeIn(link)) return link;
|
||||
}
|
||||
} catch (err) {
|
||||
// It's okay.
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
export default class Toolbar extends Component {
|
||||
@observable active: boolean = false;
|
||||
@observable link: ?Node;
|
||||
@observable top: string = '';
|
||||
@observable left: string = '';
|
||||
@observable mouseDown: boolean = false;
|
||||
|
||||
props: {
|
||||
editor: Editor,
|
||||
value: Value,
|
||||
};
|
||||
|
||||
menu: HTMLElement;
|
||||
|
||||
componentDidMount = () => {
|
||||
this.update();
|
||||
window.addEventListener('mousedown', this.handleMouseDown);
|
||||
window.addEventListener('mouseup', this.handleMouseUp);
|
||||
};
|
||||
|
||||
componentWillUnmount = () => {
|
||||
window.removeEventListener('mousedown', this.handleMouseDown);
|
||||
window.removeEventListener('mouseup', this.handleMouseUp);
|
||||
};
|
||||
|
||||
componentDidUpdate = () => {
|
||||
this.update();
|
||||
};
|
||||
|
||||
hideLinkToolbar = () => {
|
||||
this.link = undefined;
|
||||
};
|
||||
|
||||
handleMouseDown = (e: SyntheticMouseEvent) => {
|
||||
this.mouseDown = true;
|
||||
};
|
||||
|
||||
handleMouseUp = (e: SyntheticMouseEvent) => {
|
||||
this.mouseDown = false;
|
||||
this.update();
|
||||
};
|
||||
|
||||
showLinkToolbar = (ev: SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
const link = getLinkInSelection(this.props.value);
|
||||
this.link = link;
|
||||
};
|
||||
|
||||
update = () => {
|
||||
const { value } = this.props;
|
||||
const link = getLinkInSelection(value);
|
||||
|
||||
if (value.isBlurred || (value.isCollapsed && !link)) {
|
||||
if (this.active && !this.link) {
|
||||
this.active = false;
|
||||
this.link = undefined;
|
||||
this.top = '';
|
||||
this.left = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// don't display toolbar for document title
|
||||
const firstNode = value.document.nodes.first();
|
||||
if (firstNode === value.startBlock) return;
|
||||
|
||||
// don't display toolbar for code blocks, code-lines inline code.
|
||||
if (value.startBlock.type.match(/code/)) return;
|
||||
|
||||
// don't show until user has released pointing device button
|
||||
if (this.mouseDown) return;
|
||||
|
||||
this.active = true;
|
||||
this.link = this.link || link;
|
||||
|
||||
const padding = 16;
|
||||
const selection = window.getSelection();
|
||||
let rect;
|
||||
|
||||
if (link) {
|
||||
rect = findDOMNode(link).getBoundingClientRect();
|
||||
} else if (selection.rangeCount > 0) {
|
||||
const range = selection.getRangeAt(0);
|
||||
rect = range.getBoundingClientRect();
|
||||
}
|
||||
|
||||
if (!rect || (rect.top === 0 && rect.left === 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const left =
|
||||
rect.left + window.scrollX - this.menu.offsetWidth / 2 + rect.width / 2;
|
||||
this.top = `${Math.round(
|
||||
rect.top + window.scrollY - this.menu.offsetHeight
|
||||
)}px`;
|
||||
this.left = `${Math.round(Math.max(padding, left))}px`;
|
||||
};
|
||||
|
||||
setRef = (ref: HTMLElement) => {
|
||||
this.menu = ref;
|
||||
};
|
||||
|
||||
render() {
|
||||
const style = {
|
||||
top: this.top,
|
||||
left: this.left,
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Menu active={this.active} innerRef={this.setRef} style={style}>
|
||||
{this.link ? (
|
||||
<LinkToolbar
|
||||
{...this.props}
|
||||
link={this.link}
|
||||
onBlur={this.hideLinkToolbar}
|
||||
/>
|
||||
) : (
|
||||
<FormattingToolbar
|
||||
onCreateLink={this.showLinkToolbar}
|
||||
{...this.props}
|
||||
/>
|
||||
)}
|
||||
</Menu>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Menu = styled.div`
|
||||
padding: 8px 16px;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: -10000px;
|
||||
left: -10000px;
|
||||
opacity: 0;
|
||||
background-color: #2f3336;
|
||||
border-radius: 4px;
|
||||
transform: scale(0.95);
|
||||
transition: opacity 150ms cubic-bezier(0.175, 0.885, 0.32, 1.275),
|
||||
transform 150ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
transition-delay: 250ms;
|
||||
line-height: 0;
|
||||
height: 40px;
|
||||
min-width: 300px;
|
||||
|
||||
${({ active }) =>
|
||||
active &&
|
||||
`
|
||||
transform: translateY(-6px) scale(1);
|
||||
opacity: 1;
|
||||
`};
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
@@ -1,51 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { fontWeight, color } from 'shared/styles/constants';
|
||||
import Document from 'models/Document';
|
||||
import NextIcon from 'components/Icon/NextIcon';
|
||||
|
||||
type Props = {
|
||||
innerRef?: Function,
|
||||
onClick: SyntheticEvent => void,
|
||||
document: Document,
|
||||
};
|
||||
|
||||
function DocumentResult({ document, ...rest }: Props) {
|
||||
return (
|
||||
<ListItem {...rest} href="">
|
||||
<i>
|
||||
<NextIcon light />
|
||||
</i>
|
||||
{document.title}
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
|
||||
const ListItem = styled.a`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
padding: 6px 8px 6px 0;
|
||||
color: ${color.white};
|
||||
font-size: 15px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
i {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
font-weight: ${fontWeight.medium};
|
||||
outline: none;
|
||||
|
||||
i {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default DocumentResult;
|
||||
@@ -1,115 +0,0 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Editor } from 'slate-react';
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
import BoldIcon from 'components/Icon/BoldIcon';
|
||||
import CodeIcon from 'components/Icon/CodeIcon';
|
||||
import Heading1Icon from 'components/Icon/Heading1Icon';
|
||||
import Heading2Icon from 'components/Icon/Heading2Icon';
|
||||
import ItalicIcon from 'components/Icon/ItalicIcon';
|
||||
import BlockQuoteIcon from 'components/Icon/BlockQuoteIcon';
|
||||
import LinkIcon from 'components/Icon/LinkIcon';
|
||||
import StrikethroughIcon from 'components/Icon/StrikethroughIcon';
|
||||
|
||||
class FormattingToolbar extends Component {
|
||||
props: {
|
||||
editor: Editor,
|
||||
onCreateLink: SyntheticEvent => void,
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the current selection has a mark with `type` in it.
|
||||
*
|
||||
* @param {String} type
|
||||
* @return {Boolean}
|
||||
*/
|
||||
hasMark = (type: string) => {
|
||||
return this.props.editor.value.marks.some(mark => mark.type === type);
|
||||
};
|
||||
|
||||
isBlock = (type: string) => {
|
||||
const startBlock = this.props.editor.value.startBlock;
|
||||
return startBlock && startBlock.type === type;
|
||||
};
|
||||
|
||||
/**
|
||||
* When a mark button is clicked, toggle the current mark.
|
||||
*
|
||||
* @param {Event} ev
|
||||
* @param {String} type
|
||||
*/
|
||||
onClickMark = (ev: SyntheticEvent, type: string) => {
|
||||
ev.preventDefault();
|
||||
this.props.editor.change(change => change.toggleMark(type));
|
||||
};
|
||||
|
||||
onClickBlock = (ev: SyntheticEvent, type: string) => {
|
||||
ev.preventDefault();
|
||||
this.props.editor.change(change => change.setBlock(type));
|
||||
};
|
||||
|
||||
handleCreateLink = (ev: SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
const data = { href: '' };
|
||||
this.props.editor.change(change => {
|
||||
change.wrapInline({ type: 'link', data });
|
||||
this.props.onCreateLink(ev);
|
||||
});
|
||||
};
|
||||
|
||||
renderMarkButton = (type: string, IconClass: Function) => {
|
||||
const isActive = this.hasMark(type);
|
||||
const onMouseDown = ev => this.onClickMark(ev, type);
|
||||
|
||||
return (
|
||||
<ToolbarButton onMouseDown={onMouseDown} active={isActive}>
|
||||
<IconClass light />
|
||||
</ToolbarButton>
|
||||
);
|
||||
};
|
||||
|
||||
renderBlockButton = (type: string, IconClass: Function) => {
|
||||
const isActive = this.isBlock(type);
|
||||
const onMouseDown = ev =>
|
||||
this.onClickBlock(ev, isActive ? 'paragraph' : type);
|
||||
|
||||
return (
|
||||
<ToolbarButton onMouseDown={onMouseDown} active={isActive}>
|
||||
<IconClass light />
|
||||
</ToolbarButton>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<span>
|
||||
{this.renderMarkButton('bold', BoldIcon)}
|
||||
{this.renderMarkButton('italic', ItalicIcon)}
|
||||
{this.renderMarkButton('deleted', StrikethroughIcon)}
|
||||
{this.renderMarkButton('code', CodeIcon)}
|
||||
<Separator />
|
||||
{this.renderBlockButton('heading1', Heading1Icon)}
|
||||
{this.renderBlockButton('heading2', Heading2Icon)}
|
||||
{this.renderBlockButton('block-quote', BlockQuoteIcon)}
|
||||
<Separator />
|
||||
<ToolbarButton onMouseDown={this.handleCreateLink}>
|
||||
<LinkIcon light />
|
||||
</ToolbarButton>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Separator = styled.div`
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
background: #fff;
|
||||
opacity: 0.2;
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
`;
|
||||
|
||||
export default FormattingToolbar;
|
||||
@@ -1,231 +0,0 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import { observable, action } from 'mobx';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { Node } from 'slate';
|
||||
import { Editor } from 'slate-react';
|
||||
import styled from 'styled-components';
|
||||
import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
import DocumentResult from './DocumentResult';
|
||||
import DocumentsStore from 'stores/DocumentsStore';
|
||||
import keydown from 'react-keydown';
|
||||
import CloseIcon from 'components/Icon/CloseIcon';
|
||||
import OpenIcon from 'components/Icon/OpenIcon';
|
||||
import TrashIcon from 'components/Icon/TrashIcon';
|
||||
import Flex from 'shared/components/Flex';
|
||||
|
||||
@keydown
|
||||
@observer
|
||||
class LinkToolbar extends Component {
|
||||
wrapper: HTMLSpanElement;
|
||||
input: HTMLInputElement;
|
||||
firstDocument: HTMLElement;
|
||||
|
||||
props: {
|
||||
editor: Editor,
|
||||
link: Node,
|
||||
documents: DocumentsStore,
|
||||
onBlur: () => *,
|
||||
};
|
||||
|
||||
originalValue: string = '';
|
||||
@observable isEditing: boolean = false;
|
||||
@observable isFetching: boolean = false;
|
||||
@observable resultIds: string[] = [];
|
||||
@observable searchTerm: ?string = null;
|
||||
|
||||
componentDidMount() {
|
||||
this.originalValue = this.props.link.data.get('href');
|
||||
this.isEditing = !!this.originalValue;
|
||||
|
||||
setImmediate(() =>
|
||||
window.addEventListener('click', this.handleOutsideMouseClick)
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('click', this.handleOutsideMouseClick);
|
||||
}
|
||||
|
||||
handleOutsideMouseClick = (ev: SyntheticMouseEvent) => {
|
||||
const element = findDOMNode(this.wrapper);
|
||||
|
||||
if (
|
||||
!element ||
|
||||
(ev.target instanceof HTMLElement && element.contains(ev.target)) ||
|
||||
(ev.button && ev.button !== 0)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
ev.preventDefault();
|
||||
this.save(this.input.value);
|
||||
};
|
||||
|
||||
@action
|
||||
search = async () => {
|
||||
this.isFetching = true;
|
||||
|
||||
if (this.searchTerm) {
|
||||
try {
|
||||
this.resultIds = await this.props.documents.search(this.searchTerm);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
} else {
|
||||
this.resultIds = [];
|
||||
}
|
||||
|
||||
this.isFetching = false;
|
||||
};
|
||||
|
||||
selectDocument = (ev, document) => {
|
||||
ev.preventDefault();
|
||||
this.save(document.url);
|
||||
};
|
||||
|
||||
onKeyDown = (ev: SyntheticKeyboardEvent & SyntheticInputEvent) => {
|
||||
switch (ev.keyCode) {
|
||||
case 13: // enter
|
||||
ev.preventDefault();
|
||||
return this.save(ev.target.value);
|
||||
case 27: // escape
|
||||
return this.save(this.originalValue);
|
||||
case 40: // down
|
||||
ev.preventDefault();
|
||||
if (this.firstDocument) {
|
||||
const element = findDOMNode(this.firstDocument);
|
||||
if (element instanceof HTMLElement) element.focus();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
};
|
||||
|
||||
onChange = (ev: SyntheticKeyboardEvent & SyntheticInputEvent) => {
|
||||
try {
|
||||
new URL(ev.target.value);
|
||||
} catch (err) {
|
||||
// this is not a valid url, show search suggestions
|
||||
this.searchTerm = ev.target.value;
|
||||
this.search();
|
||||
return;
|
||||
}
|
||||
this.resultIds = [];
|
||||
};
|
||||
|
||||
removeLink = () => {
|
||||
this.save('');
|
||||
};
|
||||
|
||||
openLink = () => {
|
||||
const href = this.props.link.data.get('href');
|
||||
window.open(href, '_blank');
|
||||
};
|
||||
|
||||
save = (href: string) => {
|
||||
const { editor, link } = this.props;
|
||||
href = href.trim();
|
||||
|
||||
editor.change(change => {
|
||||
if (href) {
|
||||
change.setNodeByKey(link.key, { type: 'link', data: { href } });
|
||||
} else if (link) {
|
||||
change.unwrapInlineByKey(link.key);
|
||||
}
|
||||
change.deselect();
|
||||
this.props.onBlur();
|
||||
});
|
||||
};
|
||||
|
||||
setFirstDocumentRef = ref => {
|
||||
this.firstDocument = ref;
|
||||
};
|
||||
|
||||
render() {
|
||||
const href = this.props.link.data.get('href');
|
||||
const hasResults = this.resultIds.length > 0;
|
||||
|
||||
return (
|
||||
<span ref={ref => (this.wrapper = ref)}>
|
||||
<LinkEditor>
|
||||
<Input
|
||||
innerRef={ref => (this.input = ref)}
|
||||
defaultValue={href}
|
||||
placeholder="Search or paste a link…"
|
||||
onKeyDown={this.onKeyDown}
|
||||
onChange={this.onChange}
|
||||
autoFocus={href === ''}
|
||||
/>
|
||||
{this.isEditing && (
|
||||
<ToolbarButton onMouseDown={this.openLink}>
|
||||
<OpenIcon light />
|
||||
</ToolbarButton>
|
||||
)}
|
||||
<ToolbarButton onMouseDown={this.removeLink}>
|
||||
{this.isEditing ? <TrashIcon light /> : <CloseIcon light />}
|
||||
</ToolbarButton>
|
||||
</LinkEditor>
|
||||
{hasResults && (
|
||||
<SearchResults>
|
||||
<ArrowKeyNavigation
|
||||
mode={ArrowKeyNavigation.mode.VERTICAL}
|
||||
defaultActiveChildIndex={0}
|
||||
>
|
||||
{this.resultIds.map((id, index) => {
|
||||
const document = this.props.documents.getById(id);
|
||||
if (!document) return null;
|
||||
|
||||
return (
|
||||
<DocumentResult
|
||||
innerRef={ref =>
|
||||
index === 0 && this.setFirstDocumentRef(ref)
|
||||
}
|
||||
document={document}
|
||||
key={document.id}
|
||||
onClick={ev => this.selectDocument(ev, document)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ArrowKeyNavigation>
|
||||
</SearchResults>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const SearchResults = styled.div`
|
||||
background: #2f3336;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
left: 0;
|
||||
padding: 8px;
|
||||
margin-top: -3px;
|
||||
margin-bottom: 0;
|
||||
border-radius: 0 0 4px 4px;
|
||||
`;
|
||||
|
||||
const LinkEditor = styled(Flex)`
|
||||
margin-left: -8px;
|
||||
margin-right: -8px;
|
||||
`;
|
||||
|
||||
const Input = styled.input`
|
||||
font-size: 15px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 2px;
|
||||
padding: 4px 8px;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
color: #fff;
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
export default withRouter(inject('documents')(LinkToolbar));
|
||||
@@ -1,26 +0,0 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
|
||||
export default styled.button`
|
||||
display: inline-block;
|
||||
flex: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
border: none;
|
||||
background: none;
|
||||
transition: opacity 100ms ease-in-out;
|
||||
padding: 0;
|
||||
opacity: 0.7;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
${({ active }) => active && 'opacity: 1;'};
|
||||
`;
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import Toolbar from './Toolbar';
|
||||
export default Toolbar;
|
||||
@@ -1,25 +0,0 @@
|
||||
// @flow
|
||||
import { escape } from 'lodash';
|
||||
import { Document, Block, Node } from 'slate';
|
||||
import slug from 'slug';
|
||||
|
||||
// finds the index of this heading in the document compared to other headings
|
||||
// with the same slugified text
|
||||
function indexOfType(document, heading) {
|
||||
const slugified = escape(slug(heading.text));
|
||||
const headings = document.nodes.filter((node: Block) => {
|
||||
if (!node.text) return false;
|
||||
return node.type.match(/^heading/) && slugified === escape(slug(node.text));
|
||||
});
|
||||
|
||||
return headings.indexOf(heading);
|
||||
}
|
||||
|
||||
// calculates a unique slug for this heading based on it's text and position
|
||||
// in the document that is as stable as possible
|
||||
export default function headingToSlug(document: Document, node: Node) {
|
||||
const slugified = escape(slug(node.text));
|
||||
const index = indexOfType(document, node);
|
||||
if (index === 0) return slugified;
|
||||
return `${slugified}-${index}`;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import Editor from './Editor';
|
||||
export default Editor;
|
||||
@@ -1,27 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import InlineCode from './components/InlineCode';
|
||||
import { Mark } from 'slate';
|
||||
|
||||
type Props = {
|
||||
children: React$Element<*>,
|
||||
mark: Mark,
|
||||
};
|
||||
|
||||
export default function renderMark(props: Props) {
|
||||
switch (props.mark.type) {
|
||||
case 'bold':
|
||||
return <strong>{props.children}</strong>;
|
||||
case 'code':
|
||||
return <InlineCode>{props.children}</InlineCode>;
|
||||
case 'italic':
|
||||
return <em>{props.children}</em>;
|
||||
case 'underlined':
|
||||
return <u>{props.children}</u>;
|
||||
case 'deleted':
|
||||
return <del>{props.children}</del>;
|
||||
case 'added':
|
||||
return <mark>{props.children}</mark>;
|
||||
default:
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Code from './components/Code';
|
||||
import BlockToolbar from './components/Toolbar/BlockToolbar';
|
||||
import HorizontalRule from './components/HorizontalRule';
|
||||
import Image from './components/Image';
|
||||
import Link from './components/Link';
|
||||
import ListItem from './components/ListItem';
|
||||
import TodoList from './components/TodoList';
|
||||
import {
|
||||
Heading1,
|
||||
Heading2,
|
||||
Heading3,
|
||||
Heading4,
|
||||
Heading5,
|
||||
Heading6,
|
||||
} from './components/Heading';
|
||||
import Paragraph from './components/Paragraph';
|
||||
import type { SlateNodeProps } from './types';
|
||||
|
||||
type Options = {
|
||||
onInsertImage: *,
|
||||
};
|
||||
|
||||
export default function createRenderNode({ onInsertImage }: Options) {
|
||||
return function renderNode(props: SlateNodeProps) {
|
||||
const { attributes } = props;
|
||||
|
||||
switch (props.node.type) {
|
||||
case 'paragraph':
|
||||
return <Paragraph {...props} />;
|
||||
case 'block-toolbar':
|
||||
return <BlockToolbar onInsertImage={onInsertImage} {...props} />;
|
||||
case 'block-quote':
|
||||
return <blockquote {...attributes}>{props.children}</blockquote>;
|
||||
case 'bulleted-list':
|
||||
return <ul {...attributes}>{props.children}</ul>;
|
||||
case 'ordered-list':
|
||||
return <ol {...attributes}>{props.children}</ol>;
|
||||
case 'todo-list':
|
||||
return <TodoList {...attributes}>{props.children}</TodoList>;
|
||||
case 'table':
|
||||
return <table {...attributes}>{props.children}</table>;
|
||||
case 'table-row':
|
||||
return <tr {...attributes}>{props.children}</tr>;
|
||||
case 'table-head':
|
||||
return <th {...attributes}>{props.children}</th>;
|
||||
case 'table-cell':
|
||||
return <td {...attributes}>{props.children}</td>;
|
||||
case 'list-item':
|
||||
return <ListItem {...props} />;
|
||||
case 'horizontal-rule':
|
||||
return <HorizontalRule {...props} />;
|
||||
case 'code':
|
||||
return <Code {...props} />;
|
||||
case 'code-line':
|
||||
return <pre {...attributes}>{props.children}</pre>;
|
||||
case 'image':
|
||||
return <Image {...props} />;
|
||||
case 'link':
|
||||
return <Link {...props} />;
|
||||
case 'heading1':
|
||||
return <Heading1 placeholder {...props} />;
|
||||
case 'heading2':
|
||||
return <Heading2 {...props} />;
|
||||
case 'heading3':
|
||||
return <Heading3 {...props} />;
|
||||
case 'heading4':
|
||||
return <Heading4 {...props} />;
|
||||
case 'heading5':
|
||||
return <Heading5 {...props} />;
|
||||
case 'heading6':
|
||||
return <Heading6 {...props} />;
|
||||
default:
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
// @flow
|
||||
import InsertImages from '@tommoor/slate-drop-or-paste-images';
|
||||
import PasteLinkify from 'slate-paste-linkify';
|
||||
import CollapseOnEscape from 'slate-collapse-on-escape';
|
||||
import TrailingBlock from 'slate-trailing-block';
|
||||
import EditCode from 'slate-edit-code';
|
||||
import Prism from 'slate-prism';
|
||||
import EditList from './plugins/EditList';
|
||||
import KeyboardShortcuts from './plugins/KeyboardShortcuts';
|
||||
import MarkdownShortcuts from './plugins/MarkdownShortcuts';
|
||||
import { insertImageFile } from './changes';
|
||||
|
||||
type Options = {
|
||||
onImageUploadStart: () => void,
|
||||
onImageUploadStop: () => void,
|
||||
};
|
||||
|
||||
const createPlugins = ({ onImageUploadStart, onImageUploadStop }: Options) => {
|
||||
return [
|
||||
PasteLinkify({
|
||||
type: 'link',
|
||||
collapseTo: 'end',
|
||||
}),
|
||||
InsertImages({
|
||||
extensions: ['png', 'jpg', 'gif', 'webp'],
|
||||
insertImage: async (change, file, editor) => {
|
||||
return change.call(
|
||||
insertImageFile,
|
||||
file,
|
||||
editor,
|
||||
onImageUploadStart,
|
||||
onImageUploadStop
|
||||
);
|
||||
},
|
||||
}),
|
||||
EditList,
|
||||
EditCode({
|
||||
containerType: 'code',
|
||||
lineType: 'code-line',
|
||||
exitBlocktype: 'paragraph',
|
||||
allowMarks: false,
|
||||
selectAll: true,
|
||||
}),
|
||||
Prism({
|
||||
onlyIn: node => node.type === 'code',
|
||||
getSyntax: node => 'javascript',
|
||||
}),
|
||||
CollapseOnEscape({ toEdge: 'end' }),
|
||||
TrailingBlock({ type: 'paragraph' }),
|
||||
KeyboardShortcuts(),
|
||||
MarkdownShortcuts(),
|
||||
];
|
||||
};
|
||||
|
||||
export default createPlugins;
|
||||
@@ -1,8 +0,0 @@
|
||||
// @flow
|
||||
import EditList from 'slate-edit-list';
|
||||
|
||||
export default EditList({
|
||||
types: ['ordered-list', 'bulleted-list', 'todo-list'],
|
||||
typeItem: 'list-item',
|
||||
typeDefault: 'paragraph',
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
// @flow
|
||||
import { Change } from 'slate';
|
||||
import { isModKey } from '../utils';
|
||||
|
||||
export default function KeyboardShortcuts() {
|
||||
return {
|
||||
onKeyDown(ev: SyntheticKeyboardEvent, change: Change) {
|
||||
if (!isModKey(ev)) return null;
|
||||
|
||||
switch (ev.key) {
|
||||
case 'b':
|
||||
return this.toggleMark(change, 'bold');
|
||||
case 'i':
|
||||
return this.toggleMark(change, 'italic');
|
||||
case 'u':
|
||||
return this.toggleMark(change, 'underlined');
|
||||
case 'd':
|
||||
return this.toggleMark(change, 'deleted');
|
||||
case 'k':
|
||||
return change.wrapInline({ type: 'link', data: { href: '' } });
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
toggleMark(change: Change, type: string) {
|
||||
const { value } = change;
|
||||
// don't allow formatting of document title
|
||||
const firstNode = value.document.nodes.first();
|
||||
if (firstNode === value.startBlock) return;
|
||||
|
||||
change.toggleMark(type);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
// @flow
|
||||
import { Change } from 'slate';
|
||||
|
||||
const inlineShortcuts = [
|
||||
{ mark: 'bold', shortcut: '**' },
|
||||
{ mark: 'bold', shortcut: '__' },
|
||||
{ mark: 'italic', shortcut: '*' },
|
||||
{ mark: 'italic', shortcut: '_' },
|
||||
{ mark: 'code', shortcut: '`' },
|
||||
{ mark: 'added', shortcut: '++' },
|
||||
{ mark: 'deleted', shortcut: '~~' },
|
||||
];
|
||||
|
||||
export default function MarkdownShortcuts() {
|
||||
return {
|
||||
onKeyDown(ev: SyntheticKeyboardEvent, change: Change) {
|
||||
switch (ev.key) {
|
||||
case '-':
|
||||
return this.onDash(ev, change);
|
||||
case '`':
|
||||
return this.onBacktick(ev, change);
|
||||
case 'Tab':
|
||||
return this.onTab(ev, change);
|
||||
case ' ':
|
||||
return this.onSpace(ev, change);
|
||||
case 'Backspace':
|
||||
return this.onBackspace(ev, change);
|
||||
case 'Enter':
|
||||
return this.onEnter(ev, change);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* On space, if it was after an auto-markdown shortcut, convert the current
|
||||
* node into the shortcut's corresponding type.
|
||||
*/
|
||||
onSpace(ev: SyntheticKeyboardEvent, change: Change) {
|
||||
const { value } = change;
|
||||
if (value.isExpanded) return;
|
||||
const { startBlock, startOffset } = value;
|
||||
|
||||
// no markdown shortcuts work in headings
|
||||
if (startBlock.type.match(/heading/)) return;
|
||||
|
||||
const chars = startBlock.text.slice(0, startOffset).trim();
|
||||
const type = this.getType(chars);
|
||||
|
||||
if (type) {
|
||||
if (type === 'list-item' && startBlock.type === 'list-item') return;
|
||||
ev.preventDefault();
|
||||
|
||||
let checked;
|
||||
if (chars === '[x]') checked = true;
|
||||
if (chars === '[ ]') checked = false;
|
||||
|
||||
change
|
||||
.extendToStartOf(startBlock)
|
||||
.delete()
|
||||
.setBlock(
|
||||
{
|
||||
type,
|
||||
data: { checked },
|
||||
},
|
||||
{ normalize: false }
|
||||
);
|
||||
|
||||
if (type === 'list-item') {
|
||||
if (checked !== undefined) {
|
||||
change.wrapBlock('todo-list');
|
||||
} else if (chars === '1.') {
|
||||
change.wrapBlock('ordered-list');
|
||||
} else {
|
||||
change.wrapBlock('bulleted-list');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const key of inlineShortcuts) {
|
||||
// find all inline characters
|
||||
let { mark, shortcut } = key;
|
||||
let inlineTags = [];
|
||||
|
||||
// only add tags if they have spaces around them or the tag is beginning
|
||||
// or the end of the block
|
||||
for (let i = 0; i < startBlock.text.length; i++) {
|
||||
const { text } = startBlock;
|
||||
const start = i;
|
||||
const end = i + shortcut.length;
|
||||
const beginningOfBlock = start === 0;
|
||||
const endOfBlock = end === text.length;
|
||||
const surroundedByWhitespaces = [
|
||||
text.slice(start - 1, start),
|
||||
text.slice(end, end + 1),
|
||||
].includes(' ');
|
||||
|
||||
if (
|
||||
text.slice(start, end) === shortcut &&
|
||||
(beginningOfBlock || endOfBlock || surroundedByWhitespaces)
|
||||
) {
|
||||
inlineTags.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
// if we have multiple tags then mark the text between as inline code
|
||||
if (inlineTags.length > 1) {
|
||||
const firstText = startBlock.getFirstText();
|
||||
const firstCodeTagIndex = inlineTags[0];
|
||||
const lastCodeTagIndex = inlineTags[inlineTags.length - 1];
|
||||
return change
|
||||
.removeTextByKey(firstText.key, lastCodeTagIndex, shortcut.length)
|
||||
.removeTextByKey(firstText.key, firstCodeTagIndex, shortcut.length)
|
||||
.moveOffsetsTo(
|
||||
firstCodeTagIndex,
|
||||
lastCodeTagIndex - shortcut.length
|
||||
)
|
||||
.addMark(mark)
|
||||
.collapseToEnd()
|
||||
.removeMark(mark);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onDash(ev: SyntheticKeyboardEvent, change: Change) {
|
||||
const { value } = change;
|
||||
if (value.isExpanded) return;
|
||||
const { startBlock, startOffset } = value;
|
||||
const chars = startBlock.text.slice(0, startOffset).replace(/\s*/g, '');
|
||||
|
||||
if (chars === '--') {
|
||||
ev.preventDefault();
|
||||
return change
|
||||
.extendToStartOf(startBlock)
|
||||
.delete()
|
||||
.setBlock(
|
||||
{
|
||||
type: 'horizontal-rule',
|
||||
isVoid: true,
|
||||
},
|
||||
{ normalize: false }
|
||||
)
|
||||
.insertBlock('paragraph')
|
||||
.collapseToStart();
|
||||
}
|
||||
},
|
||||
|
||||
onBacktick(ev: SyntheticKeyboardEvent, change: Change) {
|
||||
const { value } = change;
|
||||
if (value.isExpanded) return;
|
||||
const { startBlock, startOffset } = value;
|
||||
const chars = startBlock.text.slice(0, startOffset).replace(/\s*/g, '');
|
||||
|
||||
if (chars === '``') {
|
||||
ev.preventDefault();
|
||||
return change
|
||||
.extendToStartOf(startBlock)
|
||||
.delete()
|
||||
.setBlock({ type: 'code' });
|
||||
}
|
||||
},
|
||||
|
||||
onBackspace(ev: SyntheticKeyboardEvent, change: Change) {
|
||||
const { value } = change;
|
||||
if (value.isExpanded) return;
|
||||
const { startBlock, selection, startOffset } = value;
|
||||
|
||||
// If at the start of a non-paragraph, convert it back into a paragraph
|
||||
if (startOffset === 0) {
|
||||
if (startBlock.type === 'paragraph') return;
|
||||
ev.preventDefault();
|
||||
|
||||
change.setBlock('paragraph');
|
||||
|
||||
if (startBlock.type === 'list-item') {
|
||||
change.unwrapBlock('bulleted-list');
|
||||
}
|
||||
|
||||
return change;
|
||||
}
|
||||
|
||||
// If at the end of a code mark hitting backspace should remove the mark
|
||||
if (selection.isCollapsed) {
|
||||
const marksAtCursor = startBlock.getMarksAtRange(selection);
|
||||
const codeMarksAtCursor = marksAtCursor.filter(
|
||||
mark => mark.type === 'code'
|
||||
);
|
||||
|
||||
if (codeMarksAtCursor.size > 0) {
|
||||
ev.preventDefault();
|
||||
|
||||
const textNode = startBlock.getTextAtOffset(startOffset);
|
||||
const charsInCodeBlock = textNode.characters
|
||||
.takeUntil((v, k) => k === startOffset)
|
||||
.reverse()
|
||||
.takeUntil((v, k) => !v.marks.some(mark => mark.type === 'code'));
|
||||
|
||||
change.removeMarkByKey(
|
||||
textNode.key,
|
||||
startOffset - charsInCodeBlock.size,
|
||||
startOffset,
|
||||
'code'
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* On tab, if at the end of the heading jump to the main body content
|
||||
* as if it is another input field (act the same as enter).
|
||||
*/
|
||||
onTab(ev: SyntheticKeyboardEvent, change: Change) {
|
||||
const { value } = change;
|
||||
|
||||
if (value.startBlock.type === 'heading1') {
|
||||
ev.preventDefault();
|
||||
change.splitBlock().setBlock('paragraph');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* On return, if at the end of a node type that should not be extended,
|
||||
* create a new paragraph below it.
|
||||
*/
|
||||
onEnter(ev: SyntheticKeyboardEvent, change: Change) {
|
||||
const { value } = change;
|
||||
if (value.isExpanded) return;
|
||||
|
||||
const { startBlock, startOffset, endOffset } = value;
|
||||
if (startOffset === 0 && startBlock.length === 0)
|
||||
return this.onBackspace(ev, change);
|
||||
|
||||
// Hitting enter at the end of the line reverts to standard behavior
|
||||
if (endOffset === startBlock.length) return;
|
||||
|
||||
// Hitting enter while an image is selected should jump caret below and
|
||||
// insert a new paragraph
|
||||
if (startBlock.type === 'image') {
|
||||
ev.preventDefault();
|
||||
return change.collapseToEnd().insertBlock('paragraph');
|
||||
}
|
||||
|
||||
// Hitting enter in a heading or blockquote will split the node at that
|
||||
// point and make the new node a paragraph
|
||||
if (
|
||||
startBlock.type.startsWith('heading') ||
|
||||
startBlock.type === 'block-quote'
|
||||
) {
|
||||
ev.preventDefault();
|
||||
return change.splitBlock().setBlock('paragraph');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the block type for a series of auto-markdown shortcut `chars`.
|
||||
*/
|
||||
getType(chars: string) {
|
||||
switch (chars) {
|
||||
case '*':
|
||||
case '-':
|
||||
case '+':
|
||||
case '1.':
|
||||
case '[ ]':
|
||||
case '[x]':
|
||||
return 'list-item';
|
||||
case '>':
|
||||
return 'block-quote';
|
||||
case '#':
|
||||
return 'heading1';
|
||||
case '##':
|
||||
return 'heading2';
|
||||
case '###':
|
||||
return 'heading3';
|
||||
case '####':
|
||||
return 'heading4';
|
||||
case '#####':
|
||||
return 'heading5';
|
||||
case '######':
|
||||
return 'heading6';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
// @flow
|
||||
import { Block, Change, Node, Mark } from 'slate';
|
||||
|
||||
const schema = {
|
||||
blocks: {
|
||||
heading1: { nodes: [{ objects: ['text'] }], marks: [''] },
|
||||
heading2: { nodes: [{ objects: ['text'] }], marks: [''] },
|
||||
heading3: { nodes: [{ objects: ['text'] }], marks: [''] },
|
||||
heading4: { nodes: [{ objects: ['text'] }], marks: [''] },
|
||||
heading5: { nodes: [{ objects: ['text'] }], marks: [''] },
|
||||
heading6: { nodes: [{ objects: ['text'] }], marks: [''] },
|
||||
'block-quote': { marks: [''] },
|
||||
table: {
|
||||
nodes: [{ types: ['table-row', 'table-head', 'table-cell'] }],
|
||||
},
|
||||
'horizontal-rule': {
|
||||
isVoid: true,
|
||||
},
|
||||
'block-toolbar': {
|
||||
isVoid: true,
|
||||
},
|
||||
},
|
||||
document: {
|
||||
nodes: [
|
||||
{ types: ['heading1'], min: 1, max: 1 },
|
||||
{
|
||||
types: [
|
||||
'paragraph',
|
||||
'heading1',
|
||||
'heading2',
|
||||
'heading3',
|
||||
'heading4',
|
||||
'heading5',
|
||||
'heading6',
|
||||
'block-quote',
|
||||
'code',
|
||||
'horizontal-rule',
|
||||
'image',
|
||||
'bulleted-list',
|
||||
'ordered-list',
|
||||
'todo-list',
|
||||
'block-toolbar',
|
||||
'table',
|
||||
],
|
||||
min: 1,
|
||||
},
|
||||
],
|
||||
normalize: (
|
||||
change: Change,
|
||||
reason: string,
|
||||
{
|
||||
node,
|
||||
child,
|
||||
mark,
|
||||
index,
|
||||
}: { node: Node, mark?: Mark, child: Node, index: number }
|
||||
) => {
|
||||
switch (reason) {
|
||||
case 'child_type_invalid': {
|
||||
return change.setNodeByKey(
|
||||
child.key,
|
||||
index === 0 ? 'heading1' : 'paragraph'
|
||||
);
|
||||
}
|
||||
case 'child_required': {
|
||||
const block = Block.create(index === 0 ? 'heading1' : 'paragraph');
|
||||
return change.insertNodeByKey(node.key, index, block);
|
||||
}
|
||||
default:
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default schema;
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import MarkdownSerializer from 'slate-md-serializer';
|
||||
export default new MarkdownSerializer();
|
||||
@@ -1,19 +0,0 @@
|
||||
// @flow
|
||||
import { Value, Change, Node } from 'slate';
|
||||
import { Editor } from 'slate-react';
|
||||
|
||||
export type SlateNodeProps = {
|
||||
children: React$Element<*>,
|
||||
readOnly: boolean,
|
||||
attributes: Object,
|
||||
value: Value,
|
||||
editor: Editor,
|
||||
node: Node,
|
||||
parent: Node,
|
||||
};
|
||||
|
||||
export type Plugin = {
|
||||
validateNode?: Node => *,
|
||||
onClick?: SyntheticEvent => *,
|
||||
onKeyDown?: (SyntheticKeyboardEvent, Change) => *,
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
// @flow
|
||||
|
||||
/**
|
||||
* Detect Cmd or Ctrl by platform for keyboard shortcuts
|
||||
*/
|
||||
export function isModKey(event: SyntheticKeyboardEvent) {
|
||||
const isMac =
|
||||
typeof window !== 'undefined' &&
|
||||
/Mac|iPod|iPhone|iPad/.test(window.navigator.platform);
|
||||
return isMac ? event.metaKey : event.ctrlKey;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'shared/styles/constants';
|
||||
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { observable } from 'mobx';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
|
||||
type Props = {
|
||||
children?: ?React.Element<any>,
|
||||
children?: ?React.Node,
|
||||
};
|
||||
|
||||
@observer
|
||||
class ErrorBoundary extends Component {
|
||||
props: Props;
|
||||
class ErrorBoundary extends React.Component<Props> {
|
||||
@observable error: boolean = false;
|
||||
|
||||
componentDidCatch(error: Error, info: Object) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import replace from 'string-replace-to-array';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'shared/styles/constants';
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function BackIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M7.20710678,8.79289322 C6.81658249,8.40236893 6.18341751,8.40236893 5.79289322,8.79289322 C5.40236893,9.18341751 5.40236893,9.81658249 5.79289322,10.2071068 L10.7928932,15.2071068 C11.1834175,15.5976311 11.8165825,15.5976311 12.2071068,15.2071068 L17.2071068,10.2071068 C17.5976311,9.81658249 17.5976311,9.18341751 17.2071068,8.79289322 C16.8165825,8.40236893 16.1834175,8.40236893 15.7928932,8.79289322 L11.5,13.0857864 L7.20710678,8.79289322 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function BlockQuoteIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M7,11 L9.00208688,11 C10.1055038,11 11,11.8982606 11,12.9979131 L11,15.0020869 C11,16.1055038 10.1017394,17 9.00208688,17 L6.99791312,17 C5.89449617,17 5,16.1017394 5,15.0020869 L5,12.9989566 L5,11 C5,8.790861 6.790861,7 9,7 L10,7 C10.5522847,7 11,7.44771525 11,8 C11,8.55228475 10.5522847,9 10,9 L9,9 C7.8954305,9 7,9.8954305 7,11 Z M15,11 L17.0020869,11 C18.1055038,11 19,11.8982606 19,12.9979131 L19,15.0020869 C19,16.1055038 18.1017394,17 17.0020869,17 L14.9979131,17 C13.8944962,17 13,16.1017394 13,15.0020869 L13,12.9989566 L13,11 C13,8.790861 14.790861,7 17,7 L18,7 C18.5522847,7 19,7.44771525 19,8 C19,8.55228475 18.5522847,9 18,9 L17,9 C15.8954305,9 15,9.8954305 15,11 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function BoldIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M18,15 C18,17.209139 16.209139,19 14,19 L8,19 C7.44771525,19 7,18.5522847 7,18 L7,6 C7,5.44771525 7.44771525,5 8,5 L13,5 C15.209139,5 17,6.790861 17,9 C17,9.9796381 16.6478342,10.8770235 16.0631951,11.5724638 C17.2238614,12.2726251 18,13.5456741 18,15 Z M9,17 L14,17 C15.1045695,17 16,16.1045695 16,15 C16,13.8954305 15.1045695,13 14,13 L9,13 L9,17 Z M9,11 L13,11 C14.1045695,11 15,10.1045695 15,9 C15,7.8954305 14.1045695,7 13,7 L9,7 L9,11 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function BulletedListIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M10,6 L19,6 C19.5522847,6 20,6.44771525 20,7 L20,7 C20,7.55228475 19.5522847,8 19,8 L10,8 C9.44771525,8 9,7.55228475 9,7 L9,7 L9,7 C9,6.44771525 9.44771525,6 10,6 Z M10,16 L19,16 C19.5522847,16 20,16.4477153 20,17 C20,17.5522847 19.5522847,18 19,18 L10,18 C9.44771525,18 9,17.5522847 9,17 C9,16.4477153 9.44771525,16 10,16 Z M10,11 L19,11 C19.5522847,11 20,11.4477153 20,12 C20,12.5522847 19.5522847,13 19,13 L10,13 C9.44771525,13 9,12.5522847 9,12 C9,11.4477153 9.44771525,11 10,11 Z M5,10.5 L5,10.5 C5.82842712,10.5 6.5,11.1715729 6.5,12 C6.5,12.8284271 5.82842712,13.5 5,13.5 C4.17157288,13.5 3.5,12.8284271 3.5,12 C3.5,11.1715729 4.17157288,10.5 5,10.5 L5,10.5 Z M5,5.5 L5,5.5 C5.82842712,5.5 6.5,6.17157288 6.5,7 L6.5,7 C6.5,7.82842712 5.82842712,8.5 5,8.5 C4.17157288,8.5 3.5,7.82842712 3.5,7 L3.5,7 L3.5,7 C3.5,6.17157288 4.17157288,5.5 5,5.5 L5,5.5 Z M5,15.5 L5,15.5 C5.82842712,15.5 6.5,16.1715729 6.5,17 C6.5,17.8284271 5.82842712,18.5 5,18.5 C4.17157288,18.5 3.5,17.8284271 3.5,17 C3.5,16.1715729 4.17157288,15.5 5,15.5 L5,15.5 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function CheckboxIcon({
|
||||
checked,
|
||||
...rest
|
||||
}: Props & { checked: boolean }) {
|
||||
return (
|
||||
<Icon {...rest}>
|
||||
{checked ? (
|
||||
<path d="M8,5 L16,5 L16,5 C17.6568542,5 19,6.34314575 19,8 L19,16 C19,17.6568542 17.6568542,19 16,19 L8,19 L8,19 C6.34314575,19 5,17.6568542 5,16 L5,8 L5,8 C5,6.34314575 6.34314575,5 8,5 L8,5 Z M10.958729,12.8883948 L9.26824635,10.8598156 C8.91468227,10.4355387 8.28411757,10.3782146 7.85984067,10.7317787 C7.43556378,11.0853428 7.37823971,11.7159075 7.73180379,12.1401844 L10.2318038,15.1401844 C10.6450125,15.6360348 11.4127535,15.616362 11.8000251,15.1 L16.3000251,9.1 C16.6313959,8.6581722 16.5418529,8.03137085 16.1000251,7.7 C15.6581973,7.36862915 15.0313959,7.4581722 14.7000251,7.9 L10.958729,12.8883948 Z" />
|
||||
) : (
|
||||
<path
|
||||
d="M8,5 L16,5 L16,5 C17.6568542,5 19,6.34314575 19,8 L19,16 C19,17.6568542 17.6568542,19 16,19 L8,19 L8,19 C6.34314575,19 5,17.6568542 5,16 L5,8 L5,8 C5,6.34314575 6.34314575,5 8,5 L8,5 Z M8,7 C7.44771525,7 7,7.44771525 7,8 L7,16 C7,16.5522847 7.44771525,17 8,17 L16,17 C16.5522847,17 17,16.5522847 17,16 L17,8 C17,7.44771525 16.5522847,7 16,7 L8,7 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
)}
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function CloseIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M12,10.5857864 L8.70710678,7.29289322 C8.31658249,6.90236893 7.68341751,6.90236893 7.29289322,7.29289322 C6.90236893,7.68341751 6.90236893,8.31658249 7.29289322,8.70710678 L10.5857864,12 L7.29289322,15.2928932 C6.90236893,15.6834175 6.90236893,16.3165825 7.29289322,16.7071068 C7.68341751,17.0976311 8.31658249,17.0976311 8.70710678,16.7071068 L12,13.4142136 L15.2928932,16.7071068 C15.6834175,17.0976311 16.3165825,17.0976311 16.7071068,16.7071068 C17.0976311,16.3165825 17.0976311,15.6834175 16.7071068,15.2928932 L13.4142136,12 L16.7071068,8.70710678 C17.0976311,8.31658249 17.0976311,7.68341751 16.7071068,7.29289322 C16.3165825,6.90236893 15.6834175,6.90236893 15.2928932,7.29289322 L12,10.5857864 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function CodeIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M11.9805807,17.1961161 C11.8722687,17.7376759 11.3454436,18.0888926 10.8038839,17.9805807 C10.2623241,17.8722687 9.91110737,17.3454436 10.0194193,16.8038839 L12.0194193,6.80388386 C12.1277313,6.26232411 12.6545564,5.91110737 13.1961161,6.01941932 C13.7376759,6.12773127 14.0888926,6.65455638 13.9805807,7.19611614 L11.9805807,17.1961161 Z M6.41421356,12 L8.70710678,14.2928932 C9.09763107,14.6834175 9.09763107,15.3165825 8.70710678,15.7071068 C8.31658249,16.0976311 7.68341751,16.0976311 7.29289322,15.7071068 L4.29289322,12.7071068 C3.90236893,12.3165825 3.90236893,11.6834175 4.29289322,11.2928932 L7.29289322,8.29289322 C7.68341751,7.90236893 8.31658249,7.90236893 8.70710678,8.29289322 C9.09763107,8.68341751 9.09763107,9.31658249 8.70710678,9.70710678 L6.41421356,12 Z M15.2928932,14.2928932 L17.5857864,12 L15.2928932,9.70710678 C14.9023689,9.31658249 14.9023689,8.68341751 15.2928932,8.29289322 C15.6834175,7.90236893 16.3165825,7.90236893 16.7071068,8.29289322 L19.7071068,11.2928932 C20.0976311,11.6834175 20.0976311,12.3165825 19.7071068,12.7071068 L16.7071068,15.7071068 C16.3165825,16.0976311 15.6834175,16.0976311 15.2928932,15.7071068 C14.9023689,15.3165825 14.9023689,14.6834175 15.2928932,14.2928932 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function CollapsedIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M8.23823905,10.6097108 L11.207376,14.4695888 L11.207376,14.4695888 C11.54411,14.907343 12.1719566,14.989236 12.6097108,14.652502 C12.6783439,14.5997073 12.7398293,14.538222 12.792624,14.4695888 L15.761761,10.6097108 L15.761761,10.6097108 C16.0984949,10.1719566 16.0166019,9.54410997 15.5788477,9.20737601 C15.4040391,9.07290785 15.1896811,9 14.969137,9 L9.03086304,9 L9.03086304,9 C8.47857829,9 8.03086304,9.44771525 8.03086304,10 C8.03086304,10.2205442 8.10377089,10.4349022 8.23823905,10.6097108 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function CollectionIcon({
|
||||
expanded,
|
||||
...rest
|
||||
}: Props & { expanded?: boolean }) {
|
||||
return (
|
||||
<Icon {...rest}>
|
||||
{expanded ? (
|
||||
<path d="M16.701875,4.16415178 L17,4.14285714 C18.1045695,4.06395932 19,5.02334914 19,6.28571429 L19,17.7142857 C19,18.9766509 18.1045695,19.9360407 17,19.8571429 L16.701875,19.8358482 C16.8928984,19.371917 17,18.8348314 17,18.25 L17,5.75 C17,5.16516859 16.8928984,4.62808299 16.701875,4.16415178 Z M14,3.36363636 C15.1045695,3.16280555 16,4.15126779 16,5.57142857 L16,18.4285714 C16,19.8487322 15.1045695,20.8371945 14,20.6363636 L7,19.3636364 C5.8954305,19.1628055 5,18.1045695 5,17 L5,7 C5,5.8954305 5.8954305,4.83719445 7,4.63636364 L14,3.36363636 Z M7.5,6.67532468 C7.22385763,6.71118732 7,6.97574633 7,7.26623377 L7,16.7337662 C7,17.0242537 7.22385763,17.2888127 7.5,17.3246753 L8.5,17.4545455 C8.77614237,17.4904081 9,17.272365 9,16.9675325 L9,7.03246753 C9,6.72763504 8.77614237,6.5095919 8.5,6.54545455 L7.5,6.67532468 Z" />
|
||||
) : (
|
||||
<path d="M7,4 L17,4 C18.1045695,4 19,4.8954305 19,6 L19,18 C19,19.1045695 18.1045695,20 17,20 L7,20 C5.8954305,20 5,19.1045695 5,18 L5,6 L5,6 C5,4.8954305 5.8954305,4 7,4 L7,4 Z M7.5,6 C7.22385763,6 7,6.22385763 7,6.5 L7,17.5 C7,17.7761424 7.22385763,18 7.5,18 L8.5,18 C8.77614237,18 9,17.7761424 9,17.5 L9,6.5 C9,6.22385763 8.77614237,6 8.5,6 L7.5,6 Z" />
|
||||
)}
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function DocumentIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M13.5,7.5 L16.5,10.5 L9.66719477,17.3328052 L9.66719477,17.3328052 C9.55724743,17.4427526 9.42317605,17.5255464 9.27563076,17.5746098 L6.42375483,18.5229479 L6.42375483,18.5229479 C6.03070288,18.65365 5.60611633,18.4409735 5.47541424,18.0479215 C5.42427386,17.8941303 5.42432697,17.727911 5.47556562,17.5741526 L5.47556562,17.5741526 L6.42535348,14.7240015 L6.42535348,14.7240015 C6.47444294,14.5766924 6.55716155,14.4428385 6.66695621,14.3330438 L13.5,7.5 Z M14.5,6.5 L15.7928932,5.20710678 L15.7928932,5.20710678 C16.1834175,4.81658249 16.8165825,4.81658249 17.2071068,5.20710678 L18.7928932,6.79289322 L18.7928932,6.79289322 C19.1834175,7.18341751 19.1834175,7.81658249 18.7928932,8.20710678 L17.5,9.5 L14.5,6.5 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function EditIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M13.5,7.5 L16.5,10.5 L9.66719477,17.3328052 L9.66719477,17.3328052 C9.55724743,17.4427526 9.42317605,17.5255464 9.27563076,17.5746098 L6.42375483,18.5229479 L6.42375483,18.5229479 C6.03070288,18.65365 5.60611633,18.4409735 5.47541424,18.0479215 C5.42427386,17.8941303 5.42432697,17.727911 5.47556562,17.5741526 L5.47556562,17.5741526 L6.42535348,14.7240015 L6.42535348,14.7240015 C6.47444294,14.5766924 6.55716155,14.4428385 6.66695621,14.3330438 L13.5,7.5 Z M14.5,6.5 L15.7928932,5.20710678 L15.7928932,5.20710678 C16.1834175,4.81658249 16.8165825,4.81658249 17.2071068,5.20710678 L18.7928932,6.79289322 L18.7928932,6.79289322 C19.1834175,7.18341751 19.1834175,7.81658249 18.7928932,8.20710678 L17.5,9.5 L14.5,6.5 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function GoToIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M14.080855,4.6060807 L8.08085497,18.6060807 C7.86329935,19.1137105 8.09845092,19.7015894 8.6060807,19.919145 C9.11371048,20.1367007 9.70158941,19.9015491 9.91914503,19.3939193 L15.919145,5.3939193 C16.1367007,4.88628952 15.9015491,4.29841059 15.3939193,4.08085497 C14.8862895,3.86329935 14.2984106,4.09845092 14.080855,4.6060807 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function Heading1Icon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M18,15 L18,8 C18,7.86192881 17.9720178,7.73039322 17.921415,7.61075487 C17.8779612,7.50332041 17.8047379,7.39052429 17.7071068,7.29289322 C17.3165825,6.90236893 16.6834175,6.90236893 16.2928932,7.29289322 L14.2928932,9.29289322 C13.9023689,9.68341751 13.9023689,10.3165825 14.2928932,10.7071068 C14.6834175,11.0976311 15.3165825,11.0976311 15.7071068,10.7071068 L16,10.4142136 L16,15 L15,15 C14.4477153,15 14,15.4477153 14,16 C14,16.5522847 14.4477153,17 15,17 L19,17 C19.5522847,17 20,16.5522847 20,16 C20,15.4477153 19.5522847,15 19,15 L18,15 Z M10,13 L6,13 L6,16 C6,16.5522847 5.55228475,17 5,17 C4.44771525,17 4,16.5522847 4,16 L4,8 L4,8 C4,7.44771525 4.44771525,7 5,7 L5,7 L5,7 C5.55228475,7 6,7.44771525 6,8 L6,11 L10,11 L10,8 L10,8 C10,7.44771525 10.4477153,7 11,7 C11.5522847,7 12,7.44771525 12,8 L12,16 C12,16.5522847 11.5522847,17 11,17 C10.4477153,17 10,16.5522847 10,16 L10,13 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function Heading2Icon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M10,13 L6,13 L6,16 C6,16.5522847 5.55228475,17 5,17 C4.44771525,17 4,16.5522847 4,16 L4,8 L4,8 C4,7.44771525 4.44771525,7 5,7 L5,7 L5,7 C5.55228475,7 6,7.44771525 6,8 L6,11 L10,11 L10,8 L10,8 C10,7.44771525 10.4477153,7 11,7 C11.5522847,7 12,7.44771525 12,8 L12,16 C12,16.5522847 11.5522847,17 11,17 C10.4477153,17 10,16.5522847 10,16 L10,13 Z M19.8087361,11.0881717 L16.96377,15 L19,15 C19.5522847,15 20,15.4477153 20,16 C20,16.5522847 19.5522847,17 19,17 L15,17 C14.1827132,17 13.710559,16.0727976 14.1912639,15.4118283 L18,10.1748162 L18,9 L16,9 L16,10 C16,10.5522847 15.5522847,11 15,11 C14.4477153,11 14,10.5522847 14,10 L14,9 C14,7.8954305 14.8954305,7 16,7 L18,7 C19.1045695,7 20,7.8954305 20,9 L20,10.5 C20,10.7113425 19.9330418,10.9172514 19.8087361,11.0881717 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function HomeIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M7,18 L9,18 L9,18 C9.55228475,18 10,17.5522847 10,17 L10,13.5 L10,13.5 C10,12.3954305 10.8954305,11.5 12,11.5 L12,11.5 L12,11.5 C13.1045695,11.5 14,12.3954305 14,13.5 L14,17 L14,17 C14,17.5522847 14.4477153,18 15,18 L17,18 L17,18 C17.5522847,18 18,17.5522847 18,17 L18,10.9367499 L18,10.9367499 C18,10.3431902 17.736354,9.78029498 17.2803688,9.40030733 L12.6401844,5.533487 L12.6401844,5.533487 C12.2693384,5.22444871 11.7306616,5.22444871 11.3598156,5.533487 L6.7196312,9.40030733 L6.7196312,9.40030733 C6.26364602,9.78029498 6,10.3431902 6,10.9367499 L6,17 L6,17 C6,17.5522847 6.44771525,18 7,18 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function HorizontalRuleIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M5,11 L19,11 C19.5522847,11 20,11.4477153 20,12 C20,12.5522847 19.5522847,13 19,13 L5,13 C4.44771525,13 4,12.5522847 4,12 C4,11.4477153 4.44771525,11 5,11 L5,11 Z M7,6 L17,6 C17.5522847,6 18,6.44771525 18,7 L18,8 C18,8.55228475 17.5522847,9 17,9 L7,9 C6.44771525,9 6,8.55228475 6,8 L6,7 L6,7 C6,6.44771525 6.44771525,6 7,6 Z M7,15 L17,15 C17.5522847,15 18,15.4477153 18,16 L18,17 C18,17.5522847 17.5522847,18 17,18 L7,18 C6.44771525,18 6,17.5522847 6,17 L6,16 C6,15.4477153 6.44771525,15 7,15 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { color } from 'shared/styles/constants';
|
||||
|
||||
export type Props = {
|
||||
className?: string,
|
||||
light?: boolean,
|
||||
black?: boolean,
|
||||
primary?: boolean,
|
||||
color?: string,
|
||||
size?: number,
|
||||
onClick?: Function,
|
||||
};
|
||||
|
||||
type BaseProps = {
|
||||
children?: React$Element<*>,
|
||||
};
|
||||
|
||||
export default function Icon({
|
||||
children,
|
||||
className,
|
||||
onClick,
|
||||
...rest
|
||||
}: Props & BaseProps) {
|
||||
const size = rest.size ? rest.size + 'px' : '24px';
|
||||
|
||||
let fill = color.slateDark;
|
||||
if (rest.color) fill = rest.color;
|
||||
if (rest.light) fill = color.white;
|
||||
if (rest.black) fill = color.black;
|
||||
if (rest.primary) fill = color.primary;
|
||||
|
||||
return (
|
||||
<svg
|
||||
fill={fill}
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function ImageIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M19,14.5857864 L13.7071068,9.29289322 C13.3165825,8.90236893 12.6834175,8.90236893 12.2928932,9.29289322 L8,13.5857864 L5,10.5857864 L5,7 L5,7 C5,5.8954305 5.8954305,5 7,5 L7,5 L17,5 L17,5 C18.1045695,5 19,5.8954305 19,7 L19,14.5857864 Z M18.9642423,17.3784559 C18.7873485,18.3020643 17.9751801,19 17,19 L7,19 L7,19 C5.8954305,19 5,18.1045695 5,17 L5,13.4142136 L7.29289322,15.7071068 C7.68341751,16.0976311 8.31658249,16.0976311 8.70710678,15.7071068 L13,11.4142136 L18.9642423,17.3784559 Z M8.5,10 C9.32842712,10 10,9.32842712 10,8.5 C10,7.67157288 9.32842712,7 8.5,7 C7.67157288,7 7,7.67157288 7,8.5 C7,9.32842712 7.67157288,10 8.5,10 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function ItalicIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M12.5,6 C11.6715729,6 11,5.32842712 11,4.5 C11,3.67157288 11.6715729,3 12.5,3 C13.3284271,3 14,3.67157288 14,4.5 C14,5.32842712 13.3284271,6 12.5,6 Z M10.7801961,10 L10,10 C9.44771525,10 9,9.55228475 9,9 C9,8.44771525 9.44771525,8 10,8 L12,8 C12.6310464,8 13.1043391,8.57732421 12.9805807,9.19611614 L11.4590271,16.8038839 C11.3507152,17.3454436 11.7019319,17.8722687 12.2434917,17.9805807 C12.3080649,17.9934953 12.3737558,18 12.4396078,18 L13,18 C13.5522847,18 14,18.4477153 14,19 C14,19.5522847 13.5522847,20 13,20 L12.4396078,20 C12.2420518,20 12.044979,19.9804859 11.8512594,19.941742 C10.2265801,19.6168062 9.17292993,18.0363309 9.49786578,16.4116516 L10.7801961,10 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function KeyboardIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M6,6 L19,6 C20.1045695,6 21,6.8954305 21,8 L21,16 C21,17.1045695 20.1045695,18 19,18 L6,18 C4.8954305,18 4,17.1045695 4,16 L4,8 L4,8 C4,6.8954305 4.8954305,6 6,6 L6,6 Z M6,14 L6,16 L19,16 L19,14 L6,14 Z M16,8 L16,10 L19,10 L19,8 L16,8 Z M6,8 L6,10 L9,10 L9,8 L6,8 Z M13,8 L13,10 L15,10 L15,8 L13,8 Z M10,8 L10,10 L12,10 L12,8 L10,8 Z M7,11 L7,13 L9,13 L9,11 L7,11 Z M10,11 L10,13 L12,13 L12,11 L10,11 Z M13,11 L13,13 L15,13 L15,11 L13,11 Z M16,11 L16,13 L18,13 L18,11 L16,11 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function LinkIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M11.0745387,14.3927712 C10.6944229,13.9921087 10.7110794,13.3591628 11.1117419,12.9790471 C11.5124045,12.5989313 12.1453503,12.6155878 12.5254661,13.0162503 C12.9108834,13.422501 13.5526571,13.4393898 13.9780169,13.0353595 L17.3813571,9.63201937 C18.2122318,8.80114469 18.2122318,7.45403068 17.3813571,6.62315601 C16.5504824,5.79228133 15.2033684,5.79228133 14.3724938,6.62315601 L13.5071091,7.48854062 C13.1165848,7.87906491 12.4834199,7.87906491 12.0928956,7.48854062 C11.7023713,7.09801633 11.7023713,6.46485135 12.0928956,6.07432706 L12.9582802,5.20894244 C14.5702035,3.59701919 17.1836474,3.59701919 18.7955707,5.20894244 C20.4074939,6.8208657 20.4074939,9.43430967 18.7955707,11.0462329 L15.3922305,14.4495731 C15.3640722,14.4774856 15.3640722,14.4774856 15.3354286,14.5048999 C14.1278529,15.6505487 12.2201874,15.6003469 11.0745387,14.3927712 Z M12.9299745,9.60955606 C13.3100902,10.0102186 13.2934337,10.6431644 12.8927712,11.0232802 C12.4921087,11.4033959 11.8591628,11.3867395 11.4790471,10.9860769 C11.0936298,10.5798262 10.4518561,10.5629375 10.0264962,10.9669677 L6.62315601,14.3703079 C5.79228133,15.2011826 5.79228133,16.5482966 6.62315601,17.3791712 C7.45403068,18.2100459 8.80114469,18.2100459 9.63201937,17.3791712 L10.497404,16.5137866 C10.8879283,16.1232623 11.5210933,16.1232623 11.9116175,16.5137866 C12.3021418,16.9043109 12.3021418,17.5374759 11.9116175,17.9280002 L11.0462329,18.7933848 C9.43430967,20.4053081 6.8208657,20.4053081 5.20894244,18.7933848 C3.59701919,17.1814616 3.59701919,14.5680176 5.20894244,12.9560943 L8.61228261,9.55275416 C8.64044095,9.52484169 8.64044095,9.52484169 8.66908451,9.49742738 C9.87666026,8.35177859 11.7843257,8.40198031 12.9299745,9.60955606 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function MenuIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M4.99992841,5.99992841 L19.0000716,5.99992841 C19.5523168,5.99992841 20,6.4476116 20,6.99985681 C20,7.55210202 19.5523168,7.99978522 19.0000716,7.99978522 L4.99992841,7.99978522 C4.4476832,7.99978522 4,7.55210202 4,6.99985681 C4,6.4476116 4.4476832,5.99992841 4.99992841,5.99992841 Z M4.99992841,15.9992125 L19.0000716,15.9992125 C19.5523168,15.9992125 20,16.4468957 20,16.9991409 C20,17.5513861 19.5523168,17.9990693 19.0000716,17.9990693 L4.99992841,17.9990693 C4.4476832,17.9990693 4,17.5513861 4,16.9991409 C4,16.4468957 4.4476832,15.9992125 4.99992841,15.9992125 Z M4.99992841,10.9995704 L19.0000716,10.9995704 C19.5523168,10.9995704 20,11.4472536 20,11.9994988 C20,12.5517441 19.5523168,12.9994273 19.0000716,12.9994273 L4.99992841,12.9994273 C4.4476832,12.9994273 4,12.5517441 4,11.9994988 C4,11.4472536 4.4476832,10.9995704 4.99992841,10.9995704 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function MoreIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M12,14 C10.8954305,14 10,13.1045695 10,12 C10,10.8954305 10.8954305,10 12,10 C13.1045695,10 14,10.8954305 14,12 C14,13.1045695 13.1045695,14 12,14 Z M18,14 C16.8954305,14 16,13.1045695 16,12 C16,10.8954305 16.8954305,10 18,10 C19.1045695,10 20,10.8954305 20,12 C20,13.1045695 19.1045695,14 18,14 Z M6,14 C4.8954305,14 4,13.1045695 4,12 C4,10.8954305 4.8954305,10 6,10 C7.1045695,10 8,10.8954305 8,12 C8,13.1045695 7.1045695,14 6,14 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function NewDocumentIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M19,18 L20,18 C20.5522847,18 21,18.4477153 21,19 C21,19.5522847 20.5522847,20 20,20 L19,20 L19,21 C19,21.5522847 18.5522847,22 18,22 C17.4477153,22 17,21.5522847 17,21 L17,20 L16,20 C15.4477153,20 15,19.5522847 15,19 C15,18.4477153 15.4477153,18 16,18 L17,18 L17,17 C17,16.4477153 17.4477153,16 18,16 C18.5522847,16 19,16.4477153 19,17 L19,18 Z M13.1000181,20 L7,20 C5.8954305,20 5,19.1045695 5,18 L5,6 L5,6 C5,4.8954305 5.8954305,4 7,4 L7,4 L14.5,4 L12,4 L12,9 C12,10.1045695 12.8954305,11 14,11 L19,11 L19,8.5 L19,14.1000181 C18.6768901,14.0344303 18.3424658,14 18,14 C15.2385763,14 13,16.2385763 13,19 C13,19.3424658 13.0344303,19.6768901 13.1000181,20 Z M14,4 L19,9 L14,9 L14,4 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function NextIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M9.29289322,16.2928932 C8.90236893,16.6834175 8.90236893,17.3165825 9.29289322,17.7071068 C9.68341751,18.0976311 10.3165825,18.0976311 10.7071068,17.7071068 L15.7071068,12.7071068 C16.0976311,12.3165825 16.0976311,11.6834175 15.7071068,11.2928932 L10.7071068,6.29289322 C10.3165825,5.90236893 9.68341751,5.90236893 9.29289322,6.29289322 C8.90236893,6.68341751 8.90236893,7.31658249 9.29289322,7.70710678 L13.5857864,12 L9.29289322,16.2928932 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function OpenIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M18,7.41421356 L11.7071068,13.7071068 C11.3165825,14.0976311 10.6834175,14.0976311 10.2928932,13.7071068 C9.90236893,13.3165825 9.90236893,12.6834175 10.2928932,12.2928932 L16.5857864,6 L14,6 C13.4477153,6 13,5.55228475 13,5 C13,4.44771525 13.4477153,4 14,4 L19,4 C19.5522847,4 20,4.44771525 20,5 L20,10 C20,10.5522847 19.5522847,11 19,11 C18.4477153,11 18,10.5522847 18,10 L18,7.41421356 Z M9,6 C9.55228475,6 10,6.44771525 10,7 C10,7.55228475 9.55228475,8 9,8 L6,8 L6,18 L16,18 L16,15 C16,14.4477153 16.4477153,14 17,14 C17.5522847,14 18,14.4477153 18,15 L18,18 C18,19.1045695 17.1045695,20 16,20 L6,20 C4.8954305,20 4,19.1045695 4,18 L4,8 C4,6.8954305 4.8954305,6 6,6 L9,6 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function OrderedListIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M5,7.99978522 L5,6.70798687 L4.85355339,6.85442299 C4.65829124,7.04967116 4.34170876,7.04967116 4.14644661,6.85442299 C3.95118446,6.65917483 3.95118446,6.342615 4.14644661,6.14736684 L5.14644661,5.14743843 C5.46142904,4.83247855 6,5.05554597 6,5.50096651 L6,7.99978522 L6.5000358,7.99978522 L6.5000358,7.99978522 C6.7761584,7.99978522 7,8.22362682 7,8.49974942 C7,8.77587203 6.7761584,8.99971363 6.5000358,8.99971363 L5.53191883,8.99971363 C5.52136474,9.00037848 5.51072178,9.00071593 5.5,9.00071593 C5.48927822,9.00071593 5.47863526,9.00037848 5.46808117,8.99971363 L4.4999642,8.99971363 L4.4999642,8.99971363 C4.2238416,8.99971363 4,8.77587203 4,8.49974942 C4,8.22362682 4.2238416,7.99978522 4.4999642,7.99978522 L4.4999642,7.99978522 L5,7.99978522 Z M9.99992841,5.99992841 L19.0000716,5.99992841 L19.0000716,5.99992841 C19.5523168,5.99992841 20,6.4476116 20,6.99985681 L20,6.99985681 L20,6.99985681 C20,7.55210202 19.5523168,7.99978522 19.0000716,7.99978522 L9.99992841,7.99978522 L9.99992841,7.99978522 C9.4476832,7.99978522 9,7.55210202 9,6.99985681 L9,6.99985681 L9,6.99985681 C9,6.4476116 9.4476832,5.99992841 9.99992841,5.99992841 Z M9.99992841,15.9992125 L19.0000716,15.9992125 L19.0000716,15.9992125 C19.5523168,15.9992125 20,16.4468957 20,16.9991409 L20,16.9991409 L20,16.9991409 C20,17.5513861 19.5523168,17.9990693 19.0000716,17.9990693 L9.99992841,17.9990693 C9.4476832,17.9990693 9,17.5513861 9,16.9991409 C9,16.4468957 9.4476832,15.9992125 9.99992841,15.9992125 Z M9.99992841,10.9995704 L19.0000716,10.9995704 L19.0000716,10.9995704 C19.5523168,10.9995704 20,11.4472536 20,11.9994988 L20,11.9994988 C20,12.5517441 19.5523168,12.9994273 19.0000716,12.9994273 L9.99992841,12.9994273 L9.99992841,12.9994273 C9.4476832,12.9994273 9,12.5517441 9,11.9994988 C9,11.4472536 9.4476832,10.9995704 9.99992841,10.9995704 Z M4.64644661,16.6466151 L5.29289322,16.0002148 L4.5,16.0002148 C4.22385763,16.0002148 4,15.7763732 4,15.5002506 C4,15.224128 4.22385763,15.0002864 4.5,15.0002864 L6.5,15.0002864 C6.94545243,15.0002864 7.16853582,15.5388188 6.85355339,15.8537787 L6.14380887,16.5634724 C6.64120863,16.728439 7,17.1973672 7,17.7500895 C7,18.440396 6.44035594,19 5.75,19 L4.5,19 C4.22385763,19 4,18.7761584 4,18.5000358 C4,18.2239132 4.22385763,18.0000716 4.5,18.0000716 L5.75,18.0000716 C5.88807119,18.0000716 6,17.8881508 6,17.7500895 C6,17.6120282 5.88807119,17.5001074 5.75,17.5001074 L5,17.5001074 C4.55454757,17.5001074 4.33146418,16.961575 4.64644661,16.6466151 Z M6.40096969,12.700451 L6.00096969,13.0004296 L6.50096969,13.0004296 C6.77711207,13.0004296 7.00096969,13.2242712 7.00096969,13.5003938 C7.00096969,13.7765164 6.77711207,14.000358 6.50096969,14.000358 L4.50096969,14.000358 C4.02046355,14.000358 3.81656478,13.3887054 4.20096969,13.1004224 L5.80096969,11.9005083 C5.92687261,11.8060879 6.00096969,11.6579043 6.00096969,11.5005369 L6.00096969,11.2505548 C6.00096969,11.1124935 5.88904088,11.0005727 5.75096969,11.0005727 L5.50096969,11.0005727 C5.22482732,11.0005727 5.00096969,11.2244143 5.00096969,11.5005369 C5.00096969,11.7766596 4.77711207,12.0005012 4.50096969,12.0005012 C4.22482732,12.0005012 4.00096969,11.7766596 4.00096969,11.5005369 C4.00096969,10.6721691 4.67254257,10.0006443 5.50096969,10.0006443 L5.75096969,10.0006443 C6.44132563,10.0006443 7.00096969,10.5602483 7.00096969,11.2505548 L7.00096969,11.5005369 C7.00096969,11.9726391 6.77867846,12.4171897 6.40096969,12.700451 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function PinIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M5.8355996,10.9120278 C5.68780303,10.7642312 5.59344653,10.5714155 5.56742853,10.3640252 C5.50276659,9.84860302 5.86817998,9.37835193 6.38360216,9.31369 C7.5908221,9.16222691 8.66384332,9.11386012 9.60266432,9.16857768 L13.3590257,6.76661181 C13.3232787,6.36756241 13.3081075,5.93429416 13.3135122,5.46680705 C13.3163525,5.22112701 13.4152107,4.98631045 13.5889444,4.81257683 C13.9562711,4.4452501 14.5518254,4.4452501 14.9191521,4.81257683 L19.1874471,9.08089577 C19.3611825,9.25463123 19.4600417,9.48945036 19.4628817,9.735133 C19.4688865,10.2545814 19.0526582,10.6805453 18.5332098,10.6865501 C18.0657301,10.6919287 17.6324685,10.6767575 17.2334248,10.641011 L14.831459,14.3973723 C14.8861764,15.3361909 14.83781,16.4092091 14.6863598,17.6164268 C14.6603417,17.823818 14.5659848,18.0166347 14.4181875,18.164432 C14.0508706,18.5317489 13.4553322,18.5317489 13.0880152,18.164432 L10.1268984,15.2033198 L6.41184151,18.9183767 C6.04452202,19.2856962 5.44897945,19.2856962 5.08165995,18.9183767 C4.71434046,18.5510572 4.71434046,17.9555146 5.08165995,17.5881951 L8.7967158,13.8731393 L5.8355996,10.9120278 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function PlusIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M13,11 L13,6 C13,5.44771525 12.5522847,5 12,5 C11.4477153,5 11,5.44771525 11,6 L11,6 L11,11 L6,11 C5.44771525,11 5,11.4477153 5,12 C5,12.5522847 5.44771525,13 6,13 L11,13 L11,18 C11,18.5522847 11.4477153,19 12,19 C12.5522847,19 13,18.5522847 13,18 L13,13 L18,13 C18.5522847,13 19,12.5522847 19,12 C19,11.4477153 18.5522847,11 18,11 L13,11 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function ProfileIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M6,6 L19,6 C20.1045695,6 21,6.8954305 21,8 L21,16 C21,17.1045695 20.1045695,18 19,18 L6,18 C4.8954305,18 4,17.1045695 4,16 L4,8 L4,8 C4,6.8954305 4.8954305,6 6,6 L6,6 Z M13,14 L13,16 L19,16 L19,14 L13,14 Z M13,11 L13,13 L17,13 L17,11 L13,11 Z M6,8 L6,16 L11,16 L11,8 L6,8 Z M13,8 L13,10 L19,10 L19,8 L13,8 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function SearchIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M16.1692714,14.047951 L19.0606602,16.9393398 C19.6464466,17.5251263 19.6464466,18.4748737 19.0606602,19.0606602 C18.4748737,19.6464466 17.5251263,19.6464466 16.9393398,19.0606602 L14.047951,16.1692714 C13.1546811,16.6971059 12.1127129,17 11,17 C7.6862915,17 5,14.3137085 5,11 C5,7.6862915 7.6862915,5 11,5 C14.3137085,5 17,7.6862915 17,11 C17,12.1127129 16.6971059,13.1546811 16.1692714,14.047951 Z M11,8 C9.34314575,8 8,9.34314575 8,11 C8,12.6568542 9.34314575,14 11,14 C12.6568542,14 14,12.6568542 14,11 C14,9.34314575 12.6568542,8 11,8 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function SettingsIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M9.5005126,5.99794961 L9.81063391,4.75746437 L9.81063391,4.75746437 C9.92192566,4.31229737 10.3219088,4 10.7807764,4 L10.7807764,4 L13.2192236,4 L13.2192236,4 C13.6780912,4 14.0780743,4.31229737 14.1893661,4.75746437 L14.4994874,5.99794961 C15.0194857,6.21474677 15.5052885,6.49715027 15.9465564,6.83482093 L17.1775368,6.48268553 L17.1775368,6.48268553 C17.6187086,6.35648351 18.0891576,6.54673036 18.3185914,6.9441214 L18.3185914,6.9441214 L19.537815,9.0558786 C19.7672488,9.45326964 19.6967829,9.95581386 19.3669029,10.2747788 L18.4467943,11.1644429 C18.4819001,11.4380051 18.5,11.7168888 18.5,12 C18.5,12.2831112 18.4819001,12.5619949 18.4467943,12.8355571 L19.3669029,13.7252212 C19.6967829,14.0441861 19.7672488,14.5467304 19.537815,14.9441214 L18.3185914,17.0558786 C18.0891576,17.4532696 17.6187086,17.6435165 17.1775368,17.5173145 L15.9465564,17.1651791 C15.5052885,17.5028497 15.0194857,17.7852532 14.4994874,18.0020504 L14.1893661,19.2425356 C14.0780743,19.6877026 13.6780912,20 13.2192236,20 L10.7807764,20 C10.3219088,20 9.92192566,19.6877026 9.81063391,19.2425356 L9.5005126,18.0020504 C8.98051425,17.7852532 8.49471153,17.5028497 8.0534436,17.1651791 L6.82246321,17.5173145 C6.3812914,17.6435165 5.91084239,17.4532696 5.68140857,17.0558786 L4.46218497,14.9441214 C4.23275115,14.5467304 4.30321706,14.0441861 4.63309711,13.7252212 L5.5532057,12.8355571 C5.51809991,12.5619949 5.5,12.2831112 5.5,12 C5.5,11.7168888 5.51809991,11.4380051 5.5532057,11.1644429 L4.63309711,10.2747788 L4.63309711,10.2747788 C4.30321706,9.95581386 4.23275115,9.45326964 4.46218497,9.0558786 L4.46218497,9.0558786 L5.68140857,6.9441214 L5.68140857,6.9441214 C5.91084239,6.54673036 6.3812914,6.35648351 6.82246321,6.48268553 L8.0534436,6.83482093 C8.49471153,6.49715027 8.98051425,6.21474677 9.5005126,5.99794961 Z M12,15 C13.6568542,15 15,13.6568542 15,12 C15,10.3431458 13.6568542,9 12,9 C10.3431458,9 9,10.3431458 9,12 C9,13.6568542 10.3431458,15 12,15 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function StarredIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M12,16.1500001 L8.79729751,17.8337604 L8.79729751,17.8337604 C8.30845292,18.0907612 7.70382577,17.9028147 7.44682496,17.4139701 C7.34448589,17.2193097 7.30917121,16.9963416 7.34634806,16.779584 L7.95800981,13.2133223 L5.36696906,10.6876818 L5.36696906,10.6876818 C4.97148548,10.3021806 4.96339318,9.66906733 5.34889439,9.27358375 C5.50240299,9.11610012 5.70354541,9.01361294 5.92118244,8.98198843 L9.50191268,8.46167787 L11.1032639,5.21698585 L11.1032639,5.21698585 C11.3476862,4.72173219 11.9473121,4.51839319 12.4425657,4.76281548 C12.6397783,4.86014572 12.7994058,5.01977324 12.8967361,5.21698585 L14.4980873,8.46167787 L18.0788176,8.98198843 L18.0788176,8.98198843 C18.6253624,9.06140605 19.0040439,9.5688489 18.9246263,10.1153938 C18.8930018,10.3330308 18.7905146,10.5341732 18.6330309,10.6876818 L16.0419902,13.2133223 L16.6536519,16.779584 L16.6536519,16.779584 C16.747013,17.3239204 16.3814251,17.8408763 15.8370887,17.9342373 C15.620331,17.9714142 15.397363,17.9360995 15.2027025,17.8337604 L12,16.1500001 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function StrikethroughIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M9.26756439,10 C9.09739429,9.70582663 9,9.36428714 9,9 C9,7.8954305 9.8954305,7 11,7 L16,7 C16.5522847,7 17,6.55228475 17,6 C17,5.44771525 16.5522847,5 16,5 L11,5 C8.790861,5 7,6.790861 7,9 C7,9.34529957 7.043753,9.68038008 7.12601749,10 L9.26756439,10 Z M16.8739825,14 C16.956247,14.3196199 17,14.6547004 17,15 C17,17.209139 15.209139,19 13,19 L8,19 C7.44771525,19 7,18.5522847 7,18 C7,17.4477153 7.44771525,17 8,17 L13,17 C14.1045695,17 15,16.1045695 15,15 C15,14.6357129 14.9026057,14.2941734 14.7324356,14 L16.8739825,14 Z M5.5,11.5 L18.5,11.5 C18.7761424,11.5 19,11.7238576 19,12 C19,12.2761424 18.7761424,12.5 18.5,12.5 L5.5,12.5 C5.22385763,12.5 5,12.2761424 5,12 C5,11.7238576 5.22385763,11.5 5.5,11.5 L5.5,11.5 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function TableIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M6,5 L18,5 C19.1045695,5 20,5.8954305 20,7 L20,17 C20,18.1045695 19.1045695,19 18,19 L6,19 C4.8954305,19 4,18.1045695 4,17 L4,7 C4,5.8954305 4.8954305,5 6,5 Z M6,7 L6,9 L11,9 L11,7 L6,7 Z M13,7 L13,9 L18,9 L18,7 L13,7 Z M6,11 L6,13 L11,13 L11,11 L6,11 Z M13,11 L13,13 L18,13 L18,11 L13,11 Z M6,15 L6,17 L11,17 L11,15 L6,15 Z M13,15 L13,17 L18,17 L18,15 L13,15 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function TodoListIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M9.99992841,5.99992841 L19.0000716,5.99992841 L19.0000716,5.99992841 C19.5523168,5.99992841 20,6.4476116 20,6.99985681 L20,6.99985681 C20,7.55210202 19.5523168,7.99978522 19.0000716,7.99978522 L9.99992841,7.99978522 L9.99992841,7.99978522 C9.4476832,7.99978522 9,7.55210202 9,6.99985681 C9,6.4476116 9.4476832,5.99992841 9.99992841,5.99992841 L9.99992841,5.99992841 Z M9.99992841,15.9992125 L19.0000716,15.9992125 L19.0000716,15.9992125 C19.5523168,15.9992125 20,16.4468957 20,16.9991409 L20,16.9991409 L20,16.9991409 C20,17.5513861 19.5523168,17.9990693 19.0000716,17.9990693 L9.99992841,17.9990693 C9.4476832,17.9990693 9,17.5513861 9,16.9991409 C9,16.4468957 9.4476832,15.9992125 9.99992841,15.9992125 Z M9.99992841,10.9995704 L19.0000716,10.9995704 L19.0000716,10.9995704 C19.5523168,10.9995704 20,11.4472536 20,11.9994988 L20,11.9994988 C20,12.5517441 19.5523168,12.9994273 19.0000716,12.9994273 L9.99992841,12.9994273 C9.4476832,12.9994273 9,12.5517441 9,11.9994988 C9,11.4472536 9.4476832,10.9995704 9.99992841,10.9995704 Z M5.22935099,7.69420576 L7.09998441,5.20002786 C7.26566855,4.97911569 7.57906677,4.93434451 7.79997895,5.10002864 C8.02089112,5.26571278 8.0656623,5.579111 7.89997817,5.80002318 L5.64999574,8.79999974 C5.45636149,9.05817875 5.07249394,9.06801504 4.86589123,8.82009178 L3.61590099,7.3201035 C3.43912033,7.10796671 3.46778214,6.79268682 3.67991893,6.61590616 C3.89205572,6.4391255 4.20733561,6.46778731 4.38411627,6.6799241 L5.22935099,7.69420576 Z M5.22935099,12.6942058 L7.09998441,10.2000279 C7.26566855,9.97911569 7.57906677,9.93434451 7.79997895,10.1000286 C8.02089112,10.2657128 8.0656623,10.579111 7.89997817,10.8000232 L5.64999574,13.7999997 C5.45636149,14.0581787 5.07249394,14.068015 4.86589123,13.8200918 L3.61590099,12.3201035 C3.43912033,12.1079667 3.46778214,11.7926868 3.67991893,11.6159062 C3.89205572,11.4391255 4.20733561,11.4677873 4.38411627,11.6799241 L5.22935099,12.6942058 Z M5.22935099,17.6942058 L7.09998441,15.2000279 C7.26566855,14.9791157 7.57906677,14.9343445 7.79997895,15.1000286 C8.02089112,15.2657128 8.0656623,15.579111 7.89997817,15.8000232 L5.64999574,18.7999997 C5.45636149,19.0581787 5.07249394,19.068015 4.86589123,18.8200918 L3.61590099,17.3201035 C3.43912033,17.1079667 3.46778214,16.7926868 3.67991893,16.6159062 C3.89205572,16.4391255 4.20733561,16.4677873 4.38411627,16.6799241 L5.22935099,17.6942058 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function TrashIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M10,6 L10,5 L10,5 C10,4.44771525 10.4477153,4 11,4 L13,4 C13.5522847,4 14,4.44771525 14,5 L14,6 L18,6 C18.5522847,6 19,6.44771525 19,7 C19,7.55228475 18.5522847,8 18,8 L17.8571429,8 L17.132679,18.1424941 C17.0579211,19.1891049 16.1870389,20 15.1377616,20 L8.86223841,20 C7.81296107,20 6.94207892,19.1891049 6.86732101,18.1424941 L6.14285714,8 L6,8 C5.44771525,8 5,7.55228475 5,7 C5,6.44771525 5.44771525,6 6,6 L6,6 L10,6 Z M8.86223841,18 L15.1377616,18 L15.8520473,8 L8.14795269,8 L8.86223841,18 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function UserIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M12,11 C10,11 9,9.43299662 9,7.5 C9,5.56700338 9.5,4 12,4 C14.5,4 15,5.56700338 15,7.5 C15,9.43299662 14,11 12,11 Z M8.0085302,19 C6.8992496,19 6,18.1052949 6,17 C6,15 7,12 12,12 C17,12 18,15 18,17 C18,18.1045695 17.0980496,19 15.9914698,19 L8.0085302,19 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import Icon from './Icon';
|
||||
export default Icon;
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import { size, color } from 'shared/styles/constants';
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import styled from 'styled-components';
|
||||
import { size } from 'shared/styles/constants';
|
||||
|
||||
type Props = {
|
||||
label: React.Element<*> | string,
|
||||
children: React.Element<*>,
|
||||
label: React.Node | string,
|
||||
children: React.Node,
|
||||
};
|
||||
|
||||
const Labeled = ({ label, children, ...props }: Props) => (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { Switch, Route, withRouter } from 'react-router-dom';
|
||||
import type { Location } from 'react-router-dom';
|
||||
import { Helmet } from 'react-helmet';
|
||||
@@ -27,17 +27,16 @@ type Props = {
|
||||
history: Object,
|
||||
location: Location,
|
||||
documents: DocumentsStore,
|
||||
children?: ?React.Element<any>,
|
||||
actions?: ?React.Element<any>,
|
||||
title?: ?React.Element<any>,
|
||||
children?: ?React.Node,
|
||||
actions?: ?React.Node,
|
||||
title?: ?React.Node,
|
||||
auth: AuthStore,
|
||||
ui: UiStore,
|
||||
notifications?: React.Element<any>,
|
||||
notifications?: React.Node,
|
||||
};
|
||||
|
||||
@observer
|
||||
class Layout extends React.Component {
|
||||
props: Props;
|
||||
class Layout extends React.Component<Props> {
|
||||
scrollable: ?HTMLDivElement;
|
||||
|
||||
@keydown(['/', 't'])
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
|
||||
@observer
|
||||
class LoadingIndicator extends React.Component {
|
||||
class LoadingIndicator extends React.Component<*> {
|
||||
componentDidMount() {
|
||||
this.props.ui.enableProgressBar();
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user