Merge pull request #388 from jorilallo/block-insert

New block insert toolbar
This commit is contained in:
Jori Lallo
2017-11-08 23:50:20 -08:00
committed by GitHub
19 changed files with 381 additions and 200 deletions

View File

@@ -44,7 +44,10 @@ type KeyData = {
constructor(props: Props) {
super(props);
this.schema = createSchema();
this.schema = createSchema({
onInsertImage: this.insertImageFile,
onChange: this.onChange,
});
this.plugins = createPlugins({
onImageUploadStart: props.onImageUploadStart,
onImageUploadStop: props.onImageUploadStop,

View File

@@ -1,193 +1,142 @@
// @flow
import React, { Component } from 'react';
import EditList from '../plugins/EditList';
import getDataTransferFiles from 'utils/getDataTransferFiles';
import Portal from 'react-portal';
import { findDOMNode, Node } from 'slate';
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';
import BlockMenu from 'menus/BlockMenu';
import type { State } from '../types';
const { transforms } = EditList;
type Props = {
state: State,
onChange: Function,
onInsertImage: File => Promise<*>,
};
function findClosestRootNode(state, ev) {
let previous;
for (const node of state.document.nodes) {
const element = findDOMNode(node);
const bounds = element.getBoundingClientRect();
if (bounds.top > ev.clientY) return previous;
previous = { node, element, bounds };
}
}
@observer
export default class BlockInsert extends Component {
props: Props;
mouseMoveTimeout: number;
file: HTMLInputElement;
mouseMovementSinceClick: number = 0;
lastClientX: number = 0;
lastClientY: number = 0;
@observable closestRootNode: Node;
@observable active: boolean = false;
@observable menuOpen: boolean = false;
@observable top: number;
@observable left: number;
@observable mouseX: number;
componentDidMount = () => {
this.update();
window.addEventListener('mousemove', this.handleMouseMove);
};
componentWillUpdate = (nextProps: Props) => {
this.update(nextProps);
};
componentWillUnmount = () => {
window.removeEventListener('mousemove', this.handleMouseMove);
};
setInactive = () => {
if (this.menuOpen) return;
this.active = false;
};
handleMouseMove = (ev: SyntheticMouseEvent) => {
const windowWidth = window.innerWidth / 3;
let active = ev.clientX < windowWidth;
const windowWidth = window.innerWidth / 2.5;
const result = findClosestRootNode(this.props.state, ev);
const movementThreshold = 200;
if (active !== this.active) {
this.active = active || this.menuOpen;
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.state.document.nodes.first();
if (result.node === firstNode || result.node.type === 'block-toolbar') {
this.left = -1000;
} else {
this.left = Math.round(result.bounds.left - 20);
this.top = Math.round(result.bounds.top + window.scrollY);
}
}
if (active) {
if (this.active) {
clearTimeout(this.mouseMoveTimeout);
this.mouseMoveTimeout = setTimeout(this.setInactive, 2000);
}
};
handleMenuOpen = () => {
this.menuOpen = true;
};
handleMenuClose = () => {
this.menuOpen = false;
};
update = (props?: Props) => {
if (!document.activeElement) return;
const { state } = props || this.props;
const boxRect = document.activeElement.getBoundingClientRect();
const selection = window.getSelection();
if (!selection.focusNode) return;
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
if (rect.top <= 0 || boxRect.left <= 0) return;
if (state.startBlock.type === 'heading1') {
this.active = false;
}
this.top = Math.round(rect.top + window.scrollY);
this.left = Math.round(boxRect.left + window.scrollX - 20);
};
insertBlock = (
ev: SyntheticEvent,
options: {
type: string | Object,
wrapper?: string | Object,
append?: string | Object,
}
) => {
ev.preventDefault();
const { type, wrapper, append } = options;
let { state } = this.props;
let transform = state.transform();
const { document } = state;
const parent = document.getParent(state.startBlock.key);
// lists get some special treatment
if (parent && parent.type === 'list-item') {
transform = transforms.unwrapList(
transforms
.splitListItem(transform.collapseToStart())
.collapseToEndOfPreviousBlock()
);
}
transform = transform.insertBlock(type);
if (wrapper) transform = transform.wrapBlock(wrapper);
if (append) transform = transform.insertBlock(append);
state = transform.focus().apply();
this.props.onChange(state);
handleClick = (ev: SyntheticMouseEvent) => {
this.mouseMovementSinceClick = 0;
this.active = false;
};
onPickImage = (ev: SyntheticEvent) => {
// simulate a click on the file upload input element
this.file.click();
};
const { state } = this.props;
const type = { type: 'block-toolbar', isVoid: true };
let transform = state.transform();
onChooseImage = async (ev: SyntheticEvent) => {
const files = getDataTransferFiles(ev);
for (const file of files) {
await this.props.onInsertImage(file);
}
// remove any existing toolbars in the document as a fail safe
state.document.nodes.forEach(node => {
if (node.type === 'block-toolbar') {
transform.removeNodeByKey(node.key);
}
});
transform
.collapseToStartOf(this.closestRootNode)
.collapseToEndOfPreviousBlock()
.insertBlock(type);
this.props.onChange(transform.apply());
};
render() {
const style = { top: `${this.top}px`, left: `${this.left}px` };
const todo = { type: 'list-item', data: { checked: false } };
const rule = { type: 'horizontal-rule', isVoid: true };
return (
<Portal isOpened>
<Trigger active={this.active} style={style}>
<HiddenInput
type="file"
innerRef={ref => (this.file = ref)}
onChange={this.onChooseImage}
accept="image/*"
/>
<BlockMenu
label={<PlusIcon />}
onPickImage={this.onPickImage}
onInsertList={ev =>
this.insertBlock(ev, {
type: 'list-item',
wrapper: 'bulleted-list',
})}
onInsertTodoList={ev =>
this.insertBlock(ev, { type: todo, wrapper: 'todo-list' })}
onInsertBreak={ev =>
this.insertBlock(ev, { type: rule, append: 'paragraph' })}
onOpen={this.handleMenuOpen}
onClose={this.handleMenuClose}
/>
<PlusIcon onClick={this.handleClick} color={color.slate} />
</Trigger>
</Portal>
);
}
}
const HiddenInput = styled.input`
position: absolute;
top: -100px;
left: -100px;
visibility: hidden;
`;
const Trigger = styled.div`
position: absolute;
z-index: 1;
opacity: 0;
background-color: ${color.white};
transition: opacity 250ms ease-in-out, transform 250ms ease-in-out;
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-top: -2px;
margin-left: -4px;
margin-left: -10px;
box-shadow: inset 0 0 0 2px ${color.slate};
border-radius: 100%;
transform: scale(.9);
cursor: pointer;
&:hover {
background-color: ${color.smokeDark};
}
${({ active }) => active && `
transform: scale(1);

View File

@@ -9,10 +9,10 @@ export default function Code({ children, node, readOnly, attributes }: Props) {
const language = node.data.get('language') || 'javascript';
return (
<Container>
<Container {...attributes}>
{readOnly && <CopyButton text={node.text} />}
<Pre className={`language-${language}`}>
<code {...attributes} className={`language-${language}`}>
<code className={`language-${language}`}>
{children}
</code>
</Pre>

View File

@@ -14,6 +14,7 @@ type Props = {
editor: Editor,
readOnly: boolean,
component?: string,
attributes: Object,
};
function Heading(props: Props) {
@@ -25,6 +26,7 @@ function Heading(props: Props) {
readOnly,
children,
component = 'h1',
attributes,
...rest
} = props;
const parentIsDocument = parent instanceof Document;
@@ -39,7 +41,7 @@ function Heading(props: Props) {
emoji && title.match(new RegExp(`^${emoji}\\s`));
return (
<Component {...rest} id={slugish}>
<Component {...attributes} {...rest} id={slugish}>
<Wrapper hasEmoji={startsWithEmojiAndSpace}>
{children}
</Wrapper>

View File

@@ -5,12 +5,15 @@ import type { Props } from '../types';
import { color } from 'shared/styles/constants';
function HorizontalRule(props: Props) {
const { state, node } = props;
const { state, node, attributes } = props;
const active = state.isFocused && state.selection.hasEdgeIn(node);
return <StyledHr active={active} />;
return <StyledHr active={active} {...attributes} />;
}
const StyledHr = styled.hr`
padding-top: .75em;
margin: 0;
border: 0;
border-bottom: 1px solid ${props => (props.active ? color.slate : color.slateLight)};
`;

View File

@@ -3,14 +3,15 @@ import React from 'react';
import type { Props } from '../types';
import TodoItem from './TodoItem';
export default function ListItem({ children, node, ...props }: Props) {
export default function ListItem({ children, node, attributes }: Props) {
const checked = node.data.get('checked');
if (checked !== undefined) {
return (
<TodoItem checked={checked} node={node} {...props}>
<TodoItem checked={checked} node={node} {...attributes}>
{children}
</TodoItem>
);
}
return <li>{children}</li>;
return <li {...attributes}>{children}</li>;
}

View File

@@ -23,7 +23,7 @@ export default function Link({
!node.text;
return (
<p>
<p {...attributes}>
{children}
{showPlaceholder &&
<Placeholder contentEditable={false}>

View File

@@ -20,10 +20,10 @@ export default class TodoItem extends Component {
};
render() {
const { children, checked, readOnly } = this.props;
const { children, checked, attributes, readOnly } = this.props;
return (
<ListItem checked={checked}>
<ListItem checked={checked} {...attributes}>
<Input
type="checkbox"
checked={checked}

View File

@@ -0,0 +1,194 @@
// @flow
import React, { Component } from 'react';
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 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 { Props as BaseProps } from '../../types';
import { color } from 'shared/styles/constants';
import { fadeIn } from 'shared/styles/animations';
import { splitAndInsertBlock } from '../../transforms';
type Props = BaseProps & {
onInsertImage: Function,
onChange: Function,
};
type Options = {
type: string | Object,
wrapper?: string | Object,
append?: string | Object,
};
class BlockToolbar extends Component {
props: Props;
file: HTMLInputElement;
componentWillReceiveProps(nextProps: Props) {
const wasActive = this.props.state.selection.hasEdgeIn(this.props.node);
const isActive = nextProps.state.selection.hasEdgeIn(nextProps.node);
const becameInactive = !isActive && wasActive;
if (becameInactive) {
const state = nextProps.state
.transform()
.removeNodeByKey(nextProps.node.key)
.apply();
this.props.onChange(state);
}
}
@keydown('esc')
removeSelf(ev: SyntheticEvent) {
ev.preventDefault();
ev.stopPropagation();
const state = this.props.state
.transform()
.removeNodeByKey(this.props.node.key)
.apply();
this.props.onChange(state);
}
insertBlock = (options: Options) => {
const { state } = this.props;
let transform = splitAndInsertBlock(state.transform(), state, options);
state.document.nodes.forEach(node => {
if (node.type === 'block-toolbar') {
transform.removeNodeByKey(node.key);
}
});
this.props.onChange(transform.focus().apply());
};
handleClickBlock = (ev: SyntheticEvent, type: string) => {
ev.preventDefault();
switch (type) {
case 'heading1':
case 'heading2':
case 'code':
return this.insertBlock({ type });
case 'horizontal-rule':
return this.insertBlock({
type: { type: 'horizontal-rule', isVoid: true },
});
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 { state, attributes, node } = this.props;
const active = state.isFocused && state.selection.hasEdgeIn(node);
return (
<Bar active={active} {...attributes}>
<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('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%;
}
`;
const HiddenInput = styled.input`
position: absolute;
top: -100px;
left: -100px;
visibility: hidden;
`;
export default BlockToolbar;

View File

@@ -140,7 +140,7 @@ export default class Toolbar extends Component {
const Menu = styled.div`
padding: 8px 16px;
position: absolute;
z-index: 1;
z-index: 2;
top: -10000px;
left: -10000px;
opacity: 0;

View File

@@ -16,9 +16,15 @@ import {
Heading6,
} from './components/Heading';
import Paragraph from './components/Paragraph';
import BlockToolbar from './components/Toolbar/BlockToolbar';
import type { Props, Node, Transform } from './types';
const createSchema = () => {
type Options = {
onInsertImage: Function,
onChange: Function,
};
const createSchema = ({ onInsertImage, onChange }: Options) => {
return {
marks: {
bold: (props: Props) => <strong>{props.children}</strong>,
@@ -30,18 +36,39 @@ const createSchema = () => {
},
nodes: {
'block-toolbar': (props: Props) => (
<BlockToolbar
onChange={onChange}
onInsertImage={onInsertImage}
{...props}
/>
),
paragraph: (props: Props) => <Paragraph {...props} />,
'block-quote': (props: Props) => (
<blockquote>{props.children}</blockquote>
<blockquote {...props.attributes}>{props.children}</blockquote>
),
'horizontal-rule': HorizontalRule,
'bulleted-list': (props: Props) => <ul>{props.children}</ul>,
'ordered-list': (props: Props) => <ol>{props.children}</ol>,
'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>,
'table-cell': (props: Props) => <td>{props.children}</td>,
'bulleted-list': (props: Props) => (
<ul {...props.attributes}>{props.children}</ul>
),
'ordered-list': (props: Props) => (
<ol {...props.attributes}>{props.children}</ol>
),
'todo-list': (props: Props) => (
<TodoList {...props.attributes}>{props.children}</TodoList>
),
table: (props: Props) => (
<table {...props.attributes}>{props.children}</table>
),
'table-row': (props: Props) => (
<tr {...props.attributes}>{props.children}</tr>
),
'table-head': (props: Props) => (
<th {...props.attributes}>{props.children}</th>
),
'table-cell': (props: Props) => (
<td {...props.attributes}>{props.children}</td>
),
code: Code,
image: Image,
link: Link,

View File

@@ -0,0 +1,37 @@
// @flow
import EditList from './plugins/EditList';
import type { State, Transform } from './types';
const { transforms } = EditList;
type Options = {
type: string | Object,
wrapper?: string | Object,
append?: string | Object,
};
export function splitAndInsertBlock(
transform: Transform,
state: State,
options: Options
) {
const { type, wrapper, append } = options;
const { document } = state;
const parent = document.getParent(state.startBlock.key);
// lists get some special treatment
if (parent && parent.type === 'list-item') {
transform = transforms.unwrapList(
transforms
.splitListItem(transform.collapseToStart())
.collapseToEndOfPreviousBlock()
);
}
transform = transform.insertBlock(type);
if (wrapper) transform = transform.wrapBlock(wrapper);
if (append) transform = transform.insertBlock(append);
return transform;
}

View File

@@ -42,7 +42,12 @@ export type StateTransform = {
wrapText: Function,
};
export type Transform = NodeTransform & StateTransform;
export type SelectionTransform = {
collapseToStart: Function,
collapseToEnd: Function,
};
export type Transform = NodeTransform & StateTransform & SelectionTransform;
export type Editor = {
props: Object,

View File

@@ -10,7 +10,7 @@ export default function CollectionIcon({
return (
<Icon {...rest}>
{expanded
? <path d="M14,3.28571429 C15.1045695,3.12791864 16,3.8954305 16,5 L16,19 C16,20.1045695 15.1045695,20.8720814 14,20.7142857 L7,19.2857143 C5.8954305,19.1279186 5,18.3284271 5,17.5 L5,6.5 C5,5.67157288 5.8954305,4.87208136 7,4.71428571 L14,3.28571429 Z M7.5,6.47598949 L8.5,6.37337629 C8.77614237,6.34504044 9,6.49817875 9,6.71542029 L9,17.2845797 C9,17.5018212 8.77614237,17.6549596 8.5,17.6266237 L7.5,17.5240105 C7.22385763,17.4956747 7,17.3042518 7,17.0964555 L7,6.90354448 C7,6.69574823 7.22385763,6.50432534 7.5,6.47598949 Z M17,4 C18.1045695,4 19,4.8954305 19,6 L19,18 C19,19.1045695 18.1045695,20 17,20 L17,4 Z" />
? <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>
);

View File

@@ -6,7 +6,7 @@ import type { Props } from './Icon';
export default function Heading1Icon(props: Props) {
return (
<Icon {...props}>
<path d="M7,4 L7,4 C7.55228475,4 8,4.44771525 8,5 L8,19 C8,19.5522847 7.55228475,20 7,20 C6.44771525,20 6,19.5522847 6,19 L6,5 L6,5 C6,4.44771525 6.44771525,4 7,4 L7,4 Z M8,11 L16,11 L16,13 L8,13 L8,11 Z M17,4 C17.5522847,4 18,4.44771525 18,5 L18,19 C18,19.5522847 17.5522847,20 17,20 C16.4477153,20 16,19.5522847 16,19 L16,5 L16,5 C16,4.44771525 16.4477153,4 17,4 Z" />
<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>
);
}

View File

@@ -6,7 +6,7 @@ import type { Props } from './Icon';
export default function Heading2Icon(props: Props) {
return (
<Icon {...props}>
<path d="M14,13 L10,13 L10,16 C10,16.5522847 9.55228475,17 9,17 C8.44771525,17 8,16.5522847 8,16 L8,8 L8,8 C8,7.44771525 8.44771525,7 9,7 L9,7 L9,7 C9.55228475,7 10,7.44771525 10,8 L10,11 L14,11 L14,8 L14,8 C14,7.44771525 14.4477153,7 15,7 C15.5522847,7 16,7.44771525 16,8 L16,16 C16,16.5522847 15.5522847,17 15,17 C14.4477153,17 14,16.5522847 14,16 L14,13 Z" />
<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>
);
}

View File

@@ -0,0 +1,12 @@
// @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>
);
}

View File

@@ -6,7 +6,7 @@ import type { Props } from './Icon';
export default function OrderedListIcon(props: Props) {
return (
<Icon {...props}>
<path d="M1,3.99978522 L1,2.70798687 L0.853553391,2.85442299 C0.658291245,3.04967116 0.341708755,3.04967116 0.146446609,2.85442299 C-0.0488155365,2.65917483 -0.0488155365,2.342615 0.146446609,2.14736684 L1.14644661,1.14743843 C1.46142904,0.83247855 2,1.05554597 2,1.50096651 L2,3.99978522 L2.5000358,3.99978522 L2.5000358,3.99978522 C2.7761584,3.99978522 3,4.22362682 3,4.49974942 C3,4.77587203 2.7761584,4.99971363 2.5000358,4.99971363 L1.53191883,4.99971363 C1.52136474,5.00037848 1.51072178,5.00071593 1.5,5.00071593 C1.48927822,5.00071593 1.47863526,5.00037848 1.46808117,4.99971363 L0.499964203,4.99971363 L0.499964203,4.99971363 C0.223841598,4.99971363 3.38152664e-17,4.77587203 0,4.49974942 C-3.38152664e-17,4.22362682 0.223841598,3.99978522 0.499964203,3.99978522 L0.499964203,3.99978522 L1,3.99978522 Z M5.99992841,1.99992841 L15.0000716,1.99992841 L15.0000716,1.99992841 C15.5523168,1.99992841 16,2.4476116 16,2.99985681 L16,2.99985681 L16,2.99985681 C16,3.55210202 15.5523168,3.99978522 15.0000716,3.99978522 L5.99992841,3.99978522 L5.99992841,3.99978522 C5.4476832,3.99978522 5,3.55210202 5,2.99985681 L5,2.99985681 L5,2.99985681 C5,2.4476116 5.4476832,1.99992841 5.99992841,1.99992841 Z M5.99992841,11.9992125 L15.0000716,11.9992125 L15.0000716,11.9992125 C15.5523168,11.9992125 16,12.4468957 16,12.9991409 L16,12.9991409 L16,12.9991409 C16,13.5513861 15.5523168,13.9990693 15.0000716,13.9990693 L5.99992841,13.9990693 C5.4476832,13.9990693 5,13.5513861 5,12.9991409 C5,12.4468957 5.4476832,11.9992125 5.99992841,11.9992125 Z M5.99992841,6.99957044 L15.0000716,6.99957044 L15.0000716,6.99957044 C15.5523168,6.99957044 16,7.44725364 16,7.99949885 L16,7.99949885 C16,8.55174406 15.5523168,8.99942725 15.0000716,8.99942725 L5.99992841,8.99942725 L5.99992841,8.99942725 C5.4476832,8.99942725 5,8.55174406 5,7.99949885 C5,7.44725364 5.4476832,6.99957044 5.99992841,6.99957044 Z M0.646446609,12.6466151 L1.29289322,12.0002148 L0.5,12.0002148 C0.223857625,12.0002148 0,11.7763732 0,11.5002506 C0,11.224128 0.223857625,11.0002864 0.5,11.0002864 L2.5,11.0002864 C2.94545243,11.0002864 3.16853582,11.5388188 2.85355339,11.8537787 L2.14380887,12.5634724 C2.64120863,12.728439 3,13.1973672 3,13.7500895 C3,14.440396 2.44035594,15 1.75,15 L0.5,15 C0.223857625,15 0,14.7761584 0,14.5000358 C0,14.2239132 0.223857625,14.0000716 0.5,14.0000716 L1.75,14.0000716 C1.88807119,14.0000716 2,13.8881508 2,13.7500895 C2,13.6120282 1.88807119,13.5001074 1.75,13.5001074 L1,13.5001074 C0.554547575,13.5001074 0.331464179,12.961575 0.646446609,12.6466151 Z M2.40096969,8.70045104 L2.00096969,9.00042956 L2.50096969,9.00042956 C2.77711207,9.00042956 3.00096969,9.22427116 3.00096969,9.50039376 C3.00096969,9.77651637 2.77711207,10.000358 2.50096969,10.000358 L0.500969693,10.000358 C0.0204635467,10.000358 -0.183435224,9.38870545 0.200969693,9.1004224 L1.80096969,7.90050831 C1.92687261,7.80608788 2.00096969,7.65790433 2.00096969,7.50053695 L2.00096969,7.25055485 C2.00096969,7.11249355 1.88904088,7.00057275 1.75096969,7.00057275 L1.50096969,7.00057275 C1.22482732,7.00057275 1.00096969,7.22441434 1.00096969,7.50053695 C1.00096969,7.77665955 0.777112068,8.00050115 0.500969693,8.00050115 C0.224827319,8.00050115 0.000969693445,7.77665955 0.000969693445,7.50053695 C0.000969693445,6.67216913 0.672542569,6.00064434 1.50096969,6.00064434 L1.75096969,6.00064434 C2.44132563,6.00064434 3.00096969,6.56024834 3.00096969,7.25055485 L3.00096969,7.50053695 C3.00096969,7.9726391 2.77867846,8.41718975 2.40096969,8.70045104 Z" />
<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>
);
}

View File

@@ -1,52 +0,0 @@
// @flow
import React, { Component } from 'react';
import ImageIcon from 'components/Icon/ImageIcon';
import BulletedListIcon from 'components/Icon/BulletedListIcon';
import HorizontalRuleIcon from 'components/Icon/HorizontalRuleIcon';
import TodoListIcon from 'components/Icon/TodoListIcon';
import { observer } from 'mobx-react';
import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
@observer class BlockMenu extends Component {
props: {
label?: React$Element<*>,
onPickImage: SyntheticEvent => void,
onInsertList: SyntheticEvent => void,
onInsertTodoList: SyntheticEvent => void,
onInsertBreak: SyntheticEvent => void,
};
render() {
const {
label,
onPickImage,
onInsertList,
onInsertTodoList,
onInsertBreak,
...rest
} = this.props;
return (
<DropdownMenu
style={{ marginRight: -70, marginTop: 5 }}
label={label}
{...rest}
>
<DropdownMenuItem onClick={onPickImage}>
<ImageIcon /> <span>Add images</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={onInsertList}>
<BulletedListIcon /> Start list
</DropdownMenuItem>
<DropdownMenuItem onClick={onInsertTodoList}>
<TodoListIcon /> Start checklist
</DropdownMenuItem>
<DropdownMenuItem onClick={onInsertBreak}>
<HorizontalRuleIcon /> Add break
</DropdownMenuItem>
</DropdownMenu>
);
}
}
export default BlockMenu;