99% there
This commit is contained in:
@ -1,14 +1,13 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import getDataTransferFiles from 'utils/getDataTransferFiles';
|
|
||||||
import Portal from 'react-portal';
|
import Portal from 'react-portal';
|
||||||
|
import { findDOMNode, Node } from 'slate';
|
||||||
import { observable } from 'mobx';
|
import { observable } from 'mobx';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { color } from 'shared/styles/constants';
|
import { color } from 'shared/styles/constants';
|
||||||
import PlusIcon from 'components/Icon/PlusIcon';
|
import PlusIcon from 'components/Icon/PlusIcon';
|
||||||
import type { State } from '../types';
|
import type { State } from '../types';
|
||||||
import { splitAndInsertBlock } from '../transforms';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
state: State,
|
state: State,
|
||||||
@ -16,83 +15,95 @@ type Props = {
|
|||||||
onInsertImage: File => Promise<*>,
|
onInsertImage: File => Promise<*>,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function findClosestRootNode(state, ev) {
|
||||||
|
let previous;
|
||||||
|
|
||||||
|
for (const node of state.document.nodes) {
|
||||||
|
const element = findDOMNode(node);
|
||||||
|
const bounds = element.getBoundingClientRect();
|
||||||
|
if (bounds.top > ev.clientY) return previous;
|
||||||
|
previous = { node, element, bounds };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export default class BlockInsert extends Component {
|
export default class BlockInsert extends Component {
|
||||||
props: Props;
|
props: Props;
|
||||||
mouseMoveTimeout: number;
|
mouseMoveTimeout: number;
|
||||||
|
mouseMovementSinceClick: number = 0;
|
||||||
|
mouseClickX: number = 0;
|
||||||
|
mouseClickY: number = 0;
|
||||||
|
|
||||||
|
@observable closestRootNode: Node;
|
||||||
@observable active: boolean = false;
|
@observable active: boolean = false;
|
||||||
@observable menuOpen: boolean = false;
|
|
||||||
@observable top: number;
|
@observable top: number;
|
||||||
@observable left: number;
|
@observable left: number;
|
||||||
@observable mouseX: number;
|
|
||||||
|
|
||||||
componentDidMount = () => {
|
componentDidMount = () => {
|
||||||
this.update();
|
window.addEventListener('mousedown', this.handleWindowClick);
|
||||||
window.addEventListener('mousemove', this.handleMouseMove);
|
window.addEventListener('mousemove', this.handleMouseMove);
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillUpdate = (nextProps: Props) => {
|
|
||||||
this.update(nextProps);
|
|
||||||
};
|
|
||||||
|
|
||||||
componentWillUnmount = () => {
|
componentWillUnmount = () => {
|
||||||
|
window.removeEventListener('mousedown', this.handleWindowClick);
|
||||||
window.removeEventListener('mousemove', this.handleMouseMove);
|
window.removeEventListener('mousemove', this.handleMouseMove);
|
||||||
};
|
};
|
||||||
|
|
||||||
setInactive = () => {
|
setInactive = () => {
|
||||||
if (this.menuOpen) return;
|
|
||||||
this.active = false;
|
this.active = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
handleMouseMove = (ev: SyntheticMouseEvent) => {
|
handleMouseMove = (ev: SyntheticMouseEvent) => {
|
||||||
const windowWidth = window.innerWidth / 3;
|
const windowWidth = window.innerWidth / 2.5;
|
||||||
let active = ev.clientX < windowWidth;
|
const result = findClosestRootNode(this.props.state, ev);
|
||||||
|
const movementToReset = 1000;
|
||||||
|
|
||||||
if (active !== this.active) {
|
this.mouseMovementSinceClick += Math.abs(this.mouseClickX - ev.clientX);
|
||||||
this.active = active || this.menuOpen;
|
this.active =
|
||||||
|
ev.clientX < windowWidth &&
|
||||||
|
this.mouseMovementSinceClick > movementToReset;
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
this.closestRootNode = result.node;
|
||||||
|
|
||||||
|
// do not show block menu on title heading or editor
|
||||||
|
const { type } = result.node;
|
||||||
|
if (type === 'heading1' || type === 'block-toolbar') {
|
||||||
|
this.left = -1000;
|
||||||
|
} else {
|
||||||
|
this.left = Math.round(result.bounds.left - 20);
|
||||||
|
this.top = Math.round(result.bounds.top + window.scrollY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (active) {
|
|
||||||
|
if (this.active) {
|
||||||
clearTimeout(this.mouseMoveTimeout);
|
clearTimeout(this.mouseMoveTimeout);
|
||||||
this.mouseMoveTimeout = setTimeout(this.setInactive, 2000);
|
this.mouseMoveTimeout = setTimeout(this.setInactive, 2000);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleMenuOpen = () => {
|
handleWindowClick = (ev: SyntheticMouseEvent) => {
|
||||||
this.menuOpen = true;
|
this.mouseClickX = ev.clientX;
|
||||||
};
|
this.mouseClickY = ev.clientY;
|
||||||
|
this.mouseMovementSinceClick = 0;
|
||||||
handleMenuClose = () => {
|
this.active = false;
|
||||||
this.menuOpen = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
update = (props?: Props) => {
|
|
||||||
if (!document.activeElement) return;
|
|
||||||
const { state } = props || this.props;
|
|
||||||
const boxRect = document.activeElement.getBoundingClientRect();
|
|
||||||
const selection = window.getSelection();
|
|
||||||
if (!selection.focusNode) return;
|
|
||||||
|
|
||||||
const range = selection.getRangeAt(0);
|
|
||||||
const rect = range.getBoundingClientRect();
|
|
||||||
|
|
||||||
if (rect.top <= 0 || boxRect.left <= 0) return;
|
|
||||||
|
|
||||||
if (state.startBlock.type === 'heading1') {
|
|
||||||
this.active = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.top = Math.round(rect.top + window.scrollY);
|
|
||||||
this.left = Math.round(boxRect.left + window.scrollX - 20);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleClick = () => {
|
handleClick = () => {
|
||||||
const transform = splitAndInsertBlock(this.props.state, {
|
const { state } = this.props;
|
||||||
type: { type: 'block-toolbar', isVoid: true },
|
const type = { type: 'block-toolbar', isVoid: true };
|
||||||
|
let transform = state.transform();
|
||||||
|
|
||||||
|
// remove any existing toolbars in the document as a fail safe
|
||||||
|
state.document.nodes.forEach(node => {
|
||||||
|
if (node.type === 'block-toolbar') {
|
||||||
|
transform.removeNodeByKey(node.key);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
const state = transform.apply();
|
|
||||||
this.props.onChange(state);
|
transform.collapseToStartOf(this.closestRootNode).insertBlock(type);
|
||||||
this.active = false;
|
|
||||||
|
this.props.onChange(transform.apply());
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -101,7 +112,7 @@ export default class BlockInsert extends Component {
|
|||||||
return (
|
return (
|
||||||
<Portal isOpened>
|
<Portal isOpened>
|
||||||
<Trigger active={this.active} style={style}>
|
<Trigger active={this.active} style={style}>
|
||||||
<PlusIcon onClick={this.handleClick} />
|
<PlusIcon onClick={this.handleClick} color={color.slate} />
|
||||||
</Trigger>
|
</Trigger>
|
||||||
</Portal>
|
</Portal>
|
||||||
);
|
);
|
||||||
@ -113,13 +124,13 @@ const Trigger = styled.div`
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
background-color: ${color.white};
|
background-color: ${color.white};
|
||||||
transition: opacity 250ms ease-in-out, transform 250ms ease-in-out;
|
transition: opacity 150ms cubic-bezier(0.175, 0.885, 0.32, 1.275), transform 150ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
margin-top: -3px;
|
|
||||||
margin-left: -10px;
|
margin-left: -10px;
|
||||||
box-shadow: inset 0 0 0 2px ${color.slateDark};
|
box-shadow: inset 0 0 0 2px ${color.slate};
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
transform: scale(.9);
|
transform: scale(.9);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: ${color.smokeDark};
|
background-color: ${color.smokeDark};
|
||||||
|
@ -9,10 +9,10 @@ export default function Code({ children, node, readOnly, attributes }: Props) {
|
|||||||
const language = node.data.get('language') || 'javascript';
|
const language = node.data.get('language') || 'javascript';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container {...attributes}>
|
||||||
{readOnly && <CopyButton text={node.text} />}
|
{readOnly && <CopyButton text={node.text} />}
|
||||||
<Pre className={`language-${language}`}>
|
<Pre className={`language-${language}`}>
|
||||||
<code {...attributes} className={`language-${language}`}>
|
<code className={`language-${language}`}>
|
||||||
{children}
|
{children}
|
||||||
</code>
|
</code>
|
||||||
</Pre>
|
</Pre>
|
||||||
|
@ -14,6 +14,7 @@ type Props = {
|
|||||||
editor: Editor,
|
editor: Editor,
|
||||||
readOnly: boolean,
|
readOnly: boolean,
|
||||||
component?: string,
|
component?: string,
|
||||||
|
attributes: Object,
|
||||||
};
|
};
|
||||||
|
|
||||||
function Heading(props: Props) {
|
function Heading(props: Props) {
|
||||||
@ -25,6 +26,7 @@ function Heading(props: Props) {
|
|||||||
readOnly,
|
readOnly,
|
||||||
children,
|
children,
|
||||||
component = 'h1',
|
component = 'h1',
|
||||||
|
attributes,
|
||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
const parentIsDocument = parent instanceof Document;
|
const parentIsDocument = parent instanceof Document;
|
||||||
@ -39,7 +41,7 @@ function Heading(props: Props) {
|
|||||||
emoji && title.match(new RegExp(`^${emoji}\\s`));
|
emoji && title.match(new RegExp(`^${emoji}\\s`));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component {...rest} id={slugish}>
|
<Component {...attributes} {...rest} id={slugish}>
|
||||||
<Wrapper hasEmoji={startsWithEmojiAndSpace}>
|
<Wrapper hasEmoji={startsWithEmojiAndSpace}>
|
||||||
{children}
|
{children}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
|
@ -5,9 +5,9 @@ import type { Props } from '../types';
|
|||||||
import { color } from 'shared/styles/constants';
|
import { color } from 'shared/styles/constants';
|
||||||
|
|
||||||
function HorizontalRule(props: Props) {
|
function HorizontalRule(props: Props) {
|
||||||
const { state, node } = props;
|
const { state, node, attributes } = props;
|
||||||
const active = state.isFocused && state.selection.hasEdgeIn(node);
|
const active = state.isFocused && state.selection.hasEdgeIn(node);
|
||||||
return <StyledHr active={active} />;
|
return <StyledHr active={active} {...attributes} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledHr = styled.hr`
|
const StyledHr = styled.hr`
|
||||||
|
@ -3,14 +3,15 @@ import React from 'react';
|
|||||||
import type { Props } from '../types';
|
import type { Props } from '../types';
|
||||||
import TodoItem from './TodoItem';
|
import TodoItem from './TodoItem';
|
||||||
|
|
||||||
export default function ListItem({ children, node, ...props }: Props) {
|
export default function ListItem({ children, node, attributes }: Props) {
|
||||||
const checked = node.data.get('checked');
|
const checked = node.data.get('checked');
|
||||||
|
|
||||||
if (checked !== undefined) {
|
if (checked !== undefined) {
|
||||||
return (
|
return (
|
||||||
<TodoItem checked={checked} node={node} {...props}>
|
<TodoItem checked={checked} node={node} {...attributes}>
|
||||||
{children}
|
{children}
|
||||||
</TodoItem>
|
</TodoItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <li>{children}</li>;
|
return <li {...attributes}>{children}</li>;
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ export default function Link({
|
|||||||
!node.text;
|
!node.text;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p>
|
<p {...attributes}>
|
||||||
{children}
|
{children}
|
||||||
{showPlaceholder &&
|
{showPlaceholder &&
|
||||||
<Placeholder contentEditable={false}>
|
<Placeholder contentEditable={false}>
|
||||||
|
@ -20,10 +20,10 @@ export default class TodoItem extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { children, checked, readOnly } = this.props;
|
const { children, checked, attributes, readOnly } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem checked={checked}>
|
<ListItem checked={checked} {...attributes}>
|
||||||
<Input
|
<Input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={checked}
|
checked={checked}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import keydown from 'react-keydown';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import getDataTransferFiles from 'utils/getDataTransferFiles';
|
import getDataTransferFiles from 'utils/getDataTransferFiles';
|
||||||
import Heading1Icon from 'components/Icon/Heading1Icon';
|
import Heading1Icon from 'components/Icon/Heading1Icon';
|
||||||
@ -38,7 +39,6 @@ class BlockToolbar extends Component {
|
|||||||
const becameInactive = !isActive && wasActive;
|
const becameInactive = !isActive && wasActive;
|
||||||
|
|
||||||
if (becameInactive) {
|
if (becameInactive) {
|
||||||
console.log('becameInactive');
|
|
||||||
const state = nextProps.state
|
const state = nextProps.state
|
||||||
.transform()
|
.transform()
|
||||||
.removeNodeByKey(nextProps.node.key)
|
.removeNodeByKey(nextProps.node.key)
|
||||||
@ -47,10 +47,23 @@ class BlockToolbar extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
insertBlock = (options: Options) => {
|
@keydown('esc')
|
||||||
let transform = splitAndInsertBlock(this.props.state, options);
|
removeSelf(ev: SyntheticEvent) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
this.props.state.document.nodes.forEach(node => {
|
const state = this.props.state
|
||||||
|
.transform()
|
||||||
|
.removeNodeByKey(this.props.node.key)
|
||||||
|
.apply();
|
||||||
|
this.props.onChange(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
insertBlock = (options: Options) => {
|
||||||
|
const { state } = this.props;
|
||||||
|
let transform = splitAndInsertBlock(state.transform(), state, options);
|
||||||
|
|
||||||
|
state.document.nodes.forEach(node => {
|
||||||
if (node.type === 'block-toolbar') {
|
if (node.type === 'block-toolbar') {
|
||||||
transform.removeNodeByKey(node.key);
|
transform.removeNodeByKey(node.key);
|
||||||
}
|
}
|
||||||
@ -61,7 +74,6 @@ class BlockToolbar extends Component {
|
|||||||
|
|
||||||
handleClickBlock = (ev: SyntheticEvent, type: string) => {
|
handleClickBlock = (ev: SyntheticEvent, type: string) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'heading1':
|
case 'heading1':
|
||||||
@ -114,11 +126,11 @@ class BlockToolbar extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { state, node } = this.props;
|
const { state, attributes, node } = this.props;
|
||||||
const active = state.isFocused && state.selection.hasEdgeIn(node);
|
const active = state.isFocused && state.selection.hasEdgeIn(node);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Bar active={active}>
|
<Bar active={active} {...attributes}>
|
||||||
<HiddenInput
|
<HiddenInput
|
||||||
type="file"
|
type="file"
|
||||||
innerRef={ref => (this.file = ref)}
|
innerRef={ref => (this.file = ref)}
|
||||||
|
@ -45,16 +45,30 @@ const createSchema = ({ onInsertImage, onChange }: Options) => {
|
|||||||
),
|
),
|
||||||
paragraph: (props: Props) => <Paragraph {...props} />,
|
paragraph: (props: Props) => <Paragraph {...props} />,
|
||||||
'block-quote': (props: Props) => (
|
'block-quote': (props: Props) => (
|
||||||
<blockquote>{props.children}</blockquote>
|
<blockquote {...props.attributes}>{props.children}</blockquote>
|
||||||
),
|
),
|
||||||
'horizontal-rule': HorizontalRule,
|
'horizontal-rule': HorizontalRule,
|
||||||
'bulleted-list': (props: Props) => <ul>{props.children}</ul>,
|
'bulleted-list': (props: Props) => (
|
||||||
'ordered-list': (props: Props) => <ol>{props.children}</ol>,
|
<ul {...props.attributes}>{props.children}</ul>
|
||||||
'todo-list': (props: Props) => <TodoList>{props.children}</TodoList>,
|
),
|
||||||
table: (props: Props) => <table>{props.children}</table>,
|
'ordered-list': (props: Props) => (
|
||||||
'table-row': (props: Props) => <tr>{props.children}</tr>,
|
<ol {...props.attributes}>{props.children}</ol>
|
||||||
'table-head': (props: Props) => <th>{props.children}</th>,
|
),
|
||||||
'table-cell': (props: Props) => <td>{props.children}</td>,
|
'todo-list': (props: Props) => (
|
||||||
|
<TodoList {...props.attributes}>{props.children}</TodoList>
|
||||||
|
),
|
||||||
|
table: (props: Props) => (
|
||||||
|
<table {...props.attributes}>{props.children}</table>
|
||||||
|
),
|
||||||
|
'table-row': (props: Props) => (
|
||||||
|
<tr {...props.attributes}>{props.children}</tr>
|
||||||
|
),
|
||||||
|
'table-head': (props: Props) => (
|
||||||
|
<th {...props.attributes}>{props.children}</th>
|
||||||
|
),
|
||||||
|
'table-cell': (props: Props) => (
|
||||||
|
<td {...props.attributes}>{props.children}</td>
|
||||||
|
),
|
||||||
code: Code,
|
code: Code,
|
||||||
image: Image,
|
image: Image,
|
||||||
link: Link,
|
link: Link,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import EditList from './plugins/EditList';
|
import EditList from './plugins/EditList';
|
||||||
import type { State } from './types';
|
import type { State, Transform } from './types';
|
||||||
|
|
||||||
const { transforms } = EditList;
|
const { transforms } = EditList;
|
||||||
|
|
||||||
@ -10,9 +10,12 @@ type Options = {
|
|||||||
append?: string | Object,
|
append?: string | Object,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function splitAndInsertBlock(state: State, options: Options) {
|
export function splitAndInsertBlock(
|
||||||
|
transform: Transform,
|
||||||
|
state: State,
|
||||||
|
options: Options
|
||||||
|
) {
|
||||||
const { type, wrapper, append } = options;
|
const { type, wrapper, append } = options;
|
||||||
let transform = state.transform();
|
|
||||||
const { document } = state;
|
const { document } = state;
|
||||||
const parent = document.getParent(state.startBlock.key);
|
const parent = document.getParent(state.startBlock.key);
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user