Correct the Note editor.
Split the source file in sub components.
Correct a timing problem on the editor checkbox.
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 (
<div className="menu toolbar-menu d-flex sticky-top bg-secondary">
<MarkButton icon='format_bold' isActive={this.hasMark('bold')} onMouseDown={(e) => this.onClickMark(e, 'bold')} />
<MarkButton icon='format_italic' isActive={this.hasMark('italic')} onMouseDown={(e) => this.onClickMark(e, 'italic')} />
<MarkButton icon='format_underlined' isActive={this.hasMark('underlined')} onMouseDown={(e) => this.onClickMark(e, 'underlined')} />
<CategoryButton
isActive={this.hasMark('category')}
onClickCategoryButton={this.onClickCategoryButton}
onCategoryClick={this.onCategoryClick}
annotationCategories={this.props.annotationCategories}
/>
<BlockButton icon='format_list_numbered' isActive={this.isBlockActive('numbered-list')} onMouseDown={e => this.onClickBlock(e, 'numbered-list')} />
<BlockButton icon='format_list_bulleted' isActive={this.isBlockActive('bulleted-list')} onMouseDown={e => this.onClickBlock(e, 'bulleted-list')} />
<ToolbarButtons
hasNote={!!this.props.note}
isButtonDisabled={this.props.isButtonDisabled}
submitNote={this.props.submitNote}
/>
</div>
)
}
}