215 lines
7.3 KiB
JavaScript
Raw Normal View History

import React, { Component, PropTypes } from 'react';
2017-07-27 18:03:13 -04:00
import { get, isEmpty } from 'lodash';
import { Editor as Slate, Raw, Block, Text } from 'slate';
import { slateToRemark, remarkToSlate, htmlToSlate } from '../../unified';
2017-06-23 14:42:40 -04:00
import registry from '../../../../../lib/registry';
2017-05-22 14:04:24 -04:00
import Toolbar from '../Toolbar/Toolbar';
2017-06-23 14:42:40 -04:00
import { Sticky } from '../../../../UI/Sticky/Sticky';
2017-07-27 18:03:13 -04:00
import { MARK_COMPONENTS, NODE_COMPONENTS } from './components';
import RULES from './rules';
import plugins, { EditListConfigured } from './plugins';
import onKeyDown from './keys';
2017-05-22 14:04:24 -04:00
import styles from './index.css';
export default class Editor extends Component {
constructor(props) {
super(props);
2017-07-27 18:03:13 -04:00
const emptyBlock = Block.create({ kind: 'block', type: 'paragraph'});
const emptyRaw = { nodes: [emptyBlock] };
const mdast = this.props.value && remarkToSlate(this.props.value);
const mdastHasNodes = !isEmpty(get(mdast, 'nodes'))
const editorState = Raw.deserialize(mdastHasNodes ? mdast : emptyRaw, { terse: true });
2016-11-01 23:31:20 -07:00
this.state = {
editorState,
schema: {
nodes: NODE_COMPONENTS,
marks: MARK_COMPONENTS,
2017-07-27 18:03:13 -04:00
rules: RULES,
},
shortcodePlugins: registry.getEditorComponents(),
2016-11-01 23:31:20 -07:00
};
}
2017-07-27 08:46:53 -04:00
shouldComponentUpdate(nextProps, nextState) {
2017-07-27 18:03:13 -04:00
return !this.state.editorState.equals(nextState.editorState);
2017-07-27 08:46:53 -04:00
}
2017-06-23 17:04:17 -04:00
handlePaste = (e, data, state) => {
if (data.type !== 'html' || data.isShift) {
return;
}
2017-07-26 20:55:30 -04:00
const ast = htmlToSlate(data.html);
2017-07-27 18:03:13 -04:00
const { document: doc } = Raw.deserialize(ast, { terse: true });
2017-07-26 20:55:30 -04:00
return state.transform().insertFragment(doc).apply();
2017-06-23 17:04:17 -04:00
}
handleDocumentChange = (doc, editorState) => {
2017-07-27 18:03:13 -04:00
const raw = Raw.serialize(editorState, { terse: true });
const plugins = this.state.shortcodePlugins;
const mdast = slateToRemark(raw, plugins);
this.props.onChange(mdast);
};
2017-07-23 19:38:05 +02:00
hasMark = type => this.state.editorState.marks.some(mark => mark.type === type);
hasBlock = type => this.state.editorState.blocks.some(node => node.type === type);
2017-07-23 19:38:05 +02:00
handleMarkClick = (event, type) => {
event.preventDefault();
2017-06-21 17:08:53 -04:00
const resolvedState = this.state.editorState.transform().focus().toggleMark(type).apply();
this.ref.onChange(resolvedState);
this.setState({ editorState: resolvedState });
2016-10-03 14:25:27 +02:00
};
handleBlockClick = (event, type) => {
event.preventDefault();
let { editorState } = this.state;
2017-07-26 21:44:39 -04:00
const { document: doc, selection } = editorState;
const transform = editorState.transform();
// Handle everything except list buttons.
if (!['bulleted-list', 'numbered-list'].includes(type)) {
const isActive = this.hasBlock(type);
2017-07-27 18:03:13 -04:00
const transformed = transform.setBlock(isActive ? 'paragraph' : type);
}
// Handle the extra wrapping required for list buttons.
else {
2017-07-26 21:44:39 -04:00
const isSameListType = editorState.blocks.some(block => {
return !!doc.getClosest(block.key, parent => parent.type === type);
});
2017-07-27 18:03:13 -04:00
const isInList = EditListConfigured.utils.isSelectionInList(editorState);
2017-07-26 21:44:39 -04:00
if (isInList && isSameListType) {
2017-07-27 18:03:13 -04:00
EditListConfigured.transforms.unwrapList(transform, type);
2017-07-26 21:44:39 -04:00
} else if (isInList) {
const currentListType = type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list';
2017-07-27 18:03:13 -04:00
EditListConfigured.transforms.unwrapList(transform, currentListType);
EditListConfigured.transforms.wrapInList(transform, type);
} else {
2017-07-27 18:03:13 -04:00
EditListConfigured.transforms.wrapInList(transform, type);
}
}
const resolvedState = transform.focus().apply();
this.ref.onChange(resolvedState);
this.setState({ editorState: resolvedState });
2016-10-03 14:25:27 +02:00
};
2017-07-27 11:02:17 -04:00
hasLinks = () => {
return this.state.editorState.inlines.some(inline => inline.type === 'link');
};
2016-11-01 17:58:19 -07:00
handleLink = () => {
2017-07-27 11:02:17 -04:00
let { editorState } = this.state;
// If the current selection contains links, clicking the "link" button
// should simply unlink them.
if (this.hasLinks()) {
editorState = editorState.transform().unwrapInline('link').apply();
}
else {
const url = window.prompt('Enter the URL of the link');
// If nothing is entered in the URL prompt, do nothing.
if (!url) return;
let transform = editorState.transform();
// If no text is selected, use the entered URL as text.
if (editorState.isCollapsed) {
transform = transform
.insertText(url)
.extend(0 - url.length);
}
editorState = transform
.wrapInline({ type: 'link', data: { url } })
.collapseToEnd()
.apply();
2016-11-01 17:58:19 -07:00
}
2017-07-27 11:02:17 -04:00
this.ref.onChange(editorState);
this.setState({ editorState });
2016-11-01 17:58:19 -07:00
};
handlePluginSubmit = (plugin, shortcodeData) => {
2017-07-13 21:33:50 -04:00
const { editorState } = this.state;
const data = {
shortcode: plugin.id,
shortcodeData,
};
2017-07-27 18:03:13 -04:00
const nodes = [Text.createFromString('')];
const block = { kind: 'block', type: 'shortcode', data, isVoid: true, nodes };
const resolvedState = editorState.transform().insertBlock(block).focus().apply();
2017-07-13 21:33:50 -04:00
this.ref.onChange(resolvedState);
this.setState({ editorState: resolvedState });
};
handleToggle = () => {
2017-05-22 14:04:24 -04:00
this.props.onMode('raw');
2016-10-03 14:25:27 +02:00
};
2017-07-27 11:02:17 -04:00
getButtonProps = (type, opts = {}) => {
const { isBlock } = opts;
const handler = opts.handler || (isBlock ? this.handleBlockClick: this.handleMarkClick);
const isActive = opts.isActive || (isBlock ? this.hasBlock : this.hasMark);
return { onAction: e => handler(e, type), active: isActive(type) };
};
render() {
2017-01-10 22:23:22 -02:00
const { onAddAsset, onRemoveAsset, getAsset } = this.props;
2017-07-13 21:33:50 -04:00
return (
2017-07-27 18:03:13 -04:00
<div className={styles.wrapper}>
2017-07-13 21:33:50 -04:00
<Sticky
className={styles.editorControlBar}
classNameActive={styles.editorControlBarSticky}
fillContainerWidth
>
<Toolbar
buttons={{
bold: this.getButtonProps('bold'),
italic: this.getButtonProps('italic'),
code: this.getButtonProps('code'),
2017-07-27 11:02:17 -04:00
link: this.getButtonProps('link', { handler: this.handleLink, isActive: this.hasLinks }),
h1: this.getButtonProps('heading-one', { isBlock: true }),
h2: this.getButtonProps('heading-two', { isBlock: true }),
list: this.getButtonProps('bulleted-list', { isBlock: true }),
listNumbered: this.getButtonProps('numbered-list', { isBlock: true }),
codeBlock: this.getButtonProps('code', { isBlock: true }),
2017-07-13 21:33:50 -04:00
}}
onToggleMode={this.handleToggle}
plugins={this.state.shortcodePlugins}
2017-07-13 21:33:50 -04:00
onSubmit={this.handlePluginSubmit}
onAddAsset={onAddAsset}
onRemoveAsset={onRemoveAsset}
getAsset={getAsset}
/>
</Sticky>
2017-07-27 18:03:13 -04:00
<Slate
className={styles.editor}
2017-07-13 21:33:50 -04:00
state={this.state.editorState}
schema={this.state.schema}
2017-07-27 18:03:13 -04:00
plugins={plugins}
2017-07-13 21:33:50 -04:00
onChange={editorState => this.setState({ editorState })}
onDocumentChange={this.handleDocumentChange}
2017-07-27 18:03:13 -04:00
onKeyDown={onKeyDown}
2017-07-13 21:33:50 -04:00
onPaste={this.handlePaste}
ref={ref => this.ref = ref}
spellCheck
/>
2017-07-13 21:33:50 -04:00
</div>
);
}
}
Editor.propTypes = {
2017-01-10 22:23:22 -02:00
onAddAsset: PropTypes.func.isRequired,
onRemoveAsset: PropTypes.func.isRequired,
getAsset: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onMode: PropTypes.func.isRequired,
value: PropTypes.object,
};