/**
* 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));