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;