diff -r 4b780ebbedc6 -r 0e6703cd0968 client/src/components/SlateEditor/Toolbar.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/SlateEditor/Toolbar.js Fri Nov 16 11:19:13 2018 +0100 @@ -0,0 +1,254 @@ +import React from 'react'; +import ToolbarButtons from './ToolbarButtons'; +import MarkButton from './MarkButton'; +import CategoryButton from './CategoryButton'; +import BlockButton from './BlockButton'; + +/** + * Define the default node type. + */ + +const DEFAULT_NODE = 'paragraph' + + +/** + * Render the toolbar. + * + * @return {Element} + */ +export default class Toolbar extends React.Component { + + /** + * Deserialize the initial editor state. + * + * @type {Object} + */ + constructor(props) { + super(props); + this.editorRef = React.createRef(); + } + + /** + * Check if the current selection has a mark with `type` in it. + * + * @param {String} type + * @return {Boolean} + */ + + hasMark = type => { + const { value } = this.props; + return value.activeMarks.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 { value } = this.props; + return value.blocks.some(node => node.type === type) + } + + + /** + * When a mark button is clicked, toggle the current mark. + * + * @param {Event} e + * @param {String} type + */ + + onClickMark = (e, type) => { + this.props.editor.toggleMark(type) + } + + isBlockActive = (type) => { + let isActive = this.hasBlock(type) + + if (['numbered-list', 'bulleted-list'].includes(type)) { + const { value } = this.props; + const firstBlock = value.blocks.first(); + if(firstBlock) { + const parent = value.document.getParent(firstBlock.key); + isActive = this.hasBlock('list-item') && parent && parent.type === type; + } + } + + return isActive; + } + + onClickCategoryButton = (openPortal, closePortal, isOpen, e) => { + e.preventDefault(); + const { value, editor } = this.props; + + // Can't use toggleMark here, because it expects the same object + // @see https://github.com/ianstormtaylor/slate/issues/873 + if (this.hasMark('category')) { + value.activeMarks.filter(mark => mark.type === 'category') + .forEach(mark => editor.removeMark(mark)); + closePortal(); + } else { + openPortal(); + } + } + + getSelectionParams = (value) => { + + const { selection } = value + const { start, end} = selection + + if (selection.isCollapsed) { + return {}; + } + + const nodes = []; + let hasStarted = false; + let hasEnded = false; + + // Keep only the relevant nodes, + // i.e. nodes which are contained within selection + value.document.nodes.forEach((node) => { + if (start.isInNode(node)) { + hasStarted = true; + } + if (hasStarted && !hasEnded) { + nodes.push(node); + } + if (end.isAtEndOfNode(node)) { + hasEnded = true; + } + }); + + // Concatenate the nodes text + const text = nodes.map((node) => { + let textStart = start.isInNode(node) ? start.offset : 0; + let textEnd = end.isInNode(node) ? end.offset : node.text.length; + return node.text.substring(textStart,textEnd); + }).join('\n'); + + return { + currentSelectionText: text, + currentSelectionStart: start.offset, + currentSelectionEnd: end.offset + }; + } + + onCategoryClick = (closePortal, category) => { + + const { value, editor } = this.props; + + const { currentSelectionText, currentSelectionStart, currentSelectionEnd } = this.getSelectionParams(value); + + if(!currentSelectionText) { + closePortal(); + return; + } + + const categoryMarks = value.activeMarks.filter(mark => mark.type === 'category') + categoryMarks.forEach(mark => this.editor.removeMark(mark)); + + editor.addMark({ + type: 'category', + data: { + text: currentSelectionText, + selection: { + start: currentSelectionStart, + end: currentSelectionEnd, + }, + color: category.color, + key: category.key, + name: category.name, + comment: category.comment + } + }) + + closePortal(); + } + + /** + * When a block button is clicked, toggle the block type. + * + * @param {Event} e + * @param {String} type + */ + + onClickBlock = (e, type) => { + e.preventDefault(); + + const { editor, value } = this.props; + + // 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) { + editor + .setBlocks(isActive ? DEFAULT_NODE : type) + .unwrapBlock('bulleted-list') + .unwrapBlock('numbered-list') + } + + else { + editor + .setBlocks(isActive ? DEFAULT_NODE : type) + } + } + + // Handle the extra wrapping required for list buttons. + else { + const isList = this.hasBlock('list-item') + const isType = value.blocks.some((block) => { + return !!document.getClosest(block.key, parent => parent.type === type) + }) + + if (isList && isType) { + editor + .setBlocks(DEFAULT_NODE) + .unwrapBlock('bulleted-list') + .unwrapBlock('numbered-list') + + } else if (isList) { + editor + .unwrapBlock(type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list') + .wrapBlock(type) + + } else { + editor + .setBlocks('list-item') + .wrapBlock(type) + + } + } + } + + + render = () => { + return ( +
+ this.onClickMark(e, 'bold')} /> + this.onClickMark(e, 'italic')} /> + this.onClickMark(e, 'underlined')} /> + + + + this.onClickBlock(e, 'numbered-list')} /> + this.onClickBlock(e, 'bulleted-list')} /> + + + +
+ ) + } +}