From 14326d89f2ae4628bacd99ac9d214ab435f8ef67 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Tue, 31 Oct 2017 22:17:14 -0700 Subject: [PATCH 1/5] WIP --- .../Editor/components/BlockInsert.js | 24 +----- .../Editor/components/BlockToolbar.js | 84 +++++++++++++++++++ app/components/Editor/schema.js | 2 + app/components/Icon/CollectionIcon.js | 2 +- app/components/Icon/KeyboardIcon.js | 12 +++ app/components/Icon/OrderedListIcon.js | 2 +- 6 files changed, 103 insertions(+), 23 deletions(-) create mode 100644 app/components/Editor/components/BlockToolbar.js create mode 100644 app/components/Icon/KeyboardIcon.js diff --git a/app/components/Editor/components/BlockInsert.js b/app/components/Editor/components/BlockInsert.js index 8988881a8..63a9288ba 100644 --- a/app/components/Editor/components/BlockInsert.js +++ b/app/components/Editor/components/BlockInsert.js @@ -8,7 +8,6 @@ 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; @@ -144,26 +143,9 @@ export default class BlockInsert extends Component { return ( - (this.file = ref)} - onChange={this.onChooseImage} - accept="image/*" - /> - } - 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} + + this.insertBlock(ev, { type: 'block-toolbar', isVoid: true })} /> diff --git a/app/components/Editor/components/BlockToolbar.js b/app/components/Editor/components/BlockToolbar.js new file mode 100644 index 000000000..08e974a38 --- /dev/null +++ b/app/components/Editor/components/BlockToolbar.js @@ -0,0 +1,84 @@ +// @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/schema.js b/app/components/Editor/schema.js index 2010e4c22..295ae01ae 100644 --- a/app/components/Editor/schema.js +++ b/app/components/Editor/schema.js @@ -16,6 +16,7 @@ import { Heading6, } from './components/Heading'; import Paragraph from './components/Paragraph'; +import BlockToolbar from './components/BlockToolbar'; import type { Props, Node, Transform } from './types'; const createSchema = () => { @@ -30,6 +31,7 @@ const createSchema = () => { }, nodes: { + 'block-toolbar': (props: Props) => , paragraph: (props: Props) => , 'block-quote': (props: Props) => (
{props.children}
diff --git a/app/components/Icon/CollectionIcon.js b/app/components/Icon/CollectionIcon.js index 411d0920f..948ccec65 100644 --- a/app/components/Icon/CollectionIcon.js +++ b/app/components/Icon/CollectionIcon.js @@ -10,7 +10,7 @@ export default function CollectionIcon({ return ( {expanded - ? + ? : } ); diff --git a/app/components/Icon/KeyboardIcon.js b/app/components/Icon/KeyboardIcon.js new file mode 100644 index 000000000..d11264b05 --- /dev/null +++ b/app/components/Icon/KeyboardIcon.js @@ -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 ( + + + + ); +} diff --git a/app/components/Icon/OrderedListIcon.js b/app/components/Icon/OrderedListIcon.js index 855794bea..488520e52 100644 --- a/app/components/Icon/OrderedListIcon.js +++ b/app/components/Icon/OrderedListIcon.js @@ -6,7 +6,7 @@ import type { Props } from './Icon'; export default function OrderedListIcon(props: Props) { return ( - + ); } From 51bc705488428bce8cc5c0b49b591f55df5790a7 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 5 Nov 2017 22:48:31 -0800 Subject: [PATCH 2/5] Working --- app/components/Editor/Editor.js | 5 +- .../Editor/components/BlockInsert.js | 76 ++------ .../Editor/components/BlockToolbar.js | 84 -------- .../Editor/components/HorizontalRule.js | 1 + .../Editor/components/Toolbar/BlockToolbar.js | 182 ++++++++++++++++++ app/components/Editor/schema.js | 17 +- app/components/Editor/transforms.js | 34 ++++ app/components/Icon/Heading1Icon.js | 2 +- app/components/Icon/Heading2Icon.js | 2 +- app/menus/BlockMenu.js | 52 ----- 10 files changed, 252 insertions(+), 203 deletions(-) delete mode 100644 app/components/Editor/components/BlockToolbar.js create mode 100644 app/components/Editor/components/Toolbar/BlockToolbar.js create mode 100644 app/components/Editor/transforms.js delete mode 100644 app/menus/BlockMenu.js 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; From 5d716d9c5f7e743afdd49be5836f0b485aa3836f Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 8 Nov 2017 00:08:35 -0800 Subject: [PATCH 3/5] 99% there --- .../Editor/components/BlockInsert.js | 111 ++++++++++-------- app/components/Editor/components/Code.js | 4 +- app/components/Editor/components/Heading.js | 4 +- .../Editor/components/HorizontalRule.js | 4 +- app/components/Editor/components/ListItem.js | 7 +- app/components/Editor/components/Paragraph.js | 2 +- app/components/Editor/components/TodoItem.js | 4 +- .../Editor/components/Toolbar/BlockToolbar.js | 26 ++-- app/components/Editor/schema.js | 30 +++-- app/components/Editor/transforms.js | 9 +- 10 files changed, 122 insertions(+), 79 deletions(-) diff --git a/app/components/Editor/components/BlockInsert.js b/app/components/Editor/components/BlockInsert.js index 21874d100..fa08e1aa2 100644 --- a/app/components/Editor/components/BlockInsert.js +++ b/app/components/Editor/components/BlockInsert.js @@ -1,14 +1,13 @@ // @flow import React, { Component } from 'react'; -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 type { State } from '../types'; -import { splitAndInsertBlock } from '../transforms'; type Props = { state: State, @@ -16,83 +15,95 @@ type Props = { 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; + mouseMovementSinceClick: number = 0; + mouseClickX: number = 0; + mouseClickY: 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('mousedown', this.handleWindowClick); window.addEventListener('mousemove', this.handleMouseMove); }; - componentWillUpdate = (nextProps: Props) => { - this.update(nextProps); - }; - componentWillUnmount = () => { + window.removeEventListener('mousedown', this.handleWindowClick); 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 movementToReset = 1000; - if (active !== this.active) { - this.active = active || this.menuOpen; + this.mouseMovementSinceClick += Math.abs(this.mouseClickX - ev.clientX); + this.active = + ev.clientX < windowWidth && + this.mouseMovementSinceClick > movementToReset; + + if (result) { + this.closestRootNode = result.node; + + // do not show block menu on title heading or editor + const { type } = result.node; + if (type === 'heading1' || 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); + handleWindowClick = (ev: SyntheticMouseEvent) => { + this.mouseClickX = ev.clientX; + this.mouseClickY = ev.clientY; + this.mouseMovementSinceClick = 0; + this.active = false; }; handleClick = () => { - const transform = splitAndInsertBlock(this.props.state, { - type: { type: 'block-toolbar', isVoid: true }, + const { state } = this.props; + const type = { type: 'block-toolbar', isVoid: true }; + let transform = state.transform(); + + // 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); + } }); - const state = transform.apply(); - this.props.onChange(state); - this.active = false; + + transform.collapseToStartOf(this.closestRootNode).insertBlock(type); + + this.props.onChange(transform.apply()); }; render() { @@ -101,7 +112,7 @@ export default class BlockInsert extends Component { return ( - + ); @@ -113,13 +124,13 @@ const Trigger = styled.div` 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: -3px; margin-left: -10px; - box-shadow: inset 0 0 0 2px ${color.slateDark}; + box-shadow: inset 0 0 0 2px ${color.slate}; border-radius: 100%; transform: scale(.9); + cursor: pointer; &:hover { background-color: ${color.smokeDark}; diff --git a/app/components/Editor/components/Code.js b/app/components/Editor/components/Code.js index 883d57535..e37455be2 100644 --- a/app/components/Editor/components/Code.js +++ b/app/components/Editor/components/Code.js @@ -9,10 +9,10 @@ export default function Code({ children, node, readOnly, attributes }: Props) { const language = node.data.get('language') || 'javascript'; return ( - + {readOnly && }
-        
+        
           {children}
         
       
diff --git a/app/components/Editor/components/Heading.js b/app/components/Editor/components/Heading.js index 29a7ccc37..72e97e0ac 100644 --- a/app/components/Editor/components/Heading.js +++ b/app/components/Editor/components/Heading.js @@ -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 ( - + {children} diff --git a/app/components/Editor/components/HorizontalRule.js b/app/components/Editor/components/HorizontalRule.js index f01d1da35..7c6d8a68e 100644 --- a/app/components/Editor/components/HorizontalRule.js +++ b/app/components/Editor/components/HorizontalRule.js @@ -5,9 +5,9 @@ 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 ; + return ; } const StyledHr = styled.hr` diff --git a/app/components/Editor/components/ListItem.js b/app/components/Editor/components/ListItem.js index 11227c2d4..defea91d3 100644 --- a/app/components/Editor/components/ListItem.js +++ b/app/components/Editor/components/ListItem.js @@ -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 ( - + {children} ); } - return
  • {children}
  • ; + return
  • {children}
  • ; } diff --git a/app/components/Editor/components/Paragraph.js b/app/components/Editor/components/Paragraph.js index 1716373b1..101227609 100644 --- a/app/components/Editor/components/Paragraph.js +++ b/app/components/Editor/components/Paragraph.js @@ -23,7 +23,7 @@ export default function Link({ !node.text; return ( -

    +

    {children} {showPlaceholder && diff --git a/app/components/Editor/components/TodoItem.js b/app/components/Editor/components/TodoItem.js index 7b18c7b88..a26182ce7 100644 --- a/app/components/Editor/components/TodoItem.js +++ b/app/components/Editor/components/TodoItem.js @@ -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 ( - + { - let transform = splitAndInsertBlock(this.props.state, options); + @keydown('esc') + removeSelf(ev: SyntheticEvent) { + ev.preventDefault(); + ev.stopPropagation(); - this.props.state.document.nodes.forEach(node => { + 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); } @@ -61,7 +74,6 @@ class BlockToolbar extends Component { handleClickBlock = (ev: SyntheticEvent, type: string) => { ev.preventDefault(); - ev.stopPropagation(); switch (type) { case 'heading1': @@ -114,11 +126,11 @@ class BlockToolbar extends Component { }; render() { - const { state, node } = this.props; + const { state, attributes, node } = this.props; const active = state.isFocused && state.selection.hasEdgeIn(node); return ( - + (this.file = ref)} diff --git a/app/components/Editor/schema.js b/app/components/Editor/schema.js index 705d3dc63..d4ed674b6 100644 --- a/app/components/Editor/schema.js +++ b/app/components/Editor/schema.js @@ -45,16 +45,30 @@ const createSchema = ({ onInsertImage, onChange }: Options) => { ), paragraph: (props: Props) => , 'block-quote': (props: Props) => ( -

    {props.children}
    +
    {props.children}
    ), 'horizontal-rule': HorizontalRule, - 'bulleted-list': (props: Props) =>
      {props.children}
    , - 'ordered-list': (props: Props) =>
      {props.children}
    , - 'todo-list': (props: Props) => {props.children}, - table: (props: Props) => {props.children}
    , - 'table-row': (props: Props) => {props.children}, - 'table-head': (props: Props) => {props.children}, - 'table-cell': (props: Props) => {props.children}, + 'bulleted-list': (props: Props) => ( +
      {props.children}
    + ), + 'ordered-list': (props: Props) => ( +
      {props.children}
    + ), + 'todo-list': (props: Props) => ( + {props.children} + ), + table: (props: Props) => ( + {props.children}
    + ), + 'table-row': (props: Props) => ( + {props.children} + ), + 'table-head': (props: Props) => ( + {props.children} + ), + 'table-cell': (props: Props) => ( + {props.children} + ), code: Code, image: Image, link: Link, diff --git a/app/components/Editor/transforms.js b/app/components/Editor/transforms.js index 35ef03e9b..d404c570f 100644 --- a/app/components/Editor/transforms.js +++ b/app/components/Editor/transforms.js @@ -1,6 +1,6 @@ // @flow import EditList from './plugins/EditList'; -import type { State } from './types'; +import type { State, Transform } from './types'; const { transforms } = EditList; @@ -10,9 +10,12 @@ type Options = { append?: string | Object, }; -export function splitAndInsertBlock(state: State, options: Options) { +export function splitAndInsertBlock( + transform: Transform, + state: State, + options: Options +) { const { type, wrapper, append } = options; - let transform = state.transform(); const { document } = state; const parent = document.getParent(state.startBlock.key); From e98caf6e5173c858b07102137a71749580762c30 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 8 Nov 2017 23:19:26 -0800 Subject: [PATCH 4/5] Final polish --- .../Editor/components/BlockInsert.js | 32 ++++++++++--------- .../Editor/components/HorizontalRule.js | 2 ++ .../Editor/components/Toolbar/Toolbar.js | 2 +- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/app/components/Editor/components/BlockInsert.js b/app/components/Editor/components/BlockInsert.js index fa08e1aa2..475b97577 100644 --- a/app/components/Editor/components/BlockInsert.js +++ b/app/components/Editor/components/BlockInsert.js @@ -31,8 +31,8 @@ export default class BlockInsert extends Component { props: Props; mouseMoveTimeout: number; mouseMovementSinceClick: number = 0; - mouseClickX: number = 0; - mouseClickY: number = 0; + lastClientX: number = 0; + lastClientY: number = 0; @observable closestRootNode: Node; @observable active: boolean = false; @@ -40,12 +40,10 @@ export default class BlockInsert extends Component { @observable left: number; componentDidMount = () => { - window.addEventListener('mousedown', this.handleWindowClick); window.addEventListener('mousemove', this.handleMouseMove); }; componentWillUnmount = () => { - window.removeEventListener('mousedown', this.handleWindowClick); window.removeEventListener('mousemove', this.handleMouseMove); }; @@ -56,19 +54,24 @@ export default class BlockInsert extends Component { handleMouseMove = (ev: SyntheticMouseEvent) => { const windowWidth = window.innerWidth / 2.5; const result = findClosestRootNode(this.props.state, ev); - const movementToReset = 1000; + const movementThreshold = 200; + + this.mouseMovementSinceClick += + Math.abs(this.lastClientX - ev.clientX) + + Math.abs(this.lastClientY - ev.clientY); + this.lastClientX = ev.clientX; + this.lastClientY = ev.clientY; - this.mouseMovementSinceClick += Math.abs(this.mouseClickX - ev.clientX); this.active = ev.clientX < windowWidth && - this.mouseMovementSinceClick > movementToReset; + this.mouseMovementSinceClick > movementThreshold; if (result) { this.closestRootNode = result.node; // do not show block menu on title heading or editor - const { type } = result.node; - if (type === 'heading1' || type === 'block-toolbar') { + 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); @@ -82,14 +85,10 @@ export default class BlockInsert extends Component { } }; - handleWindowClick = (ev: SyntheticMouseEvent) => { - this.mouseClickX = ev.clientX; - this.mouseClickY = ev.clientY; + handleClick = (ev: SyntheticMouseEvent) => { this.mouseMovementSinceClick = 0; this.active = false; - }; - handleClick = () => { const { state } = this.props; const type = { type: 'block-toolbar', isVoid: true }; let transform = state.transform(); @@ -101,7 +100,10 @@ export default class BlockInsert extends Component { } }); - transform.collapseToStartOf(this.closestRootNode).insertBlock(type); + transform + .collapseToStartOf(this.closestRootNode) + .collapseToEndOfPreviousBlock() + .insertBlock(type); this.props.onChange(transform.apply()); }; diff --git a/app/components/Editor/components/HorizontalRule.js b/app/components/Editor/components/HorizontalRule.js index 7c6d8a68e..79cd52c39 100644 --- a/app/components/Editor/components/HorizontalRule.js +++ b/app/components/Editor/components/HorizontalRule.js @@ -11,6 +11,8 @@ function HorizontalRule(props: Props) { } const StyledHr = styled.hr` + padding-top: .75em; + margin: 0; border: 0; border-bottom: 1px solid ${props => (props.active ? color.slate : color.slateLight)}; `; diff --git a/app/components/Editor/components/Toolbar/Toolbar.js b/app/components/Editor/components/Toolbar/Toolbar.js index 281d40cbc..d34966d90 100644 --- a/app/components/Editor/components/Toolbar/Toolbar.js +++ b/app/components/Editor/components/Toolbar/Toolbar.js @@ -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; From b5dad27b4ab92e20e882a5a58a66ec05d9a7f3b4 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 8 Nov 2017 23:21:32 -0800 Subject: [PATCH 5/5] :shirt: --- app/components/Editor/types.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/components/Editor/types.js b/app/components/Editor/types.js index 4406f20f9..51562825a 100644 --- a/app/components/Editor/types.js +++ b/app/components/Editor/types.js @@ -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,