import { Value } from 'slate';
import Plain from 'slate-plain-serializer';
import { Editor } from 'slate-react';
import React from 'react';
import { withNamespaces } from 'react-i18next';
import { connect } from 'react-redux';
import * as R from 'ramda';
import HtmlSerializer from './HtmlSerializer';
import { now } from '../../utils';
import Toolbar from './Toolbar';
import { getAutoSubmit } from '../../selectors/authSelectors';
/**
*
* @type {Component}
*/
class SlateEditor extends React.Component {
/**
* Deserialize the initial editor state.
*
* @type {Object}
*/
constructor(props) {
super(props);
this.state = {
value: props.note ? Value.fromJSON(JSON.parse(props.note.raw)) : Plain.deserialize(''),
startedAt: null,
finishedAt: null,
enterKeyValue: 0,
};
this.editorRef = React.createRef();
}
get editor() {
if(this.editorRef) {
return this.editorRef.current;
}
return null;
}
componentDidMount = () => {
this.focus();
}
/**
* On change, save the new state.
*
* @param {Change} change
*/
onChange = ({value, operations}) => {
const oldState = R.clone(this.state);
const newState = {
value
};
(operations || []).some((op) => {
if(['insert_text', 'remove_text', 'add_mark', 'remove_mark', 'set_mark', 'insert_node', 'merge_node', 'move_node', 'remove_node', 'set_node', 'split_node'].indexOf(op.type)>=0) {
const tsnow = now();
if(this.state.startedAt == null) {
newState.startedAt = tsnow;
}
newState.finishedAt = tsnow;
return true;
}
return false;
});
this.setState(newState, () => {
if (typeof this.props.onChange === 'function') {
this.props.onChange(R.clone(this.state), oldState, {value, operations});
}
})
}
asPlain = () => {
return Plain.serialize(this.state.value);
}
asRaw = () => {
return JSON.stringify(this.state.value.toJSON());
}
asHtml = () => {
return HtmlSerializer.serialize(this.state.value);
}
asCategories = () => {
return this.state.value.document.getMarksByType('category').map((mark) => mark.data.toJS()).toArray();
}
clear = () => {
const value = Plain.deserialize('');
this.setState({
value,
enterKeyValue: 0
})
}
focus = () => {
if(this.editor) {
this.editor.focus();
}
}
submitNote = () => {
this.setState({ enterKeyValue: 0 }, () => {
if (typeof this.props.submitNote === 'function') {
this.props.submitNote();
}
});
}
/**
* On key down, if it's a formatting command toggle a mark.
*
* @param {Event} e
* @param {Change} change
* @return {Change}
*/
onKeyUp = (e, editor, next) => {
const { value } = this.state;
const noteText = value.document.text.trim();
if(e.key === "Enter" && noteText.length !== 0) {
this.setState({ enterKeyValue: this.state.enterKeyValue + 1 });
} else if ( e.getModifierState() || (e.key !== "Control" && e.key !== "Shift" && e.key !== "Meta" && e.key !== "Alt") ) {
this.setState({ enterKeyValue: 0 });
}
return next();
}
/**
* On key down, if it's a formatting command toggle a mark.
*
* @param {Event} e
* @param {Change} change
* @return {Change}
*/
onKeyDown = (e, editor, next) => {
const { value, enterKeyValue } = this.state;
const { autoSubmit } = this.props;
const noteText = value.document.text.trim();
// we prevent empty first lines
if(e.key === "Enter" && noteText.length === 0) {
e.preventDefault();
return next();
}
// Enter submit the note
if(e.key === "Enter" && ( enterKeyValue === 2 || e.ctrlKey || autoSubmit ) && noteText.length !== 0) {
e.preventDefault();
this.submitNote();
return next();
}
if (!e.ctrlKey) {
return next();
}
e.preventDefault();
// Decide what to do based on the key code...
switch (e.key) {
default: {
break;
}
// When "B" is pressed, add a "bold" mark to the text.
case 'b': {
editor.toggleMark('bold');
break;
}
case 'i': {
// When "U" is pressed, add an "italic" mark to the text.
editor.toggleMark('italic');
break;
}
case 'u': {
// When "U" is pressed, add an "underline" mark to the text.
editor.toggleMark('underlined');
break;
}
}
return next();
}
// Add a `renderMark` method to render marks.
renderMark = (props, editor, next) => {
const { children, mark, attributes } = props
switch (mark.type) {
case 'bold':
return <strong {...attributes}>{children}</strong>
case 'code':
return <code {...attributes}>{children}</code>
case 'italic':
return <em {...attributes}>{children}</em>
case 'underlined':
return <ins {...attributes}>{children}</ins>
case 'category':
let spanStyle = {
backgroundColor: mark.data.get('color')
};
return <span {...attributes} style={ spanStyle } >{children}</span>
default:
return next();
}
}
renderNode = (props, editor, next) => {
const { attributes, children, node } = props
switch (node.type) {
case 'block-quote':
return <blockquote {...attributes}>{children}</blockquote>
case 'bulleted-list':
return <ul {...attributes}>{children}</ul>
case 'heading-one':
return <h1 {...attributes}>{children}</h1>
case 'heading-two':
return <h2 {...attributes}>{children}</h2>
case 'list-item':
return <li {...attributes}>{children}</li>
case 'numbered-list':
return <ol {...attributes}>{children}</ol>
default:
return next();
}
}
/**
* Render.
*
* @return {Element}
*/
render = () => (
<div className="bg-secondary mb-5">
<div className="sticky-top">
<Toolbar
value={this.state.value}
editor={this.editor}
note={this.props.note}
annotationCategories={this.props.annotationCategories}
isButtonDisabled={this.props.isButtonDisabled}
submitNote={this.submitNote}
/>
</div>
<div className="editor-slatejs p-2">
<Editor
ref={this.editorRef}
spellCheck
placeholder={this.props.t('slate_editor.placeholder')}
value={this.state.value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
onKeyUp={this.onKeyUp}
renderMark={this.renderMark}
renderNode = {this.renderNode}
/>
</div>
</div>
);
}
/**
* Export.
*/
function mapStateToProps(state, props) {
const autoSubmit = getAutoSubmit(state);
return {
autoSubmit,
};
}
export default withNamespaces("", {
innerRef: (ref) => {
if(!ref) {
return;
}
const wrappedRef = ref.getWrappedInstance();
const editorRef = (wrappedRef && wrappedRef.props) ? wrappedRef.props.editorRef : null;
if(editorRef && editorRef.hasOwnProperty('current')) {
editorRef.current = wrappedRef;
}
}
})(connect(mapStateToProps, null, null, { withRef: true })(SlateEditor));