UNPKG

@wix/design-system

Version:

@wix/design-system

448 lines (445 loc) 18.7 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); exports.__esModule = true; exports["default"] = void 0; var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass")); var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn")); var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf")); var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits")); var _react = _interopRequireDefault(require("react")); var _draftJs = require("draft-js"); var _EditorUtilities = _interopRequireDefault(require("./EditorUtilities")); var _constants = require("./constants"); var _VariableInputSt = require("./VariableInput.st.css.js"); var _StatusIndicator = _interopRequireDefault(require("../StatusIndicator")); var _StatusContext = require("../FormField/StatusContext"); var _jsxFileName = "/home/builduser/work/57e038ea7326c1ec/packages/wix-design-system/dist/cjs/VariableInput/VariableInput.jsx"; function _callSuper(t, o, e) { return o = (0, _getPrototypeOf2["default"])(o), (0, _possibleConstructorReturn2["default"])(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], (0, _getPrototypeOf2["default"])(t).constructor) : o.apply(t, e)); } function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); } /** * Error Boundary to catch Draft.js crashes on mobile devices with IME/autocorrect * * Known Issue: Draft.js has a fundamental incompatibility with Android's Input Method Editor (IME). * When Android autocorrect modifies the DOM directly (e.g., on SPACE key press), Draft.js attempts * to reconcile its virtual state with DOM nodes that have already been removed/modified by the IME, * resulting in "Failed to execute 'removeChild' on 'Node'" errors. * * This Error Boundary prevents the entire component from crashing and allows it to auto-recover. * While console warnings will still appear, the user experience remains functional. * * TODO: After Draft-JS migration, make sure this error is no longer thrown and then remove this error boundary. */ var EditorErrorBoundary = /*#__PURE__*/function (_React$Component) { function EditorErrorBoundary(props) { var _this; (0, _classCallCheck2["default"])(this, EditorErrorBoundary); _this = _callSuper(this, EditorErrorBoundary, [props]); _this.state = { hasError: false, errorCount: 0 }; return _this; } (0, _inherits2["default"])(EditorErrorBoundary, _React$Component); return (0, _createClass2["default"])(EditorErrorBoundary, [{ key: "componentDidCatch", value: function componentDidCatch(error, errorInfo) { console.warn('VariableInput: Draft.js error caught (likely mobile IME conflict):', error.message); } }, { key: "componentDidUpdate", value: function componentDidUpdate(prevProps, prevState) { // Auto-recover with max retry limit to prevent infinite loops if (this.state.hasError && this.state.errorCount < 5) { this.setState({ hasError: false, errorCount: this.state.errorCount + 1 }); } } }, { key: "render", value: function render() { return this.props.children; } }], [{ key: "getDerivedStateFromError", value: function getDerivedStateFromError() { return { hasError: true }; } }]); }(_react["default"].Component); /** Input with variables as tags */ var VariableInput = /*#__PURE__*/function (_React$PureComponent) { function VariableInput(props) { var _this2; (0, _classCallCheck2["default"])(this, VariableInput); _this2 = _callSuper(this, VariableInput, [props]); _this2._handleCompositionStart = function () { _this2.isComposing = true; }; _this2._handleCompositionUpdate = function (e) { // During IME composition, manually trigger onChange with current DOM text // This fixes Android autosuggestion issue where draft-js doesn't fire onChange if (_this2.props.onChange && e.target) { var text = e.target.textContent || ''; _this2.props.onChange(text); } }; _this2._handleCompositionEnd = function () { _this2.isComposing = false; }; _this2._handleInput = function (e) { if (_this2.isComposing && _this2.props.onChange && e.target) { var text = e.target.textContent || ''; _this2.props.onChange(text); } }; _this2._handlePastedText = function (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, ''); _this2._onEditorChange(_EditorUtilities["default"].insertText(editorState, text)); return true; } return false; }; _this2._isEmpty = function () { return _this2.state.editorState.getCurrentContent().getPlainText().length === 0; }; _this2._inputToTagSize = function (inputSize) { return _constants.inputToTagsSize[inputSize] || VariableInput.defaultProps.size; }; _this2._toString = function () { var _this2$props$variable = _this2.props.variableTemplate, prefix = _this2$props$variable.prefix, suffix = _this2$props$variable.suffix; var editorState = _this2.state.editorState; return _EditorUtilities["default"].convertToString({ editorState: editorState, prefix: prefix, suffix: suffix }); }; _this2._onBlur = function () { var _this2$props$onBlur = _this2.props.onBlur, onBlur = _this2$props$onBlur === void 0 ? function () {} : _this2$props$onBlur; onBlur(_this2._toString()); }; _this2._onMouseDown = function () { _this2._isMouseDown = true; }; _this2._onMouseUp = function () { _this2._isMouseDown = false; }; _this2._onFocus = function () { var _this2$props$onFocus = _this2.props.onFocus, onFocus = _this2$props$onFocus === void 0 ? function () {} : _this2$props$onFocus; onFocus(_this2._toString()); }; _this2._onSubmit = function () { var _this2$props$onSubmit = _this2.props.onSubmit, onSubmit = _this2$props$onSubmit === void 0 ? function () {} : _this2$props$onSubmit; onSubmit(_this2._toString()); }; _this2._onChange = function () { var _this2$props$onChange = _this2.props.onChange, onChange = _this2$props$onChange === void 0 ? function () {} : _this2$props$onChange; // Only call onChange if not composing (during composition, manual handlers will call it) if (!_this2.isComposing) { onChange(_this2._toString()); } }; _this2._onEditorChange = function (editorState) { var prevHasFocus = _this2.state.editorState.getSelection().getHasFocus(); var newHasFocus = editorState.getSelection().getHasFocus(); var becomesFocused = !prevHasFocus && newHasFocus; var isKeyboard = !_this2._isMouseDown; if (becomesFocused && isKeyboard) { var selectionAtEnd = _draftJs.EditorState.moveSelectionToEnd(editorState).getSelection(); editorState = _draftJs.EditorState.forceSelection(editorState, selectionAtEnd); } _this2._setEditorState(editorState); }; _this2._setEditorState = function (editorState) { var onStateChanged = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () {}; var editorStateBefore = _this2.state.editorState; var _this2$props$variable2 = _this2.props.variableTemplate, prefix = _this2$props$variable2.prefix, suffix = _this2$props$variable2.suffix; var updateEditorState = _EditorUtilities["default"].moveToEdge(editorState); var triggerCallback = function triggerCallback() {}; if (_EditorUtilities["default"].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 = _this2._onBlur; if (_EditorUtilities["default"].hasUnparsedEntity(updateEditorState, prefix, suffix)) { updateEditorState = _this2._stringToContentState(_EditorUtilities["default"].convertToString({ editorState: updateEditorState, prefix: prefix, suffix: suffix })); } } else if (_EditorUtilities["default"].isContentChanged(editorStateBefore, updateEditorState)) { triggerCallback = _this2._onChange; } _this2.setState({ editorState: updateEditorState }, function () { triggerCallback(); onStateChanged(); }); }; _this2._stringToContentState = function (str) { var _this2$props = _this2.props, _this2$props$variable3 = _this2$props.variableParser, variableParser = _this2$props$variable3 === void 0 ? function () {} : _this2$props$variable3, variableTagPropsParser = _this2$props.variableTagPropsParser, _this2$props$variable4 = _this2$props.variableTemplate, prefix = _this2$props$variable4.prefix, suffix = _this2$props$variable4.suffix; var editorState = _this2.state.editorState; var content = _EditorUtilities["default"].stringToContentState({ str: str, variableParser: variableParser, variableTagPropsParser: variableTagPropsParser, prefix: prefix, suffix: suffix }); return _EditorUtilities["default"].pushAndKeepSelection({ editorState: editorState, content: content }); }; _this2._setStringValue = function (str) { var afterUpdated = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () {}; var updatedEditorState = _draftJs.EditorState.moveSelectionToEnd(_this2._stringToContentState(str)); _this2._setEditorState(updatedEditorState, function () { afterUpdated(updatedEditorState); }); }; /** Set value to display in the input */ _this2.setValue = function (value) { _this2._setStringValue(value, function () { _this2._onSubmit(); }); }; /** Insert variable at the input cursor position */ _this2.insertVariable = function (value) { var _this2$props2 = _this2.props, variableParser = _this2$props2.variableParser, variableTagPropsParser = _this2$props2.variableTagPropsParser, _this2$props2$variabl = _this2$props2.variableTemplate, prefix = _this2$props2$variabl.prefix, suffix = _this2$props2$variabl.suffix; var editorState = _this2.state.editorState; var text = variableParser(value); var tagProps = variableTagPropsParser(value); var newState = text ? _EditorUtilities["default"].insertEntity(editorState, { text: text, value: value, tagProps: tagProps }) : _EditorUtilities["default"].insertText(editorState, "".concat(prefix).concat(value).concat(suffix, " ")); _this2._setEditorState(newState, function () { _this2._onSubmit(); }); }; /** Insert Text at the input cursor position */ _this2.insertText = function (value) { var editorState = _this2.state.editorState; var newState = _EditorUtilities["default"].insertText(editorState, value); _this2._setEditorState(newState, function () { _this2._onSubmit(); }); }; /** * Focus the input at the end of the content. * Optionally, if variableKey is provided, focus after that specific variable. */ _this2.focus = function () { var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, variableKey = _ref.variableKey; var editorState = _this2.state.editorState; var selectionToFocus; if (variableKey) { var targetRange = _EditorUtilities["default"].findEntityRangeByKey(editorState, variableKey); if (targetRange) { var blockKey = targetRange.blockKey, end = targetRange.end; selectionToFocus = new _draftJs.SelectionState({ anchorKey: blockKey, anchorOffset: end, focusKey: blockKey, focusOffset: end }); } } if (!selectionToFocus) { selectionToFocus = _draftJs.EditorState.moveSelectionToEnd(editorState).getSelection(); } var newEditorState = _draftJs.EditorState.forceSelection(editorState, selectionToFocus.merge({ hasFocus: true })); _this2.setState({ editorState: newEditorState }, function () { _this2._onFocus(); }); }; var size = props.size, disabled = props.disabled; var decorator = _EditorUtilities["default"].decoratorFactory({ tag: { size: _this2._inputToTagSize(size), disabled: disabled } }); _this2.state = { editorState: _draftJs.EditorState.createEmpty(decorator) }; _this2.editorRef = /*#__PURE__*/_react["default"].createRef(); // IME composition tracking for Android autosuggestions _this2.isComposing = false; // Track mouse-initiated focus to avoid overriding click cursor position _this2._isMouseDown = false; return _this2; } (0, _inherits2["default"])(VariableInput, _React$PureComponent); return (0, _createClass2["default"])(VariableInput, [{ key: "componentDidMount", value: function componentDidMount() { var initialValue = this.props.initialValue; this._setStringValue(initialValue); // Add IME composition event listeners to handle Android autosuggestions if (this.editorRef.current) { var editorNode = this.editorRef.current.editor; if (editorNode) { editorNode.addEventListener('compositionstart', this._handleCompositionStart); editorNode.addEventListener('compositionupdate', this._handleCompositionUpdate); editorNode.addEventListener('compositionend', this._handleCompositionEnd); editorNode.addEventListener('input', this._handleInput); } } } }, { key: "componentWillUnmount", value: function componentWillUnmount() { if (this.editorRef.current) { var editorNode = this.editorRef.current.editor; if (editorNode) { editorNode.removeEventListener('compositionstart', this._handleCompositionStart); editorNode.removeEventListener('compositionupdate', this._handleCompositionUpdate); editorNode.removeEventListener('compositionend', this._handleCompositionEnd); editorNode.removeEventListener('input', this._handleInput); } } } }, { key: "render", value: function render() { var _this$props = this.props, dataHook = _this$props.dataHook, multiline = _this$props.multiline, rows = _this$props.rows, size = _this$props.size, disabled = _this$props.disabled, readOnly = _this$props.readOnly, placeholder = _this$props.placeholder, status = _this$props.status, statusMessage = _this$props.statusMessage, className = _this$props.className; var singleLineProps = { handlePastedText: this._handlePastedText, handleReturn: function handleReturn() { return 'handled'; } }; var finalStatus = (0, _StatusContext.getStatusFromContext)(this.context, status); return /*#__PURE__*/_react["default"].createElement("div", { "data-hook": dataHook, onMouseDown: this._onMouseDown, onMouseUp: this._onMouseUp, className: (0, _VariableInputSt.st)(_VariableInputSt.classes.root, { disabled: disabled, readOnly: readOnly, size: size, status: finalStatus, singleLine: !multiline }, className), style: (0, _defineProperty2["default"])({}, _VariableInputSt.vars.rows, rows), __self: this, __source: { fileName: _jsxFileName, lineNumber: 165, columnNumber: 7 } }, /*#__PURE__*/_react["default"].createElement(EditorErrorBoundary, { __self: this, __source: { fileName: _jsxFileName, lineNumber: 182, columnNumber: 9 } }, /*#__PURE__*/_react["default"].createElement(_draftJs.Editor, (0, _extends2["default"])({ ref: this.editorRef, spellCheck: this.props.spellCheck, editorState: this.state.editorState, onChange: this._onEditorChange, onFocus: this._onFocus, placeholder: placeholder, readOnly: disabled || readOnly }, readOnly && { tabIndex: 0 }, !multiline && singleLineProps, { __self: this, __source: { fileName: _jsxFileName, lineNumber: 183, columnNumber: 11 } }))), (status || finalStatus === 'loading') && /*#__PURE__*/_react["default"].createElement("span", { className: _VariableInputSt.classes.indicatorWrapper, __self: this, __source: { fileName: _jsxFileName, lineNumber: 198, columnNumber: 11 } }, /*#__PURE__*/_react["default"].createElement(_StatusIndicator["default"], { dataHook: _constants.dataHooks.indicator, status: finalStatus, message: statusMessage, __self: this, __source: { fileName: _jsxFileName, lineNumber: 199, columnNumber: 13 } }))); } }]); }(_react["default"].PureComponent); VariableInput.contextType = _StatusContext.StatusContext; VariableInput.displayName = 'VariableInput'; VariableInput.defaultProps = { initialValue: '', multiline: true, rows: 1, spellCheck: false, size: _constants.sizeTypes.medium, variableParser: function variableParser() {}, variableTagPropsParser: function variableTagPropsParser() { return {}; }, variableTemplate: { prefix: '{{', suffix: '}}' } }; var _default = exports["default"] = VariableInput;