import React, { Component, PropTypes } from 'react'; import { Map, List } from 'immutable'; import { Editor as SlateEditor, Html as SlateHtml, Raw as SlateRaw} from 'slate'; import unified from 'unified'; import markdownToRemark from 'remark-parse'; import remarkToRehype from 'remark-rehype'; import rehypeToHtml from 'rehype-stringify'; import remarkToMarkdown from 'remark-stringify'; import htmlToRehype from 'rehype-parse'; import rehypeToRemark from 'rehype-remark'; import registry from '../../../../lib/registry'; import { createAssetProxy } from '../../../../valueObjects/AssetProxy'; import { buildKeymap } from './keymap'; import createMarkdownParser from './parser'; import Toolbar from '../Toolbar/Toolbar'; import { Sticky } from '../../../UI/Sticky/Sticky'; import styles from './index.css'; function processUrl(url) { if (url.match(/^(https?:\/\/|mailto:|\/)/)) { return url; } if (url.match(/^[^/]+\.[^/]+/)) { return `https://${ url }`; } return `/${ url }`; } const DEFAULT_NODE = 'paragraph'; function schemaWithPlugins(schema, plugins) { let nodeSpec = schema.nodeSpec; plugins.forEach((plugin) => { const attrs = {}; plugin.get('fields').forEach((field) => { attrs[field.get('name')] = { default: null }; }); nodeSpec = nodeSpec.addToEnd(`plugin_${ plugin.get('id') }`, { attrs, group: 'block', parseDOM: [{ tag: 'div[data-plugin]', getAttrs(dom) { return JSON.parse(dom.getAttribute('data-plugin')); }, }], toDOM(node) { return ['div', { 'data-plugin': JSON.stringify(node.attrs) }, plugin.get('label')]; }, }); }); return new Schema({ nodes: nodeSpec, marks: schema.markSpec, }); } function createSerializer(schema, plugins) { const serializer = Object.create(defaultMarkdownSerializer); plugins.forEach((plugin) => { serializer.nodes[`plugin_${ plugin.get('id') }`] = (state, node) => { const toBlock = plugin.get('toBlock'); state.write(`${ toBlock.call(plugin, node.attrs) }\n\n`); }; }); return serializer; } const BLOCK_TAGS = { p: 'paragraph', li: 'list-item', ul: 'bulleted-list', ol: 'numbered-list', blockquote: 'quote', pre: 'code', h1: 'heading-one', h2: 'heading-two', h3: 'heading-three', h4: 'heading-four', h5: 'heading-five', h6: 'heading-six' } const MARK_TAGS = { strong: 'bold', em: 'italic', u: 'underline', s: 'strikethrough', code: 'code' } const NODE_COMPONENTS = { 'quote': props =>
{props.children}
, 'bulleted-list': props => , 'heading-one': props =>

{props.children}

, 'heading-two': props =>

{props.children}

, 'heading-three': props =>

{props.children}

, 'heading-four': props =>

{props.children}

, 'heading-five': props =>
{props.children}
, 'heading-six': props =>
{props.children}
, 'list-item': props =>
  • {props.children}
  • , 'numbered-list': props =>
      {props.children}
    , 'code': props =>
    {props.children}
    , 'link': props => {props.children}, 'paragraph': props =>

    {props.children}

    , }; const MARK_COMPONENTS = { bold: props => {props.children}, code: props => {props.children}, italic: props => {props.children}, underlined: props => {props.children}, }; const RULES = [ { deserialize(el, next) { const block = BLOCK_TAGS[el.tagName] if (!block) return return { kind: 'block', type: block, nodes: next(el.children) } }, serialize(entity, children) { const component = NODE_COMPONENTS[entity.type] if (!component) { return; } return component({ children }); } }, { deserialize(el, next) { const mark = MARK_TAGS[el.tagName] if (!mark) return return { kind: 'mark', type: mark, nodes: next(el.children) } }, serialize(entity, children) { const component = MARK_COMPONENTS[entity.type] if (!component) { return; } return component({ children }); } }, { // Special case for code blocks, which need to grab the nested children. deserialize(el, next) { if (el.tagName != 'pre') return const code = el.children[0] const children = code && code.tagName == 'code' ? code.children : el.children return { kind: 'block', type: 'code', nodes: next(children) } }, }, /* { // Special case for links, to grab their href. deserialize(el, next) { if (el.tagName != 'a') return return { kind: 'inline', type: 'link', nodes: next(el.children), data: { href: el.attribs.href } } }, }, */ ] const serializer = new SlateHtml({ rules: RULES }); export default class Editor extends Component { constructor(props) { super(props); const plugins = registry.getEditorComponents(); const html = unified() .use(markdownToRemark) .use(remarkToRehype) .use(rehypeToHtml) .processSync(this.props.value || '') .contents; this.state = { editorState: serializer.deserialize(html || '

    '), schema: { nodes: NODE_COMPONENTS, marks: MARK_COMPONENTS, }, plugins, }; } handleDocumentChange = (doc, editorState) => { const html = serializer.serialize(editorState); const markdown = unified() .use(htmlToRehype) .use(rehypeToRemark) .use(remarkToMarkdown) .processSync(html) .contents; this.props.onChange(markdown); }; hasMark = type => this.state.editorState.marks.some(mark => mark.type === type); hasBlock = type => this.state.editorState.blocks.some(node => node.type === type); handleKeyDown = (e, data, state) => { if (!data.isMod) { return; } const marks = { b: 'bold', i: 'italic', u: 'underlined', '`': 'code', }; const mark = marks[data.key]; if (mark) { state = state.transform().toggleMark(mark).apply(); } return; }; handleMarkClick = (event, type) => { event.preventDefault(); const resolvedState = this.state.editorState.transform().toggleMark(type).apply(); this.ref.onChange(resolvedState); this.setState({ editorState: resolvedState }); }; handleBlockClick = (event, type) => { event.preventDefault(); let { editorState } = this.state; const transform = editorState.transform(); const doc = editorState.document; const isList = this.hasBlock('list-item') // Handle everything except list buttons. if (!['bulleted-list', 'numbered-list'].includes(type)) { const isActive = this.hasBlock(type); const transformed = transform.setBlock(isActive ? DEFAULT_NODE : type); if (isList) { transformed .unwrapBlock('bulleted-list') .unwrapBlock('numbered-list'); } } // Handle the extra wrapping required for list buttons. else { const isType = editorState.blocks.some(block => { return !!doc.getClosest(block.key, parent => parent.type === type); }); if (isList && isType) { transform .setBlock(DEFAULT_NODE) .unwrapBlock('bulleted-list') .unwrapBlock('numbered-list'); } else if (isList) { transform .unwrapBlock(type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list') .wrapBlock(type); } else { transform .setBlock('list-item') .wrapBlock(type); } } const resolvedState = transform.focus().apply(); this.ref.onChange(resolvedState); this.setState({ editorState: resolvedState }); }; handleLink = () => { let url = null; if (!markActive(this.view.state, this.state.schema.marks.link)) { url = prompt('Link URL:'); // eslint-disable-line no-alert } const command = toggleMark(this.state.schema.marks.link, { href: url ? processUrl(url) : null }); command(this.view.state, this.handleAction); }; handlePluginSubmit = (plugin, data) => { const { schema } = this.state; const nodeType = schema.nodes[`plugin_${ plugin.get('id') }`]; //this.view.props.onAction(this.view.state.tr.replaceSelectionWith(nodeType.create(data.toJS())).action()); }; handleDragEnter = (e) => { e.preventDefault(); this.setState({ dragging: true }); }; handleDragLeave = (e) => { e.preventDefault(); this.setState({ dragging: false }); }; handleDragOver = (e) => { e.preventDefault(); }; handleDrop = (e) => { e.preventDefault(); this.setState({ dragging: false }); const { schema } = this.state; const nodes = []; if (e.dataTransfer.files && e.dataTransfer.files.length) { Array.from(e.dataTransfer.files).forEach((file) => { createAssetProxy(file.name, file) .then((assetProxy) => { this.props.onAddAsset(assetProxy); if (file.type.split('/')[0] === 'image') { nodes.push( schema.nodes.image.create({ src: assetProxy.public_path, alt: file.name }) ); } else { nodes.push( schema.marks.link.create({ href: assetProxy.public_path, title: file.name }) ); } }); }); } else { nodes.push(schema.nodes.paragraph.create({}, e.dataTransfer.getData('text/plain'))); } nodes.forEach((node) => { //this.view.props.onAction(this.view.state.tr.replaceSelectionWith(node).action()); }); }; handleToggle = () => { this.props.onMode('raw'); }; getButtonProps = (type, isBlock) => { const handler = isBlock ? this.handleBlockClick: this.handleMarkClick; const isActive = isBlock ? this.hasBlock : this.hasMark; return { onAction: e => handler(e, type), active: isActive(type) }; }; render() { const { onAddAsset, onRemoveAsset, getAsset } = this.props; const { plugins, selectionPosition, dragging } = this.state; const classNames = [styles.editor]; if (dragging) { classNames.push(styles.dragging); } return (
    this.setState({ editorState })} onDocumentChange={this.handleDocumentChange} onKeyDown={this.onKeyDown} ref={ref => this.ref = ref} spellCheck />
    ); } } Editor.propTypes = { onAddAsset: PropTypes.func.isRequired, onRemoveAsset: PropTypes.func.isRequired, getAsset: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired, onMode: PropTypes.func.isRequired, value: PropTypes.node, };