This commit is contained in:
Tom Moor
2017-11-05 22:48:31 -08:00
parent 14326d89f2
commit 51bc705488
10 changed files with 252 additions and 203 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,6 +1,5 @@
// @flow
import React, { Component } from 'react';
import EditList from '../plugins/EditList';
import getDataTransferFiles from 'utils/getDataTransferFiles';
import Portal from 'react-portal';
import { observable } from 'mobx';
@@ -9,8 +8,7 @@ import styled from 'styled-components';
import { color } from 'shared/styles/constants';
import PlusIcon from 'components/Icon/PlusIcon';
import type { State } from '../types';
const { transforms } = EditList;
import { splitAndInsertBlock } from '../transforms';
type Props = {
state: State,
@@ -22,7 +20,6 @@ type Props = {
export default class BlockInsert extends Component {
props: Props;
mouseMoveTimeout: number;
file: HTMLInputElement;
@observable active: boolean = false;
@observable menuOpen: boolean = false;
@@ -89,77 +86,28 @@ export default class BlockInsert extends Component {
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();
handleClick = () => {
const transform = splitAndInsertBlock(this.props.state, {
type: { type: 'block-toolbar', isVoid: true },
});
const state = transform.apply();
this.props.onChange(state);
this.active = false;
};
onPickImage = (ev: SyntheticEvent) => {
// simulate a click on the file upload input element
this.file.click();
};
onChooseImage = async (ev: SyntheticEvent) => {
const files = getDataTransferFiles(ev);
for (const file of files) {
await this.props.onInsertImage(file);
}
};
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}>
<PlusIcon
onClick={ev =>
this.insertBlock(ev, { type: 'block-toolbar', isVoid: true })}
/>
<PlusIcon onClick={this.handleClick} />
</Trigger>
</Portal>
);
}
}
const HiddenInput = styled.input`
position: absolute;
top: -100px;
left: -100px;
visibility: hidden;
`;
const Trigger = styled.div`
position: absolute;
z-index: 1;
@@ -167,10 +115,16 @@ const Trigger = styled.div`
background-color: ${color.white};
transition: opacity 250ms ease-in-out, transform 250ms ease-in-out;
line-height: 0;
margin-top: -2px;
margin-left: -4px;
margin-top: -3px;
margin-left: -10px;
box-shadow: inset 0 0 0 2px ${color.slateDark};
border-radius: 100%;
transform: scale(.9);
&:hover {
background-color: ${color.smokeDark};
}
${({ active }) => active && `
transform: scale(1);
opacity: .9;

View File

@@ -1,84 +0,0 @@
// @flow
import React, { Component } from 'react';
import styled from 'styled-components';
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 type { Props } from '../types';
import { color } from 'shared/styles/constants';
import ToolbarButton from './Toolbar/components/ToolbarButton';
class BlockToolbar extends Component {
props: Props;
onClickBlock = (ev: SyntheticEvent, type: string) => {
// TODO
};
renderBlockButton = (type: string, IconClass: Function) => {
return (
<ToolbarButton onMouseDown={ev => this.onClickBlock(ev, type)}>
<IconClass color={color.text} />
</ToolbarButton>
);
};
render() {
const { state, node } = this.props;
const active = state.isFocused && state.selection.hasEdgeIn(node);
return (
<Bar active={active}>
{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)`
position: relative;
align-items: center;
background: ${color.smoke};
padding: 10px 0;
height: 44px;
&:before,
&:after {
content: "";
position: absolute;
left: -100%;
width: 100%;
height: 44px;
background: ${color.smoke};
}
&:after {
left: auto;
right: -100%;
}
`;
export default BlockToolbar;

View File

@@ -11,6 +11,7 @@ function HorizontalRule(props: Props) {
}
const StyledHr = styled.hr`
border: 0;
border-bottom: 1px solid ${props => (props.active ? color.slate : color.slateLight)};
`;

View File

@@ -0,0 +1,182 @@
// @flow
import React, { Component } from 'react';
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) {
console.log('becameInactive');
const state = nextProps.state
.transform()
.removeNodeByKey(nextProps.node.key)
.apply();
this.props.onChange(state);
}
}
insertBlock = (options: Options) => {
let transform = splitAndInsertBlock(this.props.state, options);
this.props.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();
ev.stopPropagation();
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, node } = this.props;
const active = state.isFocused && state.selection.hasEdgeIn(node);
return (
<Bar active={active}>
<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

@@ -16,10 +16,15 @@ import {
Heading6,
} from './components/Heading';
import Paragraph from './components/Paragraph';
import BlockToolbar from './components/BlockToolbar';
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>,
@@ -31,7 +36,13 @@ const createSchema = () => {
},
nodes: {
'block-toolbar': (props: Props) => <BlockToolbar {...props} />,
'block-toolbar': (props: Props) => (
<BlockToolbar
onChange={onChange}
onInsertImage={onInsertImage}
{...props}
/>
),
paragraph: (props: Props) => <Paragraph {...props} />,
'block-quote': (props: Props) => (
<blockquote>{props.children}</blockquote>

View File

@@ -0,0 +1,34 @@
// @flow
import EditList from './plugins/EditList';
import type { State } from './types';
const { transforms } = EditList;
type Options = {
type: string | Object,
wrapper?: string | Object,
append?: string | Object,
};
export function splitAndInsertBlock(state: State, options: Options) {
const { type, wrapper, append } = options;
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);
return transform;
}