2018-03-05 14:30:28 +01:00
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* Wysiwyg
|
|
|
|
|
*
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import React from 'react';
|
2018-03-08 12:56:44 +01:00
|
|
|
import {
|
|
|
|
|
ContentState,
|
|
|
|
|
convertFromHTML,
|
|
|
|
|
EditorState,
|
|
|
|
|
getDefaultKeyBinding,
|
2018-03-08 18:55:49 +01:00
|
|
|
Modifier,
|
2018-03-08 12:56:44 +01:00
|
|
|
RichUtils,
|
|
|
|
|
} from 'draft-js';
|
2018-03-05 14:30:28 +01:00
|
|
|
import PropTypes from 'prop-types';
|
2018-03-14 09:59:42 +01:00
|
|
|
import { isEmpty, replace, trimStart, trimEnd } from 'lodash';
|
2018-03-05 14:30:28 +01:00
|
|
|
import cn from 'classnames';
|
2018-03-12 15:38:38 +01:00
|
|
|
import { FormattedMessage } from 'react-intl';
|
2018-03-06 18:20:05 +01:00
|
|
|
import Controls from 'components/WysiwygInlineControls';
|
2018-03-12 13:26:07 +01:00
|
|
|
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-13 21:39:27 +01:00
|
|
|
import {
|
|
|
|
|
END_REPLACER,
|
|
|
|
|
NEW_CONTROLS,
|
|
|
|
|
SELECT_OPTIONS,
|
|
|
|
|
START_REPLACER,
|
|
|
|
|
} from './constants';
|
|
|
|
|
import {
|
|
|
|
|
getBlockStyle,
|
|
|
|
|
getInnerText,
|
|
|
|
|
getOffSets,
|
|
|
|
|
} from './helpers';
|
2018-03-05 14:30:28 +01:00
|
|
|
|
2018-03-13 13:12:06 +01:00
|
|
|
import styles from './styles.scss';
|
2018-03-06 18:20:05 +01:00
|
|
|
|
2018-03-13 21:39:27 +01:00
|
|
|
/* eslint-disable react/no-string-refs */ // NOTE: need to check eslint
|
2018-03-08 12:56:44 +01:00
|
|
|
/* eslint-disable react/jsx-handler-names */
|
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: '',
|
|
|
|
|
headerValue: '',
|
2018-03-12 14:02:34 +01:00
|
|
|
previewHTML: false,
|
|
|
|
|
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 });
|
2018-03-09 12:45:37 +01:00
|
|
|
return this.domEditor.focus();
|
2018-03-09 16:41:24 +01:00
|
|
|
};
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-14 09:59:42 +01:00
|
|
|
onChange = (editorState) => {
|
|
|
|
|
this.setState({ editorState });
|
|
|
|
|
this.props.onChange({ target: {
|
|
|
|
|
value: editorState.getCurrentContent().getPlainText(),
|
|
|
|
|
name: this.props.name,
|
|
|
|
|
type: 'textarea',
|
|
|
|
|
}});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NOTE: leave these function if we change to HTML instead of markdown
|
|
|
|
|
// toggleBlockType = (blockType) => {
|
|
|
|
|
// this.onChange(
|
|
|
|
|
// RichUtils.toggleBlockType(
|
|
|
|
|
// this.state.editorState,
|
|
|
|
|
// blockType
|
|
|
|
|
// )
|
|
|
|
|
// );
|
|
|
|
|
// }
|
|
|
|
|
//
|
|
|
|
|
// toggleInlineStyle = (inlineStyle) => {
|
|
|
|
|
// this.onChange(
|
|
|
|
|
// RichUtils.toggleInlineStyle(
|
|
|
|
|
// this.state.editorState,
|
|
|
|
|
// inlineStyle
|
|
|
|
|
// )
|
|
|
|
|
// );
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleChangeSelect = ({ target }) => {
|
|
|
|
|
this.setState({ headerValue: target.value });
|
|
|
|
|
const splitData = target.value.split('.');
|
|
|
|
|
this.addEntity(splitData[0], splitData[1]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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-13 13:12:06 +01:00
|
|
|
addEntity = (text, style) => {
|
2018-03-08 18:55:49 +01:00
|
|
|
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);
|
2018-03-13 20:47:57 +01:00
|
|
|
// Range of the text we want to replace
|
2018-03-13 21:39:27 +01:00
|
|
|
const { start, end } = getOffSets(selection);
|
|
|
|
|
// Retrieve the selected text
|
2018-03-08 18:55:49 +01:00
|
|
|
const selectedText = currentContentBlock.getText().slice(start, end);
|
2018-03-13 13:12:06 +01:00
|
|
|
const innerText = selectedText === '' ? getInnerText(style) : replace(text, 'innerText', selectedText);
|
2018-03-13 20:47:57 +01:00
|
|
|
|
2018-03-13 21:39:27 +01:00
|
|
|
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
|
2018-03-13 20:47:57 +01:00
|
|
|
const updateSelection = selection.merge({
|
2018-03-14 09:59:42 +01:00
|
|
|
anchorOffset,
|
|
|
|
|
focusOffset,
|
|
|
|
|
});
|
2018-03-13 21:39:27 +01:00
|
|
|
|
|
|
|
|
// Dynamically add some content to the one selected
|
2018-03-13 13:12:06 +01:00
|
|
|
const textWithEntity = Modifier.replaceText(currentContent, selection, innerText);
|
2018-03-13 21:39:27 +01:00
|
|
|
|
|
|
|
|
// Push the new content to the editorState
|
2018-03-13 20:47:57 +01:00
|
|
|
const newEditorState = EditorState.push(editorState, textWithEntity, 'insert-characters');
|
2018-03-08 18:55:49 +01:00
|
|
|
|
2018-03-13 21:39:27 +01:00
|
|
|
// SetState and force focus
|
2018-03-08 18:55:49 +01:00
|
|
|
this.setState({
|
2018-03-13 20:47:57 +01:00
|
|
|
editorState: EditorState.forceSelection(newEditorState, updateSelection),
|
2018-03-09 16:41:24 +01:00
|
|
|
headerValue: '',
|
2018-03-08 18:55:49 +01:00
|
|
|
}, () => {
|
2018-03-09 16:41:24 +01:00
|
|
|
this.focus();
|
2018-03-08 18:55:49 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-12 14:02:34 +01:00
|
|
|
toggleFullScreen = (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this.setState({
|
|
|
|
|
toggleFullScreen: !this.state.toggleFullScreen,
|
|
|
|
|
}, () => {
|
|
|
|
|
this.focus();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-09 16:41:24 +01:00
|
|
|
handleKeyCommand(command, editorState) {
|
|
|
|
|
const newState = RichUtils.handleKeyCommand(editorState, command);
|
|
|
|
|
if (newState) {
|
|
|
|
|
this.onChange(newState);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-08 12:56:44 +01:00
|
|
|
componentDidCatch(error, info) {
|
|
|
|
|
console.log('err', error);
|
|
|
|
|
console.log('info', info);
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-13 21:39:27 +01:00
|
|
|
// NOTE: this need to be changed to preview markdown
|
2018-03-06 18:20:05 +01:00
|
|
|
previewHTML = () => {
|
|
|
|
|
const blocksFromHTML = convertFromHTML(this.props.value);
|
2018-03-13 13:46:42 +01:00
|
|
|
|
2018-03-13 21:39:27 +01:00
|
|
|
// Make sure blocksFromHTML.contentBlocks !== null
|
2018-03-13 13:46:42 +01:00
|
|
|
if (blocksFromHTML.contentBlocks) {
|
|
|
|
|
const contentState = ContentState.createFromBlockArray(blocksFromHTML);
|
|
|
|
|
return EditorState.createWithContent(contentState);
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-13 21:39:27 +01:00
|
|
|
// Prevent errors if value is empty
|
2018-03-13 13:46:42 +01:00
|
|
|
return EditorState.createEmpty();
|
2018-03-06 18:20:05 +01:00
|
|
|
}
|
|
|
|
|
|
2018-03-05 14:30:28 +01:00
|
|
|
render() {
|
|
|
|
|
const { editorState } = this.state;
|
2018-03-13 13:12:06 +01:00
|
|
|
|
2018-03-12 15:38:38 +01:00
|
|
|
if (this.state.toggleFullScreen) {
|
2018-03-13 21:39:27 +01:00
|
|
|
// NOTE: this should be a function
|
2018-03-12 15:38:38 +01:00
|
|
|
return (
|
|
|
|
|
<div className={styles.fullscreenOverlay} onClick={this.toggleFullScreen}>
|
|
|
|
|
<div
|
|
|
|
|
className={cn(styles.editorWrapper, this.state.isFocused && styles.editorFocus)}
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
}}
|
|
|
|
|
style={{ marginTop: '0' }}
|
|
|
|
|
>
|
|
|
|
|
<div className={styles.controlsContainer}>
|
|
|
|
|
<div style={{ minWidth: '161px', marginLeft: '8px' }}>
|
|
|
|
|
<Select
|
|
|
|
|
name="headerSelect"
|
|
|
|
|
onChange={this.handleChangeSelect}
|
|
|
|
|
value={this.state.headerValue}
|
|
|
|
|
selectOptions={SELECT_OPTIONS}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
{NEW_CONTROLS.map((value, key) => (
|
|
|
|
|
<Controls
|
|
|
|
|
key={key}
|
|
|
|
|
buttons={value}
|
|
|
|
|
editorState={editorState}
|
|
|
|
|
handlers={{
|
|
|
|
|
addEntity: this.addEntity,
|
2018-03-13 21:39:27 +01:00
|
|
|
// toggleBlockType: this.toggleBlockType,
|
|
|
|
|
// toggleInlineStyle: this.toggleInlineStyle,
|
2018-03-12 15:38:38 +01:00
|
|
|
}}
|
|
|
|
|
onToggle={this.toggleInlineStyle}
|
|
|
|
|
onToggleBlock={this.toggleBlockType}
|
|
|
|
|
previewHTML={() => this.setState(prevState => ({ previewHTML: !prevState.previewHTML }))}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
<div className={styles.editor} onClick={this.focus}>
|
|
|
|
|
<WysiwygEditor
|
|
|
|
|
blockStyleFn={getBlockStyle}
|
|
|
|
|
editorState={editorState}
|
|
|
|
|
handleKeyCommand={this.handleKeyCommand}
|
|
|
|
|
keyBindingFn={this.mapKeyToEditorCommand}
|
|
|
|
|
onBlur={() => this.setState({ isFocused: false })}
|
|
|
|
|
onChange={this.onChange}
|
|
|
|
|
placeholder={this.props.placeholder}
|
|
|
|
|
setRef={(editor) => this.domEditor = editor}
|
|
|
|
|
spellCheck
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
className={cn(styles.editorWrapper)}
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
}}
|
|
|
|
|
style={{ marginTop: '0' }}
|
|
|
|
|
>
|
|
|
|
|
<div className={styles.previewControlsWrapper} onClick={this.toggleFullScreen}>
|
|
|
|
|
<div><FormattedMessage id="components.WysiwygBottomControls.charactersIndicators" values={{ characters: 0 }} /></div>
|
|
|
|
|
<div className={styles.wysiwygCollapse}>
|
|
|
|
|
<FormattedMessage id="components.Wysiwyg.collapse" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className={styles.editor}>
|
|
|
|
|
<WysiwygEditor
|
2018-03-13 21:39:27 +01:00
|
|
|
// TODO handle markdown preview
|
2018-03-13 13:46:42 +01:00
|
|
|
editorState={this.previewHTML()}
|
2018-03-12 15:38:38 +01:00
|
|
|
onChange={() => {}}
|
|
|
|
|
placeholder={this.props.placeholder}
|
2018-03-12 16:05:36 +01:00
|
|
|
setRef={(dummyEditor) => this.dummyEditor = dummyEditor}
|
2018-03-12 15:38:38 +01:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2018-03-05 14:30:28 +01:00
|
|
|
|
|
|
|
|
return (
|
2018-03-06 18:20:05 +01:00
|
|
|
<div className={cn(styles.editorWrapper, this.state.isFocused && styles.editorFocus)}>
|
2018-03-08 12:56:44 +01:00
|
|
|
<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) => (
|
2018-03-08 12:56:44 +01:00
|
|
|
<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,
|
2018-03-08 14:24:26 +01:00
|
|
|
toggleBlockType: this.toggleBlockType,
|
|
|
|
|
toggleInlineStyle: this.toggleInlineStyle,
|
|
|
|
|
}}
|
2018-03-08 12:56:44 +01:00
|
|
|
onToggle={this.toggleInlineStyle}
|
|
|
|
|
onToggleBlock={this.toggleBlockType}
|
|
|
|
|
previewHTML={() => this.setState(prevState => ({ previewHTML: !prevState.previewHTML }))}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</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}
|
2018-03-08 12:56:44 +01:00
|
|
|
spellCheck
|
2018-03-06 18:20:05 +01:00
|
|
|
/>
|
|
|
|
|
<input className={styles.editorInput} value="" tabIndex="-1" />
|
|
|
|
|
</div>
|
2018-03-12 14:02:34 +01:00
|
|
|
<WysiwygBottomControls 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,
|
2018-03-08 12:56:44 +01:00
|
|
|
onChange: () => {},
|
2018-03-06 18:20:05 +01:00
|
|
|
placeholder: '',
|
2018-03-08 12:56:44 +01:00
|
|
|
value: '',
|
2018-03-05 14:30:28 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Wysiwyg.propTypes = {
|
|
|
|
|
autoFocus: PropTypes.bool,
|
2018-03-08 12:56:44 +01:00
|
|
|
name: PropTypes.string.isRequired,
|
|
|
|
|
onChange: PropTypes.func,
|
2018-03-05 14:30:28 +01:00
|
|
|
placeholder: PropTypes.string,
|
2018-03-08 12:56:44 +01:00
|
|
|
value: PropTypes.string,
|
2018-03-05 14:30:28 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default Wysiwyg;
|