This commit is contained in:
Tom Moor
2017-12-02 23:14:27 -08:00
parent 5da809c4ab
commit 15e8e50601
29 changed files with 617 additions and 610 deletions

View File

@@ -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() {

View File

@@ -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 (

View File

@@ -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<Block> {
const { state } = this.props;
get headings(): List<block> {
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 (
<ListItem type={heading.type} active={active}>
<ListItem type={heading.type} active={active} key={slug}>
<Anchor href={`#${slug}`} active={active}>
{heading.text}
</Anchor>

View File

@@ -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,

View File

@@ -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 <StyledHr active={active} {...attributes} />;

View File

@@ -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) => {

View File

@@ -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 (
<InternalLink {...attributes} to={path}>
{children}

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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() {

View File

@@ -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 (
<Bar active={active} {...attributes}>

View File

@@ -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;

View File

@@ -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) => {

View File

@@ -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 => {