wix-style-react
Version:
251 lines (232 loc) • 8.06 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
import { EditorState, Editor, CompositeDecorator } from 'draft-js';
import { convertFromHTML } from 'draft-convert';
import { FontUpgradeContext } from '../FontUpgrade/context';
import { st, classes, vars } from './RichTextInputArea.st.css';
import RichTextToolbar from './Toolbar/RichTextToolbar';
import EditorUtilities from './EditorUtilities';
import { RichTextInputAreaContext } from './RichTextInputAreaContext';
import { defaultTexts } from './RichTextInputAreaTexts';
import StatusIndicator from '../StatusIndicator';
import deprecationLog from '../utils/deprecationLog';
const decorator = new CompositeDecorator([
{
strategy: EditorUtilities.findLinkEntities,
component: ({ contentState, entityKey, children }) => {
const { url } = contentState.getEntity(entityKey).getData();
return (
<a
data-hook="richtextarea-link"
href={url}
className={classes.link}
target="_blank"
// Avoids a potentially serious vulnerability for '_blank' links
rel="noopener noreferrer"
>
{children}
</a>
);
},
},
]);
class RichTextInputArea extends React.PureComponent {
constructor(props) {
super(props);
const { texts: consumerTexts, prependHTTP } = props;
if (prependHTTP) {
deprecationLog(
'<RichTextInputArea /> - prependHTTP prop is deprecated and will be removed in next major release, please use tooltipContent instead',
);
}
this.state = {
editorState: EditorState.createEmpty(decorator),
texts: {
toolbarButtons: {
...defaultTexts.toolbarButtons,
...consumerTexts.toolbarButtons,
},
insertionForm: {
...defaultTexts.insertionForm,
...consumerTexts.insertionForm,
},
},
};
}
componentDidMount() {
const { initialValue } = this.props;
// TODO: currently it treats the value as an initial value
this._updateContentByValue(initialValue);
this.editorRef = React.createRef();
}
render() {
const {
dataHook,
className,
placeholder,
disabled,
minHeight,
maxHeight,
status,
statusMessage,
spellCheck,
} = this.props;
const isEditorEmpty = EditorUtilities.isEditorEmpty(this.state.editorState);
return (
<FontUpgradeContext.Consumer>
{({ active: isMadefor }) => (
<div
data-hook={dataHook}
className={st(
classes.root,
{
isMadefor,
hidePlaceholder: !isEditorEmpty,
disabled,
hasError: !disabled && status === 'error',
hasWarning: !disabled && status === 'warning',
},
className,
)}
// Using CSS variable instead of applying minHeight & maxHeight on each child, down to the editor's content
style={{
[vars.minHeight]: minHeight,
[vars.maxHeight]: maxHeight,
}}
>
<RichTextInputAreaContext.Provider
value={{
texts: this.state.texts,
}}
>
<RichTextToolbar
dataHook="richtextarea-toolbar"
className={classes.toolbar}
isDisabled={disabled}
editorState={this.state.editorState}
onBold={this._setEditorState}
onItalic={this._setEditorState}
onUnderline={this._setEditorState}
onLink={newEditorState => {
this._setEditorState(newEditorState, () =>
this.editorRef.current.focus(),
);
}}
onBulletedList={this._setEditorState}
onNumberedList={this._setEditorState}
/>
</RichTextInputAreaContext.Provider>
<div className={classes.editorWrapper}>
<Editor
ref={this.editorRef}
editorState={this.state.editorState}
onChange={this._setEditorState}
placeholder={placeholder}
readOnly={disabled}
spellCheck={spellCheck}
/>
{!disabled && status && (
<span className={classes.statusIndicator}>
<StatusIndicator
dataHook="richtextarea-status-indicator"
status={status}
message={statusMessage}
/>
</span>
)}
</div>
</div>
)}
</FontUpgradeContext.Consumer>
);
}
_setEditorState = (newEditorState, onStateChanged = () => {}) => {
this.setState({ editorState: newEditorState }, () => {
const { onChange = () => {}, prependHTTP } = this.props;
const htmlText = EditorUtilities.convertToHtml(
newEditorState,
prependHTTP,
);
const plainText = newEditorState.getCurrentContent().getPlainText();
onChange(htmlText, { plainText });
onStateChanged();
});
};
_updateContentByValue = value => {
const content = convertFromHTML({
htmlToEntity: (nodeName, node, createEntity) => {
if (nodeName === 'a') {
return createEntity('LINK', 'MUTABLE', { url: node.href });
}
},
})(value);
const updatedEditorState = EditorState.push(
this.state.editorState,
content,
);
this.setState({ editorState: updatedEditorState });
};
/** Set value to display in the editor */
setValue = value => {
this._updateContentByValue(value);
};
}
RichTextInputArea.displayName = 'RichTextInputArea';
RichTextInputArea.propTypes = {
/** Specifies a CSS class name to be appended to the component’s root element. */
className: PropTypes.string,
/** Applies a data-hook HTML attribute that can be used in the tests. */
dataHook: PropTypes.string,
/** Sets the initial value to be displayed in the editor. */
initialValue: PropTypes.string,
/** Sets a placeholder message to display. */
placeholder: PropTypes.string,
/** Specifies whether an editor and its toolbar should be disabled. */
disabled: PropTypes.bool,
/** Specifies the status of a field. */
status: PropTypes.oneOf(['error', 'warning', 'loading']),
/** Defines the message to display on status icon hover. If not given or empty there will be no tooltip. */
statusMessage: PropTypes.string,
/** Defines a standard callback function for changes: `onChange(htmlText, { plainText })` */
onChange: PropTypes.func,
/** Defines a minimum height for the editor (it grows by default) */
minHeight: PropTypes.string,
/** Defines a maximum height for the editor (it grows by default) */
maxHeight: PropTypes.string,
/**
* Enables browsers spell checking.
* Do not affect IE.
* In Safari, autocorrects by default.
*/
spellCheck: PropTypes.bool,
/** Defines text styles to be shown. */
texts: PropTypes.shape({
toolbarButtons: PropTypes.shape({
boldButtonLabel: PropTypes.string,
italicButtonLabel: PropTypes.string,
underlineButtonLabel: PropTypes.string,
linkButtonLabel: PropTypes.string,
bulletedListButtonLabel: PropTypes.string,
numberedListButtonLabel: PropTypes.string,
}),
insertionForm: PropTypes.shape({
confirmButtonLabel: PropTypes.string,
cancelButtonLabel: PropTypes.string,
link: PropTypes.shape({
textInputPlaceholder: PropTypes.string,
urlInputPlaceholder: PropTypes.string,
}),
}),
}),
/** Prepend http protocol to link if it does not have it.
* (ex. typed link is wix.com becomes http://wix.com)
* @deprecated
*/
prependHTTP: PropTypes.bool,
};
RichTextInputArea.defaultProps = {
initialValue: '<p/>',
texts: {},
disabled: false,
};
export default RichTextInputArea;