From 5d716d9c5f7e743afdd49be5836f0b485aa3836f Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 8 Nov 2017 00:08:35 -0800 Subject: [PATCH] 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);