2016-08-11 11:27:09 -03:00
|
|
|
import React, { PropTypes } from 'react';
|
|
|
|
import _ from 'lodash';
|
|
|
|
import { Editor, Raw } from 'slate';
|
2016-09-29 18:53:47 +02:00
|
|
|
import PluginDropImages from 'slate-drop-or-paste-images';
|
2016-08-11 11:27:09 -03:00
|
|
|
import MarkupIt, { SlateUtils } from 'markup-it';
|
2016-09-29 18:53:47 +02:00
|
|
|
import MediaProxy from '../../../../valueObjects/MediaProxy';
|
|
|
|
import { emptyParagraphBlock, mediaproxyBlock } from '../constants';
|
2016-08-17 09:52:06 -03:00
|
|
|
import { DEFAULT_NODE, SCHEMA } from './schema';
|
2016-08-18 10:51:38 -03:00
|
|
|
import { getNodes, getSyntaxes, getPlugins } from '../../richText';
|
2016-08-11 11:27:09 -03:00
|
|
|
import StylesMenu from './StylesMenu';
|
|
|
|
import BlockTypesMenu from './BlockTypesMenu';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Slate Render Configuration
|
|
|
|
*/
|
2016-10-03 16:57:48 +02:00
|
|
|
export default class VisualEditor extends React.Component {
|
|
|
|
|
|
|
|
static propTypes = {
|
|
|
|
onChange: PropTypes.func.isRequired,
|
|
|
|
onAddMedia: PropTypes.func.isRequired,
|
|
|
|
getMedia: PropTypes.func.isRequired,
|
|
|
|
value: PropTypes.string,
|
|
|
|
};
|
|
|
|
|
2016-08-11 11:27:09 -03:00
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
|
|
|
|
2016-08-18 15:13:22 -03:00
|
|
|
const MarkdownSyntax = getSyntaxes(this.getMedia).markdown;
|
|
|
|
this.markdown = new MarkupIt(MarkdownSyntax);
|
|
|
|
|
2016-08-18 10:51:38 -03:00
|
|
|
SCHEMA.nodes = _.merge(SCHEMA.nodes, getNodes());
|
2016-08-11 11:27:09 -03:00
|
|
|
|
|
|
|
this.blockEdit = false;
|
|
|
|
|
|
|
|
let rawJson;
|
|
|
|
if (props.value !== undefined) {
|
|
|
|
const content = this.markdown.toContent(props.value);
|
2016-08-23 15:25:44 -03:00
|
|
|
rawJson = SlateUtils.encode(content, null, ['mediaproxy'].concat(getPlugins().map(plugin => plugin.id)));
|
2016-08-11 11:27:09 -03:00
|
|
|
} else {
|
|
|
|
rawJson = emptyParagraphBlock;
|
|
|
|
}
|
|
|
|
this.state = {
|
|
|
|
state: Raw.deserialize(rawJson, { terse: true })
|
|
|
|
};
|
|
|
|
|
2016-09-29 18:53:47 +02:00
|
|
|
this.plugins = [
|
|
|
|
PluginDropImages({
|
|
|
|
applyTransform: (transform, file) => {
|
|
|
|
const mediaProxy = new MediaProxy(file.name, file);
|
|
|
|
props.onAddMedia(mediaProxy);
|
|
|
|
return transform
|
|
|
|
.insertBlock(mediaproxyBlock(mediaProxy));
|
|
|
|
}
|
|
|
|
})
|
|
|
|
];
|
2016-08-11 11:27:09 -03:00
|
|
|
}
|
|
|
|
|
2016-10-03 14:25:27 +02:00
|
|
|
getMedia = src => {
|
2016-08-11 11:27:09 -03:00
|
|
|
return this.props.getMedia(src);
|
2016-10-03 14:25:27 +02:00
|
|
|
};
|
2016-08-11 11:27:09 -03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Slate keeps track of selections, scroll position etc.
|
|
|
|
* So, onChange gets dispatched on every interaction (click, arrows, everything...)
|
2016-10-03 16:57:48 +02:00
|
|
|
* It also have an onDocumentChange, that get's dispatched only when the actual
|
2016-08-11 11:27:09 -03:00
|
|
|
* content changes
|
|
|
|
*/
|
2016-10-03 14:25:27 +02:00
|
|
|
handleChange = state => {
|
2016-08-11 11:27:09 -03:00
|
|
|
if (this.blockEdit) {
|
|
|
|
this.blockEdit = false;
|
|
|
|
} else {
|
2016-09-29 18:53:47 +02:00
|
|
|
this.setState({ state });
|
2016-08-11 11:27:09 -03:00
|
|
|
}
|
2016-10-03 14:25:27 +02:00
|
|
|
};
|
2016-08-11 11:27:09 -03:00
|
|
|
|
2016-10-03 14:25:27 +02:00
|
|
|
handleDocumentChange = (document, state) => {
|
2016-08-11 11:27:09 -03:00
|
|
|
const rawJson = Raw.serialize(state, { terse: true });
|
|
|
|
const content = SlateUtils.decode(rawJson);
|
|
|
|
this.props.onChange(this.markdown.toText(content));
|
2016-10-03 14:25:27 +02:00
|
|
|
};
|
2016-08-11 11:27:09 -03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Toggle marks / blocks when button is clicked
|
|
|
|
*/
|
2016-10-03 14:25:27 +02:00
|
|
|
handleMarkStyleClick = type => {
|
2016-08-11 11:27:09 -03:00
|
|
|
let { state } = this.state;
|
|
|
|
|
|
|
|
state = state
|
|
|
|
.transform()
|
|
|
|
.toggleMark(type)
|
|
|
|
.apply();
|
|
|
|
|
|
|
|
this.setState({ state });
|
2016-10-03 14:25:27 +02:00
|
|
|
};
|
2016-08-11 11:27:09 -03:00
|
|
|
|
2016-10-03 14:25:27 +02:00
|
|
|
handleBlockStyleClick = (type, isActive, isList) => {
|
2016-08-11 11:27:09 -03:00
|
|
|
let { state } = this.state;
|
|
|
|
let transform = state.transform();
|
|
|
|
const { document } = state;
|
|
|
|
|
|
|
|
// Handle everything but list buttons.
|
|
|
|
if (type != 'unordered_list' && type != 'ordered_list') {
|
|
|
|
|
|
|
|
if (isList) {
|
|
|
|
transform = transform
|
|
|
|
.setBlock(isActive ? DEFAULT_NODE : type)
|
|
|
|
.unwrapBlock('unordered_list')
|
|
|
|
.unwrapBlock('ordered_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('unordered_list');
|
|
|
|
} else if (isList) {
|
|
|
|
transform = transform
|
|
|
|
.unwrapBlock(type == 'unordered_list')
|
|
|
|
.wrapBlock(type);
|
|
|
|
} else {
|
|
|
|
transform = transform
|
|
|
|
.setBlock('list_item')
|
|
|
|
.wrapBlock(type);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
state = transform.apply();
|
|
|
|
this.setState({ state });
|
2016-10-03 14:25:27 +02:00
|
|
|
};
|
2016-08-11 11:27:09 -03:00
|
|
|
|
|
|
|
/**
|
2016-09-28 14:05:51 +02: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
|
|
|
|
*/
|
2016-08-11 11:27:09 -03:00
|
|
|
|
2016-10-03 14:25:27 +02:00
|
|
|
handleInlineClick = (type, isActive) => {
|
2016-08-11 11:27:09 -03:00
|
|
|
let { state } = this.state;
|
|
|
|
|
|
|
|
if (type === 'link') {
|
|
|
|
if (!state.isExpanded) return;
|
|
|
|
|
|
|
|
if (isActive) {
|
|
|
|
state = state
|
|
|
|
.transform()
|
|
|
|
.unwrapInline('link')
|
|
|
|
.apply();
|
|
|
|
}
|
|
|
|
|
|
|
|
else {
|
2016-09-29 18:53:47 +02:00
|
|
|
const href = window.prompt('Enter the URL of the link:', 'http://www.'); // eslint-disable-line
|
2016-08-11 11:27:09 -03:00
|
|
|
state = state
|
|
|
|
.transform()
|
|
|
|
.wrapInline({
|
|
|
|
type: 'link',
|
|
|
|
data: { href }
|
|
|
|
})
|
|
|
|
.collapseToEnd()
|
|
|
|
.apply();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.setState({ state });
|
2016-10-03 14:25:27 +02:00
|
|
|
};
|
2016-08-11 11:27:09 -03:00
|
|
|
|
2016-10-03 14:25:27 +02:00
|
|
|
handleBlockTypeClick = type => {
|
2016-08-11 11:27:09 -03:00
|
|
|
let { state } = this.state;
|
|
|
|
|
|
|
|
state = state
|
2016-09-28 14:05:51 +02:00
|
|
|
.transform()
|
|
|
|
.insertBlock({
|
|
|
|
type: type,
|
|
|
|
isVoid: true
|
|
|
|
})
|
|
|
|
.apply();
|
2016-08-11 11:27:09 -03:00
|
|
|
|
|
|
|
this.setState({ state }, this.focusAndAddParagraph);
|
2016-10-03 14:25:27 +02:00
|
|
|
};
|
2016-08-11 11:27:09 -03:00
|
|
|
|
2016-10-03 14:25:27 +02:00
|
|
|
handlePluginClick = (type, data) => {
|
2016-08-18 15:13:22 -03:00
|
|
|
let { state } = this.state;
|
|
|
|
|
|
|
|
state = state
|
|
|
|
.transform()
|
|
|
|
.insertInline({
|
|
|
|
type: type,
|
|
|
|
data: data,
|
|
|
|
isVoid: true
|
|
|
|
})
|
|
|
|
.collapseToEnd()
|
|
|
|
.insertBlock(DEFAULT_NODE)
|
|
|
|
.focus()
|
|
|
|
.apply();
|
|
|
|
|
|
|
|
this.setState({ state });
|
2016-10-03 14:25:27 +02:00
|
|
|
};
|
2016-08-18 15:13:22 -03:00
|
|
|
|
2016-10-03 14:25:27 +02:00
|
|
|
handleImageClick = mediaProxy => {
|
2016-08-11 11:27:09 -03:00
|
|
|
let { state } = this.state;
|
|
|
|
this.props.onAddMedia(mediaProxy);
|
|
|
|
|
|
|
|
state = state
|
|
|
|
.transform()
|
2016-09-29 18:53:47 +02:00
|
|
|
.insertBlock(mediaproxyBlock(mediaProxy))
|
2016-08-11 11:27:09 -03:00
|
|
|
.apply();
|
|
|
|
|
|
|
|
this.setState({ state });
|
2016-10-03 14:25:27 +02:00
|
|
|
};
|
2016-08-11 11:27:09 -03:00
|
|
|
|
2016-10-03 14:25:27 +02:00
|
|
|
focusAndAddParagraph = () => {
|
2016-08-11 11:27:09 -03:00
|
|
|
const { state } = this.state;
|
|
|
|
const blocks = state.document.getBlocks();
|
|
|
|
const last = blocks.last();
|
|
|
|
const normalized = state
|
|
|
|
.transform()
|
|
|
|
.focus()
|
|
|
|
.collapseToEndOf(last)
|
|
|
|
.splitBlock()
|
|
|
|
.setBlock(DEFAULT_NODE)
|
|
|
|
.apply({
|
|
|
|
snapshot: false
|
|
|
|
});
|
2016-09-28 14:05:51 +02:00
|
|
|
this.setState({ state: normalized });
|
2016-10-03 14:25:27 +02:00
|
|
|
};
|
2016-08-11 11:27:09 -03:00
|
|
|
|
2016-10-03 14:25:27 +02:00
|
|
|
handleKeyDown = evt => {
|
2016-08-11 11:27:09 -03:00
|
|
|
if (evt.shiftKey && evt.key === 'Enter') {
|
|
|
|
this.blockEdit = true;
|
|
|
|
let { state } = this.state;
|
|
|
|
state = state
|
2016-09-28 14:05:51 +02:00
|
|
|
.transform()
|
2016-09-29 18:53:47 +02:00
|
|
|
.insertText('\n')
|
2016-09-28 14:05:51 +02:00
|
|
|
.apply();
|
2016-08-11 11:27:09 -03:00
|
|
|
|
|
|
|
this.setState({ state });
|
|
|
|
}
|
2016-10-03 14:25:27 +02:00
|
|
|
};
|
2016-08-11 11:27:09 -03:00
|
|
|
|
2016-10-03 14:25:27 +02:00
|
|
|
renderBlockTypesMenu = () => {
|
2016-08-11 11:27:09 -03:00
|
|
|
const currentBlock = this.state.state.blocks.get(0);
|
|
|
|
const isOpen = (this.props.value !== undefined && currentBlock.isEmpty && currentBlock.type !== 'horizontal-rule');
|
|
|
|
|
|
|
|
return (
|
|
|
|
<BlockTypesMenu
|
2016-09-28 14:05:51 +02:00
|
|
|
isOpen={isOpen}
|
|
|
|
plugins={getPlugins()}
|
|
|
|
onClickBlock={this.handleBlockTypeClick}
|
|
|
|
onClickPlugin={this.handlePluginClick}
|
|
|
|
onClickImage={this.handleImageClick}
|
2016-08-11 11:27:09 -03:00
|
|
|
/>
|
|
|
|
);
|
2016-10-03 14:25:27 +02:00
|
|
|
};
|
2016-08-11 11:27:09 -03:00
|
|
|
|
|
|
|
renderStylesMenu() {
|
|
|
|
const { state } = this.state;
|
|
|
|
const isOpen = !(state.isBlurred || state.isCollapsed);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<StylesMenu
|
2016-09-28 14:05:51 +02:00
|
|
|
isOpen={isOpen}
|
|
|
|
marks={this.state.state.marks}
|
|
|
|
blocks={this.state.state.blocks}
|
|
|
|
inlines={this.state.state.inlines}
|
|
|
|
onClickMark={this.handleMarkStyleClick}
|
|
|
|
onClickInline={this.handleInlineClick}
|
|
|
|
onClickBlock={this.handleBlockStyleClick}
|
2016-08-11 11:27:09 -03:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
{this.renderStylesMenu()}
|
|
|
|
{this.renderBlockTypesMenu()}
|
|
|
|
<Editor
|
2016-09-28 14:05:51 +02:00
|
|
|
placeholder={'Enter some rich text...'}
|
|
|
|
state={this.state.state}
|
|
|
|
schema={SCHEMA}
|
2016-09-29 18:53:47 +02:00
|
|
|
plugins={this.plugins}
|
2016-09-28 14:05:51 +02:00
|
|
|
onChange={this.handleChange}
|
|
|
|
onKeyDown={this.handleKeyDown}
|
|
|
|
onDocumentChange={this.handleDocumentChange}
|
2016-08-11 11:27:09 -03:00
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|