460 lines
15 KiB
JavaScript
Raw Normal View History

2018-03-05 14:30:28 +01:00
/**
*
* Wysiwyg
*
*/
import React from 'react';
import {
2018-03-14 22:47:56 +01:00
ContentBlock,
ContentState,
convertFromHTML,
EditorState,
getDefaultKeyBinding,
2018-03-14 22:47:56 +01:00
genKey,
2018-03-08 18:55:49 +01:00
Modifier,
RichUtils,
} from 'draft-js';
2018-03-14 22:47:56 +01:00
import { List } from 'immutable';
2018-03-05 14:30:28 +01:00
import PropTypes from 'prop-types';
import { isEmpty, isNaN, last, replace, trimStart, trimEnd, words } from 'lodash';
2018-03-05 14:30:28 +01:00
import cn from 'classnames';
2018-03-06 18:20:05 +01:00
import Controls from 'components/WysiwygInlineControls';
2018-03-14 22:20:39 +01:00
import Drop from 'components/WysiwygDropUpload';
import Select from 'components/InputSelect';
import WysiwygBottomControls from 'components/WysiwygBottomControls';
2018-03-12 14:02:34 +01:00
import WysiwygEditor from 'components/WysiwygEditor';
2018-03-14 22:47:56 +01:00
import request from 'utils/request';
2018-03-15 12:01:58 +01:00
import { END_REPLACER, NEW_CONTROLS, SELECT_OPTIONS, START_REPLACER } from './constants';
import { getBlockStyle, getInnerText, getOffSets } from './helpers';
2018-03-13 13:12:06 +01:00
import styles from './styles.scss';
2018-03-06 18:20:05 +01:00
/* eslint-disable react/jsx-handler-names */
2018-03-15 19:10:02 +01:00
/* eslint-disable react/sort-comp */
2018-03-05 14:30:28 +01:00
class Wysiwyg extends React.Component {
2018-03-06 18:20:05 +01:00
constructor(props) {
super(props);
2018-03-08 15:50:28 +01:00
this.state = {
editorState: EditorState.createEmpty(),
isFocused: false,
initialValue: '',
2018-03-14 22:20:39 +01:00
isDraging: false,
2018-03-08 15:50:28 +01:00
headerValue: '',
2018-03-12 14:02:34 +01:00
toggleFullScreen: false,
2018-03-08 15:50:28 +01:00
};
2018-03-13 21:39:27 +01:00
2018-03-06 18:20:05 +01:00
this.focus = () => {
this.setState({ isFocused: true });
return this.domEditor.focus();
};
2018-03-06 18:20:05 +01:00
}
2018-03-05 14:30:28 +01:00
componentDidMount() {
if (this.props.autoFocus) {
this.focus();
}
2018-03-05 19:23:06 +01:00
if (!isEmpty(this.props.value)) {
this.setInitialValue(this.props);
}
document.addEventListener('keydown', this.handleTabKey);
2018-03-05 19:23:06 +01:00
}
componentWillReceiveProps(nextProps) {
if (nextProps.value !== this.props.value && !this.state.hasInitialValue) {
this.setInitialValue(nextProps);
}
2018-03-06 21:32:48 +01:00
// Handle reset props
if (nextProps.value === this.state.initialValue && this.state.hasInitialValue) {
this.setInitialValue(nextProps);
}
}
componentWillUnmount() {
document.removeEventListener('keydown', this.handleTabKey);
}
2018-03-14 09:59:42 +01:00
/**
* Init the editor with data from
* @param {[type]} props [description]
*/
setInitialValue = (props) => {
const contentState = ContentState.createFromText(props.value);
let editorState = EditorState.createWithContent(contentState);
// Get the cursor at the end
editorState = EditorState.moveFocusToEnd(editorState);
this.setState({ editorState, hasInitialValue: true, initialValue: props.value });
}
2018-03-15 19:10:02 +01:00
addSimpleBlock = (content, style) => {
const selectedText = this.getSelectedText();
const defaultContent = style === 'code-block' ? 'code block' : 'quote';
const innerContent = selectedText === '' ? replace(content, 'innerText', defaultContent) : replace(content, 'innerText', selectedText);
const newBlock = this.createNewBlock(innerContent);
const newContentState = this.createNewContentStateFromBlock(newBlock);
const newEditorState = this.createNewEditorState(newContentState, innerContent);
return this.setState({ editorState: EditorState.moveFocusToEnd(newEditorState) });
}
addLink = () => {
const selectedText = this.getSelectedText();
const link = selectedText === '' ? ' [text](link)' : `[text](${selectedText})`;
const text = Modifier.replaceText(this.getEditorState().getCurrentContent(), this.getSelection(), link);
const newEditorState = EditorState.push(this.getEditorState(), text, 'insert-characters');
return this.setState({ editorState: EditorState.moveFocusToEnd(newEditorState) });
}
addLinkMediaBlockWithSelection = () => {
const selectedText = this.getSelectedText();
const link = selectedText === '' ? '![text](link)' : `![text](${selectedText})`;
const newBlock = this.createNewBlock(link);
const newContentState = this.createNewContentStateFromBlock(newBlock);
const newEditorState = this.createNewEditorState(newContentState, link);
return this.setState({ editorState: EditorState.moveFocusToEnd(newEditorState) });
}
addLinkMediaBlock = (link) => {
const { editorState } = this.state;
const newBlock = this.createNewBlock(link);
const newContentState = this.createNewContentStateFromBlock(newBlock);
const newEditorState = EditorState.push(
editorState,
newContentState,
);
this.setState({ editorState: EditorState.moveFocusToEnd(newEditorState) });
}
addEntity = (text, style) => {
const editorState = this.state.editorState;
const currentContent = editorState.getCurrentContent();
// Get the selected text
const selection = editorState.getSelection();
const anchorKey = selection.getAnchorKey();
const currentContentBlock = currentContent.getBlockForKey(anchorKey);
// Range of the text we want to replace
const { start, end } = getOffSets(selection);
// Retrieve the selected text
const selectedText = currentContentBlock.getText().slice(start, end);
const innerText = selectedText === '' ? getInnerText(style) : replace(text, 'innerText', selectedText);
const trimedStart = trimStart(innerText, START_REPLACER).length;
const trimedEnd = trimEnd(innerText, END_REPLACER).length;
// Set the correct offset
const focusOffset = start === end ? trimedEnd : start + trimedEnd;
const anchorOffset = start + innerText.length - trimedStart;
// Merge the old selection with the new one so the editorState is updated
const updateSelection = selection.merge({
anchorOffset,
focusOffset,
});
// Dynamically add some content to the one selected
const textWithEntity = Modifier.replaceText(currentContent, selection, innerText);
// Push the new content to the editorState
const newEditorState = EditorState.push(editorState, textWithEntity, 'insert-characters');
// SetState and force focus
this.setState({
editorState: EditorState.forceSelection(newEditorState, updateSelection),
headerValue: '',
}, () => {
this.focus();
});
}
addOlBlock = () => {
const previousBlockKey = last(Object.keys(this.getCurrentBlockMap()));
const previousContent = this.getEditorState().getCurrentContent().getBlockForKey(previousBlockKey).getText();
const number = previousContent ? parseInt(previousContent.split('.')[0], 10) : 0;
const liNumber = isNaN(number) ? 1 : number + 1;
const selectedText = this.getSelectedText();
const li = selectedText === '' ? `${liNumber}. ` : `${liNumber}. ${selectedText}`;
const newBlock = this.createNewBlock(li, 'block-ul');
const newContentState = this.createNewContentStateFromBlock(newBlock);
const newEditorState = this.createNewEditorState(newContentState, li);
return this.setState({ editorState: EditorState.moveFocusToEnd(newEditorState) });
}
addUlBlock = () => {
const selectedText = this.getSelectedText();
const li = selectedText === '' ? '- ' : `- ${selectedText}`;
const newBlock = this.createNewBlock(li, 'block-ul');
const newContentState = this.createNewContentStateFromBlock(newBlock);
const newEditorState = this.createNewEditorState(newContentState, li);
return this.setState({ editorState: EditorState.moveFocusToEnd(newEditorState) });
}
createNewEditorState = (newContentState, text) => {
let newEditorState;
if (getOffSets(this.getSelection()).start !== 0) {
newEditorState = EditorState.push(
this.getEditorState(),
newContentState,
);
} else {
const textWithEntity = Modifier.replaceText(this.getEditorState().getCurrentContent(), this.getSelection(), text);
newEditorState = EditorState.push(this.getEditorState(), textWithEntity, 'insert-characters');
}
return newEditorState;
}
createNewBlock = (text = '', type = 'unstyled') => (
new ContentBlock({
key: genKey(),
type,
text,
charaterList: List([]),
})
);
createNewBlockMap = (newBlock, contentState) => contentState.getBlockMap().set(newBlock.key, newBlock);
createNewContentStateFromBlock = (newBlock, contentState = this.getEditorState().getCurrentContent()) => (
ContentState
.createFromBlockArray(this.createNewBlockMap(newBlock, contentState).toArray())
.set('selectionBefore', contentState.getSelectionBefore())
.set('selectionAfter', contentState.getSelectionAfter())
)
getCharactersNumber = (editorState = this.getEditorState()) => {
const plainText = editorState.getCurrentContent().getPlainText();
const spacesNumber = plainText.split(' ').length;
return words(plainText).join('').length + spacesNumber - 1;
}
2018-03-15 11:31:07 +01:00
getEditorState = () => this.state.editorState;
getSelection = () => this.getEditorState().getSelection();
2018-03-15 16:54:28 +01:00
getCurrentBlockMap = () => this.getEditorState().getCurrentContent().getBlockMap().toJS();
2018-03-15 11:31:07 +01:00
getCurrentContentBlock = () => this.getEditorState().getCurrentContent().getBlockForKey(this.getSelection().getAnchorKey());
getSelectedText = () => {
const { start, end } = getOffSets(this.getSelection());
return this.getCurrentContentBlock().getText().slice(start, end);
}
2018-03-14 09:59:42 +01:00
handleChangeSelect = ({ target }) => {
this.setState({ headerValue: target.value });
const selectedText = this.getSelectedText();
const title = selectedText === '' ? `${target.value} ` : `${target.value} ${selectedText}`;
const newBlock = this.createNewBlock(title, 'block-ul');
const newContentState = this.createNewContentStateFromBlock(newBlock);
const newEditorState = this.createNewEditorState(newContentState, title);
return this.setState({ editorState: EditorState.moveFocusToEnd(newEditorState), headerValue: '' });
2018-03-14 09:59:42 +01:00
}
2018-03-14 22:47:56 +01:00
handleDragEnter = (e) => {
e.preventDefault();
e.stopPropagation();
if (!this.state.isDraging) {
this.setState({ isDraging: true });
}
}
2018-03-14 22:20:39 +01:00
handleDragLeave = () => this.setState({ isDraging: false });
handleDragOver = (e) => {
e.preventDefault();
e.stopPropagation();
}
handleDrop = (e) => {
e.preventDefault();
2018-03-14 22:47:56 +01:00
const { dataTransfer: { files} } = e;
const formData = new FormData();
formData.append('files', files[0]);
const headers = {
'X-Forwarded-Host': 'strapi',
};
2018-03-14 22:20:39 +01:00
2018-03-14 22:47:56 +01:00
return request('/upload', {method: 'POST', headers, body: formData }, false, false)
.then(response => {
const link = `![text](${response[0].url})`;
2018-03-15 11:31:07 +01:00
this.addLinkMediaBlock(link);
2018-03-14 22:47:56 +01:00
})
.catch(err => {
console.log('error', err.response);
})
.finally(() => {
this.setState({ isDraging: false });
});
2018-03-14 22:20:39 +01:00
}
2018-03-15 16:54:28 +01:00
handleKeyCommand = (command, editorState) => {
const newState = RichUtils.handleKeyCommand(editorState, command);
const selection = editorState.getSelection();
const currentBlock = editorState.getCurrentContent().getBlockForKey(selection.getStartKey());
if (currentBlock.getText().split('')[0] === '-' && command === 'split-block') {
this.addUlBlock();
return true;
}
2018-03-15 17:18:05 +01:00
if (currentBlock.getText().split('.').length > 1 && !isNaN(parseInt(currentBlock.getText().split('.')[0], 10)) && command === 'split-block') {
2018-03-15 16:54:28 +01:00
this.addOlBlock();
return true;
}
if (newState) {
this.onChange(newState);
return true;
}
return false;
}
handleTabKey = (e) => {
if (e.keyCode === 9 /* TAB */ && this.state.isFocused) {
e.preventDefault();
const textWithEntity = Modifier.replaceText(this.getEditorState().getCurrentContent(), this.getSelection(), ' ');
const newEditorState = EditorState.push(this.getEditorState(), textWithEntity, 'insert-characters');
return this.setState({ editorState: EditorState.moveFocusToEnd(newEditorState) });
}
}
2018-03-15 19:10:02 +01:00
onChange = (editorState) => {
// Update the state and force the focus
this.setState({ editorState }, () => { this.focus(); });
this.props.onChange({ target: {
value: editorState.getCurrentContent().getPlainText(),
name: this.props.name,
type: 'textarea',
}});
}
2018-03-14 09:59:42 +01:00
mapKeyToEditorCommand = (e) => {
if (e.keyCode === 9 /* TAB */) {
const newEditorState = RichUtils.onTab(
e,
this.state.editorState,
4, /* maxDepth */
);
if (newEditorState !== this.state.editorState) {
this.onChange(newEditorState);
}
return;
}
return getDefaultKeyBinding(e);
}
2018-03-15 19:10:02 +01:00
// NOTE: this need to be changed to preview markdown
previewHTML = () => {
const blocksFromHTML = convertFromHTML(this.props.value);
// Make sure blocksFromHTML.contentBlocks !== null
if (blocksFromHTML.contentBlocks) {
const contentState = ContentState.createFromBlockArray(blocksFromHTML);
return EditorState.createWithContent(contentState);
2018-03-15 17:18:05 +01:00
}
2018-03-15 19:10:02 +01:00
// Prevent errors if value is empty
return EditorState.createEmpty();
2018-03-15 16:54:28 +01:00
}
2018-03-12 14:02:34 +01:00
toggleFullScreen = (e) => {
e.preventDefault();
this.setState({
toggleFullScreen: !this.state.toggleFullScreen,
}, () => {
this.focus();
});
}
componentDidCatch(error, info) {
console.log('err', error);
console.log('info', info);
}
2018-03-05 14:30:28 +01:00
render() {
const { editorState } = this.state;
return (
2018-03-14 22:20:39 +01:00
<div
className={cn(
styles.editorWrapper,
this.state.isFocused && styles.editorFocus,
)}
onDragEnter={this.handleDragEnter}
onDragOver={this.handleDragOver}
>
{this.state.isDraging && (
<Drop onDrop={this.handleDrop} onDragOver={this.handleDragOver} onDragLeave={this.handleDragLeave} />
)}
<div className={styles.controlsContainer}>
2018-03-08 18:55:49 +01:00
<div style={{ minWidth: '161px', marginLeft: '8px' }}>
<Select
name="headerSelect"
onChange={this.handleChangeSelect}
value={this.state.headerValue}
selectOptions={SELECT_OPTIONS}
/>
</div>
2018-03-12 12:05:29 +01:00
{NEW_CONTROLS.map((value, key) => (
<Controls
key={key}
buttons={value}
editorState={editorState}
2018-03-08 14:24:26 +01:00
handlers={{
2018-03-12 12:05:29 +01:00
addEntity: this.addEntity,
addLink: this.addLink,
2018-03-15 11:31:07 +01:00
addLinkMediaBlockWithSelection: this.addLinkMediaBlockWithSelection,
2018-03-15 16:54:28 +01:00
addOlBlock: this.addOlBlock,
2018-03-15 18:27:18 +01:00
addSimpleBlock: this.addSimpleBlock,
2018-03-15 12:01:58 +01:00
addUlBlock: this.addUlBlock,
2018-03-08 14:24:26 +01:00
}}
onToggle={this.toggleInlineStyle}
onToggleBlock={this.toggleBlockType}
/>
))}
</div>
2018-03-06 18:20:05 +01:00
<div className={styles.editor} onClick={this.focus}>
2018-03-12 14:02:34 +01:00
<WysiwygEditor
2018-03-06 18:20:05 +01:00
blockStyleFn={getBlockStyle}
editorState={editorState}
handleKeyCommand={this.handleKeyCommand}
keyBindingFn={this.mapKeyToEditorCommand}
onBlur={() => this.setState({ isFocused: false })}
onChange={this.onChange}
placeholder={this.props.placeholder}
2018-03-12 14:02:34 +01:00
setRef={(editor) => this.domEditor = editor}
spellCheck
2018-03-06 18:20:05 +01:00
/>
<input className={styles.editorInput} value="" tabIndex="-1" />
</div>
<WysiwygBottomControls charactersNumber={this.getCharactersNumber()} onClick={this.toggleFullScreen} />
2018-03-06 18:20:05 +01:00
</div>
2018-03-05 14:30:28 +01:00
);
}
}
2018-03-13 21:39:27 +01:00
// NOTE: handle defaultProps!
2018-03-05 14:30:28 +01:00
Wysiwyg.defaultProps = {
2018-03-05 19:23:06 +01:00
autoFocus: false,
onChange: () => {},
2018-03-06 18:20:05 +01:00
placeholder: '',
value: '',
2018-03-05 14:30:28 +01:00
};
Wysiwyg.propTypes = {
autoFocus: PropTypes.bool,
name: PropTypes.string.isRequired,
onChange: PropTypes.func,
2018-03-05 14:30:28 +01:00
placeholder: PropTypes.string,
value: PropTypes.string,
2018-03-05 14:30:28 +01:00
};
export default Wysiwyg;