diff --git a/package.json b/package.json index b6f2bec3..cd998587 100644 --- a/package.json +++ b/package.json @@ -69,14 +69,13 @@ "bricks.js": "^1.7.0", "commonmark": "^0.24.0", "commonmark-react-renderer": "^4.1.2", - "draft-js": "^0.7.0", - "draft-js-export-markdown": "^0.2.0", - "draft-js-import-markdown": "^0.1.6", "fuzzy": "^0.1.1", "js-base64": "^2.1.9", "json-loader": "^0.5.4", "localforage": "^1.4.2", "lodash": "^4.13.1", - "pluralize": "^3.0.0" + "pluralize": "^3.0.0", + "slate": "^0.10.1", + "slate-markdown-serializer": "^0.1.2" } } diff --git a/src/components/Widgets/MarkdownControl.js b/src/components/Widgets/MarkdownControl.js index 20210b9f..59c911d4 100644 --- a/src/components/Widgets/MarkdownControl.js +++ b/src/components/Widgets/MarkdownControl.js @@ -1,44 +1,234 @@ import React, { PropTypes } from 'react'; -import { Editor, EditorState, RichUtils } from 'draft-js'; -import { stateToMarkdown } from 'draft-js-export-markdown'; -import { stateFromMarkdown } from 'draft-js-import-markdown'; +import { Editor } from 'slate'; +import Markdown from 'slate-markdown-serializer'; +const markdown = new Markdown(); -export default class MarkdownControl extends React.Component { + +/* + * Slate Render Configuration + */ + +// Define the default node type. +const DEFAULT_NODE = 'paragraph'; + +// Local node renderers. +const NODES = { + 'block-quote': (props) =>
{props.children}, + 'bulleted-list': props =>
{props.children}
, + 'link': (props) => { + const { data } = props.node; + const href = data.get('href'); + return {props.children}; + }, + 'image': (props) => { + const { node, state } = props; + const src = node.data.get('src'); + return ( + + ); + } +}; + +// Local mark renderers. +const MARKS = { + bold: { + fontWeight: 'bold' + }, + italic: { + fontStyle: 'italic' + }, + code: { + fontFamily: 'monospace', + backgroundColor: '#eee', + padding: '3px', + borderRadius: '4px' + } +}; + +class MarkdownControl extends React.Component { constructor(props) { super(props); this.state = { - editorState: EditorState.createWithContent(stateFromMarkdown(props.value || '')) + state: markdown.deserialize(props.value || '') }; + + this.hasMark = this.hasMark.bind(this); + this.hasBlock = this.hasBlock.bind(this); this.handleChange = this.handleChange.bind(this); - this.handleKeyCommand = this.handleKeyCommand.bind(this); + this.handleDocumentChange = this.handleDocumentChange.bind(this); + this.onClickMark = this.onClickMark.bind(this); + this.onClickBlock = this.onClickBlock.bind(this); + this.renderToolbar = this.renderToolbar.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); } - handleChange(editorState) { - const content = editorState.getCurrentContent(); - this.setState({ editorState }); - this.props.onChange(stateToMarkdown(content)); + /* + * 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); } - handleKeyCommand(command) { - const newState = RichUtils.handleKeyCommand(this.state.editorState, command); - if (newState) { - this.handleChange(newState); - return true; + /* + * 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) { + this.setState({ state }); + } + + handleDocumentChange(document, state) { + this.props.onChange(markdown.serialize(state)); + } + + + /* + * Toggle marks / blocks when button is clicked + */ + onClickMark(e, type) { + e.preventDefault(); + let { state } = this.state; + + state = state + .transform() + .toggleMark(type) + .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); + } } - return false; + + // 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 }); + } + + renderToolbar() { + return ( +