Fixed: Checklist items cant be created from shortcuts
Fixed: BlockToolbar not close on unfocus
This commit is contained in:
@ -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`
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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 }) =>
|
||||||
|
@ -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 } })
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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}>
|
||||||
|
@ -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)}
|
||||||
|
@ -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;
|
||||||
|
@ -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) => {
|
||||||
|
@ -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({
|
||||||
|
@ -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',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -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 },
|
||||||
|
@ -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)`
|
||||||
|
Reference in New Issue
Block a user