diff --git a/app/components/Editor/Editor.js b/app/components/Editor/Editor.js
index e556841d7..b6f5403e2 100644
--- a/app/components/Editor/Editor.js
+++ b/app/components/Editor/Editor.js
@@ -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,
diff --git a/app/components/Editor/components/BlockInsert.js b/app/components/Editor/components/BlockInsert.js
index 63a9288ba..21874d100 100644
--- a/app/components/Editor/components/BlockInsert.js
+++ b/app/components/Editor/components/BlockInsert.js
@@ -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 (
-
- this.insertBlock(ev, { type: 'block-toolbar', isVoid: true })}
- />
+
);
}
}
-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;
diff --git a/app/components/Editor/components/BlockToolbar.js b/app/components/Editor/components/BlockToolbar.js
deleted file mode 100644
index 08e974a38..000000000
--- a/app/components/Editor/components/BlockToolbar.js
+++ /dev/null
@@ -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 (
- this.onClickBlock(ev, type)}>
-
-
- );
- };
-
- render() {
- const { state, node } = this.props;
- const active = state.isFocused && state.selection.hasEdgeIn(node);
-
- return (
-
- {this.renderBlockButton('heading1', Heading1Icon)}
- {this.renderBlockButton('heading2', Heading2Icon)}
-
- {this.renderBlockButton('bulleted-list', BulletedListIcon)}
- {this.renderBlockButton('ordered-list', OrderedListIcon)}
- {this.renderBlockButton('todo-list', TodoListIcon)}
-
- {this.renderBlockButton('code', CodeIcon)}
- {this.renderBlockButton('horizontal-rule', HorizontalRuleIcon)}
- {this.renderBlockButton('image', ImageIcon)}
-
- );
- }
-}
-
-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;
diff --git a/app/components/Editor/components/HorizontalRule.js b/app/components/Editor/components/HorizontalRule.js
index 49cf4ca0f..f01d1da35 100644
--- a/app/components/Editor/components/HorizontalRule.js
+++ b/app/components/Editor/components/HorizontalRule.js
@@ -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)};
`;
diff --git a/app/components/Editor/components/Toolbar/BlockToolbar.js b/app/components/Editor/components/Toolbar/BlockToolbar.js
new file mode 100644
index 000000000..a15282a0c
--- /dev/null
+++ b/app/components/Editor/components/Toolbar/BlockToolbar.js
@@ -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 (
+ this.handleClickBlock(ev, type)}>
+
+
+ );
+ };
+
+ render() {
+ const { state, node } = this.props;
+ const active = state.isFocused && state.selection.hasEdgeIn(node);
+
+ return (
+
+ (this.file = ref)}
+ onChange={this.onImagePicked}
+ accept="image/*"
+ />
+ {this.renderBlockButton('heading1', Heading1Icon)}
+ {this.renderBlockButton('heading2', Heading2Icon)}
+
+ {this.renderBlockButton('bulleted-list', BulletedListIcon)}
+ {this.renderBlockButton('ordered-list', OrderedListIcon)}
+ {this.renderBlockButton('todo-list', TodoListIcon)}
+
+ {this.renderBlockButton('code', CodeIcon)}
+ {this.renderBlockButton('horizontal-rule', HorizontalRuleIcon)}
+ {this.renderBlockButton('image', ImageIcon)}
+
+ );
+ }
+}
+
+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;
diff --git a/app/components/Editor/schema.js b/app/components/Editor/schema.js
index 295ae01ae..705d3dc63 100644
--- a/app/components/Editor/schema.js
+++ b/app/components/Editor/schema.js
@@ -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) => {props.children},
@@ -31,7 +36,13 @@ const createSchema = () => {
},
nodes: {
- 'block-toolbar': (props: Props) => ,
+ 'block-toolbar': (props: Props) => (
+
+ ),
paragraph: (props: Props) => ,
'block-quote': (props: Props) => (
{props.children}
diff --git a/app/components/Editor/transforms.js b/app/components/Editor/transforms.js
new file mode 100644
index 000000000..35ef03e9b
--- /dev/null
+++ b/app/components/Editor/transforms.js
@@ -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;
+}
diff --git a/app/components/Icon/Heading1Icon.js b/app/components/Icon/Heading1Icon.js
index 41fd8552f..1fddb1075 100644
--- a/app/components/Icon/Heading1Icon.js
+++ b/app/components/Icon/Heading1Icon.js
@@ -6,7 +6,7 @@ import type { Props } from './Icon';
export default function Heading1Icon(props: Props) {
return (
-
+
);
}
diff --git a/app/components/Icon/Heading2Icon.js b/app/components/Icon/Heading2Icon.js
index 567fa4181..37a269aa8 100644
--- a/app/components/Icon/Heading2Icon.js
+++ b/app/components/Icon/Heading2Icon.js
@@ -6,7 +6,7 @@ import type { Props } from './Icon';
export default function Heading2Icon(props: Props) {
return (
-
+
);
}
diff --git a/app/menus/BlockMenu.js b/app/menus/BlockMenu.js
deleted file mode 100644
index 08192ce35..000000000
--- a/app/menus/BlockMenu.js
+++ /dev/null
@@ -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 (
-
-
- Add images
-
-
- Start list
-
-
- Start checklist
-
-
- Add break
-
-
- );
- }
-}
-
-export default BlockMenu;