Fixed: Checklist items cant be created from shortcuts

Fixed: BlockToolbar not close on unfocus
This commit is contained in:
Tom Moor
2017-12-05 20:05:43 -08:00
parent 751b468e92
commit e3f664e8a4
14 changed files with 105 additions and 90 deletions

View File

@ -8,7 +8,7 @@ type Props = {
const Container = styled.div` const Container = styled.div`
width: 100%; width: 100%;
margin: 60px; padding: 60px;
`; `;
const Content = styled.div` const Content = styled.div`

View File

@ -46,7 +46,6 @@ class MarkdownEditor extends Component {
this.renderNode = createRenderNode({ this.renderNode = createRenderNode({
onInsertImage: this.insertImageFile, onInsertImage: this.insertImageFile,
onChange: this.onChange,
}); });
this.plugins = createPlugins({ this.plugins = createPlugins({
onImageUploadStart: props.onImageUploadStart, onImageUploadStart: props.onImageUploadStart,
@ -76,6 +75,7 @@ class MarkdownEditor extends Component {
} }
onChange = (change: Change) => { onChange = (change: Change) => {
// TODO: Lets avoid constantly serializing to Markdown
if (this.editorValue !== change.value) { if (this.editorValue !== change.value) {
this.props.onChange(Markdown.serialize(change.value)); this.props.onChange(Markdown.serialize(change.value));
this.editorValue = 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 => this.editor.change(change =>
change.call( change.call(
insertImageFile, insertImageFile,

View File

@ -18,17 +18,17 @@ export function splitAndInsertBlock(change: Change, options: Options) {
// lists get some special treatment // lists get some special treatment
if (parent && parent.type === 'list-item') { if (parent && parent.type === 'list-item') {
change = changes.unwrapList( change
changes .collapseToStart()
.splitListItem(change.collapseToStart()) .call(changes.splitListItem)
.collapseToEndOfPreviousBlock() .collapseToEndOfPreviousBlock()
); .call(changes.unwrapList);
} }
change = change.insertBlock(type); change.insertBlock(type);
if (wrapper) change = change.wrapBlock(wrapper); if (wrapper) change.wrapBlock(wrapper);
if (append) change = change.insertBlock(append); if (append) change.insertBlock(append);
return change; return change;
} }

View File

@ -84,11 +84,13 @@ export default class BlockInsert extends Component {
}; };
handleClick = (ev: SyntheticMouseEvent) => { handleClick = (ev: SyntheticMouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
this.mouseMovementSinceClick = 0; this.mouseMovementSinceClick = 0;
this.active = false; this.active = false;
const { editor } = this.props; const { editor } = this.props;
const type = { type: 'block-toolbar', isVoid: true };
editor.change(change => { editor.change(change => {
// remove any existing toolbars in the document as a fail safe // remove any existing toolbars in the document as a fail safe
@ -101,7 +103,7 @@ export default class BlockInsert extends Component {
change change
.collapseToStartOf(this.closestRootNode) .collapseToStartOf(this.closestRootNode)
.collapseToEndOfPreviousBlock() .collapseToEndOfPreviousBlock()
.insertBlock(type); .insertBlock({ type: 'block-toolbar', isVoid: true });
}); });
}; };
@ -133,7 +135,11 @@ const Trigger = styled.div`
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background-color: ${color.smokeDark}; background-color: ${color.slate};
svg {
fill: ${color.white};
}
} }
${({ active }) => ${({ active }) =>

View File

@ -13,11 +13,8 @@ class Image extends Component {
const { editor, node } = this.props; const { editor, node } = this.props;
const data = node.data.toObject(); const data = node.data.toObject();
editor.onChange( editor.change(change =>
editor change.setNodeByKey(node.key, { data: { ...data, alt } })
.getState()
.change()
.setNodeByKey(node.key, { data: { ...data, alt } })
); );
}; };

View File

@ -10,15 +10,11 @@ export default function ListItem({
...props ...props
}: SlateNodeProps) { }: SlateNodeProps) {
const checked = node.data.get('checked'); const checked = node.data.get('checked');
console.log('ListItem.checked', checked);
if (checked !== undefined) { if (checked !== undefined) {
return ( return (
<TodoItem <TodoItem node={node} attributes={attributes} {...props}>
checked={checked}
node={node}
attributes={attributes}
{...props}
>
{children} {children}
</TodoItem> </TodoItem>
); );

View File

@ -5,7 +5,7 @@ import { color } from 'shared/styles/constants';
import type { SlateNodeProps } from '../types'; import type { SlateNodeProps } from '../types';
export default class TodoItem extends Component { export default class TodoItem extends Component {
props: SlateNodeProps & { checked: boolean }; props: SlateNodeProps;
handleChange = (ev: SyntheticInputEvent) => { handleChange = (ev: SyntheticInputEvent) => {
const checked = ev.target.checked; const checked = ev.target.checked;
@ -16,7 +16,8 @@ export default class TodoItem extends Component {
}; };
render() { render() {
const { children, checked, attributes, readOnly } = this.props; const { children, node, attributes, readOnly } = this.props;
const checked = node.data.get('checked');
return ( return (
<ListItem checked={checked} {...attributes}> <ListItem checked={checked} {...attributes}>

View File

@ -1,5 +1,6 @@
// @flow // @flow
import React, { Component } from 'react'; import React, { Component } from 'react';
import { findDOMNode } from 'react-dom';
import keydown from 'react-keydown'; 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';
@ -30,20 +31,29 @@ type Options = {
class BlockToolbar extends Component { class BlockToolbar extends Component {
props: Props; props: Props;
bar: HTMLDivElement;
file: HTMLInputElement; file: HTMLInputElement;
componentWillReceiveProps(nextProps: Props) { componentDidMount() {
const { editor } = this.props; window.addEventListener('click', this.handleOutsideMouseClick);
const wasActive = editor.value.selection.hasEdgeIn(this.props.node); }
const isActive = nextProps.editor.value.selection.hasEdgeIn(nextProps.node);
const becameInactive = !isActive && wasActive;
if (becameInactive) { componentWillUnmount() {
nextProps.editor.change(change => window.removeEventListener('click', this.handleOutsideMouseClick);
change.removeNodeByKey(nextProps.node.key)
);
} }
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') @keydown('esc')
removeSelf(ev: SyntheticEvent) { removeSelf(ev: SyntheticEvent) {
@ -59,7 +69,7 @@ class BlockToolbar extends Component {
const { editor } = this.props; const { editor } = this.props;
editor.change(change => { editor.change(change => {
splitAndInsertBlock(change, options); change.call(splitAndInsertBlock, options);
editor.value.document.nodes.forEach(node => { editor.value.document.nodes.forEach(node => {
if (node.type === 'block-toolbar') { if (node.type === 'block-toolbar') {
@ -130,7 +140,7 @@ class BlockToolbar extends Component {
editor.value.isFocused && editor.value.selection.hasEdgeIn(node); editor.value.isFocused && editor.value.selection.hasEdgeIn(node);
return ( return (
<Bar active={active} {...attributes}> <Bar active={active} {...attributes} ref={ref => (this.bar = ref)}>
<HiddenInput <HiddenInput
type="file" type="file"
innerRef={ref => (this.file = ref)} innerRef={ref => (this.file = ref)}

View File

@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { observable } from 'mobx'; import { observable } from 'mobx';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Portal } from 'react-portal'; import { Portal } from 'react-portal';
import { Editor } from 'slate-react'; import { Editor, findDOMNode } from 'slate-react';
import { Value } from 'slate'; import { Value } from 'slate';
import styled from 'styled-components'; import styled from 'styled-components';
import _ from 'lodash'; import _ from 'lodash';
@ -45,14 +45,14 @@ export default class Toolbar extends Component {
const { value } = this.props; const { value } = this.props;
try { try {
const selectedLinks = value.startBlock const selectedLinks = value.document
.getInlinesAtRange(value.selection) .getInlinesAtRange(value.selection)
.filter(node => node.type === 'link'); .filter(node => node.type === 'link');
if (selectedLinks.size) { if (selectedLinks.size) {
return selectedLinks.first(); return selectedLinks.first();
} }
} catch (err) { } catch (err) {
// // It's okay.
} }
} }
@ -74,8 +74,8 @@ export default class Toolbar extends Component {
const firstNode = value.document.nodes.first(); const firstNode = value.document.nodes.first();
if (firstNode === value.startBlock) return; if (firstNode === value.startBlock) return;
// don't display toolbar for code blocks // don't display toolbar for code blocks, code-lines inline code.
if (value.startBlock.type === 'code') return; if (value.startBlock.type.match(/code/)) return;
this.active = true; this.active = true;
this.focused = !!link; this.focused = !!link;
@ -84,7 +84,9 @@ export default class Toolbar extends Component {
const padding = 16; const padding = 16;
const selection = window.getSelection(); const selection = window.getSelection();
const range = selection.getRangeAt(0); const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect(); const rect = link
? findDOMNode(link).getBoundingClientRect()
: range.getBoundingClientRect();
if (rect.top === 0 && rect.left === 0) { if (rect.top === 0 && rect.left === 0) {
return; return;

View File

@ -14,7 +14,7 @@ import StrikethroughIcon from 'components/Icon/StrikethroughIcon';
class FormattingToolbar extends Component { class FormattingToolbar extends Component {
props: { props: {
editor: Editor, editor: Editor,
onCreateLink: Function, onCreateLink: () => void,
}; };
/** /**
@ -52,10 +52,10 @@ class FormattingToolbar extends Component {
ev.stopPropagation(); ev.stopPropagation();
const data = { href: '' }; const data = { href: '' };
this.props.editor.change(change => { this.props.editor.change(change =>
change.wrapInline({ type: 'link', data }); change.wrapInline({ type: 'link', data })
);
this.props.onCreateLink(); this.props.onCreateLink();
});
}; };
renderMarkButton = (type: string, IconClass: Function) => { renderMarkButton = (type: string, IconClass: Function) => {

View File

@ -10,13 +10,13 @@ import KeyboardShortcuts from './plugins/KeyboardShortcuts';
import MarkdownShortcuts from './plugins/MarkdownShortcuts'; import MarkdownShortcuts from './plugins/MarkdownShortcuts';
import { insertImageFile } from './changes'; import { insertImageFile } from './changes';
const onlyInCode = node => node.type === 'code';
type Options = { type Options = {
onImageUploadStart: Function, onImageUploadStart: () => void,
onImageUploadStop: Function, onImageUploadStop: () => void,
}; };
const onlyInCode = node => node.type === 'code';
const createPlugins = ({ onImageUploadStart, onImageUploadStop }: Options) => { const createPlugins = ({ onImageUploadStart, onImageUploadStop }: Options) => {
return [ return [
PasteLinkify({ PasteLinkify({

View File

@ -50,7 +50,17 @@ export default function MarkdownShortcuts() {
let checked; let checked;
if (chars === '[x]') checked = true; if (chars === '[x]') checked = true;
if (chars === '[ ]') checked = false; if (chars === '[ ]') checked = false;
change.setBlock({ type, data: { checked } });
change
.extendToStartOf(startBlock)
.delete()
.setBlock(
{
type,
data: { checked },
},
{ normalize: false }
);
if (type === 'list-item') { if (type === 'list-item') {
if (checked !== undefined) { if (checked !== undefined) {
@ -62,7 +72,7 @@ export default function MarkdownShortcuts() {
} }
} }
return change.extendToStartOf(startBlock).delete(); return true;
} }
for (const key of inlineShortcuts) { for (const key of inlineShortcuts) {
@ -70,7 +80,8 @@ export default function MarkdownShortcuts() {
let { mark, shortcut } = key; let { mark, shortcut } = key;
let inlineTags = []; 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++) { for (let i = 0; i < startBlock.text.length; i++) {
const { text } = startBlock; const { text } = startBlock;
const start = i; const start = i;
@ -85,16 +96,17 @@ export default function MarkdownShortcuts() {
if ( if (
text.slice(start, end) === shortcut && text.slice(start, end) === shortcut &&
(beginningOfBlock || endOfBlock || surroundedByWhitespaces) (beginningOfBlock || endOfBlock || surroundedByWhitespaces)
) ) {
inlineTags.push(i); inlineTags.push(i);
} }
}
// if we have multiple tags then mark the text between as inline code // if we have multiple tags then mark the text between as inline code
if (inlineTags.length > 1) { if (inlineTags.length > 1) {
const firstText = startBlock.getFirstText(); const firstText = startBlock.getFirstText();
const firstCodeTagIndex = inlineTags[0]; const firstCodeTagIndex = inlineTags[0];
const lastCodeTagIndex = inlineTags[inlineTags.length - 1]; const lastCodeTagIndex = inlineTags[inlineTags.length - 1];
change return change
.removeTextByKey(firstText.key, lastCodeTagIndex, shortcut.length) .removeTextByKey(firstText.key, lastCodeTagIndex, shortcut.length)
.removeTextByKey(firstText.key, firstCodeTagIndex, shortcut.length) .removeTextByKey(firstText.key, firstCodeTagIndex, shortcut.length)
.moveOffsetsTo( .moveOffsetsTo(
@ -139,9 +151,7 @@ export default function MarkdownShortcuts() {
return change return change
.extendToStartOf(startBlock) .extendToStartOf(startBlock)
.delete() .delete()
.setBlock({ .setBlock({ type: 'code' });
type: 'code',
});
} }
}, },

View File

@ -2,32 +2,26 @@
import { Block, Change, Node, Mark } from 'slate'; import { Block, Change, Node, Mark } from 'slate';
const schema = { const schema = {
blocks: { // blocks: {
heading1: { marks: [''] }, // heading1: { marks: [''] },
heading2: { marks: [''] }, // heading2: { marks: [''] },
heading3: { marks: [''] }, // heading3: { marks: [''] },
heading4: { marks: [''] }, // heading4: { marks: [''] },
heading5: { marks: [''] }, // heading5: { marks: [''] },
heading6: { marks: [''] }, // heading6: { marks: [''] },
'ordered-list': { // table: {
nodes: [{ types: ['list-item'] }], // nodes: [{ types: ['table-row', 'table-head', 'table-cell'] }],
}, // },
'bulleted-list': { // image: {
nodes: [{ types: ['list-item'] }], // isVoid: true,
}, // },
table: { // 'horizontal-rule': {
nodes: [{ types: ['table-row', 'table-head', 'table-cell'] }], // isVoid: true,
}, // },
image: { // 'block-toolbar': {
isVoid: true, // isVoid: true,
}, // },
'horizontal-rule': { // },
isVoid: true,
},
'block-toolbar': {
isVoid: true,
},
},
document: { document: {
nodes: [ nodes: [
{ types: ['heading1'], min: 1, max: 1 }, { types: ['heading1'], min: 1, max: 1 },

View File

@ -292,7 +292,6 @@ class DocumentScene extends Component {
const Container = styled(Flex)` const Container = styled(Flex)`
position: relative; position: relative;
width: 100%;
`; `;
const LoadingState = styled(LoadingPlaceholder)` const LoadingState = styled(LoadingPlaceholder)`