diff -r 885a20cde527 -r 5c91bfa8fcde client/src/components/SlateEditor.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/SlateEditor.js Tue May 23 16:18:34 2017 +0200 @@ -0,0 +1,325 @@ +import { Editor, Raw, Plain } from 'slate' +import React from 'react' +import initialState from './state.json' + +/** + * Define the default node type. + */ + +const DEFAULT_NODE = 'paragraph' + +/** + * Define a schema. + * + * @type {Object} + */ + +const schema = { + nodes: { + 'block-quote': props =>
{props.children}
, + 'bulleted-list': props => , + 'heading-one': props =>

{props.children}

, + 'heading-two': props =>

{props.children}

, + 'list-item': props =>
  • {props.children}
  • , + 'numbered-list': props =>
      {props.children}
    , + }, + marks: { + bold: { + fontWeight: 'bold' + }, + code: { + fontFamily: 'monospace', + backgroundColor: '#eee', + padding: '3px', + borderRadius: '4px' + }, + italic: { + fontStyle: 'italic' + }, + underlined: { + textDecoration: 'underline' + } + } +} + +/** + * The rich text example. + * + * @type {Component} + */ + +class RichText extends React.Component { + + /** + * Deserialize the initial editor state. + * + * @type {Object} + */ + + state = { + state: Raw.deserialize(initialState, { terse: true }) + }; + + /** + * Check if the current selection has a mark with `type` in it. + * + * @param {String} type + * @return {Boolean} + */ + + hasMark = (type) => { + const { state } = this.state + return state.marks.some(mark => mark.type === type) + } + + /** + * Check if the any of the currently selected blocks are of `type`. + * + * @param {String} type + * @return {Boolean} + */ + + hasBlock = (type) => { + const { state } = this.state + return state.blocks.some(node => node.type === type) + } + + /** + * On change, save the new state. + * + * @param {State} state + */ + + onChange = (state) => { + this.setState({ state }) + + } + + asPlain = () => { + return Plain.serialize(this.state.state); + } + + clear = () => { + const state = Plain.deserialize(''); + this.setState({ stateĀ }); + } + + /** + * On key down, if it's a formatting command toggle a mark. + * + * @param {Event} e + * @param {Object} data + * @param {State} state + * @return {State} + */ + + onKeyDown = (e, data, state) => { + if (!data.isMod) return + let mark + + switch (data.key) { + case 'b': + mark = 'bold' + break + case 'i': + mark = 'italic' + break + case 'u': + mark = 'underlined' + break + case '`': + mark = 'code' + break + default: + return + } + + state = state + .transform() + .toggleMark(mark) + .apply() + + e.preventDefault() + return state + } + + /** + * When a mark button is clicked, toggle the current mark. + * + * @param {Event} e + * @param {String} type + */ + + onClickMark = (e, type) => { + e.preventDefault() + let { state } = this.state + + state = state + .transform() + .toggleMark(type) + .apply() + + this.setState({ state }) + } + + /** + * When a block button is clicked, toggle the block type. + * + * @param {Event} e + * @param {String} type + */ + + onClickBlock = (e, type) => { + e.preventDefault() + let { state } = this.state + const 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 + .setBlock(isActive ? DEFAULT_NODE : type) + .unwrapBlock('bulleted-list') + .unwrapBlock('numbered-list') + } + + else { + transform + .setBlock(isActive ? DEFAULT_NODE : type) + } + } + + // 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.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) + } + } + + state = transform.apply() + this.setState({ state }) + } + + /** + * Render. + * + * @return {Element} + */ + + render = () => { + return ( +
    + {this.renderToolbar()} + {this.renderEditor()} +
    + ) + } + + /** + * Render the toolbar. + * + * @return {Element} + */ + + renderToolbar = () => { + return ( +
    + {this.renderMarkButton('bold', 'format_bold')} + {this.renderMarkButton('italic', 'format_italic')} + {this.renderMarkButton('underlined', 'format_underlined')} + {this.renderMarkButton('code', 'code')} + {this.renderBlockButton('heading-one', 'looks_one')} + {this.renderBlockButton('heading-two', 'looks_two')} + {this.renderBlockButton('block-quote', 'format_quote')} + {this.renderBlockButton('numbered-list', 'format_list_numbered')} + {this.renderBlockButton('bulleted-list', 'format_list_bulleted')} +
    + ) + } + + /** + * Render a mark-toggling toolbar button. + * + * @param {String} type + * @param {String} icon + * @return {Element} + */ + + renderMarkButton = (type, icon) => { + const isActive = this.hasMark(type) + const onMouseDown = e => this.onClickMark(e, type) + + return ( + + {icon} + + ) + } + + /** + * Render a block-toggling toolbar button. + * + * @param {String} type + * @param {String} icon + * @return {Element} + */ + + renderBlockButton = (type, icon) => { + const isActive = this.hasBlock(type) + const onMouseDown = e => this.onClickBlock(e, type) + + return ( + + {icon} + + ) + } + + /** + * Render the Slate editor. + * + * @return {Element} + */ + + renderEditor = () => { + return ( +
    + +
    + ) + } + +} + +/** + * Export. + */ + +export default RichText \ No newline at end of file