static-cms/src/components/Widgets/MarkdownControl.js

223 lines
5.6 KiB
JavaScript
Raw Normal View History

import React, { PropTypes } from 'react';
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';
import StylesMenu from './MarkdownControlElements/StylesMenu';
import AddBlock from './MarkdownControlElements/AddBlock';
2016-08-01 16:41:55 -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);
this.blockEdit = false;
this.stylesMenuPosition = {
top: 0,
left: 0,
width: 0,
height: 0
};
2016-05-30 16:55:32 -07:00
this.state = {
state: props.value ? markdown.deserialize(props.value) : Plain.deserialize(''),
addBlockButton:{
show: false
}
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);
this.maybeShowBlockAddButton = this.maybeShowBlockAddButton.bind(this);
this.handleMarkStyleClick = this.handleMarkStyleClick.bind(this);
this.handleBlockStyleClick = this.handleBlockStyleClick.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.renderAddBlock = this.renderAddBlock.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
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) {
if (this.blockEdit) {
this.blockEdit = false;
} else {
this.setState({ state }, this.maybeShowBlockAddButton);
}
2016-08-01 16:41:55 -03:00
}
handleDocumentChange(document, state) {
this.props.onChange(markdown.serialize(state));
}
maybeShowBlockAddButton() {
if (this.state.state.blocks.get(0).isEmpty) {
const rect = document.querySelectorAll(`[data-key='${this.state.state.selection.focusKey}']`)[0].getBoundingClientRect();
this.setState({ addBlockButton: {
show: true,
top: rect.top + window.scrollY + 2,
left: rect.left + window.scrollX - 28
} });
} else {
this.setState({ addBlockButton: {
show: false
} });
}
}
2016-08-03 10:30:42 -03:00
/**
2016-08-01 16:41:55 -03:00
* Toggle marks / blocks when button is clicked
*/
handleMarkStyleClick(type) {
2016-08-01 16:41:55 -03:00
let { state } = this.state;
state = state
.transform()
.toggleMark(type)
.apply();
this.setState({ state });
}
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 });
}
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
this.setState({ state });
}
2016-08-01 16:41:55 -03:00
}
renderAddBlock() {
return (
this.state.addBlockButton.show ? <AddBlock top={this.state.addBlockButton.top} left={this.state.addBlockButton.left} /> : null
);
}
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-03 10:30:42 -03:00
/**
2016-08-02 23:25:45 -03:00
* Update the menu's absolute position.
*/
renderStylesMenu() {
const { state } = this.state;
const rect = position();
const isOpen = !(state.isBlurred || state.isCollapsed);
2016-08-02 23:25:45 -03:00
if (isOpen) {
this.stylesMenuPosition = {
top: rect.top + window.scrollY,
left: rect.left + window.scrollX,
width: rect.width,
height: rect.height
};
2016-08-02 23:25:45 -03:00
}
return (
<StylesMenu
isOpen={isOpen}
position={this.stylesMenuPosition}
marks={this.state.state.marks}
blocks={this.state.state.blocks}
onClickMark={this.handleMarkStyleClick}
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>
{this.renderStylesMenu()}
{this.renderAddBlock()}
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-08-01 16:41:55 -03:00
export default MarkdownControl;
MarkdownControl.propTypes = {
onChange: PropTypes.func.isRequired,
value: PropTypes.node,
};