wix-style-react
Version:
308 lines (268 loc) • 9.05 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
import { Editor, EditorState } from 'draft-js';
import EditorUtilities from './EditorUtilities';
import { sizeTypes, inputToTagsSize, dataHooks } from './constants';
import { st, classes, vars } from './VariableInput.st.css';
import StatusIndicator from '../StatusIndicator';
import { FontUpgradeContext } from '../FontUpgrade/context';
/** Input with variables as tags */
class VariableInput extends React.PureComponent {
constructor(props) {
super(props);
const { size, disabled } = props;
const decorator = EditorUtilities.decoratorFactory({
tag: { size: this._inputToTagSize(size), disabled },
});
this.state = {
editorState: EditorState.createEmpty(decorator),
};
}
componentDidMount() {
const { initialValue } = this.props;
this._setStringValue(initialValue);
this.editorRef = React.createRef();
}
render() {
const {
dataHook,
multiline,
rows,
size,
disabled,
readOnly,
placeholder,
status,
statusMessage,
className,
} = this.props;
const singleLineProps = {
handlePastedText: this._handlePastedText,
handleReturn: () => 'handled',
};
return (
<FontUpgradeContext.Consumer>
{({ active: isMadefor }) => (
<div
data-hook={dataHook}
className={st(
classes.root,
{
isMadefor,
disabled,
readOnly,
size,
status,
singleLine: !multiline,
},
className,
)}
style={{ [vars.rows]: rows }}
>
<Editor
ref={this.editorRef}
editorState={this.state.editorState}
onChange={this._onEditorChange}
onFocus={this._onFocus}
placeholder={placeholder}
readOnly={disabled || readOnly}
{...(readOnly && { tabIndex: 0 })}
{...(!multiline && singleLineProps)}
/>
{/* Status */}
{status && (
<span className={classes.indicatorWrapper}>
<StatusIndicator
dataHook={dataHooks.indicator}
status={status}
message={statusMessage}
/>
</span>
)}
</div>
)}
</FontUpgradeContext.Consumer>
);
}
_handlePastedText = (text, html, editorState) => {
/** We need to prevent new line when `multilne` is false,
* here we are removing any new lines while pasting text */
if (/\r|\n/.exec(text)) {
text = text.replace(/(\r\n|\n|\r)/gm, '');
this._onEditorChange(EditorUtilities.insertText(editorState, text));
return true;
}
return false;
};
_isEmpty = () =>
this.state.editorState.getCurrentContent().getPlainText().length === 0;
_inputToTagSize = inputSize => {
return inputToTagsSize[inputSize] || VariableInput.defaultProps.size;
};
_toString = () => {
const {
variableTemplate: { prefix, suffix },
} = this.props;
const { editorState } = this.state;
return EditorUtilities.convertToString({
editorState,
prefix,
suffix,
});
};
_onBlur = () => {
const { onBlur = () => {} } = this.props;
onBlur(this._toString());
};
_onFocus = () => {
const { onFocus = () => {} } = this.props;
onFocus(this._toString());
};
_onSubmit = () => {
const { onSubmit = () => {} } = this.props;
onSubmit(this._toString());
};
_onChange = () => {
const { onChange = () => {} } = this.props;
onChange(this._toString());
};
_onEditorChange = editorState => {
this._setEditorState(editorState);
};
_setEditorState = (editorState, onStateChanged = () => {}) => {
const { editorState: editorStateBefore } = this.state;
const {
variableTemplate: { prefix, suffix },
} = this.props;
let updateEditorState = EditorUtilities.moveToEdge(editorState);
let triggerCallback = () => {};
if (EditorUtilities.isBlured(editorStateBefore, updateEditorState)) {
// onChange is called after the editor blur handler
// and we can't reflect the changes there, we moved the logic here.
triggerCallback = this._onBlur;
if (
EditorUtilities.hasUnparsedEntity(updateEditorState, prefix, suffix)
) {
updateEditorState = this._stringToContentState(
EditorUtilities.convertToString({
editorState: updateEditorState,
prefix,
suffix,
}),
);
}
} else if (
EditorUtilities.isContentChanged(editorStateBefore, updateEditorState)
) {
triggerCallback = this._onChange;
}
this.setState({ editorState: updateEditorState }, () => {
triggerCallback();
onStateChanged();
});
};
_stringToContentState = str => {
const {
variableParser = () => {},
variableTemplate: { prefix, suffix },
} = this.props;
const { editorState } = this.state;
const content = EditorUtilities.stringToContentState({
str,
variableParser,
prefix,
suffix,
});
return EditorUtilities.pushAndKeepSelection({
editorState,
content,
});
};
_setStringValue = (str, afterUpdated = () => {}) => {
const updatedEditorState = EditorState.moveSelectionToEnd(
this._stringToContentState(str),
);
this._setEditorState(updatedEditorState, () => {
afterUpdated(updatedEditorState);
});
};
/** Set value to display in the input */
setValue = value => {
this._setStringValue(value, () => {
this._onSubmit();
});
};
/** Insert variable at the input cursor position */
insertVariable = value => {
const {
variableParser,
variableTemplate: { prefix, suffix },
} = this.props;
const { editorState } = this.state;
const text = variableParser(value);
const newState = text
? EditorUtilities.insertEntity(editorState, { text, value })
: EditorUtilities.insertText(editorState, `${prefix}${value}${suffix} `);
this._setEditorState(newState, () => {
this._onSubmit();
});
};
}
VariableInput.displayName = 'VariableInput';
VariableInput.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,
/** Specifies whether input should be disabled or not */
disabled: PropTypes.bool,
/** Specifies whether input is read only */
readOnly: PropTypes.bool,
/** Defines an initial value to display */
initialValue: PropTypes.string,
/** Specifies whether component allow multiple lines or not. If false, text won’t wrap and horizontal scroll will appear inside of a component. */
multiline: PropTypes.bool,
/** Defines a callback function that is called each time value is changed:
* `onChange(value: String): void` */
onChange: PropTypes.func,
/** Defines a callback function that is called on value submit, in other words after `insertVariable()` and `setValue()`
* `onSubmit(value: String): void` */
onSubmit: PropTypes.func,
/** Defines a callback function that is called on focus out:
* `onBlur(value: String): void` */
onBlur: PropTypes.func,
/** Defines a callback function that is called on focus in:
* `onFocus(value: String): void` */
onFocus: PropTypes.func,
/** Specify 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.node,
/** Sets a placeholder message to display */
placeholder: PropTypes.string,
/** Set the height of a component to fit the given number of rows */
rows: PropTypes.number,
/** Controls the size of the input and variable tags */
size: PropTypes.oneOf(['small', 'medium', 'large']),
/** Defines the variable keys that component will parse and convert to <Tag/> components on blur and while using `insertVariable`.
* For each key `variableParser` will be called and should return a proper text for that key or false in case the key is invalid.
* `variableParser(key: String): String|boolean` */
variableParser: PropTypes.func,
/** Defines a template for variable recognition. Typed text strings with matching prefix and suffix symbols will be converted to <Tag/> components. */
variableTemplate: PropTypes.shape({
prefix: PropTypes.string,
suffix: PropTypes.string,
}),
};
VariableInput.defaultProps = {
initialValue: '',
multiline: true,
rows: 1,
size: sizeTypes.medium,
variableParser: () => {},
variableTemplate: {
prefix: '{{',
suffix: '}}',
},
};
export default VariableInput;