Merge master
This commit is contained in:
@@ -1,36 +1,37 @@
|
||||
// @flow
|
||||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import Flex from 'components/Flex';
|
||||
import classNames from 'classnames/bind';
|
||||
import styles from './Alert.scss';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'styles/constants';
|
||||
|
||||
const cx = classNames.bind(styles);
|
||||
type Props = {
|
||||
children: React.Element<*>,
|
||||
type?: 'info' | 'success' | 'warning' | 'danger' | 'offline',
|
||||
};
|
||||
|
||||
class Alert extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
danger: PropTypes.bool,
|
||||
warning: PropTypes.bool,
|
||||
success: PropTypes.bool,
|
||||
@observer class Alert extends React.Component {
|
||||
props: Props;
|
||||
defaultProps = {
|
||||
type: 'info',
|
||||
};
|
||||
|
||||
render() {
|
||||
let alertType;
|
||||
if (this.props.danger) alertType = 'danger';
|
||||
if (this.props.warning) alertType = 'warning';
|
||||
if (this.props.success) alertType = 'success';
|
||||
if (!alertType) alertType = 'info'; // default
|
||||
|
||||
return (
|
||||
<Flex
|
||||
align="center"
|
||||
justify="center"
|
||||
className={cx(styles.container, styles[alertType])}
|
||||
>
|
||||
<Container align="center" justify="center" type={this.props.type}>
|
||||
{this.props.children}
|
||||
</Flex>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Container = styled(Flex)`
|
||||
height: $headerHeight;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
|
||||
background-color: ${({ type }) => color[type]};
|
||||
`;
|
||||
|
||||
export default Alert;
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
@import '~styles/constants.scss';
|
||||
|
||||
.container {
|
||||
height: $headerHeight;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.danger {
|
||||
background-color: #f04124;
|
||||
}
|
||||
|
||||
.warning {
|
||||
background-color: #f08a24;
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: #43AC6A;
|
||||
}
|
||||
|
||||
.info {
|
||||
background-color: #a0d3e8;
|
||||
}
|
||||
|
||||
.offline {
|
||||
background-color: #000000;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import Alert from '.';
|
||||
|
||||
test('renders default as info', () => {
|
||||
snap(<Alert>default</Alert>);
|
||||
});
|
||||
|
||||
test('renders success', () => {
|
||||
snap(<Alert success>success</Alert>);
|
||||
});
|
||||
|
||||
test('renders info', () => {
|
||||
snap(<Alert info>info</Alert>);
|
||||
});
|
||||
|
||||
test('renders warning', () => {
|
||||
snap(<Alert warning>warning</Alert>);
|
||||
});
|
||||
|
||||
test('renders danger', () => {
|
||||
snap(<Alert danger>danger</Alert>);
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders danger 1`] = `
|
||||
<Flex
|
||||
align="center"
|
||||
className="container danger"
|
||||
justify="center"
|
||||
>
|
||||
danger
|
||||
</Flex>
|
||||
`;
|
||||
|
||||
exports[`renders default as info 1`] = `
|
||||
<Flex
|
||||
align="center"
|
||||
className="container info"
|
||||
justify="center"
|
||||
>
|
||||
default
|
||||
</Flex>
|
||||
`;
|
||||
|
||||
exports[`renders info 1`] = `
|
||||
<Flex
|
||||
align="center"
|
||||
className="container info"
|
||||
justify="center"
|
||||
>
|
||||
info
|
||||
</Flex>
|
||||
`;
|
||||
|
||||
exports[`renders success 1`] = `
|
||||
<Flex
|
||||
align="center"
|
||||
className="container success"
|
||||
justify="center"
|
||||
>
|
||||
success
|
||||
</Flex>
|
||||
`;
|
||||
|
||||
exports[`renders warning 1`] = `
|
||||
<Flex
|
||||
align="center"
|
||||
className="container warning"
|
||||
justify="center"
|
||||
>
|
||||
warning
|
||||
</Flex>
|
||||
`;
|
||||
@@ -25,15 +25,20 @@ const Collaborators = function({ document }: { document: Document }) {
|
||||
|
||||
return (
|
||||
<Avatars>
|
||||
<Tooltip tooltip={tooltip} placement="bottom">
|
||||
<StyledTooltip tooltip={tooltip} placement="bottom">
|
||||
{collaborators.map(user => (
|
||||
<Avatar key={user.id} src={user.avatarUrl} />
|
||||
))}
|
||||
</Tooltip>
|
||||
</StyledTooltip>
|
||||
</Avatars>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledTooltip = styled(Tooltip)`
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
`;
|
||||
|
||||
const Avatars = styled(Flex)`
|
||||
flex-direction: row-reverse;
|
||||
height: 26px;
|
||||
@@ -45,7 +50,7 @@ const Avatar = styled.img`
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
border: 2px solid ${color.white};
|
||||
margin-right: -13px;
|
||||
margin-right: -10px;
|
||||
|
||||
&:first-child {
|
||||
margin-right: 0;
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import moment from 'moment';
|
||||
|
||||
import styles from './Collection.scss';
|
||||
|
||||
@observer class Collection extends React.Component {
|
||||
static propTypes = {
|
||||
data: React.PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const data = this.props.data;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<h2>
|
||||
<Link to={data.url} className={styles.atlasLink}>{data.name}</Link>
|
||||
</h2>
|
||||
{data.recentDocuments.length > 0
|
||||
? data.recentDocuments.map(document => {
|
||||
return (
|
||||
<Link
|
||||
key={document.id}
|
||||
to={document.url}
|
||||
className={styles.link}
|
||||
>
|
||||
<h3 className={styles.title}>{document.title}</h3>
|
||||
<span className={styles.timestamp}>
|
||||
{moment(document.updatedAt).fromNow()}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
})
|
||||
: <div className={styles.description}>
|
||||
No documents. Why not
|
||||
{' '}
|
||||
<Link to={`${data.url}/new`}>create one</Link>
|
||||
?
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Collection;
|
||||
@@ -1,41 +0,0 @@
|
||||
@import '~styles/constants.scss';
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
padding-bottom: 40px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.atlasLink {
|
||||
text-decoration: none;
|
||||
color: $textColor;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: space-between;
|
||||
|
||||
margin-bottom: 20px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: normal;
|
||||
font-size: 15px;
|
||||
color: $textColor;
|
||||
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-size: 13px;
|
||||
color: #ccc;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import moment from 'moment';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import styles from './DocumentLink.scss';
|
||||
|
||||
const DocumentLink = observer(props => {
|
||||
return (
|
||||
<Link to={props.document.url} className={styles.link}>
|
||||
<h3 className={styles.title}>{props.document.title}</h3>
|
||||
<span className={styles.timestamp}>
|
||||
{moment(props.document.updatedAt).fromNow()}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
export default DocumentLink;
|
||||
@@ -1,23 +0,0 @@
|
||||
@import '~styles/constants.scss';
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: space-between;
|
||||
|
||||
margin-bottom: 20px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: normal;
|
||||
font-size: 15px;
|
||||
color: $textColor;
|
||||
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-size: 13px;
|
||||
color: #ccc;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import DocumentLink from './DocumentLink';
|
||||
export default DocumentLink;
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import Collection from './Collection';
|
||||
export default Collection;
|
||||
@@ -1,10 +1,17 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
|
||||
import styles from './Divider.scss';
|
||||
import styled from 'styled-components';
|
||||
import Flex from 'components/Flex';
|
||||
|
||||
const Divider = () => {
|
||||
return <div className={styles.divider}><span /></div>;
|
||||
return <Flex auto justify="center"><Content /></Flex>;
|
||||
};
|
||||
|
||||
const Content = styled.span`
|
||||
display: flex;
|
||||
width: 50%;
|
||||
margin: 20px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
`;
|
||||
|
||||
export default Divider;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
.divider {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
width: 50%;
|
||||
margin: 20px 0;
|
||||
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
}
|
||||
@@ -15,13 +15,18 @@ type Props = {
|
||||
innerRef?: Function,
|
||||
};
|
||||
|
||||
const StyledStar = styled(Icon).attrs({
|
||||
const StyledStar = styled(({ solid, ...props }) => <Icon {...props} />).attrs({
|
||||
type: 'Star',
|
||||
color: color.text,
|
||||
})`
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: 1px;
|
||||
margin-left: 4px;
|
||||
opacity: ${props => (props.solid ? '1 !important' : 0)};
|
||||
transition: opacity 100ms ease-in-out;
|
||||
|
||||
${props => props.solid && 'polygon { fill: #000};'}
|
||||
`;
|
||||
|
||||
const DocumentLink = styled(Link)`
|
||||
|
||||
@@ -3,20 +3,17 @@ import React, { Component } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Editor, Plain } from 'slate';
|
||||
import keydown from 'react-keydown';
|
||||
import classnames from 'classnames/bind';
|
||||
import type { Document, State, Editor as EditorType } from './types';
|
||||
import getDataTransferFiles from 'utils/getDataTransferFiles';
|
||||
import Flex from 'components/Flex';
|
||||
import ClickablePadding from './components/ClickablePadding';
|
||||
import Toolbar from './components/Toolbar';
|
||||
import Placeholder from './components/Placeholder';
|
||||
import Markdown from './serializer';
|
||||
import createSchema from './schema';
|
||||
import createPlugins from './plugins';
|
||||
import insertImage from './insertImage';
|
||||
import styled from 'styled-components';
|
||||
import styles from './Editor.scss';
|
||||
|
||||
const cx = classnames.bind(styles);
|
||||
|
||||
type Props = {
|
||||
text: string,
|
||||
@@ -188,11 +185,10 @@ type KeyData = {
|
||||
<MaxWidth column auto>
|
||||
<Header onClick={this.focusAtStart} readOnly={this.props.readOnly} />
|
||||
<Toolbar state={this.state.state} onChange={this.onChange} />
|
||||
<Editor
|
||||
ref={ref => (this.editor = ref)}
|
||||
<StyledEditor
|
||||
innerRef={ref => (this.editor = ref)}
|
||||
placeholder="Start with a title…"
|
||||
bodyPlaceholder="Insert witty platitude here"
|
||||
className={cx(styles.editor, { readOnly: this.props.readOnly })}
|
||||
schema={this.schema}
|
||||
plugins={this.plugins}
|
||||
emoji={this.props.emoji}
|
||||
@@ -225,4 +221,106 @@ const Header = styled(Flex)`
|
||||
${({ readOnly }) => !readOnly && 'cursor: text;'}
|
||||
`;
|
||||
|
||||
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;
|
||||
|
||||
.anchor {
|
||||
visibility: hidden;
|
||||
color: #dedede;
|
||||
padding-left: 0.25em;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.anchor {
|
||||
visibility: visible;
|
||||
|
||||
&:hover {
|
||||
color: #cdcdcd;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1:first-of-type {
|
||||
${Placeholder} {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
p:first-of-type {
|
||||
${Placeholder} {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: 1em 0.1em;
|
||||
padding-left: 1em;
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: 0.1em;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
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;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 5px 20px 5px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export default MarkdownEditor;
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
.editor {
|
||||
font-weight: 400;
|
||||
font-size: 1em;
|
||||
line-height: 1.7em;
|
||||
width: 100%;
|
||||
color: #1b2830;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 500;
|
||||
|
||||
.anchor {
|
||||
visibility: hidden;
|
||||
color: #dedede;
|
||||
padding-left: .25em;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.anchor {
|
||||
visibility: visible;
|
||||
|
||||
&:hover {
|
||||
color: #cdcdcd;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1:first-of-type {
|
||||
.placeholder {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
p:first-of-type {
|
||||
.placeholder {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: 1em .1em;
|
||||
padding-left: 1em;
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: .1em;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
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;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 5px 20px 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.readOnly {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.title {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
color: #B1BECC;
|
||||
}
|
||||
|
||||
@media all and (max-width: 2000px) and (min-width: 960px) {
|
||||
.container {
|
||||
// margin-top: 48px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 960px) {
|
||||
.container {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import styled from 'styled-components';
|
||||
import _ from 'lodash';
|
||||
import slug from 'slug';
|
||||
import type { Node, Editor } from '../types';
|
||||
import styles from '../Editor.scss';
|
||||
import Placeholder from './Placeholder';
|
||||
|
||||
type Props = {
|
||||
children: React$Element<any>,
|
||||
@@ -22,6 +22,27 @@ const Wrapper = styled.div`
|
||||
margin-left: ${props => (props.hasEmoji ? '-1.2em' : 0)}
|
||||
`;
|
||||
|
||||
const Anchor = styled.a`
|
||||
visibility: hidden;
|
||||
padding-left: .25em;
|
||||
color: #dedede;
|
||||
|
||||
&:hover {
|
||||
color: #cdcdcd;
|
||||
}
|
||||
`;
|
||||
|
||||
// $FlowIssue I don't know
|
||||
const titleStyles = component => styled(component)`
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
${Anchor} {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function Heading(props: Props) {
|
||||
const {
|
||||
parent,
|
||||
@@ -37,21 +58,20 @@ function Heading(props: Props) {
|
||||
const showPlaceholder = placeholder && firstHeading && !node.text;
|
||||
const slugish = _.escape(`${component}-${slug(node.text)}`);
|
||||
const showHash = readOnly && !!slugish;
|
||||
const Component = component;
|
||||
const Component = titleStyles(component);
|
||||
const emoji = editor.props.emoji || '';
|
||||
const title = node.text.trim();
|
||||
const startsWithEmojiAndSpace =
|
||||
emoji && title.match(new RegExp(`^${emoji}\\s`));
|
||||
|
||||
return (
|
||||
<Component className={styles.title}>
|
||||
<Component>
|
||||
<Wrapper hasEmoji={startsWithEmojiAndSpace}>{children}</Wrapper>
|
||||
{showPlaceholder &&
|
||||
<span className={styles.placeholder} contentEditable={false}>
|
||||
<Placeholder contentEditable={false}>
|
||||
{editor.props.placeholder}
|
||||
</span>}
|
||||
{showHash &&
|
||||
<a name={slugish} className={styles.anchor} href={`#${slugish}`}>#</a>}
|
||||
</Placeholder>}
|
||||
{showHash && <Anchor name={slugish} href={`#${slugish}`}>#</Anchor>}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import React from 'react';
|
||||
import { Document } from 'slate';
|
||||
import type { Props } from '../types';
|
||||
import styles from '../Editor.scss';
|
||||
import Placeholder from './Placeholder';
|
||||
|
||||
export default function Link({
|
||||
attributes,
|
||||
@@ -26,9 +26,9 @@ export default function Link({
|
||||
<p>
|
||||
{children}
|
||||
{showPlaceholder &&
|
||||
<span className={styles.placeholder} contentEditable={false}>
|
||||
<Placeholder contentEditable={false}>
|
||||
{editor.props.bodyPlaceholder}
|
||||
</span>}
|
||||
</Placeholder>}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
11
frontend/components/Editor/components/Placeholder.js
Normal file
11
frontend/components/Editor/components/Placeholder.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// @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,7 +1,7 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import type { Props } from '../types';
|
||||
import styles from '../Editor.scss';
|
||||
|
||||
export default class TodoItem extends Component {
|
||||
props: Props & { checked: boolean };
|
||||
@@ -22,7 +22,7 @@ export default class TodoItem extends Component {
|
||||
const { children, checked, readOnly } = this.props;
|
||||
|
||||
return (
|
||||
<li contentEditable={false} className={styles.todo}>
|
||||
<StyledLi contentEditable={false}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
@@ -33,7 +33,17 @@ export default class TodoItem extends Component {
|
||||
<span contentEditable={!readOnly} suppressContentEditableWarning>
|
||||
{children}
|
||||
</span>
|
||||
</li>
|
||||
</StyledLi>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const StyledLi = styled.li`
|
||||
input {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
&:last-child:focus {
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import Portal from 'react-portal';
|
||||
import classnames from 'classnames';
|
||||
import styled from 'styled-components';
|
||||
import _ from 'lodash';
|
||||
import type { State } from '../../types';
|
||||
import FormattingToolbar from './components/FormattingToolbar';
|
||||
import LinkToolbar from './components/LinkToolbar';
|
||||
import styles from './Toolbar.scss';
|
||||
|
||||
export default class Toolbar extends Component {
|
||||
props: {
|
||||
@@ -112,9 +111,6 @@ export default class Toolbar extends Component {
|
||||
|
||||
render() {
|
||||
const link = this.state.link;
|
||||
const classes = classnames(styles.menu, {
|
||||
[styles.active]: this.state.active,
|
||||
});
|
||||
|
||||
const style = {
|
||||
top: this.state.top,
|
||||
@@ -123,7 +119,7 @@ export default class Toolbar extends Component {
|
||||
|
||||
return (
|
||||
<Portal isOpened>
|
||||
<div className={classes} style={style} ref={this.setRef}>
|
||||
<Menu active={this.state.active} innerRef={this.setRef} style={style}>
|
||||
{link &&
|
||||
<LinkToolbar
|
||||
{...this.props}
|
||||
@@ -135,8 +131,28 @@ export default class Toolbar extends Component {
|
||||
onCreateLink={this.handleFocus}
|
||||
{...this.props}
|
||||
/>}
|
||||
</div>
|
||||
</Menu>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Menu = styled.div`
|
||||
padding: 8px 16px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: -10000px;
|
||||
left: -10000px;
|
||||
opacity: 0;
|
||||
background-color: #222;
|
||||
border-radius: 4px;
|
||||
transition: opacity 250ms ease-in-out, transform 250ms ease-in-out;
|
||||
line-height: 0;
|
||||
height: 40px;
|
||||
min-width: 260px;
|
||||
|
||||
${({ active }) => active && `
|
||||
transform: translateY(-6px);
|
||||
opacity: 1;
|
||||
`}
|
||||
`;
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
.menu {
|
||||
padding: 8px 16px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: -10000px;
|
||||
left: -10000px;
|
||||
opacity: 0;
|
||||
background-color: #222;
|
||||
border-radius: 4px;
|
||||
transition: opacity 250ms ease-in-out, transform 250ms ease-in-out;
|
||||
line-height: 0;
|
||||
height: 40px;
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
.active {
|
||||
transform: translateY(-6px);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.linkEditor {
|
||||
display: flex;
|
||||
margin-left: -8px;
|
||||
margin-right: -8px;
|
||||
|
||||
input {
|
||||
background: rgba(255,255,255,.1);
|
||||
border-radius: 2px;
|
||||
padding: 5px 8px;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
color: #fff;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.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: .7;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&[data-active="true"] {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import styles from '../Toolbar.scss';
|
||||
import type { State } from '../../../types';
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
import BoldIcon from 'components/Icon/BoldIcon';
|
||||
import CodeIcon from 'components/Icon/CodeIcon';
|
||||
import Heading1Icon from 'components/Icon/Heading1Icon';
|
||||
@@ -68,13 +68,9 @@ export default class FormattingToolbar extends Component {
|
||||
const onMouseDown = ev => this.onClickMark(ev, type);
|
||||
|
||||
return (
|
||||
<button
|
||||
className={styles.button}
|
||||
onMouseDown={onMouseDown}
|
||||
data-active={isActive}
|
||||
>
|
||||
<ToolbarButton onMouseDown={onMouseDown} active={isActive}>
|
||||
<IconClass light />
|
||||
</button>
|
||||
</ToolbarButton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -84,13 +80,9 @@ export default class FormattingToolbar extends Component {
|
||||
this.onClickBlock(ev, isActive ? 'paragraph' : type);
|
||||
|
||||
return (
|
||||
<button
|
||||
className={styles.button}
|
||||
onMouseDown={onMouseDown}
|
||||
data-active={isActive}
|
||||
>
|
||||
<ToolbarButton onMouseDown={onMouseDown} active={isActive}>
|
||||
<IconClass light />
|
||||
</button>
|
||||
</ToolbarButton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -103,9 +95,9 @@ export default class FormattingToolbar extends Component {
|
||||
{this.renderBlockButton('heading2', Heading2Icon)}
|
||||
{this.renderBlockButton('bulleted-list', BulletedListIcon)}
|
||||
{this.renderMarkButton('code', CodeIcon)}
|
||||
<button className={styles.button} onMouseDown={this.onCreateLink}>
|
||||
<ToolbarButton onMouseDown={this.onCreateLink}>
|
||||
<LinkIcon light />
|
||||
</button>
|
||||
</ToolbarButton>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
import type { State } from '../../../types';
|
||||
import keydown from 'react-keydown';
|
||||
import styles from '../Toolbar.scss';
|
||||
import Icon from 'components/Icon';
|
||||
import Flex from 'components/Flex';
|
||||
|
||||
@keydown
|
||||
export default class LinkToolbar extends Component {
|
||||
@@ -20,7 +22,7 @@ export default class LinkToolbar extends Component {
|
||||
case 13: // enter
|
||||
ev.preventDefault();
|
||||
return this.save(ev.target.value);
|
||||
case 26: // escape
|
||||
case 27: // escape
|
||||
return this.input.blur();
|
||||
default:
|
||||
}
|
||||
@@ -48,19 +50,35 @@ export default class LinkToolbar extends Component {
|
||||
render() {
|
||||
const href = this.props.link.data.get('href');
|
||||
return (
|
||||
<span className={styles.linkEditor}>
|
||||
<input
|
||||
ref={ref => (this.input = ref)}
|
||||
<LinkEditor>
|
||||
<Input
|
||||
innerRef={ref => (this.input = ref)}
|
||||
defaultValue={href}
|
||||
placeholder="http://"
|
||||
onBlur={this.props.onBlur}
|
||||
onKeyDown={this.onKeyDown}
|
||||
autoFocus
|
||||
/>
|
||||
<button className={styles.button} onMouseDown={this.removeLink}>
|
||||
<ToolbarButton onMouseDown={this.removeLink}>
|
||||
<Icon type="X" light />
|
||||
</button>
|
||||
</span>
|
||||
</ToolbarButton>
|
||||
</LinkEditor>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const LinkEditor = styled(Flex)`
|
||||
margin-left: -8px;
|
||||
margin-right: -8px;
|
||||
`;
|
||||
|
||||
const Input = styled.input`
|
||||
background: rgba(255,255,255,.1);
|
||||
border-radius: 2px;
|
||||
padding: 5px 8px;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
color: #fff;
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// @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: .7;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
${({ active }) => active && 'opacity: 1;'}
|
||||
`;
|
||||
@@ -1,5 +1,6 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import Code from './components/Code';
|
||||
import InlineCode from './components/InlineCode';
|
||||
import Image from './components/Image';
|
||||
@@ -8,7 +9,15 @@ import ListItem from './components/ListItem';
|
||||
import Heading from './components/Heading';
|
||||
import Paragraph from './components/Paragraph';
|
||||
import type { Props, Node, Transform } from './types';
|
||||
import styles from './Editor.scss';
|
||||
|
||||
const TodoList = styled.ul`
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
|
||||
ul {
|
||||
padding-left: 1em;
|
||||
}
|
||||
`;
|
||||
|
||||
const createSchema = () => {
|
||||
return {
|
||||
@@ -29,9 +38,7 @@ const createSchema = () => {
|
||||
'horizontal-rule': (props: Props) => <hr />,
|
||||
'bulleted-list': (props: Props) => <ul>{props.children}</ul>,
|
||||
'ordered-list': (props: Props) => <ol>{props.children}</ol>,
|
||||
'todo-list': (props: Props) => (
|
||||
<ul className={styles.todoList}>{props.children}</ul>
|
||||
),
|
||||
'todo-list': (props: Props) => <TodoList>{props.children}</TodoList>,
|
||||
table: (props: Props) => <table>{props.children}</table>,
|
||||
'table-row': (props: Props) => <tr>{props.children}</tr>,
|
||||
'table-head': (props: Props) => <th>{props.children}</th>,
|
||||
|
||||
@@ -3,7 +3,6 @@ import styled from 'styled-components';
|
||||
import { color } from 'styles/constants';
|
||||
|
||||
const HelpText = styled.p`
|
||||
user-select: none;
|
||||
color: ${color.slateDark};
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
|
||||
import styled from 'styled-components';
|
||||
import Mask from './components/Mask';
|
||||
import Flex from 'components/Flex';
|
||||
|
||||
export default (props: Object) => {
|
||||
type Props = {
|
||||
count?: number,
|
||||
};
|
||||
|
||||
const ListPlaceHolder = ({ count }: Props) => {
|
||||
return (
|
||||
<ReactCSSTransitionGroup
|
||||
transitionName="fadeIn"
|
||||
@@ -16,14 +21,12 @@ export default (props: Object) => {
|
||||
transitionEnter
|
||||
transitionLeave
|
||||
>
|
||||
<Item column auto>
|
||||
<Mask header />
|
||||
<Mask />
|
||||
</Item>
|
||||
<Item column auto>
|
||||
<Mask header />
|
||||
<Mask />
|
||||
</Item>
|
||||
{_.times(count || 2, index => (
|
||||
<Item key={index} column auto>
|
||||
<Mask header />
|
||||
<Mask />
|
||||
</Item>
|
||||
))}
|
||||
</ReactCSSTransitionGroup>
|
||||
);
|
||||
};
|
||||
@@ -31,3 +34,5 @@ export default (props: Object) => {
|
||||
const Item = styled(Flex)`
|
||||
padding: 18px 0;
|
||||
`;
|
||||
|
||||
export default ListPlaceHolder;
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { State, Document, Editor } from 'slate';
|
||||
import MarkdownSerializer from '../Editor/serializer';
|
||||
import type { State as StateType } from '../Editor/types';
|
||||
import schema from '../Editor/schema';
|
||||
import styles from '../Editor/Editor.scss';
|
||||
|
||||
type Props = {
|
||||
text: string,
|
||||
className: string,
|
||||
limit: number,
|
||||
};
|
||||
|
||||
function filterDocumentState({ state, characterLimit, nodeLimit }) {
|
||||
const { document } = state;
|
||||
if (document.text.length <= characterLimit) {
|
||||
return state;
|
||||
}
|
||||
|
||||
let totalCharacters = 0;
|
||||
let totalNodes = 0;
|
||||
const nodes = document.nodes.filter(childNode => {
|
||||
if (childNode.text.length + totalCharacters <= characterLimit) {
|
||||
totalCharacters += childNode.text.length;
|
||||
|
||||
if (totalNodes++ <= nodeLimit) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return State.create({
|
||||
document: Document.create({
|
||||
...document,
|
||||
nodes: nodes,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
class Markdown extends React.Component {
|
||||
props: Props;
|
||||
|
||||
state: {
|
||||
state: StateType,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const state = MarkdownSerializer.deserialize(props.text);
|
||||
const options = {
|
||||
state,
|
||||
characterLimit: props.limit,
|
||||
nodeLimit: 5,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
state: filterDocumentState(options),
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<span className={this.props.className}>
|
||||
<Editor
|
||||
className={styles.editor}
|
||||
schema={schema}
|
||||
state={this.state.state}
|
||||
readOnly
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Markdown;
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import Markdown from './Markdown';
|
||||
export default Markdown;
|
||||
Reference in New Issue
Block a user