2016-06-16 19:20:36 -03:00
|
|
|
import React, { PropTypes } from 'react';
|
2016-08-02 16:17:37 -03:00
|
|
|
import { Editor, Plain } from 'slate';
|
2016-08-02 23:25:45 -03:00
|
|
|
import position from 'selection-position';
|
2016-08-03 10:30:42 -03:00
|
|
|
import Markdown from 'slate-markdown-serializer';
|
|
|
|
import { DEFAULT_NODE, NODES, MARKS } from './MarkdownControlElements/localRenderers';
|
2016-08-04 15:49:43 -03:00
|
|
|
import StylesMenu from './MarkdownControlElements/StylesMenu';
|
2016-08-05 13:19:31 -03:00
|
|
|
import BlockTypesMenu from './MarkdownControlElements/BlockTypesMenu';
|
2016-08-01 16:41:55 -03:00
|
|
|
|
2016-08-02 16:17:37 -03:00
|
|
|
const markdown = new Markdown();
|
2016-08-03 10:30:42 -03:00
|
|
|
|
|
|
|
/**
|
2016-08-01 16:41:55 -03:00
|
|
|
* Slate Render Configuration
|
|
|
|
*/
|
|
|
|
class MarkdownControl extends React.Component {
|
2016-05-30 16:55:32 -07:00
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
2016-08-02 16:17:37 -03:00
|
|
|
this.blockEdit = false;
|
2016-08-05 13:19:31 -03:00
|
|
|
this.menuPositions = {
|
|
|
|
stylesMenu: {
|
|
|
|
top: 0,
|
|
|
|
left: 0,
|
|
|
|
width: 0,
|
|
|
|
height: 0
|
|
|
|
},
|
|
|
|
blockTypesMenu: {
|
|
|
|
top: 0,
|
|
|
|
left: 0,
|
|
|
|
width: 0,
|
|
|
|
height: 0
|
|
|
|
}
|
2016-08-04 15:49:43 -03:00
|
|
|
};
|
|
|
|
|
2016-05-30 16:55:32 -07:00
|
|
|
this.state = {
|
2016-08-05 13:19:31 -03:00
|
|
|
state: props.value ? markdown.deserialize(props.value) : Plain.deserialize('')
|
2016-05-30 16:55:32 -07:00
|
|
|
};
|
2016-08-01 16:41:55 -03:00
|
|
|
|
2016-05-30 16:55:32 -07:00
|
|
|
this.handleChange = this.handleChange.bind(this);
|
2016-08-01 16:41:55 -03:00
|
|
|
this.handleDocumentChange = this.handleDocumentChange.bind(this);
|
2016-08-04 15:49:43 -03:00
|
|
|
this.handleMarkStyleClick = this.handleMarkStyleClick.bind(this);
|
|
|
|
this.handleBlockStyleClick = this.handleBlockStyleClick.bind(this);
|
2016-08-06 18:16:30 -03:00
|
|
|
this.handleInlineClick = this.handleInlineClick.bind(this);
|
2016-08-05 13:19:31 -03:00
|
|
|
this.handleBlockTypeClick = this.handleBlockTypeClick.bind(this);
|
2016-08-02 16:17:37 -03:00
|
|
|
this.handleKeyDown = this.handleKeyDown.bind(this);
|
2016-08-05 13:19:31 -03:00
|
|
|
this.calculateMenuPositions = this.calculateMenuPositions.bind(this);
|
|
|
|
this.renderBlockTypesMenu = this.renderBlockTypesMenu.bind(this);
|
2016-08-01 16:41:55 -03:00
|
|
|
this.renderNode = this.renderNode.bind(this);
|
|
|
|
this.renderMark = this.renderMark.bind(this);
|
2016-08-02 23:25:45 -03:00
|
|
|
}
|
2016-08-03 10:30:42 -03:00
|
|
|
|
|
|
|
/**
|
2016-08-01 16:41:55 -03:00
|
|
|
* Slate keeps track of selections, scroll position etc.
|
|
|
|
* So, onChange gets dispatched on every interaction (click, arrows, everything...)
|
|
|
|
* It also have an onDocumentChange, that get's dispached only when the actual
|
|
|
|
* content changes
|
|
|
|
*/
|
|
|
|
handleChange(state) {
|
2016-08-02 16:17:37 -03:00
|
|
|
if (this.blockEdit) {
|
|
|
|
this.blockEdit = false;
|
|
|
|
} else {
|
2016-08-05 13:19:31 -03:00
|
|
|
this.setState({ state }, this.calculateMenuPositions);
|
2016-08-02 16:17:37 -03:00
|
|
|
}
|
2016-08-01 16:41:55 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
handleDocumentChange(document, state) {
|
|
|
|
this.props.onChange(markdown.serialize(state));
|
|
|
|
}
|
|
|
|
|
2016-08-05 13:19:31 -03:00
|
|
|
/**
|
|
|
|
* All menu positions are calculated accessing dom elements
|
|
|
|
* That's why calculateMenuPositions is called on handleChange's setState callback
|
|
|
|
*/
|
|
|
|
calculateMenuPositions() {
|
|
|
|
const rect1 = position();
|
|
|
|
this.menuPositions.stylesMenu = {
|
|
|
|
top: rect1.top + window.scrollY,
|
|
|
|
left: rect1.left + window.scrollX,
|
|
|
|
width: rect1.width,
|
|
|
|
height: rect1.height
|
|
|
|
};
|
2016-08-03 18:39:00 -03:00
|
|
|
|
2016-08-05 13:19:31 -03:00
|
|
|
const blockElement = document.querySelectorAll(`[data-key='${this.state.state.selection.focusKey}']`);
|
|
|
|
if (blockElement.length > 0) {
|
|
|
|
const rect2 = blockElement[0].getBoundingClientRect();
|
|
|
|
this.menuPositions.blockTypesMenu = {
|
|
|
|
top: rect2.top + window.scrollY,
|
|
|
|
left: rect2.left + window.scrollX
|
|
|
|
};
|
|
|
|
this.forceUpdate();
|
2016-08-03 18:39:00 -03:00
|
|
|
}
|
2016-08-06 18:57:24 -03:00
|
|
|
|
2016-08-03 18:39:00 -03:00
|
|
|
}
|
|
|
|
|
2016-08-03 10:30:42 -03:00
|
|
|
/**
|
2016-08-01 16:41:55 -03:00
|
|
|
* Toggle marks / blocks when button is clicked
|
|
|
|
*/
|
2016-08-04 15:49:43 -03:00
|
|
|
handleMarkStyleClick(type) {
|
2016-08-01 16:41:55 -03:00
|
|
|
let { state } = this.state;
|
|
|
|
|
|
|
|
state = state
|
|
|
|
.transform()
|
|
|
|
.toggleMark(type)
|
|
|
|
.apply();
|
|
|
|
|
|
|
|
this.setState({ state });
|
|
|
|
}
|
|
|
|
|
2016-08-04 15:49:43 -03:00
|
|
|
handleBlockStyleClick(type, isActive, isList) {
|
2016-08-01 16:41:55 -03:00
|
|
|
let { state } = this.state;
|
|
|
|
let transform = state.transform();
|
|
|
|
const { document } = state;
|
2016-05-30 16:55:32 -07:00
|
|
|
|
2016-08-01 16:41:55 -03:00
|
|
|
// Handle everything but list buttons.
|
|
|
|
if (type != 'bulleted-list' && type != 'numbered-list') {
|
|
|
|
|
|
|
|
if (isList) {
|
|
|
|
transform = transform
|
|
|
|
.setBlock(isActive ? DEFAULT_NODE : type)
|
|
|
|
.unwrapBlock('bulleted-list')
|
|
|
|
.unwrapBlock('numbered-list');
|
|
|
|
}
|
|
|
|
|
|
|
|
else {
|
|
|
|
transform = transform
|
|
|
|
.setBlock(isActive ? DEFAULT_NODE : type);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle the extra wrapping required for list buttons.
|
|
|
|
else {
|
|
|
|
const isType = state.blocks.some((block) => {
|
|
|
|
return !!document.getClosest(block, parent => parent.type == type);
|
|
|
|
});
|
|
|
|
|
|
|
|
if (isList && isType) {
|
|
|
|
transform = transform
|
|
|
|
.setBlock(DEFAULT_NODE)
|
|
|
|
.unwrapBlock('bulleted-list');
|
|
|
|
} else if (isList) {
|
|
|
|
transform = transform
|
|
|
|
.unwrapBlock(type == 'bulleted-list')
|
|
|
|
.wrapBlock(type);
|
|
|
|
} else {
|
|
|
|
transform = transform
|
|
|
|
.setBlock('list-item')
|
|
|
|
.wrapBlock(type);
|
|
|
|
}
|
2016-05-30 16:55:32 -07:00
|
|
|
}
|
2016-08-01 16:41:55 -03:00
|
|
|
|
|
|
|
state = transform.apply();
|
|
|
|
this.setState({ state });
|
|
|
|
}
|
|
|
|
|
2016-08-06 18:16:30 -03:00
|
|
|
/**
|
|
|
|
* When clicking a link, if the selection has a link in it, remove the link.
|
|
|
|
* Otherwise, add a new link with an href and text.
|
|
|
|
*
|
|
|
|
* @param {Event} e
|
|
|
|
*/
|
|
|
|
|
|
|
|
handleInlineClick(type, isActive) {
|
|
|
|
let { state } = this.state;
|
|
|
|
|
|
|
|
if (type === 'link') {
|
|
|
|
if (!state.isExpanded) return;
|
|
|
|
|
|
|
|
if (isActive) {
|
|
|
|
state = state
|
|
|
|
.transform()
|
|
|
|
.unwrapInline('link')
|
|
|
|
.apply();
|
|
|
|
}
|
|
|
|
|
|
|
|
else {
|
|
|
|
const href = window.prompt('Enter the URL of the link:', 'http://www.');
|
|
|
|
state = state
|
|
|
|
.transform()
|
|
|
|
.wrapInline({
|
|
|
|
type: 'link',
|
|
|
|
data: { href }
|
|
|
|
})
|
|
|
|
.collapseToEnd()
|
|
|
|
.apply();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.setState({ state });
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-08-05 13:19:31 -03:00
|
|
|
handleBlockTypeClick(type) {
|
|
|
|
let { state } = this.state;
|
|
|
|
|
|
|
|
state = state
|
2016-08-06 18:57:24 -03:00
|
|
|
.transform()
|
|
|
|
.insertBlock(type)
|
|
|
|
.apply();
|
|
|
|
|
|
|
|
this.setState({ state }, () => {
|
|
|
|
const blocks = this.state.state.document.getBlocks();
|
|
|
|
const last = blocks.last();
|
|
|
|
const normalized = state
|
|
|
|
.transform()
|
|
|
|
.focus()
|
|
|
|
.collapseToEndOf(last)
|
|
|
|
.splitBlock()
|
|
|
|
.setBlock(DEFAULT_NODE)
|
|
|
|
.apply({
|
|
|
|
snapshot: false
|
|
|
|
});
|
|
|
|
this.setState({ state:normalized });
|
|
|
|
});
|
2016-08-05 13:19:31 -03:00
|
|
|
}
|
|
|
|
|
2016-08-04 15:49:43 -03:00
|
|
|
handleKeyDown(evt) {
|
|
|
|
if (evt.shiftKey && evt.key === 'Enter') {
|
|
|
|
this.blockEdit = true;
|
|
|
|
let { state } = this.state;
|
|
|
|
state = state
|
|
|
|
.transform()
|
|
|
|
.insertText(' \n')
|
|
|
|
.apply();
|
2016-08-01 16:41:55 -03:00
|
|
|
|
2016-08-04 15:49:43 -03:00
|
|
|
this.setState({ state });
|
|
|
|
}
|
2016-08-01 16:41:55 -03:00
|
|
|
}
|
|
|
|
|
2016-08-03 10:30:42 -03:00
|
|
|
/**
|
2016-08-01 16:41:55 -03:00
|
|
|
* Return renderers for Slate
|
|
|
|
*/
|
|
|
|
renderNode(node) {
|
|
|
|
return NODES[node.type];
|
|
|
|
}
|
|
|
|
renderMark(mark) {
|
|
|
|
return MARKS[mark.type];
|
2016-05-30 16:55:32 -07:00
|
|
|
}
|
|
|
|
|
2016-08-05 13:19:31 -03:00
|
|
|
renderBlockTypesMenu() {
|
|
|
|
const currentBlock = this.state.state.blocks.get(0);
|
|
|
|
const isOpen = (currentBlock.isEmpty && currentBlock.type !== 'list-item' && currentBlock.type !== 'horizontal-rule');
|
|
|
|
|
|
|
|
return (
|
|
|
|
<BlockTypesMenu
|
|
|
|
isOpen={isOpen}
|
|
|
|
position={this.menuPositions.blockTypesMenu}
|
|
|
|
onClickBlock={this.handleBlockTypeClick}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2016-08-04 15:49:43 -03:00
|
|
|
renderStylesMenu() {
|
|
|
|
const { state } = this.state;
|
|
|
|
const isOpen = !(state.isBlurred || state.isCollapsed);
|
2016-08-02 23:25:45 -03:00
|
|
|
|
2016-08-04 15:49:43 -03:00
|
|
|
return (
|
|
|
|
<StylesMenu
|
|
|
|
isOpen={isOpen}
|
2016-08-05 13:19:31 -03:00
|
|
|
position={this.menuPositions.stylesMenu}
|
2016-08-04 15:49:43 -03:00
|
|
|
marks={this.state.state.marks}
|
|
|
|
blocks={this.state.state.blocks}
|
2016-08-06 18:16:30 -03:00
|
|
|
inlines={this.state.state.inlines}
|
2016-08-04 15:49:43 -03:00
|
|
|
onClickMark={this.handleMarkStyleClick}
|
2016-08-06 18:16:30 -03:00
|
|
|
onClickInline={this.handleInlineClick}
|
2016-08-04 15:49:43 -03:00
|
|
|
onClickBlock={this.handleBlockStyleClick}
|
|
|
|
/>
|
|
|
|
);
|
2016-08-02 23:25:45 -03:00
|
|
|
}
|
|
|
|
|
2016-05-30 16:55:32 -07:00
|
|
|
render() {
|
|
|
|
return (
|
2016-08-01 16:41:55 -03:00
|
|
|
<div>
|
2016-08-04 15:49:43 -03:00
|
|
|
{this.renderStylesMenu()}
|
2016-08-05 13:19:31 -03:00
|
|
|
{this.renderBlockTypesMenu()}
|
2016-08-02 23:25:45 -03:00
|
|
|
<Editor
|
|
|
|
placeholder={'Enter some rich text...'}
|
|
|
|
state={this.state.state}
|
|
|
|
renderNode={this.renderNode}
|
|
|
|
renderMark={this.renderMark}
|
|
|
|
onChange={this.handleChange}
|
|
|
|
onKeyDown={this.handleKeyDown}
|
|
|
|
onDocumentChange={this.handleDocumentChange}
|
|
|
|
/>
|
2016-08-01 16:41:55 -03:00
|
|
|
</div>
|
|
|
|
);
|
2016-05-30 16:55:32 -07:00
|
|
|
}
|
|
|
|
}
|
2016-06-16 19:20:36 -03:00
|
|
|
|
2016-08-01 16:41:55 -03:00
|
|
|
export default MarkdownControl;
|
|
|
|
|
2016-06-16 19:20:36 -03:00
|
|
|
MarkdownControl.propTypes = {
|
|
|
|
onChange: PropTypes.func.isRequired,
|
|
|
|
value: PropTypes.node,
|
|
|
|
};
|