UNPKG

wix-style-react

Version:
308 lines (268 loc) 9.05 kB
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;