Working
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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)};
|
||||
`;
|
||||
|
||||
|
||||
182
app/components/Editor/components/Toolbar/BlockToolbar.js
Normal file
182
app/components/Editor/components/Toolbar/BlockToolbar.js
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
34
app/components/Editor/transforms.js
Normal file
34
app/components/Editor/transforms.js
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user