/** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule Text * @flow */ 'use strict'; const ColorPropType = require('ColorPropType'); const EdgeInsetsPropType = require('EdgeInsetsPropType'); const NativeMethodsMixin = require('NativeMethodsMixin'); const Platform = require('Platform'); const React = require('React'); const PropTypes = require('prop-types'); const ReactNativeViewAttributes = require('ReactNativeViewAttributes'); const StyleSheetPropType = require('StyleSheetPropType'); const TextStylePropTypes = require('TextStylePropTypes'); const Touchable = require('Touchable'); const createReactClass = require('create-react-class'); const createReactNativeComponentClass = require('createReactNativeComponentClass'); const mergeFast = require('mergeFast'); const processColor = require('processColor'); const stylePropType = StyleSheetPropType(TextStylePropTypes); const viewConfig = { validAttributes: mergeFast(ReactNativeViewAttributes.UIView, { isHighlighted: true, numberOfLines: true, ellipsizeMode: true, allowFontScaling: true, disabled: true, selectable: true, selectionColor: true, adjustsFontSizeToFit: true, minimumFontScale: true, textBreakStrategy: true, }), uiViewClassName: 'RCTText', }; /** * A React component for displaying text. * * `Text` supports nesting, styling, and touch handling. * * In the following example, the nested title and body text will inherit the * `fontFamily` from `styles.baseText`, but the title provides its own * additional styles. The title and body will stack on top of each other on * account of the literal newlines: * * ```ReactNativeWebPlayer * import React, { Component } from 'react'; * import { AppRegistry, Text, StyleSheet } from 'react-native'; * * export default class TextInANest extends Component { * constructor(props) { * super(props); * this.state = { * titleText: "Bird's Nest", * bodyText: 'This is not really a bird nest.' * }; * } * * render() { * return ( * * * {this.state.titleText}{'\n'}{'\n'} * * * {this.state.bodyText} * * * ); * } * } * * const styles = StyleSheet.create({ * baseText: { * fontFamily: 'Cochin', * }, * titleText: { * fontSize: 20, * fontWeight: 'bold', * }, * }); * * // skip this line if using Create React Native App * AppRegistry.registerComponent('TextInANest', () => TextInANest); * ``` * * ## Nested text * * Both iOS and Android allow you to display formatted text by annotating * ranges of a string with specific formatting like bold or colored text * (`NSAttributedString` on iOS, `SpannableString` on Android). In practice, * this is very tedious. For React Native, we decided to use web paradigm for * this where you can nest text to achieve the same effect. * * * ```ReactNativeWebPlayer * import React, { Component } from 'react'; * import { AppRegistry, Text } from 'react-native'; * * export default class BoldAndBeautiful extends Component { * render() { * return ( * * I am bold * * and red * * * ); * } * } * * // skip this line if using Create React Native App * AppRegistry.registerComponent('AwesomeProject', () => BoldAndBeautiful); * ``` * * Behind the scenes, React Native converts this to a flat `NSAttributedString` * or `SpannableString` that contains the following information: * * ```javascript * "I am bold and red" * 0-9: bold * 9-17: bold, red * ``` * * ## Nested views (iOS only) * * On iOS, you can nest views within your Text component. Here's an example: * * ```ReactNativeWebPlayer * import React, { Component } from 'react'; * import { AppRegistry, Text, View } from 'react-native'; * * export default class BlueIsCool extends Component { * render() { * return ( * * There is a blue square * * in between my text. * * ); * } * } * * // skip this line if using Create React Native App * AppRegistry.registerComponent('AwesomeProject', () => BlueIsCool); * ``` * * > In order to use this feature, you must give the view a `width` and a `height`. * * ## Containers * * The `` element is special relative to layout: everything inside is no * longer using the flexbox layout but using text layout. This means that * elements inside of a `` are no longer rectangles, but wrap when they * see the end of the line. * * ```javascript * * First part and * second part * * // Text container: all the text flows as if it was one * // |First part | * // |and second | * // |part | * * * First part and * second part * * // View container: each text is its own block * // |First part | * // |and | * // |second part| * ``` * * ## Limited Style Inheritance * * On the web, the usual way to set a font family and size for the entire * document is to take advantage of inherited CSS properties like so: * * ```css * html { * font-family: 'lucida grande', tahoma, verdana, arial, sans-serif; * font-size: 11px; * color: #141823; * } * ``` * * All elements in the document will inherit this font unless they or one of * their parents specifies a new rule. * * In React Native, we are more strict about it: **you must wrap all the text * nodes inside of a `` component**. You cannot have a text node directly * under a ``. * * * ```javascript * // BAD: will raise exception, can't have a text node as child of a * * Some text * * * // GOOD * * * Some text * * * ``` * * You also lose the ability to set up a default font for an entire subtree. * The recommended way to use consistent fonts and sizes across your * application is to create a component `MyAppText` that includes them and use * this component across your app. You can also use this component to make more * specific components like `MyAppHeaderText` for other kinds of text. * * ```javascript * * Text styled with the default font for the entire application * Text styled as a header * * ``` * * Assuming that `MyAppText` is a component that simply renders out its * children into a `Text` component with styling, then `MyAppHeaderText` can be * defined as follows: * * ```javascript * class MyAppHeaderText extends Component { * render() { * return ( * * * {this.props.children} * * * ); * } * } * ``` * * Composing `MyAppText` in this way ensures that we get the styles from a * top-level component, but leaves us the ability to add / override them in * specific use cases. * * React Native still has the concept of style inheritance, but limited to text * subtrees. In this case, the second part will be both bold and red. * * ```javascript * * I am bold * * and red * * * ``` * * We believe that this more constrained way to style text will yield better * apps: * * - (Developer) React components are designed with strong isolation in mind: * You should be able to drop a component anywhere in your application, * trusting that as long as the props are the same, it will look and behave the * same way. Text properties that could inherit from outside of the props would * break this isolation. * * - (Implementor) The implementation of React Native is also simplified. We do * not need to have a `fontFamily` field on every single element, and we do not * need to potentially traverse the tree up to the root every time we display a * text node. The style inheritance is only encoded inside of the native Text * component and doesn't leak to other components or the system itself. * */ const Text = createReactClass({ displayName: 'Text', propTypes: { /** * When `numberOfLines` is set, this prop defines how text will be truncated. * `numberOfLines` must be set in conjunction with this prop. * * This can be one of the following values: * * - `head` - The line is displayed so that the end fits in the container and the missing text * at the beginning of the line is indicated by an ellipsis glyph. e.g., "...wxyz" * - `middle` - The line is displayed so that the beginning and end fit in the container and the * missing text in the middle is indicated by an ellipsis glyph. "ab...yz" * - `tail` - The line is displayed so that the beginning fits in the container and the * missing text at the end of the line is indicated by an ellipsis glyph. e.g., "abcd..." * - `clip` - Lines are not drawn past the edge of the text container. * * The default is `tail`. * * > `clip` is working only for iOS */ ellipsizeMode: PropTypes.oneOf(['head', 'middle', 'tail', 'clip']), /** * Used to truncate the text with an ellipsis after computing the text * layout, including line wrapping, such that the total number of lines * does not exceed this number. * * This prop is commonly used with `ellipsizeMode`. */ numberOfLines: PropTypes.number, /** * Set text break strategy on Android API Level 23+, possible values are `simple`, `highQuality`, `balanced` * The default value is `highQuality`. * @platform android */ textBreakStrategy: PropTypes.oneOf(['simple', 'highQuality', 'balanced']), /** * Invoked on mount and layout changes with * * `{nativeEvent: {layout: {x, y, width, height}}}` */ onLayout: PropTypes.func, /** * This function is called on press. * * e.g., `onPress={() => console.log('1st')}` */ onPress: PropTypes.func, /** * This function is called on long press. * * e.g., `onLongPress={this.increaseSize}>` */ onLongPress: PropTypes.func, /** * When the scroll view is disabled, this defines how far your touch may * move off of the button, before deactivating the button. Once deactivated, * try moving it back and you'll see that the button is once again * reactivated! Move it back and forth several times while the scroll view * is disabled. Ensure you pass in a constant to reduce memory allocations. */ pressRetentionOffset: EdgeInsetsPropType, /** * Lets the user select text, to use the native copy and paste functionality. */ selectable: PropTypes.bool, /** * The highlight color of the text. * @platform android */ selectionColor: ColorPropType, /** * When `true`, no visual change is made when text is pressed down. By * default, a gray oval highlights the text on press down. * @platform ios */ suppressHighlighting: PropTypes.bool, style: stylePropType, /** * Used to locate this view in end-to-end tests. */ testID: PropTypes.string, /** * Used to locate this view from native code. */ nativeID: PropTypes.string, /** * Specifies whether fonts should scale to respect Text Size accessibility settings. The * default is `true`. */ allowFontScaling: PropTypes.bool, /** * When set to `true`, indicates that the view is an accessibility element. The default value * for a `Text` element is `true`. * * See the * [Accessibility guide](docs/accessibility.html#accessible-ios-android) * for more information. */ accessible: PropTypes.bool, /** * Specifies whether font should be scaled down automatically to fit given style constraints. * @platform ios */ adjustsFontSizeToFit: PropTypes.bool, /** * Specifies smallest possible scale a font can reach when adjustsFontSizeToFit is enabled. (values 0.01-1.0). * @platform ios */ minimumFontScale: PropTypes.number, /** * Specifies the disabled state of the text view for testing purposes * @platform android */ disabled: PropTypes.bool, }, getDefaultProps(): Object { return { accessible: true, allowFontScaling: true, ellipsizeMode: 'tail', }; }, getInitialState: function(): Object { return mergeFast(Touchable.Mixin.touchableGetInitialState(), { isHighlighted: false, }); }, mixins: [NativeMethodsMixin], viewConfig: viewConfig, getChildContext(): Object { return {isInAParentText: true}; }, childContextTypes: { isInAParentText: PropTypes.bool }, contextTypes: { isInAParentText: PropTypes.bool }, /** * Only assigned if touch is needed. */ _handlers: (null: ?Object), _hasPressHandler(): boolean { return !!this.props.onPress || !!this.props.onLongPress; }, /** * These are assigned lazily the first time the responder is set to make plain * text nodes as cheap as possible. */ touchableHandleActivePressIn: (null: ?Function), touchableHandleActivePressOut: (null: ?Function), touchableHandlePress: (null: ?Function), touchableHandleLongPress: (null: ?Function), touchableGetPressRectOffset: (null: ?Function), render(): React.Element { let newProps = this.props; if (this.props.onStartShouldSetResponder || this._hasPressHandler()) { if (!this._handlers) { this._handlers = { onStartShouldSetResponder: (): bool => { const shouldSetFromProps = this.props.onStartShouldSetResponder && // $FlowFixMe(>=0.41.0) this.props.onStartShouldSetResponder(); const setResponder = shouldSetFromProps || this._hasPressHandler(); if (setResponder && !this.touchableHandleActivePressIn) { // Attach and bind all the other handlers only the first time a touch // actually happens. for (const key in Touchable.Mixin) { if (typeof Touchable.Mixin[key] === 'function') { (this: any)[key] = Touchable.Mixin[key].bind(this); } } this.touchableHandleActivePressIn = () => { if (this.props.suppressHighlighting || !this._hasPressHandler()) { return; } this.setState({ isHighlighted: true, }); }; this.touchableHandleActivePressOut = () => { if (this.props.suppressHighlighting || !this._hasPressHandler()) { return; } this.setState({ isHighlighted: false, }); }; this.touchableHandlePress = (e: SyntheticEvent<>) => { this.props.onPress && this.props.onPress(e); }; this.touchableHandleLongPress = (e: SyntheticEvent<>) => { this.props.onLongPress && this.props.onLongPress(e); }; this.touchableGetPressRectOffset = function(): RectOffset { return this.props.pressRetentionOffset || PRESS_RECT_OFFSET; }; } return setResponder; }, onResponderGrant: function(e: SyntheticEvent<>, dispatchID: string) { this.touchableHandleResponderGrant(e, dispatchID); this.props.onResponderGrant && this.props.onResponderGrant.apply(this, arguments); }.bind(this), onResponderMove: function(e: SyntheticEvent<>) { this.touchableHandleResponderMove(e); this.props.onResponderMove && this.props.onResponderMove.apply(this, arguments); }.bind(this), onResponderRelease: function(e: SyntheticEvent<>) { this.touchableHandleResponderRelease(e); this.props.onResponderRelease && this.props.onResponderRelease.apply(this, arguments); }.bind(this), onResponderTerminate: function(e: SyntheticEvent<>) { this.touchableHandleResponderTerminate(e); this.props.onResponderTerminate && this.props.onResponderTerminate.apply(this, arguments); }.bind(this), onResponderTerminationRequest: function(): bool { // Allow touchable or props.onResponderTerminationRequest to deny // the request var allowTermination = this.touchableHandleResponderTerminationRequest(); if (allowTermination && this.props.onResponderTerminationRequest) { allowTermination = this.props.onResponderTerminationRequest.apply(this, arguments); } return allowTermination; }.bind(this), }; } newProps = { ...this.props, ...this._handlers, isHighlighted: this.state.isHighlighted, }; } if (newProps.selectionColor != null) { newProps = { ...newProps, selectionColor: processColor(newProps.selectionColor) }; } if (Touchable.TOUCH_TARGET_DEBUG && newProps.onPress) { newProps = { ...newProps, style: [this.props.style, {color: 'magenta'}], }; } if (this.context.isInAParentText) { return ; } else { return ; } }, }); type RectOffset = { top: number, left: number, right: number, bottom: number, } var PRESS_RECT_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; var RCTText = createReactNativeComponentClass( viewConfig.uiViewClassName, () => viewConfig ); var RCTVirtualText = RCTText; if (Platform.OS === 'android') { RCTVirtualText = createReactNativeComponentClass('RCTVirtualText', () => ({ validAttributes: mergeFast(ReactNativeViewAttributes.UIView, { isHighlighted: true, }), uiViewClassName: 'RCTVirtualText', })); } module.exports = Text;