/** * Copyright (c) Nicolas Gallagher. * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ import applyLayout from '../../modules/applyLayout'; import applyNativeMethods from '../../modules/applyNativeMethods'; import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment'; import { Component } from 'react'; import ColorPropType from '../ColorPropType'; import createElement from '../createElement'; import css from '../StyleSheet/css'; import findNodeHandle from '../findNodeHandle'; import StyleSheetPropType from '../../modules/StyleSheetPropType'; import TextInputStylePropTypes from './TextInputStylePropTypes'; import TextInputState from '../../modules/TextInputState'; import ViewPropTypes from '../ViewPropTypes'; import { any, bool, func, number, oneOf, shape, string } from 'prop-types'; const isAndroid = canUseDOM && /Android/i.test(navigator && navigator.userAgent); const emptyObject = {}; /** * React Native events differ from W3C events. */ const normalizeEventHandler = handler => e => { if (handler) { e.nativeEvent.text = e.target.value; return handler(e); } }; /** * Determines whether a 'selection' prop differs from a node's existing * selection state. */ const isSelectionStale = (node, selection) => { if (node && selection) { const { selectionEnd, selectionStart } = node; const { start, end } = selection; return start !== selectionStart || end !== selectionEnd; } return false; }; /** * Certain input types do no support 'selectSelectionRange' and will throw an * error. */ const setSelection = (node, selection) => { try { if (isSelectionStale(node, selection)) { const { start, end } = selection; // workaround for Blink on Android: see https://github.com/text-mask/text-mask/issues/300 if (isAndroid) { setTimeout(() => node.setSelectionRange(start, end || start), 10); } else { node.setSelectionRange(start, end || start); } } } catch (e) {} }; class TextInput extends Component<*> { _node: HTMLInputElement; _nodeHeight: number; _nodeWidth: number; static displayName = 'TextInput'; static propTypes = { ...ViewPropTypes, autoCapitalize: oneOf(['characters', 'none', 'sentences', 'words']), autoComplete: string, autoCorrect: bool, autoFocus: bool, blurOnSubmit: bool, clearTextOnFocus: bool, defaultValue: string, editable: bool, inputAccessoryViewID: string, keyboardType: oneOf([ 'default', 'email-address', 'number-pad', 'numbers-and-punctuation', 'numeric', 'phone-pad', 'search', 'url', 'web-search' ]), maxFontSizeMultiplier: number, maxLength: number, multiline: bool, numberOfLines: number, onBlur: func, onChange: func, onChangeText: func, onFocus: func, onKeyPress: func, onSelectionChange: func, onSubmitEditing: func, placeholder: string, placeholderTextColor: ColorPropType, returnKeyType: oneOf(['enter', 'done', 'go', 'next', 'previous', 'search', 'send']), secureTextEntry: bool, selectTextOnFocus: bool, selection: shape({ start: number.isRequired, end: number }), spellCheck: bool, style: StyleSheetPropType(TextInputStylePropTypes), value: string, /* react-native compat */ /* eslint-disable */ caretHidden: bool, clearButtonMode: string, dataDetectorTypes: string, disableFullscreenUI: bool, enablesReturnKeyAutomatically: bool, keyboardAppearance: string, inlineImageLeft: string, inlineImagePadding: number, onContentSizeChange: func, onEndEditing: func, onScroll: func, returnKeyLabel: string, selectionColor: ColorPropType, selectionState: any, textBreakStrategy: string, underlineColorAndroid: ColorPropType /* eslint-enable */ }; static defaultProps = { autoCapitalize: 'sentences', autoComplete: 'on', autoCorrect: true, editable: true, keyboardType: 'default', multiline: false, numberOfLines: 1, secureTextEntry: false }; static State = TextInputState; clear() { this._node.value = ''; } isFocused() { return TextInputState.currentlyFocusedField() === this._node; } componentDidMount() { setSelection(this._node, this.props.selection); if (document.activeElement === this._node) { TextInputState._currentlyFocusedNode = this._node; } } componentDidUpdate() { setSelection(this._node, this.props.selection); } render() { const { autoComplete, autoCorrect, editable, keyboardType, multiline, numberOfLines, returnKeyType, secureTextEntry, /* eslint-disable */ blurOnSubmit, clearTextOnFocus, onChangeText, onLayout, onSelectionChange, onSubmitEditing, selection, selectTextOnFocus, spellCheck, /* react-native compat */ accessibilityViewIsModal, allowFontScaling, caretHidden, clearButtonMode, dataDetectorTypes, disableFullscreenUI, enablesReturnKeyAutomatically, hitSlop, inlineImageLeft, inlineImagePadding, inputAccessoryViewID, keyboardAppearance, maxFontSizeMultiplier, needsOffscreenAlphaCompositing, onAccessibilityTap, onContentSizeChange, onEndEditing, onMagicTap, onScroll, removeClippedSubviews, renderToHardwareTextureAndroid, returnKeyLabel, scrollEnabled, selectionColor, selectionState, shouldRasterizeIOS, textBreakStrategy, textContentType, underlineColorAndroid, /* eslint-enable */ ...otherProps } = this.props; let type; switch (keyboardType) { case 'email-address': type = 'email'; break; case 'number-pad': case 'numeric': type = 'number'; break; case 'phone-pad': type = 'tel'; break; case 'search': case 'web-search': type = 'search'; break; case 'url': type = 'url'; break; default: type = 'text'; } if (secureTextEntry) { type = 'password'; } const component = multiline ? 'textarea' : 'input'; Object.assign(otherProps, { // Browser's treat autocomplete "off" as "on" // https://bugs.chromium.org/p/chromium/issues/detail?id=468153#c164 autoComplete: autoComplete === 'off' ? 'noop' : autoComplete, autoCorrect: autoCorrect ? 'on' : 'off', classList: [classes.textinput], dir: 'auto', enterkeyhint: returnKeyType, onBlur: normalizeEventHandler(this._handleBlur), onChange: normalizeEventHandler(this._handleChange), onFocus: normalizeEventHandler(this._handleFocus), onKeyDown: this._handleKeyDown, onKeyPress: this._handleKeyPress, onSelect: normalizeEventHandler(this._handleSelectionChange), readOnly: !editable, ref: this._setNode, spellCheck: spellCheck != null ? spellCheck : autoCorrect }); if (multiline) { otherProps.rows = numberOfLines; } else { otherProps.type = type; } return createElement(component, otherProps); } _handleBlur = e => { const { onBlur } = this.props; TextInputState._currentlyFocusedNode = null; if (onBlur) { onBlur(e); } }; _handleContentSizeChange = () => { const { onContentSizeChange, multiline } = this.props; if (multiline && onContentSizeChange) { const newHeight = this._node.scrollHeight; const newWidth = this._node.scrollWidth; if (newHeight !== this._nodeHeight || newWidth !== this._nodeWidth) { this._nodeHeight = newHeight; this._nodeWidth = newWidth; onContentSizeChange({ nativeEvent: { contentSize: { height: this._nodeHeight, width: this._nodeWidth } } }); } } }; _handleChange = e => { const { onChange, onChangeText } = this.props; const { text } = e.nativeEvent; this._handleContentSizeChange(); if (onChange) { onChange(e); } if (onChangeText) { onChangeText(text); } this._handleSelectionChange(e); }; _handleFocus = e => { const { clearTextOnFocus, onFocus, selectTextOnFocus } = this.props; const node = this._node; TextInputState._currentlyFocusedNode = this._node; if (onFocus) { onFocus(e); } if (clearTextOnFocus) { this.clear(); } if (selectTextOnFocus) { node && node.select(); } }; _handleKeyDown = e => { // Prevent key events bubbling (see #612) e.stopPropagation(); // Backspace, Escape, Tab, Cmd+Enter, and Arrow keys only fire 'keydown' // DOM events if ( e.key === 'ArrowLeft' || e.key === 'ArrowUp' || e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === 'Backspace' || e.key === 'Escape' || (e.key === 'Enter' && e.metaKey) || e.key === 'Tab' ) { this._handleKeyPress(e); } }; _handleKeyPress = e => { const { blurOnSubmit, multiline, onKeyPress, onSubmitEditing } = this.props; const blurOnSubmitDefault = !multiline; const shouldBlurOnSubmit = blurOnSubmit == null ? blurOnSubmitDefault : blurOnSubmit; if (onKeyPress) { const keyValue = e.key; if (keyValue) { e.nativeEvent = { altKey: e.altKey, ctrlKey: e.ctrlKey, key: keyValue, metaKey: e.metaKey, shiftKey: e.shiftKey, target: e.target }; onKeyPress(e); } } if (!e.isDefaultPrevented() && e.key === 'Enter' && !e.shiftKey) { if ((blurOnSubmit || !multiline) && onSubmitEditing) { // prevent "Enter" from inserting a newline e.preventDefault(); e.nativeEvent = { target: e.target, text: e.target.value }; onSubmitEditing(e); } if (shouldBlurOnSubmit) { // $FlowFixMe this.blur(); } } }; _handleSelectionChange = e => { const { onSelectionChange, selection = emptyObject } = this.props; if (onSelectionChange) { try { const node = e.target; if (isSelectionStale(node, selection)) { const { selectionStart, selectionEnd } = node; e.nativeEvent.selection = { start: selectionStart, end: selectionEnd }; onSelectionChange(e); } } catch (e) {} } }; _setNode = component => { this._node = findNodeHandle(component); if (this._node) { this._handleContentSizeChange(); } }; } const classes = css.create({ textinput: { MozAppearance: 'textfield', WebkitAppearance: 'none', backgroundColor: 'transparent', border: '0 solid black', borderRadius: 0, boxSizing: 'border-box', font: '14px System', padding: 0, resize: 'none' } }); export default applyLayout(applyNativeMethods(TextInput));