import React, { PropTypes } from 'react';
import { Editor, Plain } from 'slate';
import Portal from 'react-portal';
import position from 'selection-position';
import Markdown from 'slate-markdown-serializer';
import { DEFAULT_NODE, NODES, MARKS } from './MarkdownControlElements/localRenderers';
import styles from './MarkdownControl.css';
const markdown = new Markdown();
/**
* Slate Render Configuration
*/
class MarkdownControl extends React.Component {
constructor(props) {
super(props);
this.blockEdit = false;
this.state = {
state: props.value ? markdown.deserialize(props.value) : Plain.deserialize('')
};
this.hasMark = this.hasMark.bind(this);
this.hasBlock = this.hasBlock.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleDocumentChange = this.handleDocumentChange.bind(this);
this.onClickMark = this.onClickMark.bind(this);
this.onClickBlock = this.onClickBlock.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.renderMenu = this.renderMenu.bind(this);
this.renderMarkButton = this.renderMarkButton.bind(this);
this.renderBlockButton = this.renderBlockButton.bind(this);
this.renderNode = this.renderNode.bind(this);
this.renderMark = this.renderMark.bind(this);
this.handleOpen = this.handleOpen.bind(this);
this.updateMenu = this.updateMenu.bind(this);
}
/**
* On update, update the menu.
*/
componentDidMount() {
this.updateMenu();
}
componentDidUpdate() {
this.updateMenu();
}
/**
* Used to set toolbar buttons to active state
*/
hasMark(type) {
const { state } = this.state;
return state.marks.some(mark => mark.type == type);
}
hasBlock(type) {
const { state } = this.state;
return state.blocks.some(node => node.type == type);
}
/**
* 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) {
if (this.blockEdit) {
this.blockEdit = false;
} else {
this.setState({ state });
}
}
handleDocumentChange(document, state) {
this.props.onChange(markdown.serialize(state));
}
/**
* Toggle marks / blocks when button is clicked
*/
onClickMark(e, type) {
let { state } = this.state;
state = state
.transform()
.toggleMark(type)
.apply();
this.setState({ state });
}
handleKeyDown(evt) {
if (evt.shiftKey && evt.key === 'Enter') {
this.blockEdit = true;
let { state } = this.state;
state = state
.transform()
.insertText(' \n')
.apply();
this.setState({ state });
}
}
onClickBlock(e, type) {
e.preventDefault();
let { state } = this.state;
let transform = state.transform();
const { document } = state;
// Handle everything but list buttons.
if (type != 'bulleted-list' && type != 'numbered-list') {
const isActive = this.hasBlock(type);
const isList = this.hasBlock('list-item');
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 isList = this.hasBlock('list-item');
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);
}
}
state = transform.apply();
this.setState({ state });
}
/**
* When the portal opens, cache the menu element.
*/
handleOpen(portal) {
this.setState({ menu: portal.firstChild });
}
renderMenu() {
const { state } = this.state;
const isOpen = state.isExpanded && state.isFocused;
return (