From 15e8e506019d683dc16285690c5ca760a44dd905 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sat, 2 Dec 2017 23:14:27 -0800 Subject: [PATCH 01/18] Slate 30 --- .flowconfig | 3 + app/components/Collaborators/Collaborators.js | 4 +- app/components/Editor/Editor.js | 114 ++++---- .../Editor/components/BlockInsert.js | 39 ++- app/components/Editor/components/Code.js | 4 +- app/components/Editor/components/Contents.js | 13 +- app/components/Editor/components/Heading.js | 7 +- .../Editor/components/HorizontalRule.js | 4 +- app/components/Editor/components/Image.js | 16 +- app/components/Editor/components/Link.js | 8 +- app/components/Editor/components/ListItem.js | 4 +- app/components/Editor/components/Paragraph.js | 4 +- app/components/Editor/components/TodoItem.js | 13 +- .../Editor/components/Toolbar/BlockToolbar.js | 49 ++-- .../Editor/components/Toolbar/Toolbar.js | 23 +- .../Toolbar/components/FormattingToolbar.js | 37 +-- .../Toolbar/components/LinkToolbar.js | 26 +- app/components/Editor/headingToSlug.js | 4 +- app/components/Editor/insertImage.js | 11 +- app/components/Editor/marks.js | 22 ++ app/components/Editor/nodes.js | 75 +++++ app/components/Editor/plugins.js | 28 +- .../Editor/plugins/KeyboardShortcuts.js | 25 +- .../Editor/plugins/MarkdownShortcuts.js | 126 ++++----- app/components/Editor/schema.js | 266 +++++++++--------- app/components/Editor/transforms.js | 25 +- app/components/Editor/types.js | 118 -------- package.json | 20 +- yarn.lock | 139 ++++++--- 29 files changed, 617 insertions(+), 610 deletions(-) create mode 100644 app/components/Editor/marks.js create mode 100644 app/components/Editor/nodes.js delete mode 100644 app/components/Editor/types.js diff --git a/.flowconfig b/.flowconfig index 7c57deb48..64330e697 100644 --- a/.flowconfig +++ b/.flowconfig @@ -8,6 +8,9 @@ .*/node_modules/polished/.* .*/node_modules/react-side-effect/.* .*/node_modules/fbjs/.* +.*/node_modules/slate-edit-code/.* +.*/node_modules/slate-edit-list/.* +.*/node_modules/slate-prism/.* .*/node_modules/config-chain/.* *.test.js diff --git a/app/components/Collaborators/Collaborators.js b/app/components/Collaborators/Collaborators.js index d7eaf16e2..88b88cfb4 100644 --- a/app/components/Collaborators/Collaborators.js +++ b/app/components/Collaborators/Collaborators.js @@ -29,8 +29,8 @@ const Collaborators = ({ document }: Props) => { {collaborators.map(user => ( - - + + ))} diff --git a/app/components/Editor/Editor.js b/app/components/Editor/Editor.js index 8fa84d74e..fdb35519d 100644 --- a/app/components/Editor/Editor.js +++ b/app/components/Editor/Editor.js @@ -2,9 +2,10 @@ import React, { Component } from 'react'; import { observable } from 'mobx'; import { observer } from 'mobx-react'; -import { Editor, Plain } from 'slate'; +import { Editor } from 'slate-react'; +import type { state, props, change } from 'slate-prop-types'; +import Plain from 'slate-plain-serializer'; import keydown from 'react-keydown'; -import type { State, Editor as EditorType } from './types'; import getDataTransferFiles from 'utils/getDataTransferFiles'; import Flex from 'shared/components/Flex'; import ClickablePadding from './components/ClickablePadding'; @@ -13,18 +14,19 @@ import BlockInsert from './components/BlockInsert'; import Placeholder from './components/Placeholder'; import Contents from './components/Contents'; import Markdown from './serializer'; -import createSchema from './schema'; import createPlugins from './plugins'; import insertImage from './insertImage'; +import renderMark from './marks'; +import createRenderNode from './nodes'; import styled from 'styled-components'; type Props = { text: string, - onChange: Function, - onSave: Function, - onCancel: Function, - onImageUploadStart: Function, - onImageUploadStop: Function, + onChange: change => *, + onSave: (redirect?: boolean) => *, + onCancel: () => void, + onImageUploadStart: () => void, + onImageUploadStop: () => void, emoji?: string, readOnly: boolean, }; @@ -37,15 +39,15 @@ type KeyData = { @observer class MarkdownEditor extends Component { props: Props; - editor: EditorType; - schema: Object; - plugins: Array; - @observable editorState: State; + editor: Editor; + renderNode: props => *; + plugins: Object[]; + @observable editorValue: state; constructor(props: Props) { super(props); - this.schema = createSchema({ + this.renderNode = createRenderNode({ onInsertImage: this.insertImageFile, onChange: this.onChange, }); @@ -55,9 +57,9 @@ class MarkdownEditor extends Component { }); if (props.text.trim().length) { - this.editorState = Markdown.deserialize(props.text); + this.editorValue = Markdown.deserialize(props.text); } else { - this.editorState = Plain.deserialize(''); + this.editorValue = Plain.deserialize(''); } } @@ -77,12 +79,11 @@ class MarkdownEditor extends Component { } } - onChange = (editorState: State) => { - if (this.editorState !== editorState) { - this.props.onChange(Markdown.serialize(editorState)); + onChange = (change: change) => { + if (this.editorValue !== change.value) { + this.props.onChange(Markdown.serialize(change.value)); } - - this.editorState = editorState; + this.editorValue = change.value; }; handleDrop = async (ev: SyntheticEvent) => { @@ -103,17 +104,16 @@ class MarkdownEditor extends Component { }; insertImageFile = async (file: window.File) => { - const state = this.editor.getState(); - let transform = state.transform(); - - transform = await insertImage( - transform, - file, - this.editor, - this.props.onImageUploadStart, - this.props.onImageUploadStop + this.editor.change( + async change => + await insertImage( + change, + file, + this.editor, + this.props.onImageUploadStart, + this.props.onImageUploadStop + ) ); - this.editor.onChange(transform.apply()); }; cancelEvent = (ev: SyntheticEvent) => { @@ -136,7 +136,7 @@ class MarkdownEditor extends Component { ev.preventDefault(); ev.stopPropagation(); - this.props.onSave({ redirect: false }); + this.props.onSave(true); } @keydown('esc') @@ -146,37 +146,33 @@ class MarkdownEditor extends Component { } // Handling of keyboard shortcuts within editor focus - onKeyDown = (ev: SyntheticKeyboardEvent, data: KeyData, state: State) => { + onKeyDown = (ev: SyntheticKeyboardEvent, data: KeyData, change: change) => { if (!data.isMeta) return; switch (data.key) { case 's': this.onSave(ev); - return state; + return change; case 'enter': this.onSaveAndExit(ev); - return state; + return change; case 'escape': this.onCancel(); - return state; + return change; default: } }; focusAtStart = () => { - const state = this.editor.getState(); - const transform = state.transform(); - transform.collapseToStartOf(state.document); - transform.focus(); - this.editorState = transform.apply(); + this.editor.change(change => + change.collapseToStartOf(change.value.document).focus() + ); }; focusAtEnd = () => { - const state = this.editor.getState(); - const transform = state.transform(); - transform.collapseToEndOf(state.document); - transform.focus(); - this.editorState = transform.apply(); + this.editor.change(change => + change.collapseToEndOf(change.value.document).focus() + ); }; render = () => { @@ -193,25 +189,27 @@ class MarkdownEditor extends Component { >
- {readOnly && } - {!readOnly && ( - - )} - {!readOnly && ( - - )} + {readOnly && this.editor && } + {!readOnly && + this.editor && ( + + )} + {!readOnly && + this.editor && ( + + )} (this.editor = ref)} placeholder="Start with a title…" bodyPlaceholder="…the rest is your canvas" - schema={this.schema} plugins={this.plugins} emoji={emoji} - state={this.editorState} + value={this.editorValue} + renderNode={this.renderNode} + renderMark={renderMark} onKeyDown={this.onKeyDown} onChange={this.onChange} onSave={onSave} diff --git a/app/components/Editor/components/BlockInsert.js b/app/components/Editor/components/BlockInsert.js index fe7ac38e8..02673b0e6 100644 --- a/app/components/Editor/components/BlockInsert.js +++ b/app/components/Editor/components/BlockInsert.js @@ -1,18 +1,16 @@ // @flow import React, { Component } from 'react'; import { Portal } from 'react-portal'; -import { findDOMNode, Node } from 'slate'; +import { Node } from 'slate'; +import { Editor, findDOMNode } from 'slate-react'; 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'; type Props = { - state: State, - onChange: Function, - onInsertImage: File => Promise<*>, + editor: Editor, }; function findClosestRootNode(state, ev) { @@ -53,7 +51,7 @@ export default class BlockInsert extends Component { handleMouseMove = (ev: SyntheticMouseEvent) => { const windowWidth = window.innerWidth / 2.5; - const result = findClosestRootNode(this.props.state, ev); + const result = findClosestRootNode(this.props.editor.value, ev); const movementThreshold = 200; this.mouseMovementSinceClick += @@ -70,7 +68,7 @@ export default class BlockInsert extends Component { this.closestRootNode = result.node; // do not show block menu on title heading or editor - const firstNode = this.props.state.document.nodes.first(); + const firstNode = this.props.editor.value.document.nodes.first(); if (result.node === firstNode || result.node.type === 'block-toolbar') { this.left = -1000; } else { @@ -89,23 +87,22 @@ export default class BlockInsert extends Component { this.mouseMovementSinceClick = 0; this.active = false; - const { state } = this.props; + const { editor } = 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); - } + editor.change(change => { + // remove any existing toolbars in the document as a fail safe + editor.value.document.nodes.forEach(node => { + if (node.type === 'block-toolbar') { + change.removeNodeByKey(node.key); + } + }); + + change + .collapseToStartOf(this.closestRootNode) + .collapseToEndOfPreviousBlock() + .insertBlock(type); }); - - transform - .collapseToStartOf(this.closestRootNode) - .collapseToEndOfPreviousBlock() - .insertBlock(type); - - this.props.onChange(transform.apply()); }; render() { diff --git a/app/components/Editor/components/Code.js b/app/components/Editor/components/Code.js index 99cef6ba6..7c2118eeb 100644 --- a/app/components/Editor/components/Code.js +++ b/app/components/Editor/components/Code.js @@ -1,11 +1,11 @@ // @flow import React from 'react'; import styled from 'styled-components'; +import type { props } from 'slate-prop-types'; import CopyButton from './CopyButton'; import { color } from 'shared/styles/constants'; -import type { Props } from '../types'; -export default function Code({ children, node, readOnly, attributes }: Props) { +export default function Code({ children, node, readOnly, attributes }: props) { const language = node.data.get('language') || 'javascript'; return ( diff --git a/app/components/Editor/components/Contents.js b/app/components/Editor/components/Contents.js index b92ef7ceb..81dcfad0d 100644 --- a/app/components/Editor/components/Contents.js +++ b/app/components/Editor/components/Contents.js @@ -2,14 +2,15 @@ import React, { Component } from 'react'; import { observable } from 'mobx'; import { observer } from 'mobx-react'; +import { Editor } from 'slate-react'; +import type { state, block } from 'slate-prop-types'; import { List } from 'immutable'; import { color } from 'shared/styles/constants'; import headingToSlug from '../headingToSlug'; -import type { State, Block } from '../types'; import styled from 'styled-components'; type Props = { - state: State, + editor: Editor, }; @observer @@ -53,10 +54,10 @@ class Contents extends Component { return elements; } - get headings(): List { - const { state } = this.props; + get headings(): List { + const { editor } = this.props; - return state.document.nodes.filter((node: Block) => { + return editor.value.document.nodes.filter((node: block) => { if (!node.text) return false; return node.type.match(/^heading/); }); @@ -74,7 +75,7 @@ class Contents extends Component { const active = this.activeHeading === slug; return ( - + {heading.text} diff --git a/app/components/Editor/components/Heading.js b/app/components/Editor/components/Heading.js index 9c29c6dbd..2a57279b1 100644 --- a/app/components/Editor/components/Heading.js +++ b/app/components/Editor/components/Heading.js @@ -1,16 +1,17 @@ // @flow import React from 'react'; import { Document } from 'slate'; +import { Editor } from 'slate-react'; import styled from 'styled-components'; +import type { node } from 'slate-prop-types'; import headingToSlug from '../headingToSlug'; -import type { Node, Editor } from '../types'; import Placeholder from './Placeholder'; type Props = { children: React$Element<*>, placeholder?: boolean, - parent: Node, - node: Node, + parent: node, + node: node, editor: Editor, readOnly: boolean, component?: string, diff --git a/app/components/Editor/components/HorizontalRule.js b/app/components/Editor/components/HorizontalRule.js index 9eed79e06..7efcbe998 100644 --- a/app/components/Editor/components/HorizontalRule.js +++ b/app/components/Editor/components/HorizontalRule.js @@ -1,10 +1,10 @@ // @flow import React from 'react'; import styled from 'styled-components'; -import type { Props } from '../types'; +import type { props } from 'slate-prop-types'; import { color } from 'shared/styles/constants'; -function HorizontalRule(props: Props) { +function HorizontalRule(props: props) { const { state, node, attributes } = props; const active = state.isFocused && state.selection.hasEdgeIn(node); return ; diff --git a/app/components/Editor/components/Image.js b/app/components/Editor/components/Image.js index 3200a104b..23ab022ed 100644 --- a/app/components/Editor/components/Image.js +++ b/app/components/Editor/components/Image.js @@ -2,23 +2,23 @@ import React, { Component } from 'react'; import ImageZoom from 'react-medium-image-zoom'; import styled from 'styled-components'; -import type { Props } from '../types'; +import type { props } from 'slate-prop-types'; import { color } from 'shared/styles/constants'; class Image extends Component { - props: Props; + props: props; handleChange = (ev: SyntheticInputEvent) => { const alt = ev.target.value; const { editor, node } = this.props; const data = node.data.toObject(); - const state = editor - .getState() - .transform() - .setNodeByKey(node.key, { data: { ...data, alt } }) - .apply(); - editor.onChange(state); + editor.onChange( + editor + .getState() + .change() + .setNodeByKey(node.key, { data: { ...data, alt } }) + ); }; handleClick = (ev: SyntheticInputEvent) => { diff --git a/app/components/Editor/components/Link.js b/app/components/Editor/components/Link.js index 0642415e6..0938dd532 100644 --- a/app/components/Editor/components/Link.js +++ b/app/components/Editor/components/Link.js @@ -1,7 +1,7 @@ // @flow import React from 'react'; import { Link as InternalLink } from 'react-router-dom'; -import type { Props } from '../types'; +import type { props } from 'slate-prop-types'; function getPathFromUrl(href: string) { if (href[0] === '/') return href; @@ -14,7 +14,7 @@ function getPathFromUrl(href: string) { } } -function isOutlineUrl(href: string) { +function isInternalUrl(href: string) { if (href[0] === '/') return true; try { @@ -26,11 +26,11 @@ function isOutlineUrl(href: string) { } } -export default function Link({ attributes, node, children, readOnly }: Props) { +export default function Link({ attributes, node, children, readOnly }: props) { const href = node.data.get('href'); const path = getPathFromUrl(href); - if (isOutlineUrl(href) && readOnly) { + if (isInternalUrl(href) && readOnly) { return ( {children} diff --git a/app/components/Editor/components/ListItem.js b/app/components/Editor/components/ListItem.js index 0c151765d..51fea4c03 100644 --- a/app/components/Editor/components/ListItem.js +++ b/app/components/Editor/components/ListItem.js @@ -1,6 +1,6 @@ // @flow import React from 'react'; -import type { Props } from '../types'; +import type { props } from 'slate-prop-types'; import TodoItem from './TodoItem'; export default function ListItem({ @@ -8,7 +8,7 @@ export default function ListItem({ node, attributes, ...props -}: Props) { +}: props) { const checked = node.data.get('checked'); if (checked !== undefined) { diff --git a/app/components/Editor/components/Paragraph.js b/app/components/Editor/components/Paragraph.js index 5b4b9a484..07801e1f1 100644 --- a/app/components/Editor/components/Paragraph.js +++ b/app/components/Editor/components/Paragraph.js @@ -1,7 +1,7 @@ // @flow import React from 'react'; import { Document } from 'slate'; -import type { Props } from '../types'; +import type { props } from 'slate-prop-types'; import Placeholder from './Placeholder'; export default function Link({ @@ -11,7 +11,7 @@ export default function Link({ parent, children, readOnly, -}: Props) { +}: props) { const parentIsDocument = parent instanceof Document; const firstParagraph = parent && parent.nodes.get(1) === node; const lastParagraph = parent && parent.nodes.last() === node; diff --git a/app/components/Editor/components/TodoItem.js b/app/components/Editor/components/TodoItem.js index 91e62cb31..bb7a9cf22 100644 --- a/app/components/Editor/components/TodoItem.js +++ b/app/components/Editor/components/TodoItem.js @@ -2,21 +2,20 @@ import React, { Component } from 'react'; import styled from 'styled-components'; import { color } from 'shared/styles/constants'; -import type { Props } from '../types'; +import type { props } from 'slate-prop-types'; export default class TodoItem extends Component { - props: Props & { checked: boolean }; + props: props & { checked: boolean }; handleChange = (ev: SyntheticInputEvent) => { const checked = ev.target.checked; const { editor, node } = this.props; - const state = editor + const change = editor .getState() - .transform() - .setNodeByKey(node.key, { data: { checked } }) - .apply(); + .change() + .setNodeByKey(node.key, { data: { checked } }); - editor.onChange(state); + editor.onChange(change); }; render() { diff --git a/app/components/Editor/components/Toolbar/BlockToolbar.js b/app/components/Editor/components/Toolbar/BlockToolbar.js index 165d5cca6..615515612 100644 --- a/app/components/Editor/components/Toolbar/BlockToolbar.js +++ b/app/components/Editor/components/Toolbar/BlockToolbar.js @@ -13,12 +13,12 @@ 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 type { props } from 'slate-prop-types'; import { color } from 'shared/styles/constants'; import { fadeIn } from 'shared/styles/animations'; import { splitAndInsertBlock } from '../../transforms'; -type Props = BaseProps & { +type Props = props & { onInsertImage: Function, onChange: Function, }; @@ -34,16 +34,15 @@ class BlockToolbar extends Component { file: HTMLInputElement; componentWillReceiveProps(nextProps: Props) { - const wasActive = this.props.state.selection.hasEdgeIn(this.props.node); - const isActive = nextProps.state.selection.hasEdgeIn(nextProps.node); + const { editor } = this.props; + const wasActive = editor.value.selection.hasEdgeIn(this.props.node); + const isActive = nextProps.editor.value.selection.hasEdgeIn(nextProps.node); const becameInactive = !isActive && wasActive; if (becameInactive) { - const state = nextProps.state - .transform() - .removeNodeByKey(nextProps.node.key) - .apply(); - this.props.onChange(state); + nextProps.editor.change(change => + change.removeNodeByKey(nextProps.node.key) + ); } } @@ -52,24 +51,25 @@ class BlockToolbar extends Component { ev.preventDefault(); ev.stopPropagation(); - const state = this.props.state - .transform() - .removeNodeByKey(this.props.node.key) - .apply(); - this.props.onChange(state); + this.props.editor.change(change => + change.removeNodeByKey(this.props.node.key) + ); } insertBlock = (options: Options) => { - const { state } = this.props; - let transform = splitAndInsertBlock(state.transform(), state, options); + const { editor } = this.props; - state.document.nodes.forEach(node => { - if (node.type === 'block-toolbar') { - transform.removeNodeByKey(node.key); - } + editor.change(change => { + splitAndInsertBlock(change, options); + + change.value.document.nodes.forEach(node => { + if (node.type === 'block-toolbar') { + change.removeNodeByKey(node.key); + } + }); + + change.focus(); }); - - this.props.onChange(transform.focus().apply()); }; handleClickBlock = (ev: SyntheticEvent, type: string) => { @@ -126,8 +126,9 @@ class BlockToolbar extends Component { }; render() { - const { state, attributes, node } = this.props; - const active = state.isFocused && state.selection.hasEdgeIn(node); + const { editor, attributes, node } = this.props; + const active = + editor.value.isFocused && editor.value.selection.hasEdgeIn(node); return ( diff --git a/app/components/Editor/components/Toolbar/Toolbar.js b/app/components/Editor/components/Toolbar/Toolbar.js index 696e3ed45..1efb85529 100644 --- a/app/components/Editor/components/Toolbar/Toolbar.js +++ b/app/components/Editor/components/Toolbar/Toolbar.js @@ -3,9 +3,10 @@ import React, { Component } from 'react'; import { observable } from 'mobx'; import { observer } from 'mobx-react'; import { Portal } from 'react-portal'; +import { Editor } from 'slate-react'; +import type { value } from 'slate-prop-types'; import styled from 'styled-components'; import _ from 'lodash'; -import type { State } from '../../types'; import FormattingToolbar from './components/FormattingToolbar'; import LinkToolbar from './components/LinkToolbar'; @@ -18,8 +19,8 @@ export default class Toolbar extends Component { @observable left: string = ''; props: { - state: State, - onChange: (state: State) => void, + editor: Editor, + value: value, }; menu: HTMLElement; @@ -41,11 +42,11 @@ export default class Toolbar extends Component { }; get linkInSelection(): any { - const { state } = this.props; + const { value } = this.props; try { - const selectedLinks = state.startBlock - .getInlinesAtRange(state.selection) + const selectedLinks = value.startBlock + .getInlinesAtRange(value.selection) .filter(node => node.type === 'link'); if (selectedLinks.size) { return selectedLinks.first(); @@ -56,10 +57,10 @@ export default class Toolbar extends Component { } update = () => { - const { state } = this.props; + const { value } = this.props; const link = this.linkInSelection; - if (state.isBlurred || (state.isCollapsed && !link)) { + if (value.isBlurred || (value.isCollapsed && !link)) { if (this.active && !this.focused) { this.active = false; this.link = undefined; @@ -70,11 +71,11 @@ export default class Toolbar extends Component { } // don't display toolbar for document title - const firstNode = state.document.nodes.first(); - if (firstNode === state.startBlock) return; + const firstNode = value.document.nodes.first(); + if (firstNode === value.startBlock) return; // don't display toolbar for code blocks - if (state.startBlock.type === 'code') return; + if (value.startBlock.type === 'code') return; this.active = true; this.focused = !!link; diff --git a/app/components/Editor/components/Toolbar/components/FormattingToolbar.js b/app/components/Editor/components/Toolbar/components/FormattingToolbar.js index df56d7037..5dd7e6c3a 100644 --- a/app/components/Editor/components/Toolbar/components/FormattingToolbar.js +++ b/app/components/Editor/components/Toolbar/components/FormattingToolbar.js @@ -1,7 +1,7 @@ // @flow import React, { Component } from 'react'; import styled from 'styled-components'; -import type { State } from '../../../types'; +import { Editor } from 'slate-react'; import ToolbarButton from './ToolbarButton'; import BoldIcon from 'components/Icon/BoldIcon'; import CodeIcon from 'components/Icon/CodeIcon'; @@ -13,8 +13,7 @@ import StrikethroughIcon from 'components/Icon/StrikethroughIcon'; class FormattingToolbar extends Component { props: { - state: State, - onChange: Function, + editor: Editor, onCreateLink: Function, }; @@ -25,11 +24,11 @@ class FormattingToolbar extends Component { * @return {Boolean} */ hasMark = (type: string) => { - return this.props.state.marks.some(mark => mark.type === type); + return this.props.editor.value.marks.some(mark => mark.type === type); }; isBlock = (type: string) => { - return this.props.state.startBlock.type === type; + return this.props.editor.value.startBlock.type === type; }; /** @@ -40,37 +39,23 @@ class FormattingToolbar extends Component { */ onClickMark = (ev: SyntheticEvent, type: string) => { ev.preventDefault(); - let { state } = this.props; - - state = state - .transform() - .toggleMark(type) - .apply(); - this.props.onChange(state); + this.props.editor.change(change => change.toggleMark(type)); }; onClickBlock = (ev: SyntheticEvent, type: string) => { ev.preventDefault(); - let { state } = this.props; - - state = state - .transform() - .setBlock(type) - .apply(); - this.props.onChange(state); + this.props.editor.change(change => change.setBlock(type)); }; onCreateLink = (ev: SyntheticEvent) => { ev.preventDefault(); ev.stopPropagation(); - let { state } = this.props; + const data = { href: '' }; - state = state - .transform() - .wrapInline({ type: 'link', data }) - .apply(); - this.props.onChange(state); - this.props.onCreateLink(); + this.props.editor.change(change => { + change.wrapInline({ type: 'link', data }); + this.props.onCreateLink(); + }); }; renderMarkButton = (type: string, IconClass: Function) => { diff --git a/app/components/Editor/components/Toolbar/components/LinkToolbar.js b/app/components/Editor/components/Toolbar/components/LinkToolbar.js index 4bdd21c7b..f0fd65a99 100644 --- a/app/components/Editor/components/Toolbar/components/LinkToolbar.js +++ b/app/components/Editor/components/Toolbar/components/LinkToolbar.js @@ -4,11 +4,12 @@ import ReactDOM from 'react-dom'; import { observable, action } from 'mobx'; import { observer, inject } from 'mobx-react'; import { withRouter } from 'react-router-dom'; +import { Editor } from 'slate-react'; import styled from 'styled-components'; import ArrowKeyNavigation from 'boundless-arrow-key-navigation'; +import type { change } from 'slate-prop-types'; import ToolbarButton from './ToolbarButton'; import DocumentResult from './DocumentResult'; -import type { State } from '../../../types'; import DocumentsStore from 'stores/DocumentsStore'; import keydown from 'react-keydown'; import CloseIcon from 'components/Icon/CloseIcon'; @@ -23,11 +24,11 @@ class LinkToolbar extends Component { firstDocument: HTMLElement; props: { - state: State, + editor: Editor, link: Object, documents: DocumentsStore, onBlur: () => void, - onChange: State => void, + onChange: change => *, }; @observable isEditing: boolean = false; @@ -112,17 +113,14 @@ class LinkToolbar extends Component { save = (href: string) => { href = href.trim(); - const { state } = this.props; - const transform = state.transform(); - - if (href) { - transform.setInline({ type: 'link', data: { href } }); - } else { - transform.unwrapInline('link'); - } - - this.props.onChange(transform.apply()); - this.props.onBlur(); + this.props.editor.change(change => { + if (href) { + change.setInline({ type: 'link', data: { href } }); + } else { + change.unwrapInline('link'); + } + this.props.onBlur(); + }); }; setFirstDocumentRef = ref => { diff --git a/app/components/Editor/headingToSlug.js b/app/components/Editor/headingToSlug.js index 04b9d6273..cf46ab2b9 100644 --- a/app/components/Editor/headingToSlug.js +++ b/app/components/Editor/headingToSlug.js @@ -1,9 +1,9 @@ // @flow import { escape } from 'lodash'; -import type { Node } from './types'; +import type { node } from 'slate-prop-types'; import slug from 'slug'; -export default function headingToSlug(node: Node) { +export default function headingToSlug(node: node) { const level = node.type.replace('heading', 'h'); return escape(`${level}-${slug(node.text)}-${node.key}`); } diff --git a/app/components/Editor/insertImage.js b/app/components/Editor/insertImage.js index d3e4c6bd7..01480cc2e 100644 --- a/app/components/Editor/insertImage.js +++ b/app/components/Editor/insertImage.js @@ -1,10 +1,11 @@ // @flow import uuid from 'uuid'; import uploadFile from 'utils/uploadFile'; -import type { Editor, Transform } from './types'; +import { Editor } from 'slate-react'; +import type { change } from 'slate-prop-types'; export default async function insertImageFile( - transform: Transform, + change: change, file: window.File, editor: Editor, onImageUploadStart: () => void, @@ -21,7 +22,7 @@ export default async function insertImageFile( const src = reader.result; // insert into document as uploading placeholder - const state = transform + const state = change .insertBlock({ type: 'image', isVoid: true, @@ -36,11 +37,11 @@ export default async function insertImageFile( const asset = await uploadFile(file); const src = asset.url; - // we dont use the original transform provided to the callback here + // we dont use the original change provided to the callback here // as the state may have changed significantly in the time it took to // upload the file. const state = editor.getState(); - const finalTransform = state.transform(); + const finalTransform = state.change(); const placeholder = state.document.findDescendant( node => node.data && node.data.get('id') === id ); diff --git a/app/components/Editor/marks.js b/app/components/Editor/marks.js new file mode 100644 index 000000000..58d25c634 --- /dev/null +++ b/app/components/Editor/marks.js @@ -0,0 +1,22 @@ +// @flow +import React from 'react'; +import InlineCode from './components/InlineCode'; +import type { props } from 'slate-prop-types'; + +export default function renderMark(props: props) { + switch (props.mark.type) { + case 'bold': + return {props.children}; + case 'code': + return {props.children}; + case 'italic': + return {props.children}; + case 'underlined': + return {props.children}; + case 'deleted': + return {props.children}; + case 'added': + return {props.children}; + default: + } +} diff --git a/app/components/Editor/nodes.js b/app/components/Editor/nodes.js new file mode 100644 index 000000000..b20e4de45 --- /dev/null +++ b/app/components/Editor/nodes.js @@ -0,0 +1,75 @@ +// @flow +import React from 'react'; +import Code from './components/Code'; +import BlockToolbar from './components/Toolbar/BlockToolbar'; +import HorizontalRule from './components/HorizontalRule'; +import Image from './components/Image'; +import Link from './components/Link'; +import ListItem from './components/ListItem'; +import TodoList from './components/TodoList'; +import { + Heading1, + Heading2, + Heading3, + Heading4, + Heading5, + Heading6, +} from './components/Heading'; +import Paragraph from './components/Paragraph'; +import type { props } from 'slate-prop-types'; + +type Options = { + onInsertImage: *, +}; + +export default function createRenderNode({ onChange, onInsertImage }: Options) { + return function renderNode(props: props) { + const { attributes } = props; + + switch (props.node.type) { + case 'paragraph': + return ; + case 'block-toolbar': + return ; + case 'block-quote': + return
{props.children}
; + case 'bulleted-list': + return
    {props.children}
; + case 'ordered-list': + return
    {props.children}
; + case 'todo-list': + return {props.children}; + case 'table': + return {props.children}
; + case 'table-row': + return {props.children}; + case 'table-head': + return {props.children}; + case 'table-cell': + return {props.children}; + case 'list-item': + return ; + case 'horizontal-rule': + return ; + case 'code': + return ; + case 'image': + return ; + case 'link': + return ; + case 'heading1': + return ; + case 'heading2': + return ; + case 'heading3': + return ; + case 'heading4': + return ; + case 'heading5': + return ; + case 'heading6': + return ; + default: + } + }; +} diff --git a/app/components/Editor/plugins.js b/app/components/Editor/plugins.js index 30688ea11..b3130aac6 100644 --- a/app/components/Editor/plugins.js +++ b/app/components/Editor/plugins.js @@ -1,5 +1,5 @@ // @flow -import DropOrPasteImages from '@tommoor/slate-drop-or-paste-images'; +// import DropOrPasteImages from '@tommoor/slate-drop-or-paste-images'; import PasteLinkify from 'slate-paste-linkify'; import CollapseOnEscape from 'slate-collapse-on-escape'; import TrailingBlock from 'slate-trailing-block'; @@ -8,7 +8,7 @@ import Prism from 'slate-prism'; import EditList from './plugins/EditList'; import KeyboardShortcuts from './plugins/KeyboardShortcuts'; import MarkdownShortcuts from './plugins/MarkdownShortcuts'; -import insertImage from './insertImage'; +// import insertImage from './insertImage'; const onlyInCode = node => node.type === 'code'; @@ -23,18 +23,18 @@ const createPlugins = ({ onImageUploadStart, onImageUploadStop }: Options) => { type: 'link', collapseTo: 'end', }), - DropOrPasteImages({ - extensions: ['png', 'jpg', 'gif'], - applyTransform: (transform, file, editor) => { - return insertImage( - transform, - file, - editor, - onImageUploadStart, - onImageUploadStop - ); - }, - }), + // DropOrPasteImages({ + // extensions: ['png', 'jpg', 'gif'], + // applyTransform: (transform, file, editor) => { + // return insertImage( + // transform, + // file, + // editor, + // onImageUploadStart, + // onImageUploadStop + // ); + // }, + // }), EditList, EditCode({ onlyIn: onlyInCode, diff --git a/app/components/Editor/plugins/KeyboardShortcuts.js b/app/components/Editor/plugins/KeyboardShortcuts.js index bf1a2fb02..e3b8c06c5 100644 --- a/app/components/Editor/plugins/KeyboardShortcuts.js +++ b/app/components/Editor/plugins/KeyboardShortcuts.js @@ -1,4 +1,5 @@ // @flow +import type { change } from 'slate-prop-types'; export default function KeyboardShortcuts() { return { @@ -10,38 +11,32 @@ export default function KeyboardShortcuts() { * @param {State} state * @return {State or Null} state */ - onKeyDown(ev: SyntheticEvent, data: Object, state: Object) { + onKeyDown(ev: SyntheticEvent, data: Object, change: change) { if (!data.isMeta) return null; switch (data.key) { case 'b': - return this.toggleMark(state, 'bold'); + return this.toggleMark(change, 'bold'); case 'i': - return this.toggleMark(state, 'italic'); + return this.toggleMark(change, 'italic'); case 'u': - return this.toggleMark(state, 'underlined'); + return this.toggleMark(change, 'underlined'); case 'd': - return this.toggleMark(state, 'deleted'); + return this.toggleMark(change, 'deleted'); case 'k': - return state - .transform() - .wrapInline({ type: 'link', data: { href: '' } }) - .apply(); + return change.wrapInline({ type: 'link', data: { href: '' } }); default: return null; } }, - toggleMark(state: Object, type: string) { + toggleMark(change: change, type: string) { + const { state } = change; // don't allow formatting of document title const firstNode = state.document.nodes.first(); if (firstNode === state.startBlock) return; - state = state - .transform() - .toggleMark(type) - .apply(); - return state; + return state.change().toggleMark(type); }, }; } diff --git a/app/components/Editor/plugins/MarkdownShortcuts.js b/app/components/Editor/plugins/MarkdownShortcuts.js index 6132a8c61..8800f4f56 100644 --- a/app/components/Editor/plugins/MarkdownShortcuts.js +++ b/app/components/Editor/plugins/MarkdownShortcuts.js @@ -1,4 +1,11 @@ // @flow +import type { change } from 'slate-prop-types'; + +type KeyData = { + isMeta: boolean, + key: string, +}; + const inlineShortcuts = [ { mark: 'bold', shortcut: '**' }, { mark: 'bold', shortcut: '__' }, @@ -14,20 +21,20 @@ export default function MarkdownShortcuts() { /** * On key down, check for our specific key shortcuts. */ - onKeyDown(ev: SyntheticEvent, data: Object, state: Object) { + onKeyDown(ev: SyntheticEvent, data: KeyData, change: change) { switch (data.key) { case '-': - return this.onDash(ev, state); + return this.onDash(ev, change); case '`': - return this.onBacktick(ev, state); + return this.onBacktick(ev, change); case 'tab': - return this.onTab(ev, state); + return this.onTab(ev, change); case 'space': - return this.onSpace(ev, state); + return this.onSpace(ev, change); case 'backspace': - return this.onBackspace(ev, state); + return this.onBackspace(ev, change); case 'enter': - return this.onEnter(ev, state); + return this.onEnter(ev, change); default: return null; } @@ -37,7 +44,8 @@ export default function MarkdownShortcuts() { * On space, if it was after an auto-markdown shortcut, convert the current * node into the shortcut's corresponding type. */ - onSpace(ev: SyntheticEvent, state: Object) { + onSpace(ev: SyntheticEvent, change: change) { + const { state } = change; if (state.isExpanded) return; const { startBlock, startOffset } = state; const chars = startBlock.text.slice(0, startOffset).trim(); @@ -50,25 +58,19 @@ export default function MarkdownShortcuts() { let checked; if (chars === '[x]') checked = true; if (chars === '[ ]') checked = false; - const transform = state - .transform() - .setBlock({ type, data: { checked } }); + const change = state.change().setBlock({ type, data: { checked } }); if (type === 'list-item') { if (checked !== undefined) { - transform.wrapBlock('todo-list'); + change.wrapBlock('todo-list'); } else if (chars === '1.') { - transform.wrapBlock('ordered-list'); + change.wrapBlock('ordered-list'); } else { - transform.wrapBlock('bulleted-list'); + change.wrapBlock('bulleted-list'); } } - state = transform - .extendToStartOf(startBlock) - .delete() - .apply(); - return state; + return change.extendToStartOf(startBlock).delete(); } for (const key of inlineShortcuts) { @@ -97,35 +99,32 @@ export default function MarkdownShortcuts() { // if we have multiple tags then mark the text between as inline code if (inlineTags.length > 1) { - const transform = state.transform(); + const change = state.change(); const firstText = startBlock.getFirstText(); const firstCodeTagIndex = inlineTags[0]; const lastCodeTagIndex = inlineTags[inlineTags.length - 1]; - transform.removeTextByKey( + change.removeTextByKey( firstText.key, lastCodeTagIndex, shortcut.length ); - transform.removeTextByKey( + change.removeTextByKey( firstText.key, firstCodeTagIndex, shortcut.length ); - transform.moveOffsetsTo( + change.moveOffsetsTo( firstCodeTagIndex, lastCodeTagIndex - shortcut.length ); - transform.addMark(mark); - state = transform - .collapseToEnd() - .removeMark(mark) - .apply(); - return state; + change.addMark(mark); + return change.collapseToEnd().removeMark(mark); } } }, - onDash(ev: SyntheticEvent, state: Object) { + onDash(ev: SyntheticEvent, change: change) { + const { state } = change; if (state.isExpanded) return; const { startBlock, startOffset } = state; const chars = startBlock.text.slice(0, startOffset).replace(/\s*/g, ''); @@ -133,7 +132,7 @@ export default function MarkdownShortcuts() { if (chars === '--') { ev.preventDefault(); return state - .transform() + .change() .extendToStartOf(startBlock) .delete() .setBlock({ @@ -141,12 +140,12 @@ export default function MarkdownShortcuts() { isVoid: true, }) .collapseToStartOfNextBlock() - .insertBlock('paragraph') - .apply(); + .insertBlock('paragraph'); } }, - onBacktick(ev: SyntheticEvent, state: Object) { + onBacktick(ev: SyntheticEvent, change: change) { + const { state } = change; if (state.isExpanded) return; const { startBlock, startOffset } = state; const chars = startBlock.text.slice(0, startOffset).replace(/\s*/g, ''); @@ -154,18 +153,18 @@ export default function MarkdownShortcuts() { if (chars === '``') { ev.preventDefault(); return state - .transform() + .change() .extendToStartOf(startBlock) .delete() .setBlock({ type: 'code', - }) - .apply(); + }); } }, - onBackspace(ev: SyntheticEvent, state: Object) { - if (state.isExpanded) return; + onBackspace(ev: SyntheticEvent, change: change) { + const { state } = change; + if (change.isExpanded) return; const { startBlock, selection, startOffset } = state; // If at the start of a non-paragraph, convert it back into a paragraph @@ -173,13 +172,11 @@ export default function MarkdownShortcuts() { if (startBlock.type === 'paragraph') return; ev.preventDefault(); - const transform = state.transform().setBlock('paragraph'); + const change = state.change().setBlock('paragraph'); if (startBlock.type === 'list-item') - transform.unwrapBlock('bulleted-list'); - - state = transform.apply(); - return state; + change.unwrapBlock('bulleted-list'); + return change; } // If at the end of a code mark hitting backspace should remove the mark @@ -198,15 +195,14 @@ export default function MarkdownShortcuts() { .reverse() .takeUntil((v, k) => !v.marks.some(mark => mark.type === 'code')); - const transform = state.transform(); - transform.removeMarkByKey( - textNode.key, - state.startOffset - charsInCodeBlock.size, - state.startOffset, - 'code' - ); - state = transform.apply(); - return state; + return state + .change() + .removeMarkByKey( + textNode.key, + change.startOffset - charsInCodeBlock.size, + change.startOffset, + 'code' + ); } } }, @@ -215,14 +211,15 @@ export default function MarkdownShortcuts() { * On tab, if at the end of the heading jump to the main body content * as if it is another input field (act the same as enter). */ - onTab(ev: SyntheticEvent, state: Object) { + onTab(ev: SyntheticEvent, change: change) { + const { state } = change; + if (state.startBlock.type === 'heading1') { ev.preventDefault(); return state - .transform() + .change() .splitBlock() - .setBlock('paragraph') - .apply(); + .setBlock('paragraph'); } }, @@ -230,11 +227,12 @@ export default function MarkdownShortcuts() { * On return, if at the end of a node type that should not be extended, * create a new paragraph below it. */ - onEnter(ev: SyntheticEvent, state: Object) { + onEnter(ev: SyntheticEvent, change: change) { + const { state } = change; if (state.isExpanded) return; const { startBlock, startOffset, endOffset } = state; if (startOffset === 0 && startBlock.length === 0) - return this.onBackspace(ev, state); + return this.onBackspace(ev, change); if (endOffset !== startBlock.length) return; // Hitting enter while an image is selected should jump caret below and @@ -242,10 +240,9 @@ export default function MarkdownShortcuts() { if (startBlock.type === 'image') { ev.preventDefault(); return state - .transform() + .change() .collapseToEnd() - .insertBlock('paragraph') - .apply(); + .insertBlock('paragraph'); } // Hitting enter in a heading or blockquote will split the node at that @@ -264,10 +261,9 @@ export default function MarkdownShortcuts() { ev.preventDefault(); return state - .transform() + .change() .splitBlock() - .setBlock('paragraph') - .apply(); + .setBlock('paragraph'); }, /** diff --git a/app/components/Editor/schema.js b/app/components/Editor/schema.js index d4ed674b6..1b59683ac 100644 --- a/app/components/Editor/schema.js +++ b/app/components/Editor/schema.js @@ -1,133 +1,133 @@ -// @flow -import React from 'react'; -import Code from './components/Code'; -import HorizontalRule from './components/HorizontalRule'; -import InlineCode from './components/InlineCode'; -import Image from './components/Image'; -import Link from './components/Link'; -import ListItem from './components/ListItem'; -import TodoList from './components/TodoList'; -import { - Heading1, - Heading2, - Heading3, - Heading4, - Heading5, - Heading6, -} from './components/Heading'; -import Paragraph from './components/Paragraph'; -import BlockToolbar from './components/Toolbar/BlockToolbar'; -import type { Props, Node, Transform } from './types'; - -type Options = { - onInsertImage: Function, - onChange: Function, -}; - -const createSchema = ({ onInsertImage, onChange }: Options) => { - return { - marks: { - bold: (props: Props) => {props.children}, - code: (props: Props) => {props.children}, - italic: (props: Props) => {props.children}, - underlined: (props: Props) => {props.children}, - deleted: (props: Props) => {props.children}, - added: (props: Props) => {props.children}, - }, - - nodes: { - 'block-toolbar': (props: Props) => ( - - ), - paragraph: (props: Props) => , - 'block-quote': (props: Props) => ( -
{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} - ), - code: Code, - image: Image, - link: Link, - 'list-item': ListItem, - heading1: (props: Props) => , - heading2: (props: Props) => , - heading3: (props: Props) => , - heading4: (props: Props) => , - heading5: (props: Props) => , - heading6: (props: Props) => , - }, - - rules: [ - // ensure first node is always a heading - { - match: (node: Node) => { - return node.kind === 'document'; - }, - validate: (document: Node) => { - const firstNode = document.nodes.first(); - return firstNode && firstNode.type === 'heading1' ? null : firstNode; - }, - normalize: (transform: Transform, document: Node, firstNode: Node) => { - transform.setBlock({ type: 'heading1' }); - }, - }, - - // automatically removes any marks in first heading - { - match: (node: Node) => { - return node.kind === 'heading1'; - }, - validate: (heading: Node) => { - const hasMarks = heading.getMarks().isEmpty(); - const hasInlines = heading.getInlines().isEmpty(); - - return !(hasMarks && hasInlines); - }, - normalize: (transform: Transform, heading: Node) => { - transform.unwrapInlineByKey(heading.key); - - heading.getMarks().forEach(mark => { - heading.nodes.forEach(textNode => { - if (textNode.kind === 'text') { - transform.removeMarkByKey( - textNode.key, - 0, - textNode.text.length, - mark - ); - } - }); - }); - - return transform; - }, - }, - ], - }; -}; - -export default createSchema; +// // @flow +// import React from 'react'; +// import Code from './components/Code'; +// import HorizontalRule from './components/HorizontalRule'; +// import InlineCode from './components/InlineCode'; +// import Image from './components/Image'; +// import Link from './components/Link'; +// import ListItem from './components/ListItem'; +// import TodoList from './components/TodoList'; +// import { +// Heading1, +// Heading2, +// Heading3, +// Heading4, +// Heading5, +// Heading6, +// } from './components/Heading'; +// import Paragraph from './components/Paragraph'; +// import BlockToolbar from './components/Toolbar/BlockToolbar'; +// import type { Props, Node, Transform } from './types'; +// +// type Options = { +// onInsertImage: Function, +// onChange: Function, +// }; +// +// const createSchema = ({ onInsertImage, onChange }: Options) => { +// return { +// marks: { +// bold: (props: Props) => {props.children}, +// code: (props: Props) => {props.children}, +// italic: (props: Props) => {props.children}, +// underlined: (props: Props) => {props.children}, +// deleted: (props: Props) => {props.children}, +// added: (props: Props) => {props.children}, +// }, +// +// nodes: { +// 'block-toolbar': (props: Props) => ( +// +// ), +// paragraph: (props: Props) => , +// 'block-quote': (props: Props) => ( +//
{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} +// ), +// code: Code, +// image: Image, +// link: Link, +// 'list-item': ListItem, +// heading1: (props: Props) => , +// heading2: (props: Props) => , +// heading3: (props: Props) => , +// heading4: (props: Props) => , +// heading5: (props: Props) => , +// heading6: (props: Props) => , +// }, +// +// rules: [ +// // ensure first node is always a heading +// { +// match: (node: Node) => { +// return node.kind === 'document'; +// }, +// validate: (document: Node) => { +// const firstNode = document.nodes.first(); +// return firstNode && firstNode.type === 'heading1' ? null : firstNode; +// }, +// normalize: (transform: Transform, document: Node, firstNode: Node) => { +// transform.setBlock({ type: 'heading1' }); +// }, +// }, +// +// // automatically removes any marks in first heading +// { +// match: (node: Node) => { +// return node.kind === 'heading1'; +// }, +// validate: (heading: Node) => { +// const hasMarks = heading.getMarks().isEmpty(); +// const hasInlines = heading.getInlines().isEmpty(); +// +// return !(hasMarks && hasInlines); +// }, +// normalize: (transform: Transform, heading: Node) => { +// transform.unwrapInlineByKey(heading.key); +// +// heading.getMarks().forEach(mark => { +// heading.nodes.forEach(textNode => { +// if (textNode.kind === 'text') { +// transform.removeMarkByKey( +// textNode.key, +// 0, +// textNode.text.length, +// mark +// ); +// } +// }); +// }); +// +// return transform; +// }, +// }, +// ], +// }; +// }; +// +// export default createSchema; diff --git a/app/components/Editor/transforms.js b/app/components/Editor/transforms.js index d404c570f..e5c30184d 100644 --- a/app/components/Editor/transforms.js +++ b/app/components/Editor/transforms.js @@ -1,6 +1,6 @@ // @flow +import type { change } from 'slate-prop-types'; import EditList from './plugins/EditList'; -import type { State, Transform } from './types'; const { transforms } = EditList; @@ -10,28 +10,25 @@ type Options = { append?: string | Object, }; -export function splitAndInsertBlock( - transform: Transform, - state: State, - options: Options -) { +export function splitAndInsertBlock(change: change, options: Options) { const { type, wrapper, append } = options; - const { document } = state; - const parent = document.getParent(state.startBlock.key); + const { value } = change; + const { document } = value; + const parent = document.getParent(value.startBlock.key); // lists get some special treatment if (parent && parent.type === 'list-item') { - transform = transforms.unwrapList( + change = transforms.unwrapList( transforms - .splitListItem(transform.collapseToStart()) + .splitListItem(change.collapseToStart()) .collapseToEndOfPreviousBlock() ); } - transform = transform.insertBlock(type); + change = change.insertBlock(type); - if (wrapper) transform = transform.wrapBlock(wrapper); - if (append) transform = transform.insertBlock(append); + if (wrapper) change = change.wrapBlock(wrapper); + if (append) change = change.insertBlock(append); - return transform; + return change; } diff --git a/app/components/Editor/types.js b/app/components/Editor/types.js deleted file mode 100644 index 51562825a..000000000 --- a/app/components/Editor/types.js +++ /dev/null @@ -1,118 +0,0 @@ -// @flow -import { List, Set, Map } from 'immutable'; -import { Selection } from 'slate'; - -export type NodeTransform = { - addMarkByKey: Function, - insertNodeByKey: Function, - insertTextByKey: Function, - moveNodeByKey: Function, - removeMarkByKey: Function, - removeNodeByKey: Function, - removeTextByKey: Function, - setMarkByKey: Function, - setNodeByKey: Function, - splitNodeByKey: Function, - unwrapInlineByKey: Function, - unwrapBlockByKey: Function, - unwrapNodeByKey: Function, - wrapBlockByKey: Function, - wrapInlineByKey: Function, -}; - -export type StateTransform = { - deleteBackward: Function, - deleteForward: Function, - delete: Function, - insertBlock: Function, - insertFragment: Function, - insertInline: Function, - insertText: Function, - addMark: Function, - setBlock: Function, - setInline: Function, - splitBlock: Function, - splitInline: Function, - removeMark: Function, - toggleMark: Function, - unwrapBlock: Function, - unwrapInline: Function, - wrapBlock: Function, - wrapInline: Function, - wrapText: Function, -}; - -export type SelectionTransform = { - collapseToStart: Function, - collapseToEnd: Function, -}; - -export type Transform = NodeTransform & StateTransform & SelectionTransform; - -export type Editor = { - props: Object, - className: string, - onChange: Function, - onDocumentChange: Function, - onSelectionChange: Function, - plugins: Array, - readOnly: boolean, - state: Object, - style: Object, - placeholder?: string, - placeholderClassName?: string, - placeholderStyle?: string, - blur: Function, - focus: Function, - getSchema: Function, - getState: Function, -}; - -export type Node = { - key: string, - kind: string, - type: string, - length: number, - text: string, - data: Map, - nodes: List, - getMarks: Function, - getBlocks: Function, - getParent: Function, - getInlines: Function, - getInlinesAtRange: Function, - setBlock: Function, -}; - -export type Block = Node & { - type: string, -}; - -export type Document = Node; - -export type State = { - document: Document, - selection: Selection, - startBlock: Block, - endBlock: Block, - startText: Node, - endText: Node, - marks: Set<*>, - blocks: List, - fragment: Document, - lines: List, - tests: List, - startBlock: Block, - transform: Function, - isBlurred: Function, -}; - -export type Props = { - node: Node, - parent?: Node, - attributes?: Object, - state: State, - editor: Editor, - readOnly?: boolean, - children?: React$Element, -}; diff --git a/package.json b/package.json index d80f7a16b..9d0cfb21b 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,7 @@ "history": "3.0.0", "html-webpack-plugin": "2.17.0", "http-errors": "1.4.0", + "immutable": "^3.8.2", "imports-loader": "0.6.5", "invariant": "^2.2.2", "isomorphic-fetch": "2.2.1", @@ -160,14 +161,17 @@ "sequelize": "^4.3.1", "sequelize-cli": "^2.7.0", "sequelize-encrypted": "0.1.0", - "slate": "^0.21.4", - "slate-collapse-on-escape": "^0.2.1", - "slate-edit-code": "^0.10.2", - "slate-edit-list": "^0.7.0", - "slate-md-serializer": "0.5.6", - "slate-paste-linkify": "^0.2.1", - "slate-prism": "^0.2.2", - "slate-trailing-block": "^0.2.4", + "slate": "^0.29.0", + "slate-collapse-on-escape": "^0.6.0", + "slate-edit-code": "^0.13.2", + "slate-edit-list": "^0.10.1", + "slate-md-serializer": "1.0.1", + "slate-paste-linkify": "^0.5.0", + "slate-plain-serializer": "^0.4.12", + "slate-prism": "^0.4.0", + "slate-prop-types": "^0.4.12", + "slate-react": "^0.10.19", + "slate-trailing-block": "^0.4.0", "slug": "0.9.1", "string-hash": "^1.1.0", "string-replace-to-array": "^1.0.3", diff --git a/yarn.lock b/yarn.lock index 2d541e7d0..974677797 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2664,7 +2664,7 @@ es6-iterator@2, es6-iterator@^2.0.1, es6-iterator@~2.0.1: es5-ext "^0.10.14" es6-symbol "^3.1" -es6-map@^0.1.3, es6-map@^0.1.4: +es6-map@^0.1.3: version "0.1.5" resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0" dependencies: @@ -3467,7 +3467,7 @@ get-caller-file@^1.0.1: get-document@1: version "1.0.0" - resolved "https://registry.yarnpkg.com/get-document/-/get-document-1.0.0.tgz#4821bce66f1c24cb0331602be6cb6b12c4f01c4b" + resolved "https://registry.npmjs.org/get-document/-/get-document-1.0.0.tgz#4821bce66f1c24cb0331602be6cb6b12c4f01c4b" get-stdin@^4.0.1: version "4.0.1" @@ -3479,7 +3479,7 @@ get-stream@^3.0.0: get-window@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/get-window/-/get-window-1.1.1.tgz#0750f8970c88a54ac1294deb97add9568b3db594" + resolved "https://registry.npmjs.org/get-window/-/get-window-1.1.1.tgz#0750f8970c88a54ac1294deb97add9568b3db594" dependencies: get-document "1" @@ -4125,9 +4125,9 @@ immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" -immutable@^3.8.1: - version "3.8.1" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.1.tgz#200807f11ab0f72710ea485542de088075f68cd2" +immutable@^3.8.2: + version "3.8.2" + resolved "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" imports-loader@0.6.5: version "0.6.5" @@ -4366,6 +4366,10 @@ is-hexadecimal@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.1.tgz#6e084bbc92061fbb0971ec58b6ce6d404e24da69" +is-hotkey@^0.1.1: + version "0.1.1" + resolved "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.1.1.tgz#b279a2fd108391be9aa93c6cb317f50357da549a" + is-image@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-image/-/is-image-1.0.1.tgz#6fd51a752a1a111506d060d952118b0b989b426e" @@ -4374,7 +4378,7 @@ is-image@^1.0.1: is-in-browser@^1.1.3: version "1.1.3" - resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" + resolved "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" is-lower-case@^1.1.0: version "1.1.3" @@ -4435,7 +4439,7 @@ is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" -is-plain-object@^2.0.1, is-plain-object@^2.0.3: +is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" dependencies: @@ -4531,7 +4535,7 @@ is-whitespace-character@^1.0.0: is-window@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/is-window/-/is-window-1.0.2.tgz#2c896ca53db97de45d3c33133a65d8c9f563480d" + resolved "https://registry.npmjs.org/is-window/-/is-window-1.0.2.tgz#2c896ca53db97de45d3c33133a65d8c9f563480d" is-windows@^0.2.0: version "0.2.0" @@ -4571,6 +4575,10 @@ isobject@^3.0.0, isobject@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" +isomorphic-base64@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/isomorphic-base64/-/isomorphic-base64-1.0.2.tgz#f426aae82569ba8a4ec5ca73ad21a44ab1ee7803" + isomorphic-fetch@2.2.1, isomorphic-fetch@^2.1.1: version "2.2.1" resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" @@ -5054,7 +5062,7 @@ jws@^3.0.0, jws@^3.1.4: keycode@^2.1.2: version "2.1.9" - resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.1.9.tgz#964a23c54e4889405b4861a5c9f0480d45141dfa" + resolved "https://registry.npmjs.org/keycode/-/keycode-2.1.9.tgz#964a23c54e4889405b4861a5c9f0480d45141dfa" keygrip@~1.0.2: version "1.0.2" @@ -5670,6 +5678,10 @@ lodash.templatesettings@^3.0.0: lodash._reinterpolate "^3.0.0" lodash.escape "^3.0.0" +lodash.throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" + lodash.toarray@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" @@ -7403,6 +7415,10 @@ react-helmet@^5.2.0: prop-types "^15.5.4" react-side-effect "^1.1.0" +react-immutable-proptypes@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/react-immutable-proptypes/-/react-immutable-proptypes-2.1.0.tgz#023d6f39bb15c97c071e9e60d00d136eac5fa0b4" + react-keydown@^1.7.3: version "1.9.4" resolved "https://registry.yarnpkg.com/react-keydown/-/react-keydown-1.9.4.tgz#22718ac95edb64dd840dfc4350abf7e693ea0b7f" @@ -7431,8 +7447,8 @@ react-modal@^3.1.2: prop-types "^15.5.10" react-portal@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-3.1.0.tgz#865c44fb72a1da106c649206936559ce891ee899" + version "3.2.0" + resolved "https://registry.npmjs.org/react-portal/-/react-portal-3.2.0.tgz#4224e19b2b05d5cbe730a7ba0e34ec7585de0043" dependencies: prop-types "^15.5.8" @@ -7993,7 +8009,7 @@ select@^1.1.2: selection-is-backward@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/selection-is-backward/-/selection-is-backward-1.0.0.tgz#97a54633188a511aba6419fc5c1fa91b467e6be1" + resolved "https://registry.npmjs.org/selection-is-backward/-/selection-is-backward-1.0.0.tgz#97a54633188a511aba6419fc5c1fa91b467e6be1" semver-diff@^2.0.0: version "2.1.0" @@ -8189,64 +8205,99 @@ slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" -slate-collapse-on-escape@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/slate-collapse-on-escape/-/slate-collapse-on-escape-0.2.1.tgz#988f474439f0a21f94cc0016da52ea3c1a061100" +slate-base64-serializer@^0.2.14: + version "0.2.14" + resolved "https://registry.npmjs.org/slate-base64-serializer/-/slate-base64-serializer-0.2.14.tgz#9970bfddde069f76ba78a5ac644a1de352e8ba42" + dependencies: + isomorphic-base64 "^1.0.2" + +slate-collapse-on-escape@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/slate-collapse-on-escape/-/slate-collapse-on-escape-0.6.0.tgz#668f2318770608a09a25a95623f5fb4329a277bf" dependencies: to-pascal-case "^1.0.0" -slate-edit-code@^0.10.2: - version "0.10.3" - resolved "https://registry.yarnpkg.com/slate-edit-code/-/slate-edit-code-0.10.3.tgz#c8211a050a127cdccc9174209f778fe489659801" +slate-dev-logger@^0.1.25, slate-dev-logger@^0.1.36: + version "0.1.36" + resolved "https://registry.npmjs.org/slate-dev-logger/-/slate-dev-logger-0.1.36.tgz#ecdb37dbf944dfc742bab23b6a20d5a0472db95e" + +slate-edit-code@^0.13.2: + version "0.13.2" + resolved "https://registry.npmjs.org/slate-edit-code/-/slate-edit-code-0.13.2.tgz#682a7640da076906e5b4a4c73ec0e46d31d92c62" dependencies: detect-indent "^4.0.0" detect-newline "^2.1.0" ends-with "^0.2.0" - immutable "^3.8.1" + is-hotkey "^0.1.1" -slate-edit-list@^0.7.0: - version "0.7.1" - resolved "https://registry.yarnpkg.com/slate-edit-list/-/slate-edit-list-0.7.1.tgz#84ee960d2d5b5a20ce267ad9df894395a91b93d5" +slate-edit-list@^0.10.1: + version "0.10.1" + resolved "https://registry.npmjs.org/slate-edit-list/-/slate-edit-list-0.10.1.tgz#9c6a142a314b0ff22a327f1b50c8f5c85468cb17" -slate-md-serializer@0.5.6: - version "0.5.6" - resolved "https://registry.npmjs.org/slate-md-serializer/-/slate-md-serializer-0.5.6.tgz#88048ae62757ce3aaf1096ebd4200c58fdd1e86c" +slate-md-serializer@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/slate-md-serializer/-/slate-md-serializer-1.0.1.tgz#10fb8118bf0b97addaf9d7fd77c1b19f3d767309" -slate-paste-linkify@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/slate-paste-linkify/-/slate-paste-linkify-0.2.1.tgz#4647b5207b910d2d084f7d5d256384869b0a9c75" +slate-paste-linkify@^0.5.0: + version "0.5.0" + resolved "https://registry.npmjs.org/slate-paste-linkify/-/slate-paste-linkify-0.5.0.tgz#fbb52216ae4c02475b42b3de3609de0ad6b07fd1" dependencies: is-url "^1.2.2" to-pascal-case "^1.0.0" -slate-prism@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/slate-prism/-/slate-prism-0.2.2.tgz#0c9d5c2bee0e94a6df5fc564b7a99f6b8e1ea492" +slate-plain-serializer@^0.4.12: + version "0.4.12" + resolved "https://registry.npmjs.org/slate-plain-serializer/-/slate-plain-serializer-0.4.12.tgz#802c246bbb7f5c03170a6b3b862251ec51d39cb6" + dependencies: + slate-dev-logger "^0.1.36" + +slate-prism@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/slate-prism/-/slate-prism-0.4.0.tgz#9d43b1fafa4c3a8e3bceaa8dbc41dd5ca39445a9" dependencies: prismjs "^1.6.0" -slate-trailing-block@^0.2.4: - version "0.2.4" - resolved "https://registry.yarnpkg.com/slate-trailing-block/-/slate-trailing-block-0.2.4.tgz#6ce9525fa15f9f098d810d9312a4267799cd0e12" +slate-prop-types@^0.4.12: + version "0.4.12" + resolved "https://registry.npmjs.org/slate-prop-types/-/slate-prop-types-0.4.12.tgz#a600030e8083fbc4a195ed98657e0d4e45caf93b" + dependencies: + slate-dev-logger "^0.1.36" -slate@^0.21.4: - version "0.21.4" - resolved "https://registry.yarnpkg.com/slate/-/slate-0.21.4.tgz#ae6113379cd838b7ec68ecd94834ce9741bc36f3" +slate-react@^0.10.19: + version "0.10.19" + resolved "https://registry.npmjs.org/slate-react/-/slate-react-0.10.19.tgz#917dac40634aeb0dc973100d492e11fc450c80ee" dependencies: debug "^2.3.2" - direction "^0.1.5" - es6-map "^0.1.4" - esrever "^0.2.0" get-window "^1.1.1" - immutable "^3.8.1" - is-empty "^1.0.0" + is-hotkey "^0.1.1" is-in-browser "^1.1.3" is-window "^1.0.2" keycode "^2.1.2" - lodash "^4.17.4" + lodash.throttle "^4.1.1" prop-types "^15.5.8" + react-immutable-proptypes "^2.1.0" react-portal "^3.1.0" selection-is-backward "^1.0.0" + slate-base64-serializer "^0.2.14" + slate-dev-logger "^0.1.36" + slate-plain-serializer "^0.4.12" + slate-prop-types "^0.4.12" + +slate-trailing-block@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/slate-trailing-block/-/slate-trailing-block-0.4.0.tgz#92573d7729e4c2a05c45521616ae2db28197476c" + +slate@^0.29.0: + version "0.29.1" + resolved "https://registry.npmjs.org/slate/-/slate-0.29.1.tgz#a9df98158e67f92456b9b8f38fb6d279ba8f9f7e" + dependencies: + debug "^2.3.2" + direction "^0.1.5" + esrever "^0.2.0" + is-empty "^1.0.0" + is-plain-object "^2.0.4" + lodash "^4.17.4" + slate-dev-logger "^0.1.25" type-of "^2.0.1" slice-ansi@0.0.4: From 802f6e659437186b896bc54e9de220f3f605d865 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 3 Dec 2017 11:13:35 -0800 Subject: [PATCH 02/18] Flowtyping --- app/components/Editor/Editor.js | 26 ++-- .../Editor/components/BlockInsert.js | 4 +- app/components/Editor/components/Code.js | 9 +- app/components/Editor/components/Contents.js | 6 +- app/components/Editor/components/Heading.js | 31 ++-- .../Editor/components/HorizontalRule.js | 9 +- app/components/Editor/components/Image.js | 9 +- app/components/Editor/components/Link.js | 9 +- app/components/Editor/components/ListItem.js | 4 +- app/components/Editor/components/Paragraph.js | 4 +- app/components/Editor/components/TodoItem.js | 4 +- .../Editor/components/Toolbar/BlockToolbar.js | 7 +- .../Editor/components/Toolbar/Toolbar.js | 4 +- .../Toolbar/components/LinkToolbar.js | 4 +- app/components/Editor/headingToSlug.js | 4 +- app/components/Editor/insertImage.js | 22 ++- app/components/Editor/marks.js | 9 +- app/components/Editor/nodes.js | 4 +- .../Editor/plugins/KeyboardShortcuts.js | 26 ++-- .../Editor/plugins/MarkdownShortcuts.js | 138 +++++++----------- app/components/Editor/transforms.js | 10 +- app/components/Editor/types.js | 13 ++ package.json | 1 - 23 files changed, 168 insertions(+), 189 deletions(-) create mode 100644 app/components/Editor/types.js diff --git a/app/components/Editor/Editor.js b/app/components/Editor/Editor.js index fdb35519d..ea8fd1b9e 100644 --- a/app/components/Editor/Editor.js +++ b/app/components/Editor/Editor.js @@ -2,8 +2,9 @@ import React, { Component } from 'react'; import { observable } from 'mobx'; import { observer } from 'mobx-react'; +import { Value, Change } from 'slate'; import { Editor } from 'slate-react'; -import type { state, props, change } from 'slate-prop-types'; +import type { SlateNodeProps } from './types'; import Plain from 'slate-plain-serializer'; import keydown from 'react-keydown'; import getDataTransferFiles from 'utils/getDataTransferFiles'; @@ -22,7 +23,7 @@ import styled from 'styled-components'; type Props = { text: string, - onChange: change => *, + onChange: Change => *, onSave: (redirect?: boolean) => *, onCancel: () => void, onImageUploadStart: () => void, @@ -31,18 +32,13 @@ type Props = { readOnly: boolean, }; -type KeyData = { - isMeta: boolean, - key: string, -}; - @observer class MarkdownEditor extends Component { props: Props; editor: Editor; - renderNode: props => *; + renderNode: SlateNodeProps => *; plugins: Object[]; - @observable editorValue: state; + @observable editorValue: Value; constructor(props: Props) { super(props); @@ -79,7 +75,7 @@ class MarkdownEditor extends Component { } } - onChange = (change: change) => { + onChange = (change: Change) => { if (this.editorValue !== change.value) { this.props.onChange(Markdown.serialize(change.value)); } @@ -146,17 +142,17 @@ class MarkdownEditor extends Component { } // Handling of keyboard shortcuts within editor focus - onKeyDown = (ev: SyntheticKeyboardEvent, data: KeyData, change: change) => { - if (!data.isMeta) return; + onKeyDown = (ev: SyntheticKeyboardEvent, change: Change) => { + if (!ev.metaKey) return; - switch (data.key) { + switch (ev.key) { case 's': this.onSave(ev); return change; - case 'enter': + case 'Enter': this.onSaveAndExit(ev); return change; - case 'escape': + case 'Escape': this.onCancel(); return change; default: diff --git a/app/components/Editor/components/BlockInsert.js b/app/components/Editor/components/BlockInsert.js index 02673b0e6..8eab208fe 100644 --- a/app/components/Editor/components/BlockInsert.js +++ b/app/components/Editor/components/BlockInsert.js @@ -13,10 +13,10 @@ type Props = { editor: Editor, }; -function findClosestRootNode(state, ev) { +function findClosestRootNode(value, ev) { let previous; - for (const node of state.document.nodes) { + for (const node of value.document.nodes) { const element = findDOMNode(node); const bounds = element.getBoundingClientRect(); if (bounds.top > ev.clientY) return previous; diff --git a/app/components/Editor/components/Code.js b/app/components/Editor/components/Code.js index 7c2118eeb..f2396b545 100644 --- a/app/components/Editor/components/Code.js +++ b/app/components/Editor/components/Code.js @@ -1,11 +1,16 @@ // @flow import React from 'react'; import styled from 'styled-components'; -import type { props } from 'slate-prop-types'; +import type { SlateNodeProps } from '../types'; import CopyButton from './CopyButton'; import { color } from 'shared/styles/constants'; -export default function Code({ children, node, readOnly, attributes }: props) { +export default function Code({ + children, + node, + readOnly, + attributes, +}: SlateNodeProps) { const language = node.data.get('language') || 'javascript'; return ( diff --git a/app/components/Editor/components/Contents.js b/app/components/Editor/components/Contents.js index 81dcfad0d..b9afe07bd 100644 --- a/app/components/Editor/components/Contents.js +++ b/app/components/Editor/components/Contents.js @@ -3,7 +3,7 @@ import React, { Component } from 'react'; import { observable } from 'mobx'; import { observer } from 'mobx-react'; import { Editor } from 'slate-react'; -import type { state, block } from 'slate-prop-types'; +import { Block } from 'slate'; import { List } from 'immutable'; import { color } from 'shared/styles/constants'; import headingToSlug from '../headingToSlug'; @@ -54,10 +54,10 @@ class Contents extends Component { return elements; } - get headings(): List { + get headings(): List { const { editor } = this.props; - return editor.value.document.nodes.filter((node: block) => { + return editor.value.document.nodes.filter((node: Block) => { if (!node.text) return false; return node.type.match(/^heading/); }); diff --git a/app/components/Editor/components/Heading.js b/app/components/Editor/components/Heading.js index 2a57279b1..03b03d168 100644 --- a/app/components/Editor/components/Heading.js +++ b/app/components/Editor/components/Heading.js @@ -1,22 +1,15 @@ // @flow import React from 'react'; import { Document } from 'slate'; -import { Editor } from 'slate-react'; +import type { SlateNodeProps } from '../types'; import styled from 'styled-components'; -import type { node } from 'slate-prop-types'; import headingToSlug from '../headingToSlug'; import Placeholder from './Placeholder'; -type Props = { - children: React$Element<*>, - placeholder?: boolean, - parent: node, - node: node, - editor: Editor, - readOnly: boolean, - component?: string, - attributes: Object, - className?: string, +type Props = SlateNodeProps & { + component: string, + className: string, + placeholder: string, }; function Heading(props: Props) { @@ -61,7 +54,7 @@ function Heading(props: Props) { const Wrapper = styled.div` display: inline; - margin-left: ${(props: Props) => (props.hasEmoji ? '-1.2em' : 0)}; + margin-left: ${(props: SlateNodeProps) => (props.hasEmoji ? '-1.2em' : 0)}; `; const Anchor = styled.a` @@ -84,21 +77,21 @@ export const StyledHeading = styled(Heading)` } } `; -export const Heading1 = (props: Props) => ( +export const Heading1 = (props: SlateNodeProps) => ( ); -export const Heading2 = (props: Props) => ( +export const Heading2 = (props: SlateNodeProps) => ( ); -export const Heading3 = (props: Props) => ( +export const Heading3 = (props: SlateNodeProps) => ( ); -export const Heading4 = (props: Props) => ( +export const Heading4 = (props: SlateNodeProps) => ( ); -export const Heading5 = (props: Props) => ( +export const Heading5 = (props: SlateNodeProps) => ( ); -export const Heading6 = (props: Props) => ( +export const Heading6 = (props: SlateNodeProps) => ( ); diff --git a/app/components/Editor/components/HorizontalRule.js b/app/components/Editor/components/HorizontalRule.js index 7efcbe998..4afd19448 100644 --- a/app/components/Editor/components/HorizontalRule.js +++ b/app/components/Editor/components/HorizontalRule.js @@ -1,12 +1,13 @@ // @flow import React from 'react'; import styled from 'styled-components'; -import type { props } from 'slate-prop-types'; +import type { SlateNodeProps } from '../types'; import { color } from 'shared/styles/constants'; -function HorizontalRule(props: props) { - const { state, node, attributes } = props; - const active = state.isFocused && state.selection.hasEdgeIn(node); +function HorizontalRule(props: SlateNodeProps) { + const { editor, node, attributes } = props; + const active = + editor.value.isFocused && editor.value.selection.hasEdgeIn(node); return ; } diff --git a/app/components/Editor/components/Image.js b/app/components/Editor/components/Image.js index 23ab022ed..c8f7cbab5 100644 --- a/app/components/Editor/components/Image.js +++ b/app/components/Editor/components/Image.js @@ -2,11 +2,11 @@ import React, { Component } from 'react'; import ImageZoom from 'react-medium-image-zoom'; import styled from 'styled-components'; -import type { props } from 'slate-prop-types'; +import type { SlateNodeProps } from '../types'; import { color } from 'shared/styles/constants'; class Image extends Component { - props: props; + props: SlateNodeProps; handleChange = (ev: SyntheticInputEvent) => { const alt = ev.target.value; @@ -26,11 +26,12 @@ class Image extends Component { }; render() { - const { attributes, state, node, readOnly } = this.props; + const { attributes, editor, node, readOnly } = this.props; const loading = node.data.get('loading'); const caption = node.data.get('alt'); const src = node.data.get('src'); - const active = state.isFocused && state.selection.hasEdgeIn(node); + const active = + editor.value.isFocused && editor.value.selection.hasEdgeIn(node); const showCaption = !readOnly || caption; return ( diff --git a/app/components/Editor/components/Link.js b/app/components/Editor/components/Link.js index 0938dd532..54302eb60 100644 --- a/app/components/Editor/components/Link.js +++ b/app/components/Editor/components/Link.js @@ -1,7 +1,7 @@ // @flow import React from 'react'; import { Link as InternalLink } from 'react-router-dom'; -import type { props } from 'slate-prop-types'; +import type { SlateNodeProps } from '../types'; function getPathFromUrl(href: string) { if (href[0] === '/') return href; @@ -26,7 +26,12 @@ function isInternalUrl(href: string) { } } -export default function Link({ attributes, node, children, readOnly }: props) { +export default function Link({ + attributes, + node, + children, + readOnly, +}: SlateNodeProps) { const href = node.data.get('href'); const path = getPathFromUrl(href); diff --git a/app/components/Editor/components/ListItem.js b/app/components/Editor/components/ListItem.js index 51fea4c03..7941f97e1 100644 --- a/app/components/Editor/components/ListItem.js +++ b/app/components/Editor/components/ListItem.js @@ -1,6 +1,6 @@ // @flow import React from 'react'; -import type { props } from 'slate-prop-types'; +import type { SlateNodeProps } from '../types'; import TodoItem from './TodoItem'; export default function ListItem({ @@ -8,7 +8,7 @@ export default function ListItem({ node, attributes, ...props -}: props) { +}: SlateNodeProps) { const checked = node.data.get('checked'); if (checked !== undefined) { diff --git a/app/components/Editor/components/Paragraph.js b/app/components/Editor/components/Paragraph.js index 07801e1f1..952ae174e 100644 --- a/app/components/Editor/components/Paragraph.js +++ b/app/components/Editor/components/Paragraph.js @@ -1,7 +1,7 @@ // @flow import React from 'react'; import { Document } from 'slate'; -import type { props } from 'slate-prop-types'; +import type { SlateNodeProps } from '../types'; import Placeholder from './Placeholder'; export default function Link({ @@ -11,7 +11,7 @@ export default function Link({ parent, children, readOnly, -}: props) { +}: SlateNodeProps) { const parentIsDocument = parent instanceof Document; const firstParagraph = parent && parent.nodes.get(1) === node; const lastParagraph = parent && parent.nodes.last() === node; diff --git a/app/components/Editor/components/TodoItem.js b/app/components/Editor/components/TodoItem.js index bb7a9cf22..a1e7fed12 100644 --- a/app/components/Editor/components/TodoItem.js +++ b/app/components/Editor/components/TodoItem.js @@ -2,10 +2,10 @@ import React, { Component } from 'react'; import styled from 'styled-components'; import { color } from 'shared/styles/constants'; -import type { props } from 'slate-prop-types'; +import type { SlateNodeProps } from '../types'; export default class TodoItem extends Component { - props: props & { checked: boolean }; + props: SlateNodeProps & { checked: boolean }; handleChange = (ev: SyntheticInputEvent) => { const checked = ev.target.checked; diff --git a/app/components/Editor/components/Toolbar/BlockToolbar.js b/app/components/Editor/components/Toolbar/BlockToolbar.js index 615515612..e1aa29e33 100644 --- a/app/components/Editor/components/Toolbar/BlockToolbar.js +++ b/app/components/Editor/components/Toolbar/BlockToolbar.js @@ -13,14 +13,13 @@ 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 } from 'slate-prop-types'; +import type { SlateNodeProps } from '../../types'; import { color } from 'shared/styles/constants'; import { fadeIn } from 'shared/styles/animations'; import { splitAndInsertBlock } from '../../transforms'; -type Props = props & { - onInsertImage: Function, - onChange: Function, +type Props = SlateNodeProps & { + onInsertImage: *, }; type Options = { diff --git a/app/components/Editor/components/Toolbar/Toolbar.js b/app/components/Editor/components/Toolbar/Toolbar.js index 1efb85529..75d978475 100644 --- a/app/components/Editor/components/Toolbar/Toolbar.js +++ b/app/components/Editor/components/Toolbar/Toolbar.js @@ -4,7 +4,7 @@ import { observable } from 'mobx'; import { observer } from 'mobx-react'; import { Portal } from 'react-portal'; import { Editor } from 'slate-react'; -import type { value } from 'slate-prop-types'; +import { Value } from 'slate'; import styled from 'styled-components'; import _ from 'lodash'; import FormattingToolbar from './components/FormattingToolbar'; @@ -20,7 +20,7 @@ export default class Toolbar extends Component { props: { editor: Editor, - value: value, + value: Value, }; menu: HTMLElement; diff --git a/app/components/Editor/components/Toolbar/components/LinkToolbar.js b/app/components/Editor/components/Toolbar/components/LinkToolbar.js index f0fd65a99..72623c0bb 100644 --- a/app/components/Editor/components/Toolbar/components/LinkToolbar.js +++ b/app/components/Editor/components/Toolbar/components/LinkToolbar.js @@ -4,10 +4,10 @@ import ReactDOM from 'react-dom'; import { observable, action } from 'mobx'; import { observer, inject } from 'mobx-react'; import { withRouter } from 'react-router-dom'; +import { Change } from 'slate'; import { Editor } from 'slate-react'; import styled from 'styled-components'; import ArrowKeyNavigation from 'boundless-arrow-key-navigation'; -import type { change } from 'slate-prop-types'; import ToolbarButton from './ToolbarButton'; import DocumentResult from './DocumentResult'; import DocumentsStore from 'stores/DocumentsStore'; @@ -28,7 +28,7 @@ class LinkToolbar extends Component { link: Object, documents: DocumentsStore, onBlur: () => void, - onChange: change => *, + onChange: Change => *, }; @observable isEditing: boolean = false; diff --git a/app/components/Editor/headingToSlug.js b/app/components/Editor/headingToSlug.js index cf46ab2b9..eb06dfcde 100644 --- a/app/components/Editor/headingToSlug.js +++ b/app/components/Editor/headingToSlug.js @@ -1,9 +1,9 @@ // @flow import { escape } from 'lodash'; -import type { node } from 'slate-prop-types'; +import { Node } from 'slate'; import slug from 'slug'; -export default function headingToSlug(node: node) { +export default function headingToSlug(node: Node) { const level = node.type.replace('heading', 'h'); return escape(`${level}-${slug(node.text)}-${node.key}`); } diff --git a/app/components/Editor/insertImage.js b/app/components/Editor/insertImage.js index 01480cc2e..2e0fb4c61 100644 --- a/app/components/Editor/insertImage.js +++ b/app/components/Editor/insertImage.js @@ -1,11 +1,11 @@ // @flow import uuid from 'uuid'; import uploadFile from 'utils/uploadFile'; +import { Change } from 'slate'; import { Editor } from 'slate-react'; -import type { change } from 'slate-prop-types'; export default async function insertImageFile( - change: change, + change: Change, file: window.File, editor: Editor, onImageUploadStart: () => void, @@ -22,14 +22,11 @@ export default async function insertImageFile( const src = reader.result; // insert into document as uploading placeholder - const state = change - .insertBlock({ - type: 'image', - isVoid: true, - data: { src, id, alt, loading: true }, - }) - .apply(); - editor.onChange(state); + change.insertBlock({ + type: 'image', + isVoid: true, + data: { src, id, alt, loading: true }, + }); }); reader.readAsDataURL(file); @@ -40,9 +37,8 @@ export default async function insertImageFile( // we dont use the original change provided to the callback here // as the state may have changed significantly in the time it took to // upload the file. - const state = editor.getState(); - const finalTransform = state.change(); - const placeholder = state.document.findDescendant( + const finalTransform = editor.value.change(); + const placeholder = editor.value.document.findDescendant( node => node.data && node.data.get('id') === id ); diff --git a/app/components/Editor/marks.js b/app/components/Editor/marks.js index 58d25c634..a88b6c279 100644 --- a/app/components/Editor/marks.js +++ b/app/components/Editor/marks.js @@ -1,9 +1,14 @@ // @flow import React from 'react'; import InlineCode from './components/InlineCode'; -import type { props } from 'slate-prop-types'; +import { Mark } from 'slate'; -export default function renderMark(props: props) { +type Props = { + children: React$Element<*>, + mark: Mark, +}; + +export default function renderMark(props: Props) { switch (props.mark.type) { case 'bold': return {props.children}; diff --git a/app/components/Editor/nodes.js b/app/components/Editor/nodes.js index b20e4de45..62a0dd966 100644 --- a/app/components/Editor/nodes.js +++ b/app/components/Editor/nodes.js @@ -16,14 +16,14 @@ import { Heading6, } from './components/Heading'; import Paragraph from './components/Paragraph'; -import type { props } from 'slate-prop-types'; +import type { SlateNodeProps } from './types'; type Options = { onInsertImage: *, }; export default function createRenderNode({ onChange, onInsertImage }: Options) { - return function renderNode(props: props) { + return function renderNode(props: SlateNodeProps) { const { attributes } = props; switch (props.node.type) { diff --git a/app/components/Editor/plugins/KeyboardShortcuts.js b/app/components/Editor/plugins/KeyboardShortcuts.js index e3b8c06c5..7ec48efca 100644 --- a/app/components/Editor/plugins/KeyboardShortcuts.js +++ b/app/components/Editor/plugins/KeyboardShortcuts.js @@ -1,20 +1,12 @@ // @flow -import type { change } from 'slate-prop-types'; +import { Change } from 'slate'; export default function KeyboardShortcuts() { return { - /** - * On key down, check for our specific key shortcuts. - * - * @param {Event} e - * @param {Data} data - * @param {State} state - * @return {State or Null} state - */ - onKeyDown(ev: SyntheticEvent, data: Object, change: change) { - if (!data.isMeta) return null; + onKeyDown(ev: SyntheticKeyboardEvent, change: Change) { + if (!ev.metaKey) return null; - switch (data.key) { + switch (ev.key) { case 'b': return this.toggleMark(change, 'bold'); case 'i': @@ -30,13 +22,13 @@ export default function KeyboardShortcuts() { } }, - toggleMark(change: change, type: string) { - const { state } = change; + toggleMark(change: Change, type: string) { + const { value } = change; // don't allow formatting of document title - const firstNode = state.document.nodes.first(); - if (firstNode === state.startBlock) return; + const firstNode = value.document.nodes.first(); + if (firstNode === value.startBlock) return; - return state.change().toggleMark(type); + change.toggleMark(type); }, }; } diff --git a/app/components/Editor/plugins/MarkdownShortcuts.js b/app/components/Editor/plugins/MarkdownShortcuts.js index 8800f4f56..651ae2074 100644 --- a/app/components/Editor/plugins/MarkdownShortcuts.js +++ b/app/components/Editor/plugins/MarkdownShortcuts.js @@ -1,10 +1,5 @@ // @flow -import type { change } from 'slate-prop-types'; - -type KeyData = { - isMeta: boolean, - key: string, -}; +import { Change } from 'slate'; const inlineShortcuts = [ { mark: 'bold', shortcut: '**' }, @@ -18,22 +13,19 @@ const inlineShortcuts = [ export default function MarkdownShortcuts() { return { - /** - * On key down, check for our specific key shortcuts. - */ - onKeyDown(ev: SyntheticEvent, data: KeyData, change: change) { - switch (data.key) { + onKeyDown(ev: SyntheticKeyboardEvent, change: Change) { + switch (ev.key) { case '-': return this.onDash(ev, change); case '`': return this.onBacktick(ev, change); - case 'tab': + case 'Tab': return this.onTab(ev, change); - case 'space': + case ' ': return this.onSpace(ev, change); - case 'backspace': + case 'Backspace': return this.onBackspace(ev, change); - case 'enter': + case 'Enter': return this.onEnter(ev, change); default: return null; @@ -44,10 +36,10 @@ export default function MarkdownShortcuts() { * On space, if it was after an auto-markdown shortcut, convert the current * node into the shortcut's corresponding type. */ - onSpace(ev: SyntheticEvent, change: change) { - const { state } = change; - if (state.isExpanded) return; - const { startBlock, startOffset } = state; + onSpace(ev: SyntheticKeyboardEvent, change: Change) { + const { value } = change; + if (value.isExpanded) return; + const { startBlock, startOffset } = value; const chars = startBlock.text.slice(0, startOffset).trim(); const type = this.getType(chars); @@ -58,7 +50,7 @@ export default function MarkdownShortcuts() { let checked; if (chars === '[x]') checked = true; if (chars === '[ ]') checked = false; - const change = state.change().setBlock({ type, data: { checked } }); + change.setBlock({ type, data: { checked } }); if (type === 'list-item') { if (checked !== undefined) { @@ -99,40 +91,32 @@ export default function MarkdownShortcuts() { // if we have multiple tags then mark the text between as inline code if (inlineTags.length > 1) { - const change = state.change(); const firstText = startBlock.getFirstText(); const firstCodeTagIndex = inlineTags[0]; const lastCodeTagIndex = inlineTags[inlineTags.length - 1]; - change.removeTextByKey( - firstText.key, - lastCodeTagIndex, - shortcut.length - ); - change.removeTextByKey( - firstText.key, - firstCodeTagIndex, - shortcut.length - ); - change.moveOffsetsTo( - firstCodeTagIndex, - lastCodeTagIndex - shortcut.length - ); - change.addMark(mark); - return change.collapseToEnd().removeMark(mark); + change + .removeTextByKey(firstText.key, lastCodeTagIndex, shortcut.length) + .removeTextByKey(firstText.key, firstCodeTagIndex, shortcut.length) + .moveOffsetsTo( + firstCodeTagIndex, + lastCodeTagIndex - shortcut.length + ) + .addMark(mark) + .collapseToEnd() + .removeMark(mark); } } }, - onDash(ev: SyntheticEvent, change: change) { - const { state } = change; - if (state.isExpanded) return; - const { startBlock, startOffset } = state; + onDash(ev: SyntheticKeyboardEvent, change: Change) { + const { value } = change; + if (value.isExpanded) return; + const { startBlock, startOffset } = value; const chars = startBlock.text.slice(0, startOffset).replace(/\s*/g, ''); if (chars === '--') { ev.preventDefault(); - return state - .change() + return change .extendToStartOf(startBlock) .delete() .setBlock({ @@ -144,16 +128,15 @@ export default function MarkdownShortcuts() { } }, - onBacktick(ev: SyntheticEvent, change: change) { - const { state } = change; - if (state.isExpanded) return; - const { startBlock, startOffset } = state; + onBacktick(ev: SyntheticKeyboardEvent, change: Change) { + const { value } = change; + if (value.isExpanded) return; + const { startBlock, startOffset } = value; const chars = startBlock.text.slice(0, startOffset).replace(/\s*/g, ''); if (chars === '``') { ev.preventDefault(); - return state - .change() + return change .extendToStartOf(startBlock) .delete() .setBlock({ @@ -162,20 +145,22 @@ export default function MarkdownShortcuts() { } }, - onBackspace(ev: SyntheticEvent, change: change) { - const { state } = change; - if (change.isExpanded) return; - const { startBlock, selection, startOffset } = state; + onBackspace(ev: SyntheticKeyboardEvent, change: Change) { + const { value } = change; + if (value.isExpanded) return; + const { startBlock, selection, startOffset } = value; // If at the start of a non-paragraph, convert it back into a paragraph if (startOffset === 0) { if (startBlock.type === 'paragraph') return; ev.preventDefault(); - const change = state.change().setBlock('paragraph'); + change.setBlock('paragraph'); - if (startBlock.type === 'list-item') + if (startBlock.type === 'list-item') { change.unwrapBlock('bulleted-list'); + } + return change; } @@ -195,14 +180,12 @@ export default function MarkdownShortcuts() { .reverse() .takeUntil((v, k) => !v.marks.some(mark => mark.type === 'code')); - return state - .change() - .removeMarkByKey( - textNode.key, - change.startOffset - charsInCodeBlock.size, - change.startOffset, - 'code' - ); + change.removeMarkByKey( + textNode.key, + change.startOffset - charsInCodeBlock.size, + change.startOffset, + 'code' + ); } } }, @@ -211,15 +194,12 @@ export default function MarkdownShortcuts() { * On tab, if at the end of the heading jump to the main body content * as if it is another input field (act the same as enter). */ - onTab(ev: SyntheticEvent, change: change) { - const { state } = change; + onTab(ev: SyntheticKeyboardEvent, change: Change) { + const { value } = change; - if (state.startBlock.type === 'heading1') { + if (value.startBlock.type === 'heading1') { ev.preventDefault(); - return state - .change() - .splitBlock() - .setBlock('paragraph'); + change.splitBlock().setBlock('paragraph'); } }, @@ -227,10 +207,10 @@ export default function MarkdownShortcuts() { * On return, if at the end of a node type that should not be extended, * create a new paragraph below it. */ - onEnter(ev: SyntheticEvent, change: change) { - const { state } = change; - if (state.isExpanded) return; - const { startBlock, startOffset, endOffset } = state; + onEnter(ev: SyntheticKeyboardEvent, change: Change) { + const { value } = change; + if (value.isExpanded) return; + const { startBlock, startOffset, endOffset } = value; if (startOffset === 0 && startBlock.length === 0) return this.onBackspace(ev, change); if (endOffset !== startBlock.length) return; @@ -239,10 +219,7 @@ export default function MarkdownShortcuts() { // insert a new paragraph if (startBlock.type === 'image') { ev.preventDefault(); - return state - .change() - .collapseToEnd() - .insertBlock('paragraph'); + return change.collapseToEnd().insertBlock('paragraph'); } // Hitting enter in a heading or blockquote will split the node at that @@ -260,10 +237,7 @@ export default function MarkdownShortcuts() { } ev.preventDefault(); - return state - .change() - .splitBlock() - .setBlock('paragraph'); + change.splitBlock().setBlock('paragraph'); }, /** diff --git a/app/components/Editor/transforms.js b/app/components/Editor/transforms.js index e5c30184d..9acd47874 100644 --- a/app/components/Editor/transforms.js +++ b/app/components/Editor/transforms.js @@ -1,8 +1,8 @@ // @flow -import type { change } from 'slate-prop-types'; +import { Change } from 'slate'; import EditList from './plugins/EditList'; -const { transforms } = EditList; +const { changes } = EditList; type Options = { type: string | Object, @@ -10,7 +10,7 @@ type Options = { append?: string | Object, }; -export function splitAndInsertBlock(change: change, options: Options) { +export function splitAndInsertBlock(change: Change, options: Options) { const { type, wrapper, append } = options; const { value } = change; const { document } = value; @@ -18,8 +18,8 @@ export function splitAndInsertBlock(change: change, options: Options) { // lists get some special treatment if (parent && parent.type === 'list-item') { - change = transforms.unwrapList( - transforms + change = changes.unwrapList( + changes .splitListItem(change.collapseToStart()) .collapseToEndOfPreviousBlock() ); diff --git a/app/components/Editor/types.js b/app/components/Editor/types.js new file mode 100644 index 000000000..842ffd930 --- /dev/null +++ b/app/components/Editor/types.js @@ -0,0 +1,13 @@ +// @flow +import { Value, Node } from 'slate'; +import { Editor } from 'slate-react'; + +export type SlateNodeProps = { + children: React$Element<*>, + readOnly: boolean, + attributes: Object, + value: Value, + editor: Editor, + node: Node, + parent: Node, +}; diff --git a/package.json b/package.json index 9d0cfb21b..0dffb3c53 100644 --- a/package.json +++ b/package.json @@ -169,7 +169,6 @@ "slate-paste-linkify": "^0.5.0", "slate-plain-serializer": "^0.4.12", "slate-prism": "^0.4.0", - "slate-prop-types": "^0.4.12", "slate-react": "^0.10.19", "slate-trailing-block": "^0.4.0", "slug": "0.9.1", From 751b468e92654404601e22c7253c75ae11a65891 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 3 Dec 2017 19:59:54 -0800 Subject: [PATCH 03/18] More and more fixes --- app/components/Editor/Editor.js | 38 ++-- .../Editor/{insertImage.js => changes.js} | 47 +++- app/components/Editor/components/TodoItem.js | 9 +- .../Editor/components/Toolbar/BlockToolbar.js | 4 +- .../Toolbar/components/LinkToolbar.js | 2 - app/components/Editor/nodes.js | 2 +- app/components/Editor/plugins.js | 27 ++- app/components/Editor/plugins/EditList.js | 1 + .../Editor/plugins/MarkdownShortcuts.js | 24 +- app/components/Editor/schema.js | 215 +++++++----------- app/components/Editor/transforms.js | 34 --- app/components/Editor/types.js | 8 +- package.json | 4 +- yarn.lock | 36 ++- 14 files changed, 194 insertions(+), 257 deletions(-) rename app/components/Editor/{insertImage.js => changes.js} (52%) delete mode 100644 app/components/Editor/transforms.js diff --git a/app/components/Editor/Editor.js b/app/components/Editor/Editor.js index ea8fd1b9e..9de55c504 100644 --- a/app/components/Editor/Editor.js +++ b/app/components/Editor/Editor.js @@ -4,7 +4,7 @@ import { observable } from 'mobx'; import { observer } from 'mobx-react'; import { Value, Change } from 'slate'; import { Editor } from 'slate-react'; -import type { SlateNodeProps } from './types'; +import type { SlateNodeProps, Plugin } from './types'; import Plain from 'slate-plain-serializer'; import keydown from 'react-keydown'; import getDataTransferFiles from 'utils/getDataTransferFiles'; @@ -16,9 +16,10 @@ import Placeholder from './components/Placeholder'; import Contents from './components/Contents'; import Markdown from './serializer'; import createPlugins from './plugins'; -import insertImage from './insertImage'; +import { insertImageFile } from './changes'; import renderMark from './marks'; import createRenderNode from './nodes'; +import schema from './schema'; import styled from 'styled-components'; type Props = { @@ -37,7 +38,7 @@ class MarkdownEditor extends Component { props: Props; editor: Editor; renderNode: SlateNodeProps => *; - plugins: Object[]; + plugins: Plugin[]; @observable editorValue: Value; constructor(props: Props) { @@ -60,12 +61,11 @@ class MarkdownEditor extends Component { } componentDidMount() { - if (!this.props.readOnly) { - if (this.props.text) { - this.focusAtEnd(); - } else { - this.focusAtStart(); - } + if (this.props.readOnly) return; + if (this.props.text) { + this.focusAtEnd(); + } else { + this.focusAtStart(); } } @@ -78,8 +78,8 @@ class MarkdownEditor extends Component { onChange = (change: Change) => { if (this.editorValue !== change.value) { this.props.onChange(Markdown.serialize(change.value)); + this.editorValue = change.value; } - this.editorValue = change.value; }; handleDrop = async (ev: SyntheticEvent) => { @@ -100,15 +100,13 @@ class MarkdownEditor extends Component { }; insertImageFile = async (file: window.File) => { - this.editor.change( - async change => - await insertImage( - change, - file, - this.editor, - this.props.onImageUploadStart, - this.props.onImageUploadStop - ) + this.editor.change(change => + change.call( + insertImageFile, + file, + this.props.onImageUploadStart, + this.props.onImageUploadStop + ) ); }; @@ -206,10 +204,12 @@ class MarkdownEditor extends Component { value={this.editorValue} renderNode={this.renderNode} renderMark={renderMark} + schema={schema} onKeyDown={this.onKeyDown} onChange={this.onChange} onSave={onSave} readOnly={readOnly} + spellCheck={!readOnly} /> void, onImageUploadStop: () => void ) { onImageUploadStart(); - + console.log(file); try { // load the file as a data URL const id = uuid.v4(); @@ -27,6 +55,7 @@ export default async function insertImageFile( isVoid: true, data: { src, id, alt, loading: true }, }); + console.log('insertBlock', change); }); reader.readAsDataURL(file); @@ -37,12 +66,12 @@ export default async function insertImageFile( // we dont use the original change provided to the callback here // as the state may have changed significantly in the time it took to // upload the file. - const finalTransform = editor.value.change(); - const placeholder = editor.value.document.findDescendant( + const placeholder = change.value.document.findDescendant( node => node.data && node.data.get('id') === id ); + console.log('placeholder', placeholder); - return finalTransform.setNodeByKey(placeholder.key, { + return change.setNodeByKey(placeholder.key, { data: { src, alt, loading: false }, }); } catch (err) { diff --git a/app/components/Editor/components/TodoItem.js b/app/components/Editor/components/TodoItem.js index a1e7fed12..e6020228e 100644 --- a/app/components/Editor/components/TodoItem.js +++ b/app/components/Editor/components/TodoItem.js @@ -10,12 +10,9 @@ export default class TodoItem extends Component { handleChange = (ev: SyntheticInputEvent) => { const checked = ev.target.checked; const { editor, node } = this.props; - const change = editor - .getState() - .change() - .setNodeByKey(node.key, { data: { checked } }); - - editor.onChange(change); + editor.change(change => + change.setNodeByKey(node.key, { data: { checked } }) + ); }; render() { diff --git a/app/components/Editor/components/Toolbar/BlockToolbar.js b/app/components/Editor/components/Toolbar/BlockToolbar.js index e1aa29e33..ded10b94c 100644 --- a/app/components/Editor/components/Toolbar/BlockToolbar.js +++ b/app/components/Editor/components/Toolbar/BlockToolbar.js @@ -16,7 +16,7 @@ import ToolbarButton from './components/ToolbarButton'; import type { SlateNodeProps } from '../../types'; import { color } from 'shared/styles/constants'; import { fadeIn } from 'shared/styles/animations'; -import { splitAndInsertBlock } from '../../transforms'; +import { splitAndInsertBlock } from '../../changes'; type Props = SlateNodeProps & { onInsertImage: *, @@ -61,7 +61,7 @@ class BlockToolbar extends Component { editor.change(change => { splitAndInsertBlock(change, options); - change.value.document.nodes.forEach(node => { + editor.value.document.nodes.forEach(node => { if (node.type === 'block-toolbar') { change.removeNodeByKey(node.key); } diff --git a/app/components/Editor/components/Toolbar/components/LinkToolbar.js b/app/components/Editor/components/Toolbar/components/LinkToolbar.js index 72623c0bb..d3209e8cc 100644 --- a/app/components/Editor/components/Toolbar/components/LinkToolbar.js +++ b/app/components/Editor/components/Toolbar/components/LinkToolbar.js @@ -4,7 +4,6 @@ import ReactDOM from 'react-dom'; import { observable, action } from 'mobx'; import { observer, inject } from 'mobx-react'; import { withRouter } from 'react-router-dom'; -import { Change } from 'slate'; import { Editor } from 'slate-react'; import styled from 'styled-components'; import ArrowKeyNavigation from 'boundless-arrow-key-navigation'; @@ -28,7 +27,6 @@ class LinkToolbar extends Component { link: Object, documents: DocumentsStore, onBlur: () => void, - onChange: Change => *, }; @observable isEditing: boolean = false; diff --git a/app/components/Editor/nodes.js b/app/components/Editor/nodes.js index 62a0dd966..68ab6e2da 100644 --- a/app/components/Editor/nodes.js +++ b/app/components/Editor/nodes.js @@ -22,7 +22,7 @@ type Options = { onInsertImage: *, }; -export default function createRenderNode({ onChange, onInsertImage }: Options) { +export default function createRenderNode({ onInsertImage }: Options) { return function renderNode(props: SlateNodeProps) { const { attributes } = props; diff --git a/app/components/Editor/plugins.js b/app/components/Editor/plugins.js index b3130aac6..46e59d893 100644 --- a/app/components/Editor/plugins.js +++ b/app/components/Editor/plugins.js @@ -1,5 +1,5 @@ // @flow -// import DropOrPasteImages from '@tommoor/slate-drop-or-paste-images'; +import InsertImages from 'slate-drop-or-paste-images'; import PasteLinkify from 'slate-paste-linkify'; import CollapseOnEscape from 'slate-collapse-on-escape'; import TrailingBlock from 'slate-trailing-block'; @@ -8,7 +8,7 @@ import Prism from 'slate-prism'; import EditList from './plugins/EditList'; import KeyboardShortcuts from './plugins/KeyboardShortcuts'; import MarkdownShortcuts from './plugins/MarkdownShortcuts'; -// import insertImage from './insertImage'; +import { insertImageFile } from './changes'; const onlyInCode = node => node.type === 'code'; @@ -23,18 +23,17 @@ const createPlugins = ({ onImageUploadStart, onImageUploadStop }: Options) => { type: 'link', collapseTo: 'end', }), - // DropOrPasteImages({ - // extensions: ['png', 'jpg', 'gif'], - // applyTransform: (transform, file, editor) => { - // return insertImage( - // transform, - // file, - // editor, - // onImageUploadStart, - // onImageUploadStop - // ); - // }, - // }), + InsertImages({ + extensions: ['png', 'jpg', 'gif'], + insertImage(change, file) { + return change.call( + insertImageFile, + file, + onImageUploadStart, + onImageUploadStop + ); + }, + }), EditList, EditCode({ onlyIn: onlyInCode, diff --git a/app/components/Editor/plugins/EditList.js b/app/components/Editor/plugins/EditList.js index 7bef46f48..9d3dea9a7 100644 --- a/app/components/Editor/plugins/EditList.js +++ b/app/components/Editor/plugins/EditList.js @@ -4,4 +4,5 @@ import EditList from 'slate-edit-list'; export default EditList({ types: ['ordered-list', 'bulleted-list', 'todo-list'], typeItem: 'list-item', + typeDefault: 'paragraph', }); diff --git a/app/components/Editor/plugins/MarkdownShortcuts.js b/app/components/Editor/plugins/MarkdownShortcuts.js index 651ae2074..8d4e7b663 100644 --- a/app/components/Editor/plugins/MarkdownShortcuts.js +++ b/app/components/Editor/plugins/MarkdownShortcuts.js @@ -182,8 +182,8 @@ export default function MarkdownShortcuts() { change.removeMarkByKey( textNode.key, - change.startOffset - charsInCodeBlock.size, - change.startOffset, + startOffset - charsInCodeBlock.size, + startOffset, 'code' ); } @@ -210,10 +210,13 @@ export default function MarkdownShortcuts() { onEnter(ev: SyntheticKeyboardEvent, change: Change) { const { value } = change; if (value.isExpanded) return; + const { startBlock, startOffset, endOffset } = value; if (startOffset === 0 && startBlock.length === 0) return this.onBackspace(ev, change); - if (endOffset !== startBlock.length) return; + + // Hitting enter at the end of the line reverts to standard behavior + if (endOffset === startBlock.length) return; // Hitting enter while an image is selected should jump caret below and // insert a new paragraph @@ -225,19 +228,12 @@ export default function MarkdownShortcuts() { // Hitting enter in a heading or blockquote will split the node at that // point and make the new node a paragraph if ( - startBlock.type !== 'heading1' && - startBlock.type !== 'heading2' && - startBlock.type !== 'heading3' && - startBlock.type !== 'heading4' && - startBlock.type !== 'heading5' && - startBlock.type !== 'heading6' && - startBlock.type !== 'block-quote' + startBlock.type.startsWith('heading') || + startBlock.type === 'block-quote' ) { - return; + ev.preventDefault(); + return change.splitBlock().setBlock('paragraph'); } - - ev.preventDefault(); - change.splitBlock().setBlock('paragraph'); }, /** diff --git a/app/components/Editor/schema.js b/app/components/Editor/schema.js index 1b59683ac..14ae2baff 100644 --- a/app/components/Editor/schema.js +++ b/app/components/Editor/schema.js @@ -1,133 +1,82 @@ -// // @flow -// import React from 'react'; -// import Code from './components/Code'; -// import HorizontalRule from './components/HorizontalRule'; -// import InlineCode from './components/InlineCode'; -// import Image from './components/Image'; -// import Link from './components/Link'; -// import ListItem from './components/ListItem'; -// import TodoList from './components/TodoList'; -// import { -// Heading1, -// Heading2, -// Heading3, -// Heading4, -// Heading5, -// Heading6, -// } from './components/Heading'; -// import Paragraph from './components/Paragraph'; -// import BlockToolbar from './components/Toolbar/BlockToolbar'; -// import type { Props, Node, Transform } from './types'; -// -// type Options = { -// onInsertImage: Function, -// onChange: Function, -// }; -// -// const createSchema = ({ onInsertImage, onChange }: Options) => { -// return { -// marks: { -// bold: (props: Props) => {props.children}, -// code: (props: Props) => {props.children}, -// italic: (props: Props) => {props.children}, -// underlined: (props: Props) => {props.children}, -// deleted: (props: Props) => {props.children}, -// added: (props: Props) => {props.children}, -// }, -// -// nodes: { -// 'block-toolbar': (props: Props) => ( -// -// ), -// paragraph: (props: Props) => , -// 'block-quote': (props: Props) => ( -//
{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} -// ), -// code: Code, -// image: Image, -// link: Link, -// 'list-item': ListItem, -// heading1: (props: Props) => , -// heading2: (props: Props) => , -// heading3: (props: Props) => , -// heading4: (props: Props) => , -// heading5: (props: Props) => , -// heading6: (props: Props) => , -// }, -// -// rules: [ -// // ensure first node is always a heading -// { -// match: (node: Node) => { -// return node.kind === 'document'; -// }, -// validate: (document: Node) => { -// const firstNode = document.nodes.first(); -// return firstNode && firstNode.type === 'heading1' ? null : firstNode; -// }, -// normalize: (transform: Transform, document: Node, firstNode: Node) => { -// transform.setBlock({ type: 'heading1' }); -// }, -// }, -// -// // automatically removes any marks in first heading -// { -// match: (node: Node) => { -// return node.kind === 'heading1'; -// }, -// validate: (heading: Node) => { -// const hasMarks = heading.getMarks().isEmpty(); -// const hasInlines = heading.getInlines().isEmpty(); -// -// return !(hasMarks && hasInlines); -// }, -// normalize: (transform: Transform, heading: Node) => { -// transform.unwrapInlineByKey(heading.key); -// -// heading.getMarks().forEach(mark => { -// heading.nodes.forEach(textNode => { -// if (textNode.kind === 'text') { -// transform.removeMarkByKey( -// textNode.key, -// 0, -// textNode.text.length, -// mark -// ); -// } -// }); -// }); -// -// return transform; -// }, -// }, -// ], -// }; -// }; -// -// export default createSchema; +// @flow +import { Block, Change, Node, Mark } from 'slate'; + +const schema = { + blocks: { + heading1: { marks: [''] }, + heading2: { marks: [''] }, + heading3: { marks: [''] }, + heading4: { marks: [''] }, + heading5: { marks: [''] }, + heading6: { marks: [''] }, + 'ordered-list': { + nodes: [{ types: ['list-item'] }], + }, + 'bulleted-list': { + nodes: [{ types: ['list-item'] }], + }, + table: { + nodes: [{ types: ['table-row', 'table-head', 'table-cell'] }], + }, + image: { + isVoid: true, + }, + 'horizontal-rule': { + isVoid: true, + }, + 'block-toolbar': { + isVoid: true, + }, + }, + document: { + nodes: [ + { types: ['heading1'], min: 1, max: 1 }, + { + types: [ + 'paragraph', + 'heading1', + 'heading2', + 'heading3', + 'heading4', + 'heading5', + 'heading6', + 'code', + 'horizontal-rule', + 'image', + 'bulleted-list', + 'ordered-list', + 'todo-list', + 'block-toolbar', + 'table', + ], + min: 1, + }, + ], + normalize: ( + change: Change, + reason: string, + { + node, + child, + mark, + index, + }: { node: Node, mark?: Mark, child: Node, index: number } + ) => { + switch (reason) { + case 'child_type_invalid': { + return change.setNodeByKey( + child.key, + index === 0 ? 'heading1' : 'paragraph' + ); + } + case 'child_required': { + const block = Block.create(index === 0 ? 'heading1' : 'paragraph'); + return change.insertNodeByKey(node.key, index, block); + } + default: + } + }, + }, +}; + +export default schema; diff --git a/app/components/Editor/transforms.js b/app/components/Editor/transforms.js deleted file mode 100644 index 9acd47874..000000000 --- a/app/components/Editor/transforms.js +++ /dev/null @@ -1,34 +0,0 @@ -// @flow -import { Change } from 'slate'; -import EditList from './plugins/EditList'; - -const { changes } = EditList; - -type Options = { - type: string | Object, - wrapper?: string | Object, - append?: string | Object, -}; - -export function splitAndInsertBlock(change: Change, options: Options) { - const { type, wrapper, append } = options; - const { value } = change; - const { document } = value; - const parent = document.getParent(value.startBlock.key); - - // lists get some special treatment - if (parent && parent.type === 'list-item') { - change = changes.unwrapList( - changes - .splitListItem(change.collapseToStart()) - .collapseToEndOfPreviousBlock() - ); - } - - change = change.insertBlock(type); - - if (wrapper) change = change.wrapBlock(wrapper); - if (append) change = change.insertBlock(append); - - return change; -} diff --git a/app/components/Editor/types.js b/app/components/Editor/types.js index 842ffd930..d39ed74e2 100644 --- a/app/components/Editor/types.js +++ b/app/components/Editor/types.js @@ -1,5 +1,5 @@ // @flow -import { Value, Node } from 'slate'; +import { Value, Change, Node } from 'slate'; import { Editor } from 'slate-react'; export type SlateNodeProps = { @@ -11,3 +11,9 @@ export type SlateNodeProps = { node: Node, parent: Node, }; + +export type Plugin = { + validateNode?: Node => *, + onClick?: SyntheticEvent => *, + onKeyDown?: (SyntheticKeyboardEvent, Change) => *, +}; diff --git a/package.json b/package.json index 0dffb3c53..5e50210c0 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,6 @@ "url": "git+ssh://git@github.com/outline/outline.git" }, "dependencies": { - "@tommoor/slate-drop-or-paste-images": "0.5.1", "aws-sdk": "^2.135.0", "babel-core": "^6.24.1", "babel-eslint": "^7.2.3", @@ -163,9 +162,10 @@ "sequelize-encrypted": "0.1.0", "slate": "^0.29.0", "slate-collapse-on-escape": "^0.6.0", + "slate-drop-or-paste-images": "^0.8.0", "slate-edit-code": "^0.13.2", "slate-edit-list": "^0.10.1", - "slate-md-serializer": "1.0.1", + "slate-md-serializer": "^1.0.4", "slate-paste-linkify": "^0.5.0", "slate-plain-serializer": "^0.4.12", "slate-prism": "^0.4.0", diff --git a/yarn.lock b/yarn.lock index 974677797..46907ed0c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,18 +2,6 @@ # yarn lockfile v1 -"@tommoor/slate-drop-or-paste-images@0.5.1": - version "0.5.1" - resolved "https://registry.yarnpkg.com/@tommoor/slate-drop-or-paste-images/-/slate-drop-or-paste-images-0.5.1.tgz#67a8853bb59d3a449f2fe7c7071dc19fe3aff93d" - dependencies: - data-uri-to-blob "0.0.4" - es6-promise "^4.0.5" - image-to-data-uri "^1.0.0" - is-data-uri "^0.1.0" - is-image "^1.0.1" - is-url "^1.2.2" - mime-types "^2.1.11" - "@types/geojson@^1.0.0": version "1.0.3" resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-1.0.3.tgz#fbcf7fa5eb6dd108d51385cc6987ec1f24214523" @@ -2172,10 +2160,6 @@ data-uri-regex@^0.1.2: version "0.1.4" resolved "https://registry.yarnpkg.com/data-uri-regex/-/data-uri-regex-0.1.4.tgz#1e1db6c8397eca8a48ecdb55ad1b927ec0bbac2e" -data-uri-to-blob@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/data-uri-to-blob/-/data-uri-to-blob-0.0.4.tgz#087a7bff42f41a6cc0b2e2fb7312a7c29904fbaa" - date-fns@^1.27.2: version "1.28.5" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.28.5.tgz#257cfc45d322df45ef5658665967ee841cd73faf" @@ -8217,10 +8201,22 @@ slate-collapse-on-escape@^0.6.0: dependencies: to-pascal-case "^1.0.0" -slate-dev-logger@^0.1.25, slate-dev-logger@^0.1.36: +slate-dev-logger@^0.1.0, slate-dev-logger@^0.1.25, slate-dev-logger@^0.1.36: version "0.1.36" resolved "https://registry.npmjs.org/slate-dev-logger/-/slate-dev-logger-0.1.36.tgz#ecdb37dbf944dfc742bab23b6a20d5a0472db95e" +slate-drop-or-paste-images@^0.8.0: + version "0.8.0" + resolved "https://registry.npmjs.org/slate-drop-or-paste-images/-/slate-drop-or-paste-images-0.8.0.tgz#2c363a117688c1b57517ab9cd468c4060e09824e" + dependencies: + es6-promise "^4.0.5" + image-to-data-uri "^1.0.0" + is-data-uri "^0.1.0" + is-image "^1.0.1" + is-url "^1.2.2" + mime-types "^2.1.11" + slate-dev-logger "^0.1.0" + slate-edit-code@^0.13.2: version "0.13.2" resolved "https://registry.npmjs.org/slate-edit-code/-/slate-edit-code-0.13.2.tgz#682a7640da076906e5b4a4c73ec0e46d31d92c62" @@ -8234,9 +8230,9 @@ slate-edit-list@^0.10.1: version "0.10.1" resolved "https://registry.npmjs.org/slate-edit-list/-/slate-edit-list-0.10.1.tgz#9c6a142a314b0ff22a327f1b50c8f5c85468cb17" -slate-md-serializer@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/slate-md-serializer/-/slate-md-serializer-1.0.1.tgz#10fb8118bf0b97addaf9d7fd77c1b19f3d767309" +slate-md-serializer@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/slate-md-serializer/-/slate-md-serializer-1.0.4.tgz#519b819b436706a31d93a8e787657694c0c75d35" slate-paste-linkify@^0.5.0: version "0.5.0" From e3f664e8a43b8565d9f4d3b84181ed06e6348c50 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Tue, 5 Dec 2017 20:05:43 -0800 Subject: [PATCH 04/18] Fixed: Checklist items cant be created from shortcuts Fixed: BlockToolbar not close on unfocus --- .../CenteredContent/CenteredContent.js | 2 +- app/components/Editor/Editor.js | 4 +- app/components/Editor/changes.js | 16 +++---- .../Editor/components/BlockInsert.js | 12 +++-- app/components/Editor/components/Image.js | 7 +-- app/components/Editor/components/ListItem.js | 8 +--- app/components/Editor/components/TodoItem.js | 5 +- .../Editor/components/Toolbar/BlockToolbar.js | 36 +++++++++------ .../Editor/components/Toolbar/Toolbar.js | 14 +++--- .../Toolbar/components/FormattingToolbar.js | 10 ++-- app/components/Editor/plugins.js | 8 ++-- .../Editor/plugins/MarkdownShortcuts.js | 26 +++++++---- app/components/Editor/schema.js | 46 ++++++++----------- app/scenes/Document/Document.js | 1 - 14 files changed, 105 insertions(+), 90 deletions(-) diff --git a/app/components/CenteredContent/CenteredContent.js b/app/components/CenteredContent/CenteredContent.js index bcfaedb8f..b3736f6c8 100644 --- a/app/components/CenteredContent/CenteredContent.js +++ b/app/components/CenteredContent/CenteredContent.js @@ -8,7 +8,7 @@ type Props = { const Container = styled.div` width: 100%; - margin: 60px; + padding: 60px; `; const Content = styled.div` diff --git a/app/components/Editor/Editor.js b/app/components/Editor/Editor.js index 9de55c504..d767e602f 100644 --- a/app/components/Editor/Editor.js +++ b/app/components/Editor/Editor.js @@ -46,7 +46,6 @@ class MarkdownEditor extends Component { this.renderNode = createRenderNode({ onInsertImage: this.insertImageFile, - onChange: this.onChange, }); this.plugins = createPlugins({ onImageUploadStart: props.onImageUploadStart, @@ -76,6 +75,7 @@ class MarkdownEditor extends Component { } onChange = (change: Change) => { + // TODO: Lets avoid constantly serializing to Markdown if (this.editorValue !== change.value) { this.props.onChange(Markdown.serialize(change.value)); this.editorValue = change.value; @@ -99,7 +99,7 @@ class MarkdownEditor extends Component { } }; - insertImageFile = async (file: window.File) => { + insertImageFile = (file: window.File) => { this.editor.change(change => change.call( insertImageFile, diff --git a/app/components/Editor/changes.js b/app/components/Editor/changes.js index d4e35ec78..7a1829944 100644 --- a/app/components/Editor/changes.js +++ b/app/components/Editor/changes.js @@ -18,17 +18,17 @@ export function splitAndInsertBlock(change: Change, options: Options) { // lists get some special treatment if (parent && parent.type === 'list-item') { - change = changes.unwrapList( - changes - .splitListItem(change.collapseToStart()) - .collapseToEndOfPreviousBlock() - ); + change + .collapseToStart() + .call(changes.splitListItem) + .collapseToEndOfPreviousBlock() + .call(changes.unwrapList); } - change = change.insertBlock(type); + change.insertBlock(type); - if (wrapper) change = change.wrapBlock(wrapper); - if (append) change = change.insertBlock(append); + if (wrapper) change.wrapBlock(wrapper); + if (append) change.insertBlock(append); return change; } diff --git a/app/components/Editor/components/BlockInsert.js b/app/components/Editor/components/BlockInsert.js index 8eab208fe..372a196d0 100644 --- a/app/components/Editor/components/BlockInsert.js +++ b/app/components/Editor/components/BlockInsert.js @@ -84,11 +84,13 @@ export default class BlockInsert extends Component { }; handleClick = (ev: SyntheticMouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + this.mouseMovementSinceClick = 0; this.active = false; const { editor } = this.props; - const type = { type: 'block-toolbar', isVoid: true }; editor.change(change => { // remove any existing toolbars in the document as a fail safe @@ -101,7 +103,7 @@ export default class BlockInsert extends Component { change .collapseToStartOf(this.closestRootNode) .collapseToEndOfPreviousBlock() - .insertBlock(type); + .insertBlock({ type: 'block-toolbar', isVoid: true }); }); }; @@ -133,7 +135,11 @@ const Trigger = styled.div` cursor: pointer; &:hover { - background-color: ${color.smokeDark}; + background-color: ${color.slate}; + + svg { + fill: ${color.white}; + } } ${({ active }) => diff --git a/app/components/Editor/components/Image.js b/app/components/Editor/components/Image.js index c8f7cbab5..50784ab4f 100644 --- a/app/components/Editor/components/Image.js +++ b/app/components/Editor/components/Image.js @@ -13,11 +13,8 @@ class Image extends Component { const { editor, node } = this.props; const data = node.data.toObject(); - editor.onChange( - editor - .getState() - .change() - .setNodeByKey(node.key, { data: { ...data, alt } }) + editor.change(change => + change.setNodeByKey(node.key, { data: { ...data, alt } }) ); }; diff --git a/app/components/Editor/components/ListItem.js b/app/components/Editor/components/ListItem.js index 7941f97e1..abb1283cc 100644 --- a/app/components/Editor/components/ListItem.js +++ b/app/components/Editor/components/ListItem.js @@ -10,15 +10,11 @@ export default function ListItem({ ...props }: SlateNodeProps) { const checked = node.data.get('checked'); + console.log('ListItem.checked', checked); if (checked !== undefined) { return ( - + {children} ); diff --git a/app/components/Editor/components/TodoItem.js b/app/components/Editor/components/TodoItem.js index e6020228e..f2e5b77ef 100644 --- a/app/components/Editor/components/TodoItem.js +++ b/app/components/Editor/components/TodoItem.js @@ -5,7 +5,7 @@ import { color } from 'shared/styles/constants'; import type { SlateNodeProps } from '../types'; export default class TodoItem extends Component { - props: SlateNodeProps & { checked: boolean }; + props: SlateNodeProps; handleChange = (ev: SyntheticInputEvent) => { const checked = ev.target.checked; @@ -16,7 +16,8 @@ export default class TodoItem extends Component { }; render() { - const { children, checked, attributes, readOnly } = this.props; + const { children, node, attributes, readOnly } = this.props; + const checked = node.data.get('checked'); return ( diff --git a/app/components/Editor/components/Toolbar/BlockToolbar.js b/app/components/Editor/components/Toolbar/BlockToolbar.js index ded10b94c..cd94d9dbf 100644 --- a/app/components/Editor/components/Toolbar/BlockToolbar.js +++ b/app/components/Editor/components/Toolbar/BlockToolbar.js @@ -1,5 +1,6 @@ // @flow import React, { Component } from 'react'; +import { findDOMNode } from 'react-dom'; import keydown from 'react-keydown'; import styled from 'styled-components'; import getDataTransferFiles from 'utils/getDataTransferFiles'; @@ -30,21 +31,30 @@ type Options = { class BlockToolbar extends Component { props: Props; + bar: HTMLDivElement; file: HTMLInputElement; - componentWillReceiveProps(nextProps: Props) { - const { editor } = this.props; - const wasActive = editor.value.selection.hasEdgeIn(this.props.node); - const isActive = nextProps.editor.value.selection.hasEdgeIn(nextProps.node); - const becameInactive = !isActive && wasActive; - - if (becameInactive) { - nextProps.editor.change(change => - change.removeNodeByKey(nextProps.node.key) - ); - } + componentDidMount() { + window.addEventListener('click', this.handleOutsideMouseClick); } + componentWillUnmount() { + window.removeEventListener('click', this.handleOutsideMouseClick); + } + + handleOutsideMouseClick = (ev: SyntheticMouseEvent) => { + const element = findDOMNode(this.bar); + + if ( + !element || + (ev.target instanceof Node && element.contains(ev.target)) || + (ev.button && ev.button !== 0) + ) { + return; + } + this.removeSelf(ev); + }; + @keydown('esc') removeSelf(ev: SyntheticEvent) { ev.preventDefault(); @@ -59,7 +69,7 @@ class BlockToolbar extends Component { const { editor } = this.props; editor.change(change => { - splitAndInsertBlock(change, options); + change.call(splitAndInsertBlock, options); editor.value.document.nodes.forEach(node => { if (node.type === 'block-toolbar') { @@ -130,7 +140,7 @@ class BlockToolbar extends Component { editor.value.isFocused && editor.value.selection.hasEdgeIn(node); return ( - + (this.bar = ref)}> (this.file = ref)} diff --git a/app/components/Editor/components/Toolbar/Toolbar.js b/app/components/Editor/components/Toolbar/Toolbar.js index 75d978475..1032abfac 100644 --- a/app/components/Editor/components/Toolbar/Toolbar.js +++ b/app/components/Editor/components/Toolbar/Toolbar.js @@ -3,7 +3,7 @@ import React, { Component } from 'react'; import { observable } from 'mobx'; import { observer } from 'mobx-react'; import { Portal } from 'react-portal'; -import { Editor } from 'slate-react'; +import { Editor, findDOMNode } from 'slate-react'; import { Value } from 'slate'; import styled from 'styled-components'; import _ from 'lodash'; @@ -45,14 +45,14 @@ export default class Toolbar extends Component { const { value } = this.props; try { - const selectedLinks = value.startBlock + const selectedLinks = value.document .getInlinesAtRange(value.selection) .filter(node => node.type === 'link'); if (selectedLinks.size) { return selectedLinks.first(); } } catch (err) { - // + // It's okay. } } @@ -74,8 +74,8 @@ export default class Toolbar extends Component { const firstNode = value.document.nodes.first(); if (firstNode === value.startBlock) return; - // don't display toolbar for code blocks - if (value.startBlock.type === 'code') return; + // don't display toolbar for code blocks, code-lines inline code. + if (value.startBlock.type.match(/code/)) return; this.active = true; this.focused = !!link; @@ -84,7 +84,9 @@ export default class Toolbar extends Component { const padding = 16; const selection = window.getSelection(); const range = selection.getRangeAt(0); - const rect = range.getBoundingClientRect(); + const rect = link + ? findDOMNode(link).getBoundingClientRect() + : range.getBoundingClientRect(); if (rect.top === 0 && rect.left === 0) { return; diff --git a/app/components/Editor/components/Toolbar/components/FormattingToolbar.js b/app/components/Editor/components/Toolbar/components/FormattingToolbar.js index 5dd7e6c3a..5c988fbf9 100644 --- a/app/components/Editor/components/Toolbar/components/FormattingToolbar.js +++ b/app/components/Editor/components/Toolbar/components/FormattingToolbar.js @@ -14,7 +14,7 @@ import StrikethroughIcon from 'components/Icon/StrikethroughIcon'; class FormattingToolbar extends Component { props: { editor: Editor, - onCreateLink: Function, + onCreateLink: () => void, }; /** @@ -52,10 +52,10 @@ class FormattingToolbar extends Component { ev.stopPropagation(); const data = { href: '' }; - this.props.editor.change(change => { - change.wrapInline({ type: 'link', data }); - this.props.onCreateLink(); - }); + this.props.editor.change(change => + change.wrapInline({ type: 'link', data }) + ); + this.props.onCreateLink(); }; renderMarkButton = (type: string, IconClass: Function) => { diff --git a/app/components/Editor/plugins.js b/app/components/Editor/plugins.js index 46e59d893..e8dc46aaa 100644 --- a/app/components/Editor/plugins.js +++ b/app/components/Editor/plugins.js @@ -10,13 +10,13 @@ import KeyboardShortcuts from './plugins/KeyboardShortcuts'; import MarkdownShortcuts from './plugins/MarkdownShortcuts'; import { insertImageFile } from './changes'; -const onlyInCode = node => node.type === 'code'; - type Options = { - onImageUploadStart: Function, - onImageUploadStop: Function, + onImageUploadStart: () => void, + onImageUploadStop: () => void, }; +const onlyInCode = node => node.type === 'code'; + const createPlugins = ({ onImageUploadStart, onImageUploadStop }: Options) => { return [ PasteLinkify({ diff --git a/app/components/Editor/plugins/MarkdownShortcuts.js b/app/components/Editor/plugins/MarkdownShortcuts.js index 8d4e7b663..a6862da1a 100644 --- a/app/components/Editor/plugins/MarkdownShortcuts.js +++ b/app/components/Editor/plugins/MarkdownShortcuts.js @@ -50,7 +50,17 @@ export default function MarkdownShortcuts() { let checked; if (chars === '[x]') checked = true; if (chars === '[ ]') checked = false; - change.setBlock({ type, data: { checked } }); + + change + .extendToStartOf(startBlock) + .delete() + .setBlock( + { + type, + data: { checked }, + }, + { normalize: false } + ); if (type === 'list-item') { if (checked !== undefined) { @@ -62,7 +72,7 @@ export default function MarkdownShortcuts() { } } - return change.extendToStartOf(startBlock).delete(); + return true; } for (const key of inlineShortcuts) { @@ -70,7 +80,8 @@ export default function MarkdownShortcuts() { let { mark, shortcut } = key; let inlineTags = []; - // only add tags if they have spaces around them or the tag is beginning or the end of the block + // only add tags if they have spaces around them or the tag is beginning + // or the end of the block for (let i = 0; i < startBlock.text.length; i++) { const { text } = startBlock; const start = i; @@ -85,8 +96,9 @@ export default function MarkdownShortcuts() { if ( text.slice(start, end) === shortcut && (beginningOfBlock || endOfBlock || surroundedByWhitespaces) - ) + ) { inlineTags.push(i); + } } // if we have multiple tags then mark the text between as inline code @@ -94,7 +106,7 @@ export default function MarkdownShortcuts() { const firstText = startBlock.getFirstText(); const firstCodeTagIndex = inlineTags[0]; const lastCodeTagIndex = inlineTags[inlineTags.length - 1]; - change + return change .removeTextByKey(firstText.key, lastCodeTagIndex, shortcut.length) .removeTextByKey(firstText.key, firstCodeTagIndex, shortcut.length) .moveOffsetsTo( @@ -139,9 +151,7 @@ export default function MarkdownShortcuts() { return change .extendToStartOf(startBlock) .delete() - .setBlock({ - type: 'code', - }); + .setBlock({ type: 'code' }); } }, diff --git a/app/components/Editor/schema.js b/app/components/Editor/schema.js index 14ae2baff..a8331e3d3 100644 --- a/app/components/Editor/schema.js +++ b/app/components/Editor/schema.js @@ -2,32 +2,26 @@ import { Block, Change, Node, Mark } from 'slate'; const schema = { - blocks: { - heading1: { marks: [''] }, - heading2: { marks: [''] }, - heading3: { marks: [''] }, - heading4: { marks: [''] }, - heading5: { marks: [''] }, - heading6: { marks: [''] }, - 'ordered-list': { - nodes: [{ types: ['list-item'] }], - }, - 'bulleted-list': { - nodes: [{ types: ['list-item'] }], - }, - table: { - nodes: [{ types: ['table-row', 'table-head', 'table-cell'] }], - }, - image: { - isVoid: true, - }, - 'horizontal-rule': { - isVoid: true, - }, - 'block-toolbar': { - isVoid: true, - }, - }, + // blocks: { + // heading1: { marks: [''] }, + // heading2: { marks: [''] }, + // heading3: { marks: [''] }, + // heading4: { marks: [''] }, + // heading5: { marks: [''] }, + // heading6: { marks: [''] }, + // table: { + // nodes: [{ types: ['table-row', 'table-head', 'table-cell'] }], + // }, + // image: { + // isVoid: true, + // }, + // 'horizontal-rule': { + // isVoid: true, + // }, + // 'block-toolbar': { + // isVoid: true, + // }, + // }, document: { nodes: [ { types: ['heading1'], min: 1, max: 1 }, diff --git a/app/scenes/Document/Document.js b/app/scenes/Document/Document.js index 7aa08b9cb..f116a909c 100644 --- a/app/scenes/Document/Document.js +++ b/app/scenes/Document/Document.js @@ -292,7 +292,6 @@ class DocumentScene extends Component { const Container = styled(Flex)` position: relative; - width: 100%; `; const LoadingState = styled(LoadingPlaceholder)` From e64ca3ca438be54ff2de8871c2d0d01f091cd00d Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Tue, 5 Dec 2017 22:29:07 -0800 Subject: [PATCH 05/18] Cleanup --- app/components/Editor/changes.js | 8 +--- app/components/Editor/components/ListItem.js | 1 - .../Editor/components/Toolbar/BlockToolbar.js | 1 - app/components/Editor/plugins.js | 2 +- app/components/Editor/schema.js | 37 +++++++++---------- 5 files changed, 19 insertions(+), 30 deletions(-) diff --git a/app/components/Editor/changes.js b/app/components/Editor/changes.js index 7a1829944..9c44c28b6 100644 --- a/app/components/Editor/changes.js +++ b/app/components/Editor/changes.js @@ -9,11 +9,10 @@ const { changes } = EditList; type Options = { type: string | Object, wrapper?: string | Object, - append?: string | Object, }; export function splitAndInsertBlock(change: Change, options: Options) { - const { type, wrapper, append } = options; + const { type, wrapper } = options; const parent = change.value.document.getParent(change.value.startBlock.key); // lists get some special treatment @@ -28,8 +27,6 @@ export function splitAndInsertBlock(change: Change, options: Options) { change.insertBlock(type); if (wrapper) change.wrapBlock(wrapper); - if (append) change.insertBlock(append); - return change; } @@ -40,7 +37,6 @@ export async function insertImageFile( onImageUploadStop: () => void ) { onImageUploadStart(); - console.log(file); try { // load the file as a data URL const id = uuid.v4(); @@ -55,7 +51,6 @@ export async function insertImageFile( isVoid: true, data: { src, id, alt, loading: true }, }); - console.log('insertBlock', change); }); reader.readAsDataURL(file); @@ -69,7 +64,6 @@ export async function insertImageFile( const placeholder = change.value.document.findDescendant( node => node.data && node.data.get('id') === id ); - console.log('placeholder', placeholder); return change.setNodeByKey(placeholder.key, { data: { src, alt, loading: false }, diff --git a/app/components/Editor/components/ListItem.js b/app/components/Editor/components/ListItem.js index abb1283cc..227a3ad12 100644 --- a/app/components/Editor/components/ListItem.js +++ b/app/components/Editor/components/ListItem.js @@ -10,7 +10,6 @@ export default function ListItem({ ...props }: SlateNodeProps) { const checked = node.data.get('checked'); - console.log('ListItem.checked', checked); if (checked !== undefined) { return ( diff --git a/app/components/Editor/components/Toolbar/BlockToolbar.js b/app/components/Editor/components/Toolbar/BlockToolbar.js index cd94d9dbf..26c114ce2 100644 --- a/app/components/Editor/components/Toolbar/BlockToolbar.js +++ b/app/components/Editor/components/Toolbar/BlockToolbar.js @@ -26,7 +26,6 @@ type Props = SlateNodeProps & { type Options = { type: string | Object, wrapper?: string | Object, - append?: string | Object, }; class BlockToolbar extends Component { diff --git a/app/components/Editor/plugins.js b/app/components/Editor/plugins.js index e8dc46aaa..217bfd1c6 100644 --- a/app/components/Editor/plugins.js +++ b/app/components/Editor/plugins.js @@ -25,7 +25,7 @@ const createPlugins = ({ onImageUploadStart, onImageUploadStop }: Options) => { }), InsertImages({ extensions: ['png', 'jpg', 'gif'], - insertImage(change, file) { + insertImage: (change, file) => { return change.call( insertImageFile, file, diff --git a/app/components/Editor/schema.js b/app/components/Editor/schema.js index a8331e3d3..8e2abcc2d 100644 --- a/app/components/Editor/schema.js +++ b/app/components/Editor/schema.js @@ -2,26 +2,23 @@ import { Block, Change, Node, Mark } from 'slate'; const schema = { - // blocks: { - // heading1: { marks: [''] }, - // heading2: { marks: [''] }, - // heading3: { marks: [''] }, - // heading4: { marks: [''] }, - // heading5: { marks: [''] }, - // heading6: { marks: [''] }, - // table: { - // nodes: [{ types: ['table-row', 'table-head', 'table-cell'] }], - // }, - // image: { - // isVoid: true, - // }, - // 'horizontal-rule': { - // isVoid: true, - // }, - // 'block-toolbar': { - // isVoid: true, - // }, - // }, + blocks: { + heading1: { marks: [''] }, + heading2: { marks: [''] }, + heading3: { marks: [''] }, + heading4: { marks: [''] }, + heading5: { marks: [''] }, + heading6: { marks: [''] }, + table: { + nodes: [{ types: ['table-row', 'table-head', 'table-cell'] }], + }, + 'horizontal-rule': { + isVoid: true, + }, + 'block-toolbar': { + isVoid: true, + }, + }, document: { nodes: [ { types: ['heading1'], min: 1, max: 1 }, From 5c38ff9e63f2ee48ee20ba6bc8ada537f5d50a0c Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Tue, 5 Dec 2017 23:25:09 -0800 Subject: [PATCH 06/18] Fixes: Allow BlockInsert on final block (previously didnt work in Prod) --- .../Editor/components/BlockInsert.js | 21 +++++++++++++++---- .../ClickablePadding.js | 0 .../components/ClickablePadding/index.js | 3 --- .../Editor/components/Toolbar/BlockToolbar.js | 2 +- app/components/Editor/marks.js | 4 ++-- app/components/Editor/plugins.js | 2 +- .../Editor/plugins/MarkdownShortcuts.js | 15 +++++++------ 7 files changed, 30 insertions(+), 17 deletions(-) rename app/components/Editor/components/{ClickablePadding => }/ClickablePadding.js (100%) delete mode 100644 app/components/Editor/components/ClickablePadding/index.js diff --git a/app/components/Editor/components/BlockInsert.js b/app/components/Editor/components/BlockInsert.js index 372a196d0..0f6615bf0 100644 --- a/app/components/Editor/components/BlockInsert.js +++ b/app/components/Editor/components/BlockInsert.js @@ -22,6 +22,8 @@ function findClosestRootNode(value, ev) { if (bounds.top > ev.clientY) return previous; previous = { node, element, bounds }; } + + return previous; } @observer @@ -100,10 +102,21 @@ export default class BlockInsert extends Component { } }); - change - .collapseToStartOf(this.closestRootNode) - .collapseToEndOfPreviousBlock() - .insertBlock({ type: 'block-toolbar', isVoid: true }); + change.collapseToStartOf(this.closestRootNode); + + // if we're on an empty paragraph then just replace it with the block + // toolbar. Otherwise insert the toolbar as an extra Node. + if ( + !this.closestRootNode.text && + this.closestRootNode.type === 'paragraph' + ) { + change.setNodeByKey(this.closestRootNode.key, { + type: 'block-toolbar', + isVoid: true, + }); + } else { + change.insertBlock({ type: 'block-toolbar', isVoid: true }); + } }); }; diff --git a/app/components/Editor/components/ClickablePadding/ClickablePadding.js b/app/components/Editor/components/ClickablePadding.js similarity index 100% rename from app/components/Editor/components/ClickablePadding/ClickablePadding.js rename to app/components/Editor/components/ClickablePadding.js diff --git a/app/components/Editor/components/ClickablePadding/index.js b/app/components/Editor/components/ClickablePadding/index.js deleted file mode 100644 index 76c2ddada..000000000 --- a/app/components/Editor/components/ClickablePadding/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// @flow -import ClickablePadding from './ClickablePadding'; -export default ClickablePadding; diff --git a/app/components/Editor/components/Toolbar/BlockToolbar.js b/app/components/Editor/components/Toolbar/BlockToolbar.js index 26c114ce2..355c81636 100644 --- a/app/components/Editor/components/Toolbar/BlockToolbar.js +++ b/app/components/Editor/components/Toolbar/BlockToolbar.js @@ -72,7 +72,7 @@ class BlockToolbar extends Component { editor.value.document.nodes.forEach(node => { if (node.type === 'block-toolbar') { - change.removeNodeByKey(node.key); + change.removeNodeByKey(node.key, undefined, { normalize: false }); } }); diff --git a/app/components/Editor/marks.js b/app/components/Editor/marks.js index a88b6c279..eaa4c3349 100644 --- a/app/components/Editor/marks.js +++ b/app/components/Editor/marks.js @@ -1,6 +1,6 @@ // @flow import React from 'react'; -import InlineCode from './components/InlineCode'; +import Code from './components/InlineCode'; import { Mark } from 'slate'; type Props = { @@ -13,7 +13,7 @@ export default function renderMark(props: Props) { case 'bold': return {props.children}; case 'code': - return {props.children}; + return {props.children}; case 'italic': return {props.children}; case 'underlined': diff --git a/app/components/Editor/plugins.js b/app/components/Editor/plugins.js index 217bfd1c6..499b8b1b4 100644 --- a/app/components/Editor/plugins.js +++ b/app/components/Editor/plugins.js @@ -24,7 +24,7 @@ const createPlugins = ({ onImageUploadStart, onImageUploadStop }: Options) => { collapseTo: 'end', }), InsertImages({ - extensions: ['png', 'jpg', 'gif'], + extensions: ['png', 'jpg', 'gif', 'webp'], insertImage: (change, file) => { return change.call( insertImageFile, diff --git a/app/components/Editor/plugins/MarkdownShortcuts.js b/app/components/Editor/plugins/MarkdownShortcuts.js index a6862da1a..0cd0a5b72 100644 --- a/app/components/Editor/plugins/MarkdownShortcuts.js +++ b/app/components/Editor/plugins/MarkdownShortcuts.js @@ -131,12 +131,15 @@ export default function MarkdownShortcuts() { return change .extendToStartOf(startBlock) .delete() - .setBlock({ - type: 'horizontal-rule', - isVoid: true, - }) - .collapseToStartOfNextBlock() - .insertBlock('paragraph'); + .setBlock( + { + type: 'horizontal-rule', + isVoid: true, + }, + { normalize: false } + ) + .insertBlock('paragraph') + .collapseToStart(); } }, From b794a0cc70968bd2d5e310e73c328b19b76eb100 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Tue, 5 Dec 2017 23:41:01 -0800 Subject: [PATCH 07/18] Cleaner API for BlockToolbar cursor positioning --- app/components/Editor/changes.js | 1 + .../Editor/components/Toolbar/BlockToolbar.js | 30 +++++++++++-------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/app/components/Editor/changes.js b/app/components/Editor/changes.js index 9c44c28b6..01be8901d 100644 --- a/app/components/Editor/changes.js +++ b/app/components/Editor/changes.js @@ -24,6 +24,7 @@ export function splitAndInsertBlock(change: Change, options: Options) { .call(changes.unwrapList); } + if (wrapper) change.collapseToStartOfNextBlock(); change.insertBlock(type); if (wrapper) change.wrapBlock(wrapper); diff --git a/app/components/Editor/components/Toolbar/BlockToolbar.js b/app/components/Editor/components/Toolbar/BlockToolbar.js index 355c81636..4d284cfc9 100644 --- a/app/components/Editor/components/Toolbar/BlockToolbar.js +++ b/app/components/Editor/components/Toolbar/BlockToolbar.js @@ -64,19 +64,22 @@ class BlockToolbar extends Component { ); } - insertBlock = (options: Options) => { + insertBlock = ( + options: Options, + cursorPosition: 'before' | 'on' | 'after' = 'on' + ) => { const { editor } = this.props; editor.change(change => { - change.call(splitAndInsertBlock, options); + change + .collapseToEndOf(this.props.node) + .removeNodeByKey(this.props.node.key) + .collapseToEnd() + .call(splitAndInsertBlock, options); - editor.value.document.nodes.forEach(node => { - if (node.type === 'block-toolbar') { - change.removeNodeByKey(node.key, undefined, { normalize: false }); - } - }); - - change.focus(); + if (cursorPosition === 'before') change.collapseToStartOfPreviousBlock(); + if (cursorPosition === 'after') change.collapseToStartOfNextBlock(); + return change.focus(); }); }; @@ -89,9 +92,12 @@ class BlockToolbar extends Component { case 'code': return this.insertBlock({ type }); case 'horizontal-rule': - return this.insertBlock({ - type: { type: 'horizontal-rule', isVoid: true }, - }); + return this.insertBlock( + { + type: { type: 'horizontal-rule', isVoid: true }, + }, + 'after' + ); case 'bulleted-list': return this.insertBlock({ type: 'list-item', From 30b2b91bbc56139835a6bc583a4d65985ebf0b84 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 6 Dec 2017 07:53:54 -0800 Subject: [PATCH 08/18] Fixed: Creating checklists from block toolbar --- app/components/Editor/changes.js | 4 +++- app/components/Editor/components/Toolbar/BlockToolbar.js | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/components/Editor/changes.js b/app/components/Editor/changes.js index 01be8901d..c08e2cbf7 100644 --- a/app/components/Editor/changes.js +++ b/app/components/Editor/changes.js @@ -25,7 +25,9 @@ export function splitAndInsertBlock(change: Change, options: Options) { } if (wrapper) change.collapseToStartOfNextBlock(); - change.insertBlock(type); + + // this is a hack as insertBlock with normalize: false does not appear to work + change.insertBlock('paragraph').setBlock(type, { normalize: false }); if (wrapper) change.wrapBlock(wrapper); return change; diff --git a/app/components/Editor/components/Toolbar/BlockToolbar.js b/app/components/Editor/components/Toolbar/BlockToolbar.js index 4d284cfc9..faecb09d8 100644 --- a/app/components/Editor/components/Toolbar/BlockToolbar.js +++ b/app/components/Editor/components/Toolbar/BlockToolbar.js @@ -73,9 +73,9 @@ class BlockToolbar extends Component { editor.change(change => { change .collapseToEndOf(this.props.node) + .call(splitAndInsertBlock, options) .removeNodeByKey(this.props.node.key) - .collapseToEnd() - .call(splitAndInsertBlock, options); + .collapseToEnd(); if (cursorPosition === 'before') change.collapseToStartOfPreviousBlock(); if (cursorPosition === 'after') change.collapseToStartOfNextBlock(); From 64c26244795ad43ec42318e0e8cedc00c75ab24c Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 6 Dec 2017 08:15:06 -0800 Subject: [PATCH 09/18] Fixes: Image upload. Back to using our own plugin to show progress --- app/components/Editor/Editor.js | 7 +++++-- app/components/Editor/changes.js | 26 ++++++++++++++++++++------ app/components/Editor/plugins.js | 5 +++-- package.json | 2 +- yarn.lock | 24 ++++++++++++------------ 5 files changed, 41 insertions(+), 23 deletions(-) diff --git a/app/components/Editor/Editor.js b/app/components/Editor/Editor.js index d767e602f..a95079ff7 100644 --- a/app/components/Editor/Editor.js +++ b/app/components/Editor/Editor.js @@ -75,9 +75,11 @@ class MarkdownEditor extends Component { } onChange = (change: Change) => { - // TODO: Lets avoid constantly serializing to Markdown if (this.editorValue !== change.value) { - this.props.onChange(Markdown.serialize(change.value)); + const text = Markdown.serialize(change.value); + if (text !== this.props.text) { + this.props.onChange(text); + } this.editorValue = change.value; } }; @@ -104,6 +106,7 @@ class MarkdownEditor extends Component { change.call( insertImageFile, file, + this.editor, this.props.onImageUploadStart, this.props.onImageUploadStop ) diff --git a/app/components/Editor/changes.js b/app/components/Editor/changes.js index c08e2cbf7..8e96d78ff 100644 --- a/app/components/Editor/changes.js +++ b/app/components/Editor/changes.js @@ -1,5 +1,6 @@ // @flow import { Change } from 'slate'; +import { Editor } from 'slate-react'; import uuid from 'uuid'; import EditList from './plugins/EditList'; import uploadFile from 'utils/uploadFile'; @@ -36,6 +37,7 @@ export function splitAndInsertBlock(change: Change, options: Options) { export async function insertImageFile( change: Change, file: window.File, + editor: Editor, onImageUploadStart: () => void, onImageUploadStop: () => void ) { @@ -47,13 +49,24 @@ export async function insertImageFile( const reader = new FileReader(); reader.addEventListener('load', () => { const src = reader.result; - - // insert into document as uploading placeholder - change.insertBlock({ + const node = { type: 'image', isVoid: true, data: { src, id, alt, loading: true }, - }); + }; + + // insert / replace into document as uploading placeholder replacing + // empty paragraphs if available. + if ( + !change.value.startBlock.text && + change.value.startBlock.type === 'paragraph' + ) { + change.setBlock(node); + } else { + change.insertBlock(node); + } + + editor.onChange(change); }); reader.readAsDataURL(file); @@ -64,13 +77,14 @@ export async function insertImageFile( // we dont use the original change provided to the callback here // as the state may have changed significantly in the time it took to // upload the file. - const placeholder = change.value.document.findDescendant( + const placeholder = editor.value.document.findDescendant( node => node.data && node.data.get('id') === id ); - return change.setNodeByKey(placeholder.key, { + change.setNodeByKey(placeholder.key, { data: { src, alt, loading: false }, }); + editor.onChange(change); } catch (err) { throw err; } finally { diff --git a/app/components/Editor/plugins.js b/app/components/Editor/plugins.js index 499b8b1b4..2362d07cb 100644 --- a/app/components/Editor/plugins.js +++ b/app/components/Editor/plugins.js @@ -1,5 +1,5 @@ // @flow -import InsertImages from 'slate-drop-or-paste-images'; +import InsertImages from '@tommoor/slate-drop-or-paste-images'; import PasteLinkify from 'slate-paste-linkify'; import CollapseOnEscape from 'slate-collapse-on-escape'; import TrailingBlock from 'slate-trailing-block'; @@ -25,10 +25,11 @@ const createPlugins = ({ onImageUploadStart, onImageUploadStop }: Options) => { }), InsertImages({ extensions: ['png', 'jpg', 'gif', 'webp'], - insertImage: (change, file) => { + insertImage: async (change, file, editor) => { return change.call( insertImageFile, file, + editor, onImageUploadStart, onImageUploadStop ); diff --git a/package.json b/package.json index 5e50210c0..cdbc5b00e 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "url": "git+ssh://git@github.com/outline/outline.git" }, "dependencies": { + "@tommoor/slate-drop-or-paste-images": "^0.8.1", "aws-sdk": "^2.135.0", "babel-core": "^6.24.1", "babel-eslint": "^7.2.3", @@ -162,7 +163,6 @@ "sequelize-encrypted": "0.1.0", "slate": "^0.29.0", "slate-collapse-on-escape": "^0.6.0", - "slate-drop-or-paste-images": "^0.8.0", "slate-edit-code": "^0.13.2", "slate-edit-list": "^0.10.1", "slate-md-serializer": "^1.0.4", diff --git a/yarn.lock b/yarn.lock index 46907ed0c..00bbdb2f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,18 @@ # yarn lockfile v1 +"@tommoor/slate-drop-or-paste-images@^0.8.1": + version "0.8.1" + resolved "https://registry.npmjs.org/@tommoor/slate-drop-or-paste-images/-/slate-drop-or-paste-images-0.8.1.tgz#4d94b5c1dd2de109546ee1f38a1e4a18df078c1e" + dependencies: + es6-promise "^4.0.5" + image-to-data-uri "^1.0.0" + is-data-uri "^0.1.0" + is-image "^1.0.1" + is-url "^1.2.2" + mime-types "^2.1.11" + slate-dev-logger "^0.1.0" + "@types/geojson@^1.0.0": version "1.0.3" resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-1.0.3.tgz#fbcf7fa5eb6dd108d51385cc6987ec1f24214523" @@ -8205,18 +8217,6 @@ slate-dev-logger@^0.1.0, slate-dev-logger@^0.1.25, slate-dev-logger@^0.1.36: version "0.1.36" resolved "https://registry.npmjs.org/slate-dev-logger/-/slate-dev-logger-0.1.36.tgz#ecdb37dbf944dfc742bab23b6a20d5a0472db95e" -slate-drop-or-paste-images@^0.8.0: - version "0.8.0" - resolved "https://registry.npmjs.org/slate-drop-or-paste-images/-/slate-drop-or-paste-images-0.8.0.tgz#2c363a117688c1b57517ab9cd468c4060e09824e" - dependencies: - es6-promise "^4.0.5" - image-to-data-uri "^1.0.0" - is-data-uri "^0.1.0" - is-image "^1.0.1" - is-url "^1.2.2" - mime-types "^2.1.11" - slate-dev-logger "^0.1.0" - slate-edit-code@^0.13.2: version "0.13.2" resolved "https://registry.npmjs.org/slate-edit-code/-/slate-edit-code-0.13.2.tgz#682a7640da076906e5b4a4c73ec0e46d31d92c62" From 7b9fa9f4a732a94552e9951aaaf7b0e9cdda393b Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 6 Dec 2017 08:40:43 -0800 Subject: [PATCH 10/18] Fixed: Spurious save prompts --- app/components/Editor/Editor.js | 5 +---- app/scenes/Document/Document.js | 1 + 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/components/Editor/Editor.js b/app/components/Editor/Editor.js index a95079ff7..5edfd4d31 100644 --- a/app/components/Editor/Editor.js +++ b/app/components/Editor/Editor.js @@ -76,10 +76,7 @@ class MarkdownEditor extends Component { onChange = (change: Change) => { if (this.editorValue !== change.value) { - const text = Markdown.serialize(change.value); - if (text !== this.props.text) { - this.props.onChange(text); - } + this.props.onChange(Markdown.serialize(change.value)); this.editorValue = change.value; } }; diff --git a/app/scenes/Document/Document.js b/app/scenes/Document/Document.js index f116a909c..0f2d3a21f 100644 --- a/app/scenes/Document/Document.js +++ b/app/scenes/Document/Document.js @@ -183,6 +183,7 @@ class DocumentScene extends Component { onChange = text => { if (!this.document) return; + if (this.document.text === text) return; this.document.updateData({ text }, true); }; From d7d13179d63c3063046f8e6cc20a68a4eec88a44 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Fri, 8 Dec 2017 19:13:35 -0800 Subject: [PATCH 11/18] Remove slate-plain-serializer dependency --- app/components/Editor/Editor.js | 8 ++------ .../components/Toolbar/components/FormattingToolbar.js | 3 ++- package.json | 1 - 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/components/Editor/Editor.js b/app/components/Editor/Editor.js index 5edfd4d31..f7d09b5d1 100644 --- a/app/components/Editor/Editor.js +++ b/app/components/Editor/Editor.js @@ -5,7 +5,6 @@ import { observer } from 'mobx-react'; import { Value, Change } from 'slate'; import { Editor } from 'slate-react'; import type { SlateNodeProps, Plugin } from './types'; -import Plain from 'slate-plain-serializer'; import keydown from 'react-keydown'; import getDataTransferFiles from 'utils/getDataTransferFiles'; import Flex from 'shared/components/Flex'; @@ -52,11 +51,7 @@ class MarkdownEditor extends Component { onImageUploadStop: props.onImageUploadStop, }); - if (props.text.trim().length) { - this.editorValue = Markdown.deserialize(props.text); - } else { - this.editorValue = Plain.deserialize(''); - } + this.editorValue = Markdown.deserialize(props.text); } componentDidMount() { @@ -77,6 +72,7 @@ class MarkdownEditor extends Component { onChange = (change: Change) => { if (this.editorValue !== change.value) { this.props.onChange(Markdown.serialize(change.value)); + console.log('this.props.onChange', Markdown.serialize(change.value)); this.editorValue = change.value; } }; diff --git a/app/components/Editor/components/Toolbar/components/FormattingToolbar.js b/app/components/Editor/components/Toolbar/components/FormattingToolbar.js index 5c988fbf9..77fd7839a 100644 --- a/app/components/Editor/components/Toolbar/components/FormattingToolbar.js +++ b/app/components/Editor/components/Toolbar/components/FormattingToolbar.js @@ -28,7 +28,8 @@ class FormattingToolbar extends Component { }; isBlock = (type: string) => { - return this.props.editor.value.startBlock.type === type; + const startBlock = this.props.editor.value.startBlock; + return startBlock && startBlock.type === type; }; /** diff --git a/package.json b/package.json index cdbc5b00e..5ed7c4ee3 100644 --- a/package.json +++ b/package.json @@ -167,7 +167,6 @@ "slate-edit-list": "^0.10.1", "slate-md-serializer": "^1.0.4", "slate-paste-linkify": "^0.5.0", - "slate-plain-serializer": "^0.4.12", "slate-prism": "^0.4.0", "slate-react": "^0.10.19", "slate-trailing-block": "^0.4.0", From 466986eabb914eecbf85b568538452125f76ee8a Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Fri, 8 Dec 2017 20:08:47 -0800 Subject: [PATCH 12/18] Remove logging --- app/components/Editor/Editor.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/components/Editor/Editor.js b/app/components/Editor/Editor.js index f7d09b5d1..3b3f366bb 100644 --- a/app/components/Editor/Editor.js +++ b/app/components/Editor/Editor.js @@ -72,7 +72,6 @@ class MarkdownEditor extends Component { onChange = (change: Change) => { if (this.editorValue !== change.value) { this.props.onChange(Markdown.serialize(change.value)); - console.log('this.props.onChange', Markdown.serialize(change.value)); this.editorValue = change.value; } }; From bbb9fc328a0a53f56abf6b8017247a6e802e04ee Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Fri, 8 Dec 2017 21:51:20 -0800 Subject: [PATCH 13/18] Update Slate to 0.31.5 --- app/scenes/Document/Document.js | 10 ++++++---- package.json | 2 +- yarn.lock | 10 +++++----- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/scenes/Document/Document.js b/app/scenes/Document/Document.js index 5cb9ea2e0..af96675b6 100644 --- a/app/scenes/Document/Document.js +++ b/app/scenes/Document/Document.js @@ -230,10 +230,12 @@ class DocumentScene extends Component { {!isFetching && document && ( - + {this.isEditing && ( + + )} Date: Sun, 10 Dec 2017 15:39:02 -0800 Subject: [PATCH 14/18] Refactoring and improved error handling around Link Toolbar --- app/components/Editor/components/Link.js | 2 +- .../Editor/components/Toolbar/Toolbar.js | 70 ++++++++++--------- .../Toolbar/components/DocumentResult.js | 6 +- .../Toolbar/components/FormattingToolbar.js | 14 ++-- .../Toolbar/components/LinkToolbar.js | 59 ++++++++++------ 5 files changed, 88 insertions(+), 63 deletions(-) diff --git a/app/components/Editor/components/Link.js b/app/components/Editor/components/Link.js index 54302eb60..57a99feb6 100644 --- a/app/components/Editor/components/Link.js +++ b/app/components/Editor/components/Link.js @@ -43,7 +43,7 @@ export default function Link({ ); } else { return ( - + {children} ); diff --git a/app/components/Editor/components/Toolbar/Toolbar.js b/app/components/Editor/components/Toolbar/Toolbar.js index 9e5e16b2f..5721ed8f9 100644 --- a/app/components/Editor/components/Toolbar/Toolbar.js +++ b/app/components/Editor/components/Toolbar/Toolbar.js @@ -4,17 +4,29 @@ import { observable } from 'mobx'; import { observer } from 'mobx-react'; import { Portal } from 'react-portal'; import { Editor, findDOMNode } from 'slate-react'; -import { Value } from 'slate'; +import { Node, Value } from 'slate'; import styled from 'styled-components'; import _ from 'lodash'; import FormattingToolbar from './components/FormattingToolbar'; import LinkToolbar from './components/LinkToolbar'; +function getLinkInSelection(value): any { + try { + const selectedLinks = value.document + .getInlinesAtRange(value.selection) + .filter(node => node.type === 'link'); + if (selectedLinks.size) { + return selectedLinks.first(); + } + } catch (err) { + // It's okay. + } +} + @observer export default class Toolbar extends Component { @observable active: boolean = false; - @observable focused: boolean = false; - @observable link: ?React$Element; + @observable link: ?Node; @observable top: string = ''; @observable left: string = ''; @@ -33,35 +45,24 @@ export default class Toolbar extends Component { this.update(); }; - handleFocus = () => { - this.focused = true; + hideLinkToolbar = () => { + this.link = undefined; }; - handleBlur = () => { - this.focused = false; + showLinkToolbar = (ev: SyntheticEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + const link = getLinkInSelection(this.props.value); + this.link = link; }; - get linkInSelection(): any { - const { value } = this.props; - - try { - const selectedLinks = value.document - .getInlinesAtRange(value.selection) - .filter(node => node.type === 'link'); - if (selectedLinks.size) { - return selectedLinks.first(); - } - } catch (err) { - // It's okay. - } - } - update = () => { const { value } = this.props; - const link = this.linkInSelection; + const link = getLinkInSelection(value); if (value.isBlurred || (value.isCollapsed && !link)) { - if (this.active && !this.focused) { + if (this.active && !this.link) { this.active = false; this.link = undefined; this.top = ''; @@ -78,17 +79,20 @@ export default class Toolbar extends Component { if (value.startBlock.type.match(/code/)) return; this.active = true; - this.focused = !!link; - this.link = link; + this.link = this.link || link; const padding = 16; const selection = window.getSelection(); - const range = selection.getRangeAt(0); - const rect = link - ? findDOMNode(link).getBoundingClientRect() - : range.getBoundingClientRect(); + let rect; - if (rect.top === 0 && rect.left === 0) { + if (link) { + rect = findDOMNode(link).getBoundingClientRect(); + } else if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + rect = range.getBoundingClientRect(); + } + + if (!rect || (rect.top === 0 && rect.left === 0)) { return; } @@ -117,11 +121,11 @@ export default class Toolbar extends Component { ) : ( )} diff --git a/app/components/Editor/components/Toolbar/components/DocumentResult.js b/app/components/Editor/components/Toolbar/components/DocumentResult.js index 68aa6f9af..6f0c83e0e 100644 --- a/app/components/Editor/components/Toolbar/components/DocumentResult.js +++ b/app/components/Editor/components/Toolbar/components/DocumentResult.js @@ -25,10 +25,12 @@ function DocumentResult({ document, ...rest }: Props) { const ListItem = styled.a` display: flex; align-items: center; - height: 24px; - padding: 4px 8px 4px 0; + height: 28px; + padding: 6px 8px 6px 0; color: ${color.white}; font-size: 15px; + overflow: hidden; + white-space: nowrap; i { visibility: hidden; diff --git a/app/components/Editor/components/Toolbar/components/FormattingToolbar.js b/app/components/Editor/components/Toolbar/components/FormattingToolbar.js index 77fd7839a..dcf21e074 100644 --- a/app/components/Editor/components/Toolbar/components/FormattingToolbar.js +++ b/app/components/Editor/components/Toolbar/components/FormattingToolbar.js @@ -14,7 +14,7 @@ import StrikethroughIcon from 'components/Icon/StrikethroughIcon'; class FormattingToolbar extends Component { props: { editor: Editor, - onCreateLink: () => void, + onCreateLink: SyntheticEvent => void, }; /** @@ -48,15 +48,15 @@ class FormattingToolbar extends Component { this.props.editor.change(change => change.setBlock(type)); }; - onCreateLink = (ev: SyntheticEvent) => { + handleCreateLink = (ev: SyntheticEvent) => { ev.preventDefault(); ev.stopPropagation(); const data = { href: '' }; - this.props.editor.change(change => - change.wrapInline({ type: 'link', data }) - ); - this.props.onCreateLink(); + this.props.editor.change(change => { + change.wrapInline({ type: 'link', data }); + this.props.onCreateLink(ev); + }); }; renderMarkButton = (type: string, IconClass: Function) => { @@ -93,7 +93,7 @@ class FormattingToolbar extends Component { {this.renderBlockButton('heading1', Heading1Icon)} {this.renderBlockButton('heading2', Heading2Icon)} - + diff --git a/app/components/Editor/components/Toolbar/components/LinkToolbar.js b/app/components/Editor/components/Toolbar/components/LinkToolbar.js index d3209e8cc..afe84be60 100644 --- a/app/components/Editor/components/Toolbar/components/LinkToolbar.js +++ b/app/components/Editor/components/Toolbar/components/LinkToolbar.js @@ -1,9 +1,10 @@ // @flow import React, { Component } from 'react'; -import ReactDOM from 'react-dom'; +import { findDOMNode } from 'react-dom'; import { observable, action } from 'mobx'; import { observer, inject } from 'mobx-react'; import { withRouter } from 'react-router-dom'; +import { Node } from 'slate'; import { Editor } from 'slate-react'; import styled from 'styled-components'; import ArrowKeyNavigation from 'boundless-arrow-key-navigation'; @@ -19,12 +20,13 @@ import Flex from 'shared/components/Flex'; @keydown @observer class LinkToolbar extends Component { + wrapper: HTMLSpanElement; input: HTMLElement; firstDocument: HTMLElement; props: { editor: Editor, - link: Object, + link: Node, documents: DocumentsStore, onBlur: () => void, }; @@ -34,10 +36,35 @@ class LinkToolbar extends Component { @observable resultIds: string[] = []; @observable searchTerm: ?string = null; - componentWillMount() { + componentDidMount() { this.isEditing = !!this.props.link.data.get('href'); + setImmediate(() => + window.addEventListener('click', this.handleOutsideMouseClick) + ); } + componentWillUnmount() { + window.removeEventListener('click', this.handleOutsideMouseClick); + } + + handleOutsideMouseClick = (ev: SyntheticMouseEvent) => { + const element = findDOMNode(this.wrapper); + + if ( + !element || + (ev.target instanceof HTMLElement && element.contains(ev.target)) || + (ev.button && ev.button !== 0) + ) { + return; + } + + if (this.input.value) { + this.props.onBlur(); + } else { + this.removeLink(); + } + }; + @action search = async () => { this.isFetching = true; @@ -70,7 +97,7 @@ class LinkToolbar extends Component { case 40: // down ev.preventDefault(); if (this.firstDocument) { - const element = ReactDOM.findDOMNode(this.firstDocument); + const element = findDOMNode(this.firstDocument); if (element instanceof HTMLElement) element.focus(); } break; @@ -90,16 +117,6 @@ class LinkToolbar extends Component { this.resultIds = []; }; - onBlur = () => { - if (!this.resultIds.length) { - if (this.input.value) { - this.props.onBlur(); - } else { - this.removeLink(); - } - } - }; - removeLink = () => { this.save(''); }; @@ -110,13 +127,15 @@ class LinkToolbar extends Component { }; save = (href: string) => { + const { editor, link } = this.props; href = href.trim(); - this.props.editor.change(change => { + editor.change(change => { if (href) { change.setInline({ type: 'link', data: { href } }); - } else { - change.unwrapInline('link'); + } else if (link) { + change.unwrapInlineByKey(link.key); } + change.deselect(); this.props.onBlur(); }); }; @@ -126,17 +145,17 @@ class LinkToolbar extends Component { }; render() { - const href = this.props.link.data.get('href'); + const { link } = this.props; + const href = link && link.data.get('href'); const hasResults = this.resultIds.length > 0; return ( - + (this.wrapper = ref)}> (this.input = ref)} defaultValue={href} placeholder="Search or paste a link…" - onBlur={this.onBlur} onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus From 7e845242de34cfc486e347d3fe20caa6f80dce0a Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 10 Dec 2017 15:54:54 -0800 Subject: [PATCH 15/18] Fix: Esc to close link toolbar Error when selection changes to not include link --- .../components/Toolbar/components/LinkToolbar.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/components/Editor/components/Toolbar/components/LinkToolbar.js b/app/components/Editor/components/Toolbar/components/LinkToolbar.js index afe84be60..9a3966688 100644 --- a/app/components/Editor/components/Toolbar/components/LinkToolbar.js +++ b/app/components/Editor/components/Toolbar/components/LinkToolbar.js @@ -58,6 +58,10 @@ class LinkToolbar extends Component { return; } + this.close(); + }; + + close = () => { if (this.input.value) { this.props.onBlur(); } else { @@ -93,7 +97,7 @@ class LinkToolbar extends Component { ev.preventDefault(); return this.save(ev.target.value); case 27: // escape - return this.input.blur(); + return this.close(); case 40: // down ev.preventDefault(); if (this.firstDocument) { @@ -129,11 +133,13 @@ class LinkToolbar extends Component { save = (href: string) => { const { editor, link } = this.props; href = href.trim(); + editor.change(change => { if (href) { change.setInline({ type: 'link', data: { href } }); } else if (link) { - change.unwrapInlineByKey(link.key); + const selContainsLink = !!change.value.startBlock.getChild(link.key); + if (selContainsLink) change.unwrapInlineByKey(link.key); } change.deselect(); this.props.onBlur(); @@ -145,8 +151,7 @@ class LinkToolbar extends Component { }; render() { - const { link } = this.props; - const href = link && link.data.get('href'); + const href = this.props.link.data.get('href'); const hasResults = this.resultIds.length > 0; return ( From 5c9c788d5da0dc3af4a738d55f28764fa158a9a7 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 11 Dec 2017 21:16:37 -0800 Subject: [PATCH 16/18] Fix first run with Docker --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6a8d4b860..89f8f1ccd 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ up: docker-compose up -d redis postgres s3 - docker-compose run --rm outline yarn sequelize db:migrate + docker-compose run --rm outline bash -c "yarn && yarn sequelize db:migrate" docker-compose up outline build: From 5900fa1fbbd2aa61b2fe7f7a84b3d65d823f7492 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 11 Dec 2017 21:50:53 -0800 Subject: [PATCH 17/18] Fixes #475 - serializer now ensures paragraph node in list items --- package.json | 2 +- yarn.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index b863817c7..3bee2d0e9 100644 --- a/package.json +++ b/package.json @@ -166,7 +166,7 @@ "slate-collapse-on-escape": "^0.6.0", "slate-edit-code": "^0.13.2", "slate-edit-list": "^0.10.1", - "slate-md-serializer": "^1.0.4", + "slate-md-serializer": "^1.0.5", "slate-paste-linkify": "^0.5.0", "slate-prism": "^0.4.0", "slate-react": "^0.10.19", diff --git a/yarn.lock b/yarn.lock index d874100fb..c72fa3564 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8241,9 +8241,9 @@ slate-edit-list@^0.10.1: version "0.10.1" resolved "https://registry.npmjs.org/slate-edit-list/-/slate-edit-list-0.10.1.tgz#9c6a142a314b0ff22a327f1b50c8f5c85468cb17" -slate-md-serializer@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/slate-md-serializer/-/slate-md-serializer-1.0.4.tgz#519b819b436706a31d93a8e787657694c0c75d35" +slate-md-serializer@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/slate-md-serializer/-/slate-md-serializer-1.0.5.tgz#51f8732493a88272560108cbf945c848abfbe4d8" slate-paste-linkify@^0.5.0: version "0.5.0" From b70cf29aaecb41c4a41111cf492baac9526dc5c7 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 11 Dec 2017 22:37:38 -0800 Subject: [PATCH 18/18] Fixed: Another spurious save prompt --- app/scenes/Document/Document.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/scenes/Document/Document.js b/app/scenes/Document/Document.js index af96675b6..dc6efe6db 100644 --- a/app/scenes/Document/Document.js +++ b/app/scenes/Document/Document.js @@ -183,9 +183,10 @@ class DocumentScene extends Component { }; onChange = text => { - if (!this.document) return; - if (this.document.text === text) return; - this.document.updateData({ text }, true); + let document = this.document; + if (!document) return; + if (document.text.trim() === text.trim()) return; + document.updateData({ text }, true); }; onDiscard = () => {