Update master

This commit is contained in:
Tom Moor
2017-10-21 12:22:02 -07:00
59 changed files with 691 additions and 677 deletions

View File

@@ -9,6 +9,7 @@ import getDataTransferFiles from 'utils/getDataTransferFiles';
import Flex from 'components/Flex';
import ClickablePadding from './components/ClickablePadding';
import Toolbar from './components/Toolbar';
import BlockInsert from './components/BlockInsert';
import Placeholder from './components/Placeholder';
import Contents from './components/Contents';
import Markdown from './serializer';
@@ -24,7 +25,7 @@ type Props = {
onCancel: Function,
onImageUploadStart: Function,
onImageUploadStop: Function,
emoji: string,
emoji?: string,
readOnly: boolean,
};
@@ -172,6 +173,8 @@ type KeyData = {
};
render = () => {
const { readOnly, emoji, onSave } = this.props;
return (
<Flex
onDrop={this.handleDrop}
@@ -182,25 +185,32 @@ type KeyData = {
auto
>
<MaxWidth column auto>
<Header onClick={this.focusAtStart} readOnly={this.props.readOnly} />
<Toolbar state={this.editorState} onChange={this.onChange} />
<Header onClick={this.focusAtStart} readOnly={readOnly} />
<Contents state={this.editorState} />
{!readOnly &&
<Toolbar state={this.editorState} onChange={this.onChange} />}
{!readOnly &&
<BlockInsert
state={this.editorState}
onChange={this.onChange}
onInsertImage={this.insertImageFile}
/>}
<StyledEditor
innerRef={ref => (this.editor = ref)}
placeholder="Start with a title…"
bodyPlaceholder="…the rest is your canvas"
schema={this.schema}
plugins={this.plugins}
emoji={this.props.emoji}
emoji={emoji}
state={this.editorState}
onKeyDown={this.onKeyDown}
onChange={this.onChange}
onDocumentChange={this.onDocumentChange}
onSave={this.props.onSave}
readOnly={this.props.readOnly}
onSave={onSave}
readOnly={readOnly}
/>
<ClickablePadding
onClick={!this.props.readOnly ? this.focusAtEnd : undefined}
onClick={!readOnly ? this.focusAtEnd : undefined}
grow
/>
</MaxWidth>
@@ -281,6 +291,10 @@ const StyledEditor = styled(Editor)`
position: relative;
}
a:hover {
text-decoration: ${({ readOnly }) => (readOnly ? 'underline' : 'none')};
}
li p {
display: inline;
margin: 0;
@@ -322,6 +336,10 @@ const StyledEditor = styled(Editor)`
td {
padding: 5px 20px 5px 0;
}
b, strong {
font-weight: 600;
}
`;
export default MarkdownEditor;

View File

@@ -0,0 +1,197 @@
// @flow
import React, { Component } from 'react';
import EditList from '../plugins/EditList';
import getDataTransferFiles from 'utils/getDataTransferFiles';
import Portal from 'react-portal';
import { observable } from 'mobx';
import { observer } from 'mobx-react';
import styled from 'styled-components';
import { color } from 'styles/constants';
import Icon from 'components/Icon';
import BlockMenu from 'menus/BlockMenu';
import type { State } from '../types';
const { transforms } = EditList;
type Props = {
state: State,
onChange: Function,
onInsertImage: File => Promise<*>,
};
@observer
export default class BlockInsert extends Component {
props: Props;
mouseMoveTimeout: number;
file: HTMLInputElement;
@observable active: boolean = false;
@observable menuOpen: boolean = false;
@observable top: number;
@observable left: number;
@observable mouseX: number;
componentDidMount = () => {
this.update();
window.addEventListener('mousemove', this.handleMouseMove);
};
componentWillUpdate = (nextProps: Props) => {
this.update(nextProps);
};
componentWillUnmount = () => {
window.removeEventListener('mousemove', this.handleMouseMove);
};
setInactive = () => {
if (this.menuOpen) return;
this.active = false;
};
handleMouseMove = (ev: SyntheticMouseEvent) => {
const windowWidth = window.innerWidth / 3;
let active = ev.clientX < windowWidth;
if (active !== this.active) {
this.active = active || this.menuOpen;
}
if (active) {
clearTimeout(this.mouseMoveTimeout);
this.mouseMoveTimeout = setTimeout(this.setInactive, 2000);
}
};
handleMenuOpen = () => {
this.menuOpen = true;
};
handleMenuClose = () => {
this.menuOpen = false;
};
update = (props?: Props) => {
if (!document.activeElement) return;
const { state } = props || this.props;
const boxRect = document.activeElement.getBoundingClientRect();
const selection = window.getSelection();
if (!selection.focusNode) return;
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
if (rect.top <= 0 || boxRect.left <= 0) return;
if (state.startBlock.type === 'heading1') {
this.active = false;
}
this.top = Math.round(rect.top + window.scrollY);
this.left = Math.round(boxRect.left + window.scrollX - 20);
};
insertBlock = (
ev: SyntheticEvent,
options: {
type: string | Object,
wrapper?: string | Object,
append?: string | Object,
}
) => {
ev.preventDefault();
const { type, wrapper, append } = options;
let { state } = this.props;
let transform = state.transform();
const { document } = state;
const parent = document.getParent(state.startBlock.key);
// lists get some special treatment
if (parent && parent.type === 'list-item') {
transform = transforms.unwrapList(
transforms
.splitListItem(transform.collapseToStart())
.collapseToEndOfPreviousBlock()
);
}
transform = transform.insertBlock(type);
if (wrapper) transform = transform.wrapBlock(wrapper);
if (append) transform = transform.insertBlock(append);
state = transform.focus().apply();
this.props.onChange(state);
this.active = false;
};
onPickImage = (ev: SyntheticEvent) => {
// simulate a click on the file upload input element
this.file.click();
};
onChooseImage = async (ev: SyntheticEvent) => {
const files = getDataTransferFiles(ev);
for (const file of files) {
await this.props.onInsertImage(file);
}
};
render() {
const style = { top: `${this.top}px`, left: `${this.left}px` };
const todo = { type: 'list-item', data: { checked: false } };
const rule = { type: 'horizontal-rule', isVoid: true };
return (
<Portal isOpened>
<Trigger active={this.active} style={style}>
<HiddenInput
type="file"
innerRef={ref => (this.file = ref)}
onChange={this.onChooseImage}
accept="image/*"
/>
<BlockMenu
label={<Icon type="PlusCircle" />}
onPickImage={this.onPickImage}
onInsertList={ev =>
this.insertBlock(ev, {
type: 'list-item',
wrapper: 'bulleted-list',
})}
onInsertTodoList={ev =>
this.insertBlock(ev, { type: todo, wrapper: 'todo-list' })}
onInsertBreak={ev =>
this.insertBlock(ev, { type: rule, append: 'paragraph' })}
onOpen={this.handleMenuOpen}
onClose={this.handleMenuClose}
/>
</Trigger>
</Portal>
);
}
}
const HiddenInput = styled.input`
position: absolute;
top: -100px;
left: -100px;
visibility: hidden;
`;
const Trigger = styled.div`
position: absolute;
z-index: 1;
opacity: 0;
background-color: ${color.white};
border-radius: 4px;
transition: opacity 250ms ease-in-out, transform 250ms ease-in-out;
line-height: 0;
height: 16px;
width: 16px;
transform: scale(.9);
${({ active }) => active && `
transform: scale(1);
opacity: .9;
`}
`;

View File

@@ -0,0 +1,17 @@
// @flow
import React from 'react';
import styled from 'styled-components';
import type { Props } from '../types';
import { color } from 'styles/constants';
function HorizontalRule(props: Props) {
const { state, node } = props;
const active = state.isFocused && state.selection.hasEdgeIn(node);
return <StyledHr active={active} />;
}
const StyledHr = styled.hr`
border-bottom: 1px solid ${props => (props.active ? color.slate : color.slateLight)};
`;
export default HorizontalRule;

View File

@@ -9,7 +9,6 @@ import Heading2Icon from 'components/Icon/Heading2Icon';
import ItalicIcon from 'components/Icon/ItalicIcon';
import LinkIcon from 'components/Icon/LinkIcon';
import StrikethroughIcon from 'components/Icon/StrikethroughIcon';
import BulletedListIcon from 'components/Icon/BulletedListIcon';
export default class FormattingToolbar extends Component {
props: {
@@ -95,7 +94,6 @@ export default class FormattingToolbar extends Component {
{this.renderMarkButton('deleted', StrikethroughIcon)}
{this.renderBlockButton('heading1', Heading1Icon)}
{this.renderBlockButton('heading2', Heading2Icon)}
{this.renderBlockButton('bulleted-list', BulletedListIcon)}
{this.renderMarkButton('code', CodeIcon)}
<ToolbarButton onMouseDown={this.onCreateLink}>
<LinkIcon light />

View File

@@ -15,7 +15,6 @@ export default async function insertImageFile(
try {
// load the file as a data URL
const id = uuid.v4();
const alt = file.name;
const reader = new FileReader();
reader.addEventListener('load', () => {
const src = reader.result;
@@ -25,7 +24,7 @@ export default async function insertImageFile(
.insertBlock({
type: 'image',
isVoid: true,
data: { src, alt, id, loading: true },
data: { src, id, loading: true },
})
.apply();
editor.onChange(state);
@@ -46,7 +45,7 @@ export default async function insertImageFile(
);
return finalTransform.setNodeByKey(placeholder.key, {
data: { src, alt, loading: false },
data: { src, loading: false },
});
} catch (err) {
throw err;

View File

@@ -1,11 +1,11 @@
// @flow
import DropOrPasteImages from '@tommoor/slate-drop-or-paste-images';
import PasteLinkify from 'slate-paste-linkify';
import EditList from 'slate-edit-list';
import CollapseOnEscape from 'slate-collapse-on-escape';
import TrailingBlock from 'slate-trailing-block';
import EditCode from 'slate-edit-code';
import Prism from 'slate-prism';
import EditList from './plugins/EditList';
import KeyboardShortcuts from './plugins/KeyboardShortcuts';
import MarkdownShortcuts from './plugins/MarkdownShortcuts';
import insertImage from './insertImage';
@@ -35,10 +35,7 @@ const createPlugins = ({ onImageUploadStart, onImageUploadStop }: Options) => {
);
},
}),
EditList({
types: ['ordered-list', 'bulleted-list', 'todo-list'],
typeItem: 'list-item',
}),
EditList,
EditCode({
onlyIn: onlyInCode,
containerType: 'code',

View File

@@ -0,0 +1,7 @@
// @flow
import EditList from 'slate-edit-list';
export default EditList({
types: ['ordered-list', 'bulleted-list', 'todo-list'],
typeItem: 'list-item',
});

View File

@@ -112,19 +112,17 @@ export default function MarkdownShortcuts() {
if (chars === '--') {
ev.preventDefault();
const transform = state
return state
.transform()
.extendToStartOf(startBlock)
.delete()
.setBlock({
type: 'horizontal-rule',
isVoid: true,
});
state = transform
})
.collapseToStartOfNextBlock()
.insertBlock('paragraph')
.apply();
return state;
}
},

View File

@@ -1,6 +1,7 @@
// @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';
@@ -33,7 +34,7 @@ const createSchema = () => {
'block-quote': (props: Props) => (
<blockquote>{props.children}</blockquote>
),
'horizontal-rule': (props: Props) => <hr />,
'horizontal-rule': HorizontalRule,
'bulleted-list': (props: Props) => <ul>{props.children}</ul>,
'ordered-list': (props: Props) => <ol>{props.children}</ol>,
'todo-list': (props: Props) => <TodoList>{props.children}</TodoList>,